前言
这篇是自制机械臂项目的蓝牙代码优化。想尝试做一个机械臂的可以参考我上篇文章: 《如何做个机械臂》。
蓝牙代码主要优化的部分是对蓝牙收到'写'特征(Characteristic)的逻辑处理和删除部分不需要的代码,比如自定义广播,长数据的分包处理,数据存储处理等。目前的蓝牙所承载的功能是与网页进行数据交互,所以并不要太过复杂的逻辑控制(比如引入很多FreeRTOS的信号量),写代码我还是觉得越简单越好。完整文件可见github。
先看结果
首先看下修改后的性能,现在可以每隔15ms(优化之前是低于200ms会发生阻塞报错)向单片机发送条蓝牙消息而不会发生阻塞。每秒66条的传输,对于后续的引入贝塞尔曲线来优化电机运动姿态应该是足够了。
主要修改内容
修改后的主要业务逻辑代码如下:
case ESP_GATTS_WRITE_EVT: {
//ESP_LOGI(GATTS_TAG, "蓝牙写入事件 GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
esp_gatt_status_t status = ESP_GATT_OK;
//ESP_LOGW(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
//esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
// 将接收到的蓝牙数据做处理,提取出想要格式的帧id和帧数据
unsigned int frameId = 0;
int frameData[8];
for(int i = 0; i < param->write.len; i++) {
// printf("遍历蓝牙%d值: %d", i, param->write.value[i]);
if(i < 4) {
frameId = frameId * 16 * 16 + param->write.value[i];
// ESP_LOGI(GATTS_TAG, "----蓝牙 16进制 转换帧id: %u", frameId);
}else {
frameData[i - 4] = param->write.value[i];
}
}
// printf("frameId%u值: %d, --- %d \n", frameId, frameData[0], frameData[7]);
// ESP_LOGI(EXAMPLE_TAG, "-----发送can数据指令------");
emitMsg(frameId, frameData);
// ESP_LOGI(EXAMPLE_TAG, "-----发送can数据指令结束-----");
// 客户端的请求是需要需要回复时候
if(param->write.need_rsp) {
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
}
break;
功能就是接收到网页传来的信息之后,对信息进行解析,并组装成小米电机需要的帧格式,然后调用TWAI的消息发送函数(这块调用twai的消息调用,感觉可以使用观察者模式进行优化,以后考虑再改)。
遇到的问题以及解决方法
在优化之前,蓝牙部分有两个很突出的问题:
- 单片机经常会报错:BTC stack overflow。
- 蓝牙传输稍微快一些就会报错。
首先简单看下什么是BTC,以下是蓝牙层次关系图,来自ESP32蓝牙架构:
BTC 层主要负责向应⽤层提供接⼝⽀持、处理基于 GATT 的规范、处理杂项等,并向应⽤ 层提供以“esp”为前缀的接⼝。所有的 API 都在 ESP_API 层,开发者应当使⽤“esp”为前缀 的蓝⽛ API(特殊的除外)。
简单来说就是,用户调用btc(蓝牙控制层)提供的接口来设置蓝牙信号的广播、连接和编写服务等。
问题1
大概知道BTC是什么了,那如何解决,有两种:
①. 给相关的内存分配大一些的空间,算是适当解决了问题,但治标不治本:
②. 优化代码逻辑,估计是自己当初改官网代码改出了些问题。通过这次代码优化,已经不再出现蓝牙栈溢出问题了。
问题2
错误截图图下所示:
分析原因:报错内容提示是上一条的蓝牙请求还在处理中,所以本次就被阻塞了。
如何解决:检查蓝牙服务端(单片机)的代码,检查是否在哪里花了超时的时间。看了示例项目的readme解析之后,发现是原示例项目对收到的消息做了复杂的处理,比如indicate指示的处理,长消息的处理等,代码如下:
case ESP_GATTS_WRITE_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
if (!param->write.is_prep){
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
if (gl_profile_tab[PROFILE_A_APP_ID].descr_handle == param->write.handle && param->write.len == 2){
uint16_t descr_value = param->write.value[1]<<8 | param->write.value[0];
if (descr_value == 0x0001){
if (a_property & ESP_GATT_CHAR_PROP_BIT_NOTIFY){
ESP_LOGI(GATTS_TAG, "notify enable");
uint8_t notify_data[15];
for (int i = 0; i < sizeof(notify_data); ++i)
{
notify_data[i] = i%0xff;
}
//the size of notify_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_A_APP_ID].char_handle,
sizeof(notify_data), notify_data, false);
}
}else if (descr_value == 0x0002){
if (a_property & ESP_GATT_CHAR_PROP_BIT_INDICATE){
ESP_LOGI(GATTS_TAG, "indicate enable");
uint8_t indicate_data[15];
for (int i = 0; i < sizeof(indicate_data); ++i)
{
indicate_data[i] = i%0xff;
}
//the size of indicate_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_A_APP_ID].char_handle,
sizeof(indicate_data), indicate_data, true);
}
}
else if (descr_value == 0x0000){
ESP_LOGI(GATTS_TAG, "notify/indicate disable ");
}else{
ESP_LOGE(GATTS_TAG, "unknown descr value");
esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
}
}
}
example_write_event_env(gatts_if, &a_prepare_write_env, param);
break;
}
直接将这些不需要的代码删掉即可(example_write_event_env(gatts_if, &a_prepare_write_env, param);函数是处理长消息的)。
性能优化
① 降低蓝牙消息的连接周期
// 从机连接的最小连接间隔 0x0006 * 1.25ms = 7.5ms slave connection min interval, Time = min_interval * 1.25 msec
.min_interval = 0x0006,
// 从机连接的最大连接间隔 0x0010 * 1.25ms = 16 * 1.25 = 20ms slave connection max interval, Time = max_interval * 1.25 msec
.max_interval = 0x000A,
通过豆包解读下max_interval是什么意思:
② 修改‘写’命令调用的方式:直接调用characteristic.writeValue是需要等待蓝牙的response。看过蓝牙文档之后,可以选择不要需要蓝牙的回复,调用characteristic.writeValueWithoutResponse方法就可以了。
function rotateMotor({motorId, limit_spd, loc_ref}) {
// motorId, limit_spd;
// let a = loc_ref * 0.017;
// debugger;
let rad = loc_ref * Math.PI / 180
// let cmdArr = Loc_Director({motorId, limit_spd, loc_ref: loc_ref * 0.017});
let cmdArr = Loc_Director({motorId, limit_spd, loc_ref: rad});
console.log('---cmdArr: ', cmdArr);
for(let i = 0; i < cmdArr.length; i++) {
setTimeout(() => {
try {
// BleCharacteristic.writeValue(cmdArr[i]);
BleCharacteristic.writeValueWithoutResponse(cmdArr[i]);
console.log("---指令发送成功---", cmdArr[i]);
recordCmd.value.push({type: 'send', data: cmdArr[i], status: 'success'});
} catch (error) {
console.log("---指令发送失败---", cmdArr[i]);
recordCmd.value.push({type: 'send', data: cmdArr[i], status: 'fail'});
}
}, 15);
}
}
具体能提升多少性能暂不清楚,看下豆包的解释: