SMARTWATCH 学习手册有 C 基础 · 嵌入式从零开始
无线通信子篇 / 02

项目实现:一条消息怎样走完三端

已经知道怎样组帧和逐字节解析,现在沿项目的一条电量上报向外走,再沿一条手机命令向内走。每一步都找到实际函数,分清哪里生成数据、哪里只转发、哪里决定执行什么。

前置:无线基础与手写协议沿当前源码阅读保留驱动,先理解调用关系
源码入口

协议规范保存共同约定;STM32 的 ble_uart.c、CH582M 的 peripheral.c、手机的 ble_service.dart 分别完成链路上的三部分。

第 1 节 · 电池读数怎样进入发送帧?

ADC 篇已经介绍 battery_read_mv():无参数,返回毫伏数,当前实现以 0 表示失败。现在假设已经得到一次有效的 4000 毫伏读数和 60% 电量,先看发送,不重新展开电池测量。

ble_uart_send_battery(mv, pct) 接收 uint16_t 毫伏值和 uint8_t 百分比;返回 0 表示本次串口发送成功,−1 表示失败。它的函数体先放三个载荷字节,再交给通用发送函数:

uint8_t p[3];
memcpy(&p[0], &mv, 2);
p[2] = pct;
return ble_uart_send(BLE_MSG_BATTERY, p, 3);

当前 STM32 用小端保存整数,所以复制 mv 的两个内存字节,得到前篇的 A0、0F;第三个是 3C。这里的 memcpy 是标准 C 内存复制函数,第一个参数是目标地址、第二个是来源地址、第三个是字节数。项目双方约定小端,换平台时仍要确认字节序。

通用发送函数又做了什么?

ble_uart_send(type, payload, len) 接收类型、载荷地址、载荷长度。它限制载荷最多 124 字节,把 AA、类型、长度放进 tx_buf,复制载荷、计算校验,再调用:

HAL_StatusTypeDef ret =
    HAL_UART_Transmit(&huart1, tx_buf, 4 + len, 50);
return (ret == HAL_OK) ? 0 : -1;

因此本例送上 USART1 的内容为 AA 06 03 A0 0F 3C 62。当前发送采用阻塞接口,最多等待 50 毫秒;有些头文件旧注释写“非阻塞”,应以函数体为准。tx_buf 是共享发送数组,当前流程需要安排发送先后,不能让多个调用者同时改写它。

程序启动时,ble_uart_init() 无参数,清除接收队列与解析状态,并调用 HAL_UART_Receive_IT 接收第一个字节。先完成 USART1 初始化,再初始化这层通信。之后可以在主循环中发送一条测试消息:

int result = ble_uart_send_battery(4000, 60);
if (result != 0) {
    /* 本次串口发送失败,保留失败信息,稍后再决定是否重试。 */
}

这里的 4000、60 是已知测试值,方便三端逐字节对照。跑通以后再换成前篇的实际测量;避免在排查无线链路时同时怀疑采集结果。

第 2 节 · CH582M 收到字节后,怎样交给手机?

CH582M 不解析 AA 帧,也不知道 06 是电池。它保存顺序正确的字节,在蓝牙允许发送时取出一段,交给 FFE3 通知特征。即使一帧被拆成两次通知,消息的意义仍由手机协议层恢复。

  1. UART1 中断取走硬件里的字节。UART1_IRQHandler() 把收到的数据放入串口到蓝牙方向的循环队列,避免主循环不在场时硬件接收区被塞满。
  2. 主循环安排转发。Main_Circulation() 反复调用 TMOS_SystemProcess() 处理无线协议栈事件,再调用 app_uart_process() 安排串口相关工作。这里的 TMOS 是配套库提供的事件调度机制。
  3. 等蓝牙接口可用,取一块发送。UART_TO_BLE_SEND_EVT 对应的处理确认连接、通知订阅和可发送长度,再把队列里的字节交给 ble_uart_notify()。暂时发不出去时,需要按代码的重试与缓存规则处理。

这几个函数的完整上下文在 Main.c、peripheral.c 和 蓝牙服务实现。本篇按经过顺序读这些函数,不把协议栈初始化代码整段复制到 STM32 工程。

为什么两边都需要队列?

串口和无线发送速度不总一样。115200、8N1 下,每个有效数据字节要占 10 个串口位,理论上每秒约能传 11520 字节。无线端某一时刻可能忙、未订阅或断开,字节就需要先排队;队列容量有限,长期进得比出得快还是会满。

当前 CH582M 为“串口到蓝牙”准备 4096 字节队列,为“蓝牙到串口”准备 512 字节队列。后者空间不足时,整块接收或整块拒绝,避免只转发某条写入的一半。缓存只能吸收一定突发量,不能让一条长期过慢的链路变快。

