简介:STM32F030多点温度采集系统源码包,为物联网嵌入式开发人员提供一套基于DHT20传感器与移远BC260Y-CN NB-IoT模块的完整代码实现,主要解决低功耗环境下多点温度数据采集、本地显示与云平台接入问题。压缩包共24个文件,包括10个C源文件、10个头文件、2个MD说明文档,以及gitignore、inscode配置,整体约40KB。C源文件覆盖主程序、DHT20驱动、BC26通信、定时器、蜂鸣器报警、硬件初始化等模块,头文件接口清晰,便于阅读和二次开发。源码包内目录划分清楚,C文件与头文件一一对应,可快速定位功能模块;两个MD文档对硬件选型、NB-IoT原理及OneNet平台配置等做了梳理,降低了上手门槛。已有39人学习。开发者可结合源码理解NB-IoT与OneNet平台的MQTT接入流程,掌握OLED本地显示、报警逻辑以及系统时钟配置等关键设计,适合正在学习嵌入式物联网或准备相关比赛、毕业设计的读者参考借鉴。
1. 用 F030 搭多点温度采集:这套代码包解决的不只是测温
做某工控项目时被一个需求卡过:一条产线上要布十几个温度点,预算只给到一片几块钱的 MCU,还要把数据一并汇总上传。STM32F030 就是这个场景下的典型选择,M0 内核、主频 48MHz、资源刚好够用,而多点温度采集系统的关键难点不在 ADC,而在总线时序和 ROM 管理——这套代码包把这两块都做完整了。适合正在做温度采集、传感器网络、低成本改造的嵌入式开发者,也适合刚入门想看懂 1-Wire 协议和器件驱动怎么写的人。它解决的是从“单点测温”到“多点组网”之间那段没人帮你垫平的落差。
2. 方案选型与代码包结构:F030 为什么够用,DS18B20 为什么合适
2.1 主控选型逻辑:为什么不是 F103 也不是 8 位机
先聊一个常见选择误区:有人做多点测温第一反应是上 F103 甚至 F407。实际上温度传感器是低速率器件,单个 12 位温度转换也就 750ms 级别,十几个点轮询一次也不到 2 秒,48MHz 的 F030 处理起来余量非常大,而且它的 FLASH 容量从 16KB 起跳,跑完整套 1-Wire 协议栈加协议解析加命令分发完全够用。选 F030 的核心理由是性价比和封装:TSSOP20 的时候可以做得很小,BOM 成本压得低,这是量产项目里非常实际的考量。
另一个选择是 8 位机如 STM8,但 F030 的优势在于调试工具链统一——用 ST-Link 就能覆盖整条产品线的调试,不需要额外买烧录器,而且库函数与 F1/F4 系一脉相承,后续要升级主控时迁移成本低。这套代码包里已经包含了标准外设库的初始化文件,拿到就是一个完整的工程,不需要去官方例程里翻找再拼装。
2.2 传感器选型:DS18B20 的天然优势与局限
温度点比较多的时候,常用的传感器无非三类:热电偶、NTC 热敏电阻、DS18B20。热电偶要配冷端补偿和运放电路,成本高;NTC 便宜但要每一路单独校准、单独接 ADC,引线一长就受干扰。DS18B20 在这个场景里胜出靠的是三点:单总线协议简化了走线(数据线一拉到底)、数字输出免去了 ADC 校准、温度值直接以 12 位补码读出。
但这玩意儿有局限,代码包里也做了对应处理:单总线上每个器件有个 64 位 ROM 序列号,多点采集必须通过搜索算法枚举所有在线的器件地址;器件对时序非常敏感,读时序和写时序的延时窗口以微秒计,这些在代码里都已经调过一轮。温度测量范围是 -55°C 到 +125°C,普通工业环境够用,所以这套方案的适用边界就在“常规温度、多点、低成本”这个三角区域内。
2.3 代码包文件结构:每个文件管什么
工程目录大约是这么组织的,拿到手上先对着确认一遍:
| 文件/目录 | 职责 | 关键内容 |
|---|---|---|
Inc/ | 头文件集合 | 定时器配置、GPIO 映射、DS18B20 驱动声明 |
Src/main.c | 主流程 | 初始化顺序、轮询调度、串口打印 |
Src/ds18b20.c | 单总线驱动 | 复位、读写时序、ROM 操作 |
Src/onewire_search.c | 多点搜索算法 | ROM 搜索、CRC 校验 |
Src/delay.c | 微秒延时 | 基于 SysTick 的延时适配 |
MDK-ARM/ | Keil 工程 | 芯片型号 STM32F030C8T6,已配置时钟树 |
代码包的区分度在于它把“单点驱动”和“多点管理”分开写了:ds18b20.c只管操作一个器件,onewire_search.c负责在总线上枚举所有器件编号。很多网上代码把这两层混在一起,改起来牵一发动全身,这套分层在后续加功能时会省心很多。
3. 单总线时序与多点轮询核心逻辑:从物理层往上拆
3.1 复位与存在检测时序的实现
DS18B20 的一切操作从复位开始。主控拉低总线至少 480μs 后释放,器件检测到上升沿会等待 15-60μs,然后拉低总线 60-240μs 作为存在脉冲。这里有个容易翻车的地方:标准里写 480μs 只是下限,实际过长反而可能让器件误判,所以代码里用的是 600μs 拉低加 40μs 等待。
uint8_t ds18b20_reset(void) { uint8_t presence = 0; DS18B20_DATA_LOW(); // 拉低总线 delay_us(600); // 复位脉冲宽度,600us 比标准下限留了裕量 DS18B20_DATA_HIGH(); // 释放总线,进入接收模式 delay_us(40); // 等器件响应窗口 DS18B20_SET_INPUT(); // 切换为输入模式,读取电平 presence = DS18B20_READ_PIN(); // 读取存在脉冲:0 表示器件在位 DS18B20_SET_OUTPUT(); // 恢复输出模式,避免影响后续时序 delay_us(400); // 存在脉冲结束后等待恢复 return presence; // 返回 0 表示检测到器件 }代码逻辑是按协议标准拆开的四步:先拉低产生复位脉冲,再释放总线拾取器件的存在回应,然后立刻把引脚切回输入读取电平,最后切回输出。DS18B20_SET_INPUT()和DS18B20_SET_OUTPUT()是驱动里封装好的 GPIO 模式切换,这个细节很关键——如果一直保持推挽输出,器件根本拉不低总线,你会看到复位永远失败。
参数说明:复位脉冲 600μs、等待窗口 40μs 是代码里的默认值。F0 系列 GPIO 翻转速度快,延时函数如果基于 48MHz 主频跑,这两个值是不需要动的。注意如果你的板子用了外部 8MHz 晶振倍频到 48MHz,延时基准变了,要按新主频重算delay_us里的循环次数。
3.2 读写时序窗口怎么卡才稳
单总线通信全靠时序窗口区分 0 和 1,读操作尤其容易出问题。代码里写的读时序是:拉低总线 6μs 后释放,在 15μs 内采样引脚电平。这个“采样点”是整个驱动里最玄学的地方——采样太早会读到主控自己拉低的电平,太晚器件可能已经开始发下一位了。
uint8_t ds18b20_read_bit(void) { uint8_t bit = 0; DS18B20_DATA_LOW(); delay_us(6); // 启动读时序的拉低时间 DS18B20_DATA_HIGH(); DS18B20_SET_INPUT(); // 读时序必须切输入模式,否则读不到器件电平 delay_us(9); // 采样点:在 15us 窗口的中间偏前位置采样 bit = DS18B20_READ_PIN(); // 电平为高读 1,为低读 0 DS18B20_SET_OUTPUT(); delay_us(50); // 剩余时间片补满,保证总线恢复 return bit; }这里delay_us(9)是经过实测调出来的值,它决定了采样点位置。改这段代码时有个经验:不采样太早、不采样太晚,两个极端都会导致读出来的数据出现“全是 0xFF”或者“前几位漂移”的怪现象。参数上,6μs 拉低对应数据手册规范,50μs 收尾是为了位时间内让总线有充分恢复,相邻位之间不留足够恢复时间,连续读取时数据会错位。
3.3 ROM 搜索:多点采集的第一步是“数人头”
单总线上挂多个器件时,必须先做 ROM 搜索,把所有器件的 64 位序列号枚举出来。这套代码采用标准的三字节搜索流程:主控逐位发“读 ROM 位”请求,再发“写 0/写 1 选择位”决定走向哪个分支。当总线上多位器件在某一位上出现 0 和 1 分叉时,就需要记录分歧点并回溯。
核心片段:
uint8_t ow_search(uint64_t *rom_codes, uint8_t max_devices) { uint8_t count = 0; int16_t last_discrepancy = -1; uint64_t current_rom = 0; int16_t discrepancy = -1; while (count < max_devices) { uint8_t bit_index; uint8_t dir = 0; discrepancy = -1; current_rom = 0; ds18b20_reset(); ds18b20_write_byte(0xF0); // 搜索 ROM 命令 for (bit_index = 0; bit_index < 64; bit_index++) { uint8_t bitA = ds18b20_read_bit(); // 读原码 uint8_t bitB = ds18b20_read_bit(); // 读反码 if (bitA == 1 && bitB == 1) { return count; // 无器件响应,搜索结束 } if (bitA != bitB) { dir = bitA ? 1 : 0; // 该位无分叉,直接确定方向 } else { if (bit_index < last_discrepancy) { dir = (current_rom >> bit_index) & 0x01; } else if (bit_index == last_discrepancy) { dir = 1; // 上次分歧位,本次走 1 分支 } else { dir = 0; // 新分歧,先走 0 分支 discrepancy = bit_index; } } // 写入选择位,同时拼装当前 ROM 值 ds18b20_write_bit(dir); if (dir) current_rom |= ((uint64_t)1 << bit_index); } rom_codes[count++] = current_rom; last_discrepancy = discrepancy; if (discrepancy == -1) break; } return count; }逻辑说明:这个函数把标准搜索算法拆成了三个状态判断。bitA和bitB分别代表总线上所有器件的原码与反码采样结果;两者都为 1 意味着总线上没有器件(线路短路或断开);两者不同说明该位所有器件一致;两者相同则说明该位存在分歧点。通过维护last_discrepancy记录上一次搜索走到的分歧位置,搜索完一个器件后回到最近的分歧点走另一条分支,就能穷举所有 ROM 编号。
参数说明:max_devices限制最多枚举多少个,建议按硬件实际挂载数加 2 设上限,避免总线上有杂质或虚焊时搜索死循环。rom_codes是 64 位数组,每项一个器件序列号,后续读温度时用匹配 ROM 命令直接寻址。
4. 移植与工程集成:改三个地方就能在自己的板子上跑
4.1 引脚映射的修改方式
代码包默认把数据线放在 PA4,这个选择是有讲究的:PA4 在 F030 上没有额外的复用功能冲突,方便独立控制方向。要改引脚只需要动ds18b20.c头文件顶部的宏定义。
// 修改引脚映射,以 PA6 为例 #define DS18B20_GPIO_PORT GPIOA #define DS18B20_GPIO_PIN GPIO_PIN_6 #define DS18B20_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() #define DS18B20_DATA_LOW() HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_RESET) #define DS18B20_DATA_HIGH() HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_SET) #define DS18B20_READ_PIN() HAL_GPIO_ReadPin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN)注意改引脚时需同步调整DS18B20_SET_INPUT()和DS18B20_SET_OUTPUT()里的 GPIO 模式设置,宏只覆盖了电平操作和读取,方向切换在驱动函数里用HAL_GPIO_Init重新配置。这里有个新手常犯的错:只改了端口和引脚号,没改DS18B20_GPIO_CLK_ENABLE(),结果引脚永远没时钟,读回来的数据全是 1。
4.2 延时函数与主频解耦
代码包自带一套基于 SysTick 的delay_us,它直接读 SysTick 计数器的当前值做递减,不依赖中断。移植到其他主频时要改一个宏定义:
// 48MHz 主频下的延时基准:1us 对应 48 个计数 // 如果改到 72MHz 主频,把这里的 48 改成 72 #define SYSTICK_PER_US 48修改完成后,最好用逻辑分析仪实测一下 10μs 延时的实际宽度。以前帮某开发者排查过一个问题:他用了外部 16MHz 晶振然后倍频到 64MHz,但忘了改SYSTICK_PER_US,导致所有时序都偏快约 25%,表现是单个器件能读到温度但挂第二个器件后随机丢数据——这就是时序漂移在多点拓扑下被放大的典型案例。
4.3 工程选项上的三个注意
第一,编译优化等级建议从-O0开始调试,确认逻辑正确后再改-O2。优化等级对微秒级延时的指令周期影响很大,delay_us里的空循环如果被优化掉,复位时序直接失效。第二,F030 的 GPIO 输出速度设置为GPIO_SPEED_FREQ_LOW,单总线速率本身只有几十 kbps,低速档位能减小过冲。第三,开漏输出模式不是必须的,只要在推挽模式下手动切换输入输出即可,但如果你同时接了外部上拉电阻,建议直接用开漏模式加 4.7k 上拉,总线冲突风险更小。
4.4 主循环调度写法:轮询与超时保护
while (1) { // 触发一次温度转换,参数 1 表示非寄生供电 ds18b20_start_conversion(1); delay_ms(5); // 转换启动时间 for (uint8_t i = 0; i < device_count; i++) { int16_t temp = ds18b20_read_temp(&rom_codes[i]); if (temp == DS18B20_READ_ERROR) { // 打印掉线提示,但不阻塞后续通道 continue; } printf("CH%d: %.2f\r\n", i, temp / 16.0f); } delay_ms(900); // 等待 12 位转换完成(750ms 典型值) }主调度逻辑很简单:先广播转换命令让所有器件同时开始转换,等 750ms 后再按 ROM 编号逐个读取。这里有个小技巧——转换命令是广播的,不需要每个器件单独发,这样 10 个点也只消耗一次转换等待时间。读温度时单点失败先记录下来继续走后续点,不要一封掉线就拦停整轮采集,工程上这叫“降级不宕机”。
5. 常见问题与避坑记录:多点采集最容易踩的六个坑
5.1 复位总是不成功,示波器看波形是对的
现象:复位函数返回失败概率很高,特别是温度变化时,有时能扫到器件有时扫不到。
原因:排查后发现板子上拉了 2kΩ 上拉电阻。DS18B20 规范建议上拉电阻 4.7kΩ,阻值太小会导致器件拉低总线时电流过大,电平被拉到接近 0V 但恢复时间变慢。更典型的是阻值太大,比如用了 10kΩ,总线恢复沿变缓,时序窗口偏移。
解决:换成 4.7kΩ 上拉到 3.3V,所有异常消失。注意 F030 的 GPIO 输入阈值是 TTL 电平,不是 5V 器件,上拉必须接 3.3V 而不是 VDD 5V。
5.2 单个器件正常,挂第二个器件后数据随机错乱
现象:第一路温度稳定,第二路接上后两路数据偶尔互换或出现 -55°C。
原因:这是典型的 ROM 搜索出错。两个器件在 64 位 ROM 上某些位存在分歧,搜索算法如果没有正确维护last_discrepancy状态,第二次搜索就从错误的分支继续,导致拿到的 ROM 编号不匹配实际器件。
解决:重新检查ow_search里分歧位判断逻辑。一个实操经验是把搜索到的 ROM 编号打印出来比对,如果发现第二路的编号是“分叉点没回溯”产生的错误地址,确认last_discrepancy在每次结束时要更新为当前循环的discrepancy,而不是沿用初始值。
5.3 数据连续读三遍才稳定,第一遍总有一路是残值
现象:开机第一次轮询总会有一个通道读到 85°C,之后正常。
原因:这是 DS18B20 的上电默认值。器件上电后温度寄存器复位为 0x0550,对应的就是 +85°C,这是数据手册明确写的初始化状态,不是硬件故障。直接读温度而不发起转换时,读到这个值是正常的。
解决:启动后先发一次广播转换命令,等待 800ms 再进入正常轮询循环,把“预热轮询”纳入初始化流程。或者代码里把 85°C 当作无效数据过滤掉,但这个方法不严谨——环境温度真在 85°C 时会被误滤。
5.4 引线超过 1 米后偶尔误码,总线挂不住多个器件
现象:传感器线从 30cm 延长到 2 米,原来稳定的一主三从开始随机丢点。
原因:总线电容变大,沿变缓,加上没有终端匹配。DS18B20 对总线上升沿的容忍度有限,远距离传输时信号质量明显下降。
解决:线长超过 1 米建议用屏蔽双绞线,屏蔽层单端接地;总线两端各加一个 4.7kΩ 上拉(不是并联,是分别在物理两端);软件上把读取采样点从 9μs 调整到 12μs,给信号多留点稳定时间。代码包里这个采样点抽出来单独定义,就是为了这种场景能快速调。
5.5 F030 进入 STOP 模式后唤醒,总线上器件全部丢失
现象:低功耗模式休眠后唤醒,搜索到的器件数量从 6 变成 1。
原因:STOP 模式会把 GPIO 和内部时钟全部停掉,总线电平悬空,器件可能进入一种异常状态。唤醒后没有先做一次完整复位就继续轮询,器件状态机已经错乱。
解决:唤醒流程里加一次“总线复位循环”——连续做三次ds18b20_reset(),间隔 10ms,确保总线上所有器件状态归零后再搜索。这是从低功耗产品上学到的经验,多点场景下唤醒后的复位次数比单点要多两次才保险。
5.6 时序函数在优化等级切换后失效
现象:Debug 模式一切正常,Release 模式(-O2)下复位和搜索全部失败。
原因:delay_us里的空循环被编译器优化掉一部分,函数整体延时不达标。另一个问题是编译器对寄存器变量的处理影响 GPIO 交替翻转速度。
解决:delay_us函数里的延时变量用volatile修饰,防止被优化;两个忙等循环之间不要做多余内存访问;如果改优化等级后仍然失败,用示波器测量实际延时并校准SYSTICK_PER_US的取值。
6. 多点采集的进阶用法:掉线自愈与批量数据校验
这套代码包再往上走,有四个直接能用的扩展方向。
第一个是掉线自愈轮询。单总线上的器件接触不良是常态,尤其在振动环境下。我给代码加了一个“健康位图”数组,每一轮搜索成功后更新位图:连续 3 轮都枚举不到的器件标记为掉线,不再参与轮询;掉线器件恢复后自动重新入队。这个机制让整个采集系统在传感器接触不良时不会丢一路数据就停摆。
第二个是批量数据校验。一个轮询周期里读完所有温度后,对所有通道做一个异或校验和,与上次周期比较,如果校验和突变但温度变化率小于 0.5°C 就判定为总线干扰,丢弃该轮数据再补采一轮。这个方法能有效滤掉 2 米长线场景里的偶发抖动,比单纯滤波更干净。
第三个是低功耗定时采集。单纯把主频降到 8MHz 不够,要让 MCU 在两次温度采集之间进 STOP 模式。唤醒前用 RTC 定时器打点,唤醒后先做三次总线复位再搜索,我实测过一轮 10 点采集加 5 秒休眠的平均电流在 120μA 左右,比一直轮询省了一个量级。
第四个是地址表自动错位检测。多点系统的隐藏风险是某一路传感器被换成了同一型号的备用件后,ROM 编号变了,采集程序还按旧地址匹配,导致该通道读回的是另一个位置的温度。可以在每次搜索后对比 ROM 表,发现新增 ROM 时打印警告并重新映射通道号。
这套代码包给我最大的教训在“时序参数不能用一级优化跑”:某次为了赶项目把优化等级全开,结果温度数据在 75°C 附近出现固定偏差,排查了两天才发现是delay_us里的延时循环被优化掉了一部分。从那以后我每次改完时序相关代码,都强制在-O2下完整跑一遍 ROM 搜索验证。
多点温度采集的坑非常分散,硬件、协议、固件三层都有。希望这套拆解能帮你少走一轮冤枉路。
本文还有配套的精品资源,点击获取