ESP32-S3 BLE 双角色蓝牙模块重构实战
项目背景
一个基于 ESP32-S3 的空气质量采样设备,搭载 TJC 串口屏作为人机交互界面。蓝牙模块承担双重角色:
- BLE 外设(Peripheral):通过 Nordic UART Service 与手机 App 通信
- BLE 中心设备(Central):连接蓝牙热敏打印机,打印检测记录
蓝牙模块的核心文件为 printer_scan.c(约 1000 行)和 printer_scan.h。
问题发现:标志位混乱
项目运行中出现以下异常现象:
- 蓝牙关闭时仍能触发连接 —— 串口屏的
connecting按钮在蓝牙关闭状态下仍可被物理按键触发 - 连接状态显示不同步 —— 屏幕显示”已连接”但实际连接尚未建立
- MAC 设备列表间歇性空 —— 开启蓝牙后扫描正常,但设备列表不显示任何设备
- 标志位多源写入 —— 同一个连接状态标志被 UI 处理函数和 BLE 任务同时写入
原始标志位全景
1 | "蓝牙开关" → sys_status.bluetooth_open ──┐ |
总计 7 个标志变量分布在 3 个文件中,存在严重的职责重叠和多源写入问题。
问题 1:sys_status.bluetooth_connecting 被两处争抢写入
根因
tjc_uart2.c 中 UI 按键处理直接写入了本应只反映实际连接状态的标志:
1 | // tjc_uart2.c — UI 按键处理 |
同时 ble_task 也在写同一个标志:
1 | // printer_scan.c — ble_task |
4 个执行上下文同时读写同一组 bool 标志,且零互斥保护:
| 上下文 | 载体 |
|---|---|
| GAP 事件回调 | NimBLE Host Task |
| ble_task | 独立 FreeRTOS Task |
| 触摸屏处理 | tjc_uart2(主 Task) |
| 物理按键 | key.c(主 Task) |
修复
原则:每个标志只能有一个写入方。
1 | // tjc_uart2.c — 修改后:只设动作标志,不越权写状态 |
sys_status.bluetooth_connecting 仅由 ble_task 根据实际连接结果写入。
问题 2:标志位冗余——ble_page_connected 合并
ble_page_connected 和 sys_status.bluetooth_connecting 的生命周期完全一致:
1 | // 同时设 true |
全局替换 ble_page_connected → sys_status.bluetooth_connecting,删除冗余标志。
问题 3:ble_scan_enabled 卡死导致列表为空
根因
1 | void ble_stop_scan(void) { |
当 ble_gap_disc_cancel() 返回非预期的错误码时,ble_scan_enabled 保持 true。下次开启蓝牙时,ble_start_scan() 检测到 true 直接返回,扫描根本不启动。
修复
1 | void ble_stop_scan(void) { |
问题 4:扫描设备列表填充过慢
根因
1 | disc_params.filter_duplicates = 1; // 过滤重复广播包 |
同一设备在扫描窗口内只上报一次,新设备需要等待下一个广播周期才能出现。
修复
1 | disc_params.filter_duplicates = 0; // 不滤重 |
ble_update_device_list() 内部已有去重逻辑(memcmp 比较 MAC 地址),不会导致重复条目。
精简:删除 12 个废弃符号
printer_scan.h 删除
| 声明 | 原因 |
|---|---|
extern bool ble_enabled |
僵尸声明,从未定义 |
void printer_scan_deinit(void) |
从未调用 |
void ble_clear_selected_device(void) |
从未外部调用 |
const char* ble_get_device_display_name(...) |
从未调用 |
ble_set_scan_enabled / ble_get_scan_enabled |
从未调用 |
void ble_set_current_mac_index(...) |
从未调用 |
const char* ble_get_current_mac_str(void) |
从未调用 |
bool ble_get_switch_state(void) |
从未调用 |
printer_scan.c 删除
| 符号 | 原因 |
|---|---|
ble_switch_enabled |
与 sys_status.bluetooth_open 完全冗余 |
selected_device_addr[6] |
只写不读 |
has_selected_device |
只写不读 |
| 对应 8 个废弃函数的实现 | 从未调用 |
.h 从 165 行缩减到 75 行,.c 从 28 个函数缩减到 27 个。
连接失败的屏幕回传
蓝牙异步连接失败后,需要恢复串口屏上 connecting 按钮的视觉状态。
方案
在 ble_task 中新增 ble_reset_screen_button(),向屏幕发送 click connecting,1/0 指令:
1 | static void ble_reset_screen_button(void) { |
在三处调用:连接失败、主动断开、被动断线。
设计要点:用 click 而非直接写 connecting.val=0,因为 click 会触发按钮的 press/release 动画和图片切换。由于按钮当前处于 val=1 状态,click 翻转后输出 connecting=0 给 ESP32,而 ESP32 端只对 value==1 做处理,因此 echo 天然安全。
物理按键防御
key.c 中增加:蓝牙关闭时,OK 键只允许触发 index 0(bluetooth_open 按钮),不能触发 connecting 按钮:
1 | case bluetooth: |
配合屏幕端脚本 tsw connecting,0 禁用触摸,形成三层防御。
最终架构
标志位写权限
1 | 串口屏按钮 ──→ sys_status.bluetooth_open (开关, 仅一处写入) |
模块划分
1 | 打印机模块 |
总结
| 指标 | 修改前 | 修改后 |
|---|---|---|
| 标志位数量 | 7 个 | 4 个 |
| 多源写入标志 | 2 个 | 0 个 |
.h 外部声明 |
20 个 | 12 个 |
.c 函数 |
28 个 | 27 个 |
| 红线问题 | 4 个 | 0 个 |
核心经验:
- 单写原则 —— 每个标志位只有一个写入方,从源头消灭竞态
- 串口屏只管发指令 —— 状态由 BLE 任务根据实际结果维护,UI 不越权
- 异步操作用标志位桥接 ——
ble_task和 UI 线程通过ble_connect_flag解耦 - 无条件清理 ——
ble_stop_scan()的ble_scan_enabled不管取消失败都清零