第 3 节 · 手机收到一块数据,怎样恢复 4000?

手机连接并订阅 FFE3 后,ble_service.dart 的 _onNotify(data) 接到本次通知的字节列表。它先把这些字节追加到接收缓冲,再寻找 AA;至少有三个字节后读长度,等待“长度+4”个字节齐全,才取出一帧。

随后 parseFrame(frame) 检查帧头、长度和校验。检查通过,_dispatch(type, payload) 根据类型分发;电池消息交给 parseBattery()。在 ble_protocol.dart 中,它从前两个字节按小端恢复电压,第三个字节取得百分比。

把收到的数据倒推回去

AA 06 03 A0 0F 3C 62 → 外层格式检查通过 → 类型 06、载荷 A0 0F 3C → 0x0FA0=4000 毫伏、0x3C=60% → 发布给手机界面。

如果调试工具已经看到这一组字节,而 App 没显示,排查重点就转到手机的组帧、解码和界面订阅。此时先保留原始字节,再逐层确认,不要一开始就改传感器寄存器。

第 4 节 · 手机发来的命令,怎样改变手表?

方向反过来,手机先按同一格式组帧,经 FFE2 写入。App 的 _writeFrame() 按实际 MTU 减 3 的长度分块,并依次等待写入。CH582M 服务的写入回调把字节交给串口发送队列,由 UART1 送到 STM32。

STM32 每次接到一个字节,HAL_UART_RxCpltCallback 把它放进 rx_ring,再重新启动下一个字节的接收。主循环或任务调用 ble_uart_process(),依次从循环队列取字节,经过前篇解释过的五个解析状态,完整且校验正确才调用 ble_uart_dispatch。

用时间命令看一次完整分发

上一篇使用的 AA 91 04 00 00 00 01 C0 表示时间设置。91 类型的四个载荷字节使用大端,和电池消息的小端不同:最前面是最高字节。源码先要求长度正好为 4,再按下面的式子恢复整数:

uint32_t unix = ((uint32_t)payload[0] << 24)
              | ((uint32_t)payload[1] << 16)
              | ((uint32_t)payload[2] << 8)
              |  (uint32_t)payload[3];
rtc_set_unix(unix);

这次恢复得到 1,也就是时间起点后 1 秒。先转为 uint32_t 再移位,给高位运算足够宽度。时间接口对应 rtc_time.c;发送这一命令会改变手表日期,实验后再同步正确时间。

其他命令按自己的字段处理。例如 80 类型里先看子命令,01 是用户参数,02 是模块开关。当前实现会回传确认值;收到正确确认再更新手机状态,比只看“写入成功”更能说明业务已生效。当前 91 时间命令没有对应的应用确认帧,不能假设所有命令都已有同样的确认流程。

“最近收过数据”不等于真实连接状态

当前 ble_uart_is_connected() 返回的是 STM32 最近接收字节形成的状态,超过五秒无接收会清除。它没有直接读取 CH582M 的真实蓝牙连接状态。手机已连接但暂时没发字节时,两者可能不同;需要严格连接状态时,应增加明确的状态上报或心跳约定。

第 5 节 · 三端都能定位,通信才算接起来了

现象先检查哪一步
扫描不到手表CH582M 是否广播、扫描响应与手机扫描权限
能连接,通知里没字节FFE3 是否订阅、STM32 是否发送、CH582M 队列是否转出
调试工具有字节,App 不显示完整帧是否齐全、校验和字段解析、界面是否监听结果
手机写入成功,手表没动作FFE2 转串口、USART1 接收回调、解析函数是否持续执行、命令字段是否正确
短包正常,大包失败实际 MTU、分块顺序、各级队列容量和应用层确认

先记录本例固定的七字节电池帧,确认 STM32 发送端、无线通知和 App 解码三处一致;再用时间命令验证反向。之后把固定电量换成真实读数,通信和采集这两条链就接到一起了。

当前项目的调用位置可在 main.c 与 app.c 中追踪。仓库已经使用实时操作系统,这里先读懂收发流程;任务如何安排留到后续章节。App/task_ble.c 已弃用,当前通信实现看 Core/Src/ble_uart.c。

下一篇 DMA:数据怎样自动搬运。屏幕、声音和串口已经用过它,现在回头解释搬运请求、地址、宽度与缓冲时限,完成项目裸机部分需要的外设与通信知识。

继续读 CH582M 的实际实现

进入 支线二:CH582M 开发与蓝牙桥接,从与 STM32 的开发差异开始,沿串口、事件循环、FFE2 写入和 FFE3 通知,读清这条双向链路。手机端随后在支线三展开。