拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BC26 NB-IoT室温采集插座固件开发实战:从架构到量产踩坑记录

BC26 NB-IoT室温采集插座固件开发实战:从架构到量产踩坑记录

从立项到今天正式进入量产,这个 BC26 NB 室温采集插座终于让我有底气写点东西了。项目本身不复杂,但涉及的链路很长:温度采集、NB 模组控制、低功耗调度、云端通信、产测校准,每一环都会在量产阶段变成新的麻烦。今天不聊市场也不聊外壳,专门讲讲这套固件的源代码是怎么组织的,以及开发过程中那些差点让我放弃的插曲。

这篇内容适合正在做 NB-IoT 产品、准备量产、或者想了解 BC26 模组实际工程用法的朋友。我会把代码设计思路、关键实现、踩坑记录都摊开来说,尽量少用行业黑话,遇到必要概念我会解释到能直接上手为止。

1. 固件整体架构:把插座拆成“会睡觉的采集终端”来设计

1.1 这个插座本质上是什么

你可以把 BC26 NB 室温采集插座理解成三样东西的组合:一个能通断电路的继电器模块、一个高精度温度传感器、以及一个能定时把温度数据送到云端的 NB 通信模组。

当时我们定的功能基线很简单:插座每 30 分钟上报一次室温,支持远程通断电,空闲时进入低功耗状态,电池供电情况下能撑一年以上。这个目标直接决定了软件架构的写法——一切都围绕“活下来、按时醒、说清楚”展开。

在业务逻辑上,固件采用的是“唤醒-采集-上报-休眠”循环模型。正常工作时长只占几秒,其余时间处理器和 BC26 模组都必须处于低功耗状态。源代码里最核心的就是管理这个状态迁移的调度器。

1.2 状态机:让不可控的网络变成可控的程序流程

NB-IoT 网络最大的特点就是不可控。信号可能时好时坏,基站可能暂时拒绝附着,核心网可能在半夜升级,这些都会导致指令卡死。如果代码是顺序执行的,一条 AT 指令等了 20 秒没回复,整个设备就废了。

所以我第一件事就是把固件写成状态机,核心状态只有四个:INIT、WAKE、JOIN、SLEEP,外加一个ERROR兜底。

typedef enum { STATE_INIT = 0, STATE_WAKE, STATE_JOIN, STATE_SLEEP, STATE_ERROR } system_state_t;

一开始可能觉得没必要,一个插座而已,干嘛搞这么复杂?但当你发现设备部署后经常失联,回来查日志才发现程序死等一条 AT 指令时,就会后悔没早点上状态机。状态机的价值在于:每一个等待都有超时,每一个超时都有去处。这比任何调试技巧都重要。

1.3 目录结构:量产固件需要“可维护”而不是“能跑”

很多工程师在原型阶段喜欢把所有代码堆在一个 main.c 里,几百行函数上下翻,开发确实很快。但我们这套固件从第一天起就按功能分目录,原因很简单:到量产阶段你会面临产测改代码、客户定制改代码、测试反馈改代码,一个人维护一堆 3000 行的 C 文件是灾难。

我们的源码结构大致如下:

app/ ├── main.c ├── state_machine.c ├── business/ │ ├── measure.c │ └── report.c └── drivers/ ├── bc26_uart.c ├── bc26_at.c ├── ntc_adc.c └── flash_param.c

business层放业务逻辑,比如多久采一次温、数据怎么拼包;drivers层放外设驱动,比如 BC26 的 AT 指令交互、ADC 读取。业务不直接操作寄存器,驱动也不管业务逻辑。这样改上报协议时不必碰传感器代码,换传感器时不会动 NB 通信。实测下来,分工清楚以后 Bug 定位速度快了不止一倍。

2. BC26 通信模块源码:一条温度数据从设备到云端的完整链路

2.1 BC26 的初始化与网络附着流程

BC26 是移远的 NB-IoT 模组,采用 AT 指令控制。固件上电后第一件事不是立刻上报,而是要给模组留出启动时间,然后开始初始化流程。

实际工程中我封装了一个bc26_at_send()核心函数,所有指令都走它:发送数据、等待应答、超时重试。

