简介:本资源是一套基于STM32L151单片机与BC20-NB-IoT模块的嵌入式物联网实战项目,面向嵌入式初学者、单片机开发者及物联网应用工程师,解决温湿度数据通过MQTT协议稳定上云至阿里云IoT平台的核心开发难题。压缩包共1527个文件,涵盖313个C源码、261个头文件(h)、248个HTML/JS/CSS前端页面(用于本地调试界面或文档)、157个PNG/BMP图像(含阿里云三要素、平台配置等关键操作图示),以及KEIL工程文件、链接脚本、编译输出(axf/hex/map)等完整开发资产,总大小29.9MB。已有91人学习下载,说明其在低功耗NB-IoT接入场景中具备较强实操参考价值。读者可直接复用已注释清晰的驱动代码、串口AT指令解析逻辑、MQTT连接与发布流程、传感器数据采集框架,并结合接线定义与芯片适配说明快速移植到STM32L15X系列其他型号,显著缩短从硬件联调到云端验证的开发周期。
1. 为什么 STM32L151 + BC20-NB-IoT 跑 MQTT 到阿里云 IoT 平台不是“接上线就完事”?
很多工程师拿到 BC20-NB-IoT 模块和 STM32L151 开发板后,第一反应是“串口发 AT 指令连上网络,再发 MQTT CONNECT 就能传温湿度了”。但真实项目里,90% 的失败卡在:AT 指令时序错半拍、BC20 的 PSM/DRX 模式让心跳超时、阿里云 IoT 的三元组校验格式多一个空格、STM32L151 的 32KB RAM 在 TLS 握手阶段直接溢出。这不是模块不行,而是 NB-IoT 的低功耗特性、MQTT 协议的会话状态管理、阿里云平台对设备认证的强约束,三者叠加后,必须把每个环节的边界条件都显式处理——比如 BC20 的+QMTCONN响应不是“OK”,而是带CONNECTED字符串的异步事件;STM32L151 的 UART 接收缓冲区若未做环形队列+超时重置,一条+QMTRECV长报文就会丢帧;阿里云要求的 ClientID 必须包含时间戳且不能重复,否则CONACK=0x05(Connection Refused, Not Authorized)错误根本不会告诉你缺了哪一环。本文聚焦可复现的最小闭环:用标准 HAL 库驱动 STM32L151,通过 BC20 的固件 AT 指令集(V1.4.2),在无外部 RTOS 的裸机环境下,完成从 DHT22 读取温湿度、封装 JSON、建立 TLS 加密 MQTT 连接、稳定上报到阿里云 IoT 平台物模型属性的全流程。
2. BC20-NB-IoT 模块与 STM32L151 的硬件连接与 AT 指令初始化
BC20-NB-IoT 模块与 STM32L151 的物理连接必须满足 NB-IoT 的供电与信号完整性要求。BC20 工作电流峰值达 250mA(附着阶段),仅靠 USB 5V 供电极易导致模块复位。常见错误是直接将 BC20 的 VCC_IO 接 STM32 的 3.3V,而忽略其 VBAT 引脚需独立接入 3.4–4.2V 锂电池或稳压电源。我们采用以下连接方案:
| BC20 引脚 | STM32L151 引脚 | 说明 |
|---|---|---|
| VBAT | 外部 3.8V 电源 | 必须独立供电,不可共用 MCU 3.3V |
| VCC_IO | PA9 (USART1_TX) | 电平匹配:BC20 IO 为 1.8V,需加电平转换芯片 SN74LVC1T45 或直接使用 BC20 的 3.3V 兼容模式(出厂默认) |
| GND | GND | 共地,线长 <15cm,避免干扰 |
| RESET | PC13 | 开漏输出,上拉至 VCC_IO,软件可控复位 |
| STATUS | PB0 | 输入,检测模块是否已启动(高电平有效) |
提示:BC20 启动后 STATUS 引脚需保持高电平至少 500ms 才进入正常工作态。实测中若 STATUS 未稳定即发送 AT 指令,模块返回
ERROR且无日志,此时需强制硬件复位。
2.1 初始化流程:从上电到网络附着的 7 步 AT 指令序列
BC20 的 AT 指令执行有严格依赖关系,顺序错误会导致后续指令全部失败。以下是在 STM32L151 上用 HAL_UART_Transmit + HAL_UART_Receive_IT 实现的最小可靠序列(每条指令后必须等待OK或+QIURC:事件响应,超时设为 3s):
// 1. 查询模块基本信息(确认通信正常) HAL_UART_Transmit(&huart1, (uint8_t*)"AT\r\n", 4, 1000); // 等待响应 "OK" // 2. 关闭回显,减少解析负担 HAL_UART_Transmit(&huart1, (uint8_t*)"ATE0\r\n", 6, 1000); // 3. 设置 NB-IoT 网络注册模式(必须先于 CGATT) HAL_UART_Transmit(&huart1, (uint8_t*)"AT+CGDCONT=1,\"IP\",\"CMNET\"\r\n", 28, 1000); // 4. 启用自动附着(关键!否则无法注册) HAL_UART_Transmit(&huart1, (uint8_t*)"AT+CGATT=1\r\n", 12, 1000); // 等待 "+CGATT: 1" 响应 // 5. 查询信号质量(验证是否入网) HAL_UART_Transmit(&huart1, (uint8_t*)"AT+CSQ\r\n", 8, 1000); // 响应示例:+CSQ: 22,99 → RSSI ≥ 15 才可建链 // 6. 获取 IP 地址(NB-IoT 为私网地址,但 MQTT 连接必需) HAL_UART_Transmit(&huart1, (uint8_t*)"AT+CIICR\r\n", 10, 1000); // 等待 "OK" 后立即查 IP HAL_UART_Transmit(&huart1, (uint8_t*)"AT+CIFSR\r\n", 10, 1000); // 响应示例:10.192.10.100 // 7. 配置 MQTT 客户端(阿里云要求 TLSv1.2,端口 1883 或 8883) HAL_UART_Transmit(&huart1, (uint8_t*)"AT+QMTCFG=\"ssl\",1,1,1,\"/etc/ssl/certs/ca-bundle.crt\"\r\n", 54, 1000); // 注意:BC20 内置根证书已含阿里云 IoT 的 GlobalSign R3,此处路径为占位,实际使用内置证书无需指定2.1.1 关键参数说明与常见失败点
AT+CGDCONT中的CMNET是中国移动 NB-IoT 默认 APN,若使用电信则改为ctnet,联通为uninet。错误 APN 导致+CGATT: 0。AT+CGATT=1必须在AT+CGDCONT之后、AT+CIICR之前执行。跳过此步,AT+CIICR返回ERROR。AT+CIFSR返回空字符串?说明 PDP 上下文未激活,需检查 SIM 卡是否欠费、NB-IoT 卡是否开通物联网服务。AT+QMTCFG="ssl"的第三个参数1表示启用 SSL/TLS,第四个参数1表示验证服务器证书。阿里云 IoT 平台强制要求证书校验,设为0会连接失败。
2.2 STM32L151 的 UART 接收可靠性设计
BC20 的 AT 响应是异步事件流(如+QMTSTAT: 1,1表示 MQTT 连接成功),传统轮询方式极易丢帧。我们在 HAL 库基础上构建轻量级接收状态机:
// 定义接收缓冲区(大小需 ≥ BC20 最大单行响应长度 256B) uint8_t uart_rx_buffer[256]; uint16_t rx_index = 0; uint8_t in_frame = 0; void USART1_IRQHandler(void) { uint8_t byte; if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) { byte = (uint8_t)(huart1.Instance->RDR & 0xFF); if (byte == '\r' || byte == '\n') { if (rx_index > 0 && uart_rx_buffer[0] != '+') { // 处理 OK/ERROR 等基础响应 parse_basic_response(); } else if (rx_index > 2 && uart_rx_buffer[0] == '+') { // 处理 +QMTCONN/+QMTRECV 等事件 parse_mqtt_event(); } rx_index = 0; // 清空缓冲区 } else { if (rx_index < sizeof(uart_rx_buffer)-1) { uart_rx_buffer[rx_index++] = byte; } } } }注意:BC20 的
+QMTRECV事件中,数据长度字段为十六进制(如+QMTRECV: 0,0,48表示 topic 长度 0、QoS 0、payload 长度 0x48=72 字节),必须用strtol(buf+12, NULL, 16)解析,而非十进制atoi()。
3. 在 STM32L151 裸机环境下实现 MQTT 连接与阿里云三元组认证
阿里云 IoT 平台不接受通用 MQTT 客户端连接,必须使用平台颁发的三元组(ProductKey、DeviceName、DeviceSecret)生成签名,并构造符合物模型规范的 MQTT 连接参数。BC20 内置 MQTT 客户端支持AT+QMTOPEN和AT+QMTCONN,但所有参数必须由 STM32L151 计算并拼装,不能硬编码。
3.1 阿里云 MQTT 连接参数生成逻辑
连接时需向 BC20 发送AT+QMTCONN指令,其参数格式为:
AT+QMTCONN=<connect_id>,<server_address>,<port>,<client_id>,<username>,<password>其中:
<connect_id>:任意整数(如 0),用于标识本次连接<server_address>:iot-as-mqtt.cn-shanghai.aliyuncs.com(地域需与控制台创建产品时一致)<port>:1883(非 TLS)或8883(TLS,必须用此端口)<client_id>:{device_name}|securemode=3,signmethod=hmacsha256,timestamp={timestamp}|<username>:{device_name}&{product_key}<password>:hmacsha256({device_secret}, ${content}),其中${content}为clientId{client_id}username{username}password{password}timestamp{timestamp}的拼接字符串(注意无分隔符)
3.1.1 STM32L151 上的 HMAC-SHA256 实现要点
STM32L151 无硬件加密引擎,需使用轻量级 SHA256 库(如 mbedtls 的mbedtls_sha256.h)。关键约束:
- 输入密钥
device_secret长度必须为 32 字节(不足补 0,超长截断) timestamp使用 Unix 时间戳(秒级),必须与阿里云服务器时间误差 < 15 分钟,否则签名失效- 拼接字符串
${content}中的client_id、username、password均为原始值(不含|符号),且password字段在拼接时为空字符串(因密码本身是签名结果)
// 示例:生成 password 字段 char content[256]; char timestamp_str[12]; int ts = get_unix_timestamp(); // 自行实现,如 RTC 获取 sprintf(timestamp_str, "%d", ts); sprintf(content, "clientId%susername%spassword%stimestamp%s", client_id_raw, username_raw, "", timestamp_str); // password 位置留空! uint8_t hash[32]; mbedtls_sha256_context ctx; mbedtls_sha256_init(&ctx); mbedtls_sha256_starts(&ctx, 0); // 0 for SHA256 mbedtls_sha256_update(&ctx, (uint8_t*)content, strlen(content)); mbedtls_sha256_finish(&ctx, hash); mbedtls_sha256_free(&ctx); // Base64 编码 hash 得到 password(BC20 要求 Base64) char password_b64[64]; base64_encode(hash, 32, password_b64);提示:阿里云控制台生成的
device_secret是 Base64 编码字符串,需先base64_decode()得到原始 32 字节二进制密钥,再参与 HMAC 计算。直接使用 Base64 字符串会导致签名错误。
3.2 建立 MQTT 连接并订阅物模型 Topic
完成参数生成后,向 BC20 发送连接指令:
char conn_cmd[512]; sprintf(conn_cmd, "AT+QMTCONN=0,\"iot-as-mqtt.cn-shanghai.aliyuncs.com\",8883,\"%s\",\"%s\",\"%s\"\r\n", client_id_full, username_full, password_b64); HAL_UART_Transmit(&huart1, (uint8_t*)conn_cmd, strlen(conn_cmd), 5000);BC20 返回+QMTCONN: 0,0表示连接成功(第二个 0 为CONACK=0x00)。此时需立即订阅阿里云物模型的属性上报响应 Topic,以确认平台接收状态:
// 订阅 /sys/{productKey}/{deviceName}/thing/property/post_reply char sub_topic[128]; sprintf(sub_topic, "/sys/%s/%s/thing/property/post_reply", PRODUCT_KEY, DEVICE_NAME); char sub_cmd[256]; sprintf(sub_cmd, "AT+QMTSUB=0,1,\"%s\",1\r\n", sub_topic); HAL_UART_Transmit(&huart1, (uint8_t*)sub_cmd, strlen(sub_cmd), 1000);3.2.1 物模型属性上报的 JSON 格式规范
阿里云要求上报数据必须符合物模型定义的 JSON Schema。假设温湿度属性名为temperature和humidity,则上报 payload 为:
{ "method": "thing.event.property.post", "params": { "temperature": 25.3, "humidity": 65.2 }, "id": "12345" }其中id为任意数字字符串,用于去重和调试。BC20 发送命令为:
char pub_cmd[512]; sprintf(pub_cmd, "AT+QMTPUB=0,1,0,0,\"/sys/%s/%s/thing/property/post\",%d\r\n", PRODUCT_KEY, DEVICE_NAME, json_len); HAL_UART_Transmit(&huart1, (uint8_t*)pub_cmd, strlen(pub_cmd), 1000); // 等待 ">" HAL_UART_Transmit(&huart1, (uint8_t*)json_payload, json_len, 1000);注意:
AT+QMTPUB的第四个参数0表示 QoS=0(最多一次),阿里云物模型默认支持;若设为1,需处理+QMTRECV的 PUBACK 响应,增加复杂度。
4. 温湿度数据采集、低功耗调度与异常恢复机制
STM32L151 的典型应用场景是电池供电的远距离传感器节点,因此必须将功耗控制在微安级别。DHT22 为单总线器件,其读取过程耗时约 4ms,但若在 NB-IoT 附着期间读取,可能因电压波动导致数据错误。
4.1 DHT22 数据采集与校验
DHT22 的 40 位数据包含湿度整数/小数、温度整数/小数、校验和。关键点:
- STM32L151 的 GPIO 必须配置为开漏输出+上拉(10kΩ),因 DHT22 为集电极开路输出
- 启动信号需主机拉低 ≥ 18ms,再拉高 20–40μs
- 主机释放总线后,DHT22 拉低 80μs 作为响应,再拉高 80μs 表示准备就绪
// 使用 HAL_GPIO_WritePin 控制 DATA 引脚 HAL_GPIO_WritePin(DHT22_GPIO_Port, DHT22_Pin, GPIO_PIN_RESET); HAL_Delay(20); // 拉低 20ms HAL_GPIO_WritePin(DHT22_GPIO_Port, DHT22_Pin, GPIO_PIN_SET); delay_us(30); // 拉高 30μs // 切换为输入模式,等待 DHT22 响应 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT22_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(DHT22_GPIO_Port, &GPIO_InitStruct); // 检测 80μs 低电平响应(需用定时器捕获,此处简化为延时) delay_us(80); if (HAL_GPIO_ReadPin(DHT22_GPIO_Port, DHT22_Pin) == GPIO_PIN_RESET) { // 响应有效,开始读取 40 位数据 read_dht22_bits(); }4.1.1 数据校验与容错
DHT22 的校验和为前 4 字节之和(8 位和),若校验失败,应丢弃本次数据并重试(最多 3 次)。实测中,当电池电压低于 3.0V 时,DHT22 响应时序偏移,校验失败率升至 30%,此时需在read_dht22_bits()中加入HAL_GetTick()超时保护,避免死循环。
4.2 低功耗调度策略:PSM 模式下的周期上报
NB-IoT 的 PSM(Power Saving Mode)可将模组休眠电流降至 3.5μA,但需协调 STM32L151 的 STOP 模式。典型调度流程:
- STM32L151 从 STOP 模式唤醒(RTC Alarm)
- 初始化 BC20(若已断电则需重新附着)
- 读取 DHT22
- 构造 MQTT 报文并发送
- 等待
+QMTRECV确认上报成功 - 发送
AT+CFUN=0关闭 BC20 射频(保留串口) - STM32L151 进入 STOP 模式,等待下次 Alarm
// 进入 STOP 模式前关闭外设 __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // RTC Alarm 唤醒后,需重新初始化 RCC、GPIO、UART提示:BC20 的
AT+CFUN=0仅关闭射频,串口仍可通信;若完全断电,则每次唤醒需执行完整 AT 初始化序列(耗时约 8s),大幅增加功耗。
4.3 异常恢复:网络中断与 MQTT 重连
BC20 的+QMTSTAT事件会实时通知连接状态:
+QMTSTAT: 0,0:连接已断开(如信号丢失)+QMTSTAT: 0,1:连接成功+QMTSTAT: 0,2:连接被拒绝(需检查三元组或时间戳)
在 UART 中断中解析该事件后,触发重连:
if (strstr(uart_rx_buffer, "+QMTSTAT: 0,0")) { mqtt_reconnect_flag = 1; } // 主循环中检测标志位 if (mqtt_reconnect_flag) { mqtt_disconnect(); // AT+QMTDISC=0 delay_ms(1000); mqtt_connect(); // 重新执行 3.1 流程 mqtt_reconnect_flag = 0; }5. 验证 MQTT 上报是否被阿里云 IoT 平台正确接收的 3 种方法
仅凭 BC20 返回+QMTSTAT: 0,1不能证明数据已写入阿里云数据库。必须通过平台侧验证,以下是三种可落地的验证手段,按优先级排序:
5.1 查看物联网平台控制台的“设备日志”
登录 阿里云 IoT 平台控制台 → 选择对应实例 → 设备管理 → 找到目标设备 → 点击“查看日志”。关键日志项:
publish日志:显示/sys/{pk}/{dn}/thing/property/post的 QoS、Payload Size、Timestampsubscribe日志:确认/sys/{pk}/{dn}/thing/property/post_reply是否成功订阅error日志:若出现Invalid signature,说明三元组或时间戳错误;Device not found表示 ProductKey/DeviceName 不匹配
注意:日志默认保留 7 天,且需设备在线时才实时推送。若设备离线,日志可能延迟 1–2 分钟。
5.2 订阅post_replyTopic 解析平台响应
阿里云在收到thing.event.property.post后,会向post_replyTopic 发送 JSON 响应:
{ "code": 200, "data": {}, "id": "12345", "message": "success", "method": "thing.event.property.post.reply", "status": "success" }在 STM32L151 的parse_mqtt_event()中捕获该消息:
if (strstr(uart_rx_buffer, "/thing/property/post_reply")) { if (strstr(uart_rx_buffer, "\"code\":200")) { led_green_on(); // 上报成功指示 } else { led_red_blink(3); // 错误码非200,需排查 } }5.2.1 常见 reply code 含义表
| code | 含义 | 排查方向 |
|---|---|---|
| 200 | 成功 | 无需操作 |
| 460 | 设备未激活 | 检查控制台设备状态是否为“已激活” |
| 425 | 请求频率超限 | 阿里云默认 100 次/分钟,降低上报间隔 |
| 401 | 签名无效 | 检查 DeviceSecret 解码、timestamp 误差、HMAC 输入字符串拼接 |
| 400 | JSON 格式错误 | 检查params中字段名是否与物模型定义完全一致(大小写敏感) |
5.3 使用 MQTT.fx 工具模拟设备行为进行交叉验证
当嵌入式端上报失败时,可用 PC 端工具排除平台配置问题:
- 下载 MQTT.fx
- Broker Address 填
iot-as-mqtt.cn-shanghai.aliyuncs.com - Port 填
8883,勾选 SSL/TLS - Client ID、Username、Password 按 3.1 节规则生成(可用 Python 脚本计算)
- 连接成功后,手动 Publish 到
/sys/{pk}/{dn}/thing/property/post,观察控制台日志是否出现
若 MQTT.fx 可成功上报,说明问题一定在 STM32L151 的 AT 指令序列、JSON 构造或时间戳同步上;若 MQTT.fx 也失败,则检查阿里云产品、设备、物模型的配置一致性。
最后一步,确保 DHT22 的temperature和humidity字段类型与物模型中定义的数据类型(float/double)完全匹配——若物模型定义为double,而上报 JSON 中写"temperature": 25(整数),阿里云会静默丢弃该字段,日志中无任何错误提示。
本文还有配套的精品资源,点击获取