一、 项目介绍
本项目旨在构建一个低功耗、可视化的温湿度数据显示系统。核心控制器采用 Analog Devices MAX32655 超低功耗微控制器,作为 BLE Central(主机)设备,负责通过蓝牙扫描并连接远程温湿度传感器,定期读取环境数据。
MAX32655 采用超低功耗双核架构,主核(Arm Cortex-M4F (100MHz)),负责高性能计算,协处理器(RISC-V),专门分担蓝牙协议栈的时序关键任务,支持蓝牙长距离模式 (Long Range)、高吞吐量 (2Mbps) 以及多连接,能够极大降低系统功耗。开发板板载板载数字麦克风和低功耗立体声音频编解码器;集成 MAX20303 PMIC,支持锂电池充电、电量计 (Fuel Gauge) 和电源路径管理;DAPLink 调试器,无需额外编程器;还有128Mb FLASH,非常适合开发语音/音频类可穿戴应用。
项目目标:
- 使用蓝牙连接小米温湿度计
- 通过串口传递温湿度数据
- 在LCD屏幕上显示温湿度数据
二. 设计思路
基于 SDK 的示例工程(ble_otac)进行修改,已经配置好了蓝牙相关配置和连接功能实现。
使用没有修改固件的小米温湿度传感器,只能通过长连接的方式获取数据;订阅需要米家的密钥,所以无法广播读取数据。
使用 CodeFusion Studio(vs code)进行开发。
1. 系统架构
系统采用 “采集-转发-显示” 的三层架构:
- 感知层:BLE 传感器(LYWSD03MMC)。
- 传输/网关层 (MAX32655):
- 运行 BLE 协议栈,作为 Client 角色。
- 实现自动扫描、过滤特定 MAC 地址、建立连接。
- 利用定时器(Timer)周期性发起 GATT Read 请求。
- 定义自定义串口通信协议,将解析后的数据封包发送。
- 应用/展示层 (M1w dock suit):
- 监听 UART 端口,基于帧头/帧尾和校验和进行解包。
- 利用 GUI 库绘制仪表盘、电池电量图标和实时数值。
2. 通信协议设计 (UART)
为了保证数据传输的可靠性,设计了一套定长二进制通信协议(10 Bytes):
- 帧头 (2 Bytes): 0xAA 0x55 (用于数据同步)
- 数据载荷 (5 Bytes):
- 温度 (2 Bytes, int16, 单位 0.01°C)
- 湿度 (1 Byte, uint8, 单位 %)
- 电压 (2 Bytes, uint16, 单位 mV)
- 校验和 (1 Byte): Checksum (数据载荷的累加和)
- 帧尾 (2 Bytes): 0x0D 0x0A
3. 软件流程图:


三、 实现过程
1. 开发环境安装
CodeFusion Studio 提供一键式的开发环境安装,非常方便。
2. MAX32655 固件开发
基于 BLE_otac 示例工程进行深度改造。
A. 内存池优化
为了适应大量 BLE 事件和长数据包的处理,首先调整了 WSF 缓冲池配置。如果不增加 buffer 大小,在连接过程中容易出现内存分配失败导致的 HardFault。
/* 增加 buffer 大小以适应复杂 BLE 操作 */
static wsfBufPoolDesc_t mainPoolDesc[] = { { 16, 16 }, { 32, 16 }, { 192, 64 }, { 256, 32 } };
B. 扫描与自动连接逻辑
修改 datcScanReport 回调函数。原程序仅打印扫描结果,修改后增加了 MAC 地址比对逻辑。一旦匹配到目标 MAC (B1:17:8D:38:C1:A4),立即停止扫描并设置 doConnect = TRUE 标志,在扫描停止回调中发起连接。
static void datcScanReport(dmEvt_t *pMsg)
{
/* 打印扫描到的所有设备 MAC,用于调试 (调试完成后可注释掉) */
/* 注意:如果不打印,你不知道设备是否真的被扫描到了 */
APP_TRACE_INFO6("Scan: %02x:%02x:%02x:%02x:%02x:%02x",
pMsg->scanReport.addr[5], pMsg->scanReport.addr[4],
pMsg->scanReport.addr[3], pMsg->scanReport.addr[2],
pMsg->scanReport.addr[1], pMsg->scanReport.addr[0]);
/* 如果已经在连接或没在扫描,忽略 */
if (!datcCb.scanning || !datcCb.autoConnect) {
return;
}
/* === 修改: 检查 MAC 地址是否匹配 === */
// BdaCmp 返回 TRUE 表示地址相同
if (BdaCmp(pMsg->scanReport.addr, targetAddr)) {
APP_TRACE_INFO0(">>> Target discovered! connecting... <<<");
/* 停止扫描 */
datcCb.autoConnect = FALSE;
AppScanStop();
/* 保存连接信息 */
datcConnInfo.addrType = DmHostAddrType(pMsg->scanReport.addrType);
memcpy(datcConnInfo.addr, pMsg->scanReport.addr, sizeof(bdAddr_t));
datcConnInfo.dbHdl = APP_DB_HDL_NONE; // 不使用绑定数据库,直接连
datcConnInfo.doConnect = TRUE;
}
}
C. 定时读取机制
本项目采用主动 Read 模式。
- 新增事件 ID:定义 READ_TIMER_EVT (0x98)。
- 服务发现完成:在 datcDiscCback 的 APP_DISC_CMPL 状态下,启动 1秒 的定时器。
- 消息处理循环:在 datcProcMsg 中增加对 READ_TIMER_EVT 的处理,调用 AttcReadReq 读取句柄 0x0036(传感器数据特征值)。
- 循环读取:收到 ATTC_READ_RSP 后,解析数据并再次启动 2秒 定时器,实现周期性轮询。
static void datcDiscCback(dmConnId_t connId, uint8_t status)
{
switch (status) {
case APP_DISC_INIT:
AppDiscSetHdlList(connId, datcCb.hdlListLen, datcCb.hdlList[connId - 1]);
break;
case APP_DISC_READ_DATABASE_HASH:
AppDiscReadDatabaseHash(connId);
break;
case APP_DISC_SEC_REQUIRED:
AppMasterSecurityReq(connId);
break;
case APP_DISC_START:
datcCb.discState[connId - 1] = DATC_DISC_GATT_SVC;
AppDbNvmStoreCacheByHash(AppDbGetHdl(connId));
GattDiscover(connId, pDatcGattHdlList[connId - 1]);
break;
case APP_DISC_FAILED:
// 即使失败也继续,只要连接不断开
case APP_DISC_CMPL:
datcCb.discState[connId - 1]++;
if (datcCb.discState[connId - 1] == DATC_DISC_GAP_SVC) {
GapDiscover(connId, pDatcGapHdlList[connId - 1]);
}
else {
/* === 发现结束 === */
AppDiscComplete(connId, APP_DISC_CMPL);
datcDiscGapCmpl(connId);
AppDbNvmStoreHdlList(AppDbGetHdl(connId));
// 简单的配置,其实可以跳过
AppDiscConfigure(connId, APP_DISC_CFG_START, DATC_DISC_CFG_LIST_LEN,
(attcDiscCfg_t *)datcDiscCfgList, DATC_DISC_HDL_LIST_LEN,
datcCb.hdlList[connId - 1]);
APP_TRACE_INFO0(">>> Service found, reading data <<<");
/* === 启动定时器,1秒后开始读取 === */
datcCb.readTimer.handlerId = datcCb.handlerId;
datcCb.readTimer.msg.event = READ_TIMER_EVT;
datcCb.readTimer.msg.param = connId; // 把 connId 传参
WsfTimerStartMs(&datcCb.readTimer, 1000);
}
break;
case APP_DISC_CFG_START:
case APP_DISC_CFG_CONN_START:
datcCb.discState[connId - 1] = DATC_DISC_SVC_MAX;
AppDiscConfigure(connId, APP_DISC_CFG_START, DATC_DISC_CFG_LIST_LEN,
(attcDiscCfg_t *)datcDiscCfgList, DATC_DISC_HDL_LIST_LEN,
datcCb.hdlList[connId - 1]);
break;
case APP_DISC_CFG_CMPL:
AppDiscComplete(connId, status);
break;
default:
break;
}
}
D. UART 数据转发
利用 MAX32655FTHR 的 P2.6/P2.7 引脚配置 LPUART。编写了 SendBinaryPacket 函数,负责将解析出的 tempRaw、humidity 和 voltRaw 按照设计的协议进行打包、计算 Checksum 并发送。
3. m1w dock suit 开发 (MicroPython)
A. 串口数据解析
使用 MicroPython 的 machine.UART 读取数据流。为了防止串口粘包或错位,编写了 parse_uart_buffer 函数,采用滑动窗口算法查找帧头 0xAA 55,并验证校验和。
def parse_uart_buffer(buf):
"""
在缓冲区中寻找有效帧
帧格式 (10 Bytes): AA 55 TL TH H VL VH CS 0D 0A
"""
total_len = len(buf)
if total_len < 10:
return False
# 遍历缓冲区寻找帧头 0xAA 0x55
# 我们只处理最新的完整帧,忽略旧数据
# 从后往前找,或者找到最后一个完整的帧
found = False
idx = 0
# 简单的滑动窗口查找
while idx <= total_len - 10:
# 1. 检查帧头
if buf[idx] == 0xAA and buf[idx+1] == 0x55:
# 2. 检查帧尾
if buf[idx+8] == 0x0D and buf[idx+9] == 0x0A:
# 3. 校验和计算
# 数据段: buf[idx+2] 到 buf[idx+6]
payload = buf[idx+2 : idx+7]
recv_sum = buf[idx+7]
calc_sum = sum(payload) & 0xFF # 累加并取低8位
if calc_sum == recv_sum:
# === 校验通过,提取数据 ===
# 温度 (int16 小端序)
raw_temp = payload[0] | (payload[1] << 8)
# 处理负数 (16位补码)
if raw_temp > 32767:
raw_temp -= 65536
sensor.temp = raw_temp / 100.0
# 湿度 (uint8)
sensor.hum = payload[2]
# 电压 (uint16 小端序)
raw_volt = payload[3] | (payload[4] << 8)
sensor.volt = raw_volt / 1000.0
sensor.last_update = time.ticks_ms()
sensor.connected = True
found = True
# 跳过这个包,继续看有没有更新的
idx += 10
continue
idx += 1
return found
B. 图形化界面 (GUI)
利用 K210 的 image 和 lcd 库绘制界面:
- 状态监测:通过 time.ticks_diff 监测数据更新时间,若超过 3秒 未收到数据,界面显示 "Waiting..." 提示,增强用户体验。
- 可视化设计:使用不同颜色区分温度(红)、湿度(蓝)和电压(绿)。
- 动态电量条:根据电压值(2.0V - 3.0V)动态绘制矩形填充,直观展示传感器剩余电量。
四、实现效果
LCD 屏幕正确显示温湿度和电压数据。