int bc26_at_send(const char *cmd, const char *expect, uint32_t timeout_ms) { char buf[256] = {0}; uint32_t elapsed = 0; uart_flush_rx(); uart_send_str(cmd); while (elapsed < timeout_ms) { if (uart_rx_line(buf, sizeof(buf)) > 0) { if (strstr(buf, expect) != NULL) return 0; } delay_ms(10); elapsed += 10; } return -1; }

这里有个关键点:不能傻等一条指令发完再发下一条,而是每发一条就要确认回复符合预期。例如初始化流程我们用的指令序列如下:

步骤AT 指令预期应答说明
1ATOK通信握手
2AT+CFUN=1OK打开射频功能
3AT+CGATT?+CGATT:1查询是否附着网络
4AT+CEREG?+CEREG:1查询是否注册成功
5AT+CSQ+CSQ:xx查询信号强度
6AT+COPS?+COPS:0确认运营商网络

BC26 的启动和附着不是瞬间完成的,尤其在地下室或者弱信号环境里,附着可能耗时十几秒。所以初始化必须有耐心:失败就把状态机踢回到重试通道,重试次数超过设定值再进入ERROR状态。

2.2 数据上报:为什么用 UDP 而不是 TCP

NB-IoT 传数据的方式有很多种,但我们最终选的是 UDP + CoAP 格式:BC26 内部本身有 CoAP 协议栈,直接用指令AT+NMGR和AT+NQMGR等管理数据,比较省电。要知道 TCP 在高延迟的 NB 网络里建立连接太昂贵,每次上报几秒钟的功耗都比传感器采集大得多,非必要不要用 TCP。

实际代码里我们做了一个简洁的报文封装:统一上报温湿度、电量、信号值、设备状态。每条 payload 只有 20 字节左右。

typedef struct { uint8_t cmd; uint32_t temperature_x100; uint32_t voltage_mv; int8_t signal_rsrp; uint8_t flags; } report_t;

拼包时有个值得分享的教训:NB-IoT 网络传输速度慢、单位流量贵,所以字段能压缩就压缩。温度保留两位小数,我们直接存temperature_x100,用整数 2501 表示 25.01℃,省掉了浮点数传输。这条看起来土气的优化,让整个模块的报文长度缩短了 30%,低功耗效果显著。

2.3 掉网重连:藏在源代码里最耗费精力的部分

量产之后你会发现,插座安装位置的信号千差万别:有的在弱电箱里,有的在铁皮电表箱后面。BC26 偶尔会掉网,这时如果不去查模块状态,数据就一直静默。

我们为了让系统够健壮,每次上报前都会先检查状态:

if (bc26_check_registration() != 0) { state_machine_set(STATE_JOIN); return; }

然后在STATE_JOIN状态里,反复执行AT+CGATT=1,并设置重试上限。这样做之后,设备基本做到“只要网络恢复,两分钟内必定自动回网并补报数据”。我见过不少项目死在这上面:模组掉网后不复位、重连,产品就直接变砖,这也是量产设备的常见死法。

3. 室温采集代码:不是读个 ADC 这么简单

3.1 热敏电阻采样与查表法

我们这个插座用的是 NTC 热敏电阻做温度采集,因为它便宜、稳定、够用。电路上就是一个 NTC 跟 10kΩ 精密电阻分压,然后进 MCU 的 ADC。ADC 采样值首先换算成电压,然后反推 NTC 电阻阻值,再通过温度阻值表查温度。

NTC 的阻值和温度不是线性关系,所以我提前做了一张 0℃ 到 60℃ 的电阻-温度对照表。代码不用算数学公式,直接线性插值:

float ntc_to_temperature(uint16_t adc_value) { uint32_t rntc = resistance_from_adc(adc_value); // 查表并做线性插值,返回温度,单位 ℃ return table_lookup(rntc); }

有一个细节要注意:NTC 的供电最好用 MCU 引脚控制,只有采集时才给传感器供电,采完立刻断电。这样避免了传感器持续耗电。代码里就是一组 GPIO 操作,但省下的电量非常可观。

3.2 软件滤波与多点多批校准

室温不是恒定不变的,空调开一下,温度波动半度都很正常。但云端如果看到数据一直在 25.12℃、25.13℃、25.09℃ 这样跳,用户体验会很差。我们做了两个处理:

  • 连续采 5 次,去掉最大值和最小值,取中间 3 次的平均值;
  • 叠加一阶低通滤波,让温度变化曲线平滑。

代码上大概是这个模式:

float smooth_temp = 0.0f; #define ALPHA 0.3f smooth_temp = ALPHA * current_temp + (1 - ALPHA) * smooth_temp;

滤波会让响应变慢,但室温本身就属于慢变量,慢一点反而更真实。真正需要看重量的还是产测校准,我们的做法是:产线用恒温槽设置 25.0℃,读取采集读数,生成两个校准系数存进 Flash。量产设备全部过一遍,才能保证“每台设备温度差都在 ±0.5℃ 以内”。

3.3 把插座自身发热从数据里抠掉

这是开发中最开始没料到的坑。插座通电后,继电器线圈、电源模块、甚至 MCU 自身都会发热。如果传感器离这些发热源太近,采集到的温度可能比真实室温高两三度。

解决思路有两个:硬件上尽量把 NTC 放到插座边缘,远离继电器;软件上做“待机补偿校准”,在不通电情况下采一批基线数据,并测试通电后的温度偏差,写进算法里。这个没办法给通用公式,因为外壳、结构、布局都影响热阻,但我建议每个项目都用实测数据建一次热补偿模型。我们到最后测下来,通电 2 小时后的热偏差能被补偿到 0.3℃ 以内。

4. 量产固件里的那些“看不见的细节”:低功耗、Flash 存储与看门狗

4.1 低功耗的矛盾:BC26 待机电流与 PSM 模式

BC26 模组本身支持 PSM(Power Saving Mode,省电模式)和 eDRX(扩展非连续接收)。PSM 模式下模组几乎“假死”,电流降到微安级别,但代价是云端无法随时下行控制它。我们的插座本来就不是强交互设备,所以大胆开了 PSM。

固件里设置 PSM 的指令类似:

AT+CPSMS=1, , , "00100001", "00000011"

含义是允许 PSM,并且设置了 T3324 定时器。不熟悉 AT 指令的朋友先不用背参数,重点是知道原理:PSM 模式下,模组仍然附着网络,但进入休眠,只在 TAU(跟踪区更新)时间到才醒一下。

这里最怕的是云端的“远程控制”需求。如果你要做实时响应的智能插座,PSM 会害死你,因为下行数据根本推不进来。我们的解决方式是:上报间隔刚好是 30 分钟,用户远程控制时,设备会在下一次上报后执行动作,实时性要求高的功能全部砍掉。

4.2 Flash 存储:固件如何记住自己的“身份”

量产固件必须处理掉电保存。设备重启后要记得自己是哪个型号、批次号是多少、上报过哪些数据。这些数据和普通变量不一样,必须写进 Flash。

我用 Flash 分了两块区域:

区域存储内容
参数区设备 ID、批次号、校准系数、上报间隔
日志区最近 10 次运行状态、错误码、信号值

Flash 写入次数有限制,所以不能每次上报都写日志。我们的策略是只在状态异常时写日志,正常运行最多 2 小时写一次参数。这样做既保留了现场信息,又不会把 Flash 写穿。

4.3 看门狗:量产设备最后的保命符

开发时你可能觉得主频高、逻辑简单,死循环不至于。量产之后各种极限情况就来了:弱信号导致模组频繁重连,UART 缓冲区溢出,甚至一次静电干扰让 MCU 跑飞。没有看门狗的设备出问题后只能断电恢复,这绝对不行。

我强烈推荐双保险:内部看门狗(IWDG)负责 MCU 级死循环,外部看门狗(如果有)负责模组级异常。在源代码里,喂狗的位置也很讲究:不能在主循环随意喂,最好在“完成一轮正常调度”后喂。如果状态机卡在某个状态,狗就会超时复位,让设备重新初始化。

4.4 软件升级预留:哪怕现在用不上

量产固件上通常都有 OTA 的影子。也许你现在觉得没必要,但设备卖出去后如果发现传感器校准算法有 Bug,总不能全量召回。BC26 模组本身支持通过 NB 网络下载固件,但通道带宽很窄,所以我们把方案简化为“双区备份 + 差分升级”:固件运行时存在 A 区,升级包写入 B 区,校验通过后切换启动。

代价是 Flash 占用翻倍,但可靠性和可维护性值回票价。源代码里预留了一个fota.c的空实现,以后接平台时可以快速填坑。

5. 开发小插曲:从熬夜到量产,那些差点让人崩溃的 Bug

5.1 插曲一:PSM 唤醒后 UART 丢数据

设备进入 PSM 后,BC26 的串口不再主动发数据。醒来后如果 MCU 立刻发 AT 指令,模组可能还没来得及把串口唤醒,导致发送的数据丢失。

我们第一次遇到时,设备表现为“上报成功率只有 70%”,但手动调试时一切都正常。后来定位到正是唤醒瞬间串口时钟未稳,发送太快。修复方法很简单,模组唤醒后加 50ms 延时,再发第一条指令。代码里就是这个改动,让成功率一下子回到 99.9%。

这个小问题提醒我:模组与 MCU 的时序也是代码的一部分,不能只看逻辑,还要看信号时序。

5.2 插曲二:批量部署时一半设备不上报

小批量验证时一切正常,但第一批 500 个设备装出去,后台发现约一半没上报。查日志发现这些设备信号强度很差,有的根本没附着网络。

最初我们以为这是信号覆盖问题,让运营商的工程师跑了几趟基站,后来才发现问题出在出厂程序上:产测时模组附着网络成功一次后,固件把网络状态存进 Flash,用户使用时直接读取“已附着”状态,导致真正的初始化流程被跳过。这样只要设备挪了地方、网络参数变了,它就用错误的状态去上报。

解法也直接:每次上电都强制重新附着,不信任任何存储下来的网络状态。这个经验也许不算高级,但它挽救了一大批设备。

5.3 插曲三:一个 printf 引发的功耗灾难

原型开发时,为了调试方便,我们在代码里加了很多串口打印。一切正常,直到测功耗时傻眼:设备电流比预想高了十几倍。

原因是改造之后打印模块忘了关掉,MCU 每次上报都在通过串口输出几百字节日志,长时间占用 UART 和 Flash,导致睡眠状态也无法进入。解决办法是加了一个调试宏:

#ifdef DEBUG_ENABLE printf("[TEMP] %d.%02d ℃\n", ...); #endif

量产固件编译时直接关掉DEBUG_ENABLE。这是老生常谈,但越是赶进度越容易忘。

5.4 插曲四:云端收到的“重复数据”与设备时间

最后说说数据幂等的问题。NB 网络不稳定时,我们的 CoAP 报文可能重发,云端会收到重复数据。要解决这个问题,上报报文里必须带一个递增的序列号或者时间戳,云端按“设备ID+序列号”去重。

设备本身没有 RTC,或者 RTC 不准,怎么办?我们的做法是:每次上报成功后,从云端回复里获取服务器时间,校准本地 RTC。代码里就是一条AT+NTP指令的事,但很多小项目会忘记做,结果数据时间错乱,后台图表惨不忍睹。

6. 尾声:量产源代码的取舍之道

如果你问我这套源码最值得分享的是什么,我会说是“克制”。量产代码不追求炫技,它追求的是可预测:每个函数都有明确的输入输出,每个状态都有超时退出,每个外设都有错误的兜底。开发阶段多花点时间把状态机设计清楚,把打印口控制住,把 Flash 读写策略理顺,量产后你的命会好很多。

BC26 这款模组本身能力有限,但配合 NB 网络,做低功耗、小流量、低频率的采集场景非常合适。室温采集插座只是其中一个例子,同样的代码骨架,你换成水浸传感器、烟感、地磁检测器,只需要改业务层即可。

最后再给一个很实用的小建议:量产固件在合入代码前,一定要留出至少一周的“老化测试时间”,在弱信号环境、高温环境、电压波动环境下持续跑,所有日志都保留。你真不知道哪一天某个莫名其妙的掉线 bug,会靠这一周的老化数据才暴露出来。祝大家的量产路都比我们顺利。

返回列表