STM32F030驱动DS18B20与TM1637及USART温度上报
简介:围绕STM32F030R8T6平台展开的USART通信、DS18B20测温与TM1637数码管显示联合例程,面向已掌握GPIO、定时器等基础外设、希望打通传感器采集与串口调试链路的嵌入式学习者。例程中TM1637负责显示两位整数温度,小数部分通过USART按格式上传,便于观察DS18B20在0.5℃测量精度与0.0625℃最高分辨率下的数据变化,可解决温度采集、数码管驱动和串口格式化输出之间的联调问题。压缩包共866个文件,约6.54MB,包含.c/.h源码、Keil工程uvprojx/uvoptx、hex与axf编译产物,以及html、js、png构成的说明页面和图片,另有pdf、txt文档辅助查阅,目录按工程、驱动、资料与输出分类组织。目前已有235人学习下载。读者可据此梳理STM32F0外设初始化、单总线时序、TM1637显示编码与串口打印小数的实现思路,并借助完整工程和编译输出快速复现实验、对照排查通信与显示异常,对刚接触STM32F0、单总线与数码管扫描的读者较友好。
1. 需求与选型:一套温度采集显示系统为什么要凑齐 USART、DS18B20 和 TM1637
某高校的课程设计里,常有一个看起来简单、真动手却处处卡壳的需求:主控采集环境温度,本地四位数字码管实时显示,同时把数据通过串口上报给上位机。主控选了 STM32F030,传感器用 DS18B20,显示用 TM1637,通信走 USART,组合起来是一套典型的「采集—显示—上报」闭环。三者凑在一起不是随便搭配:DS18B20 单总线只占一个 GPIO,TM1637 两线驱动也只要两个引脚,USART 一收一发再加两个,STM32F030 这点引脚预算完全兜得住。这套方案适合做嵌入式课设、毕设的选题,也适合需要低成本温度终端的场景。真正难的不是写代码,而是单总线微秒时序、两线串行时序和串口帧格式三件事同时做到稳定,这篇按可复现的工程路径拆开讲。
2. 总体架构设计:三层结构与四个模块的职责划分
拿到需求后第一件要定下来的不是代码怎么写,而是资源怎么分。STM32F030 系列常见型号主频 48MHz,片上 Flash 与 RAM 都不宽裕,所以模块之间要尽量低耦合,一个模块出问题不能把整条采集链路带崩。工程上我一般把它切成三层:硬件抽象层只负责引脚电平操作,驱动层把时序打包成函数,应用层做采样调度、显示刷新和串口上报。这样后面调试哪个模块异常,替换或停用都不影响另外两个。
2.1 硬件资源分配与时钟配置
引脚分配要提前画成表,写进工程注释里,后期改硬件才不至于乱。一份可用的分配表通常是下面这样:
| 模块 | 信号 | 引脚 | 模式 | 备注 |
|---|---|---|---|---|
| DS18B20 | DQ | PA1 | 开漏输出/浮空输入切换 | 外接 4.7k 上拉 |
| TM1637 | CLK | PA4 | 推挽输出 | 与 DIO 不同组 |
| TM1637 | DIO | PA5 | 推挽输出/浮空输入切换 | 需要读 ACK |
| USART1 | TX | PA9 | 复用推挽 | 接 USB-TTL 的 RX |
| USART1 | RX | PA10 | 浮空输入 | 接 USB-TTL 的 TX |
时钟上,STM32F030 默认走内部 8MHz HSI,经 PLL 倍频到 48MHz 作为系统时钟。APB2 上挂 USART1,APB1 上挂通用定时器。之所以强调时钟,是因为 DS18B20 和 TM1637 的延时精度直接依赖系统主频,主频改了延时函数要重算,这是很多人换板子后时序突然失效的根因。配置通常在system_stm32f0xx.c或者初始化函数里完成,主频、AHB、APB 分频都要和延时参数对齐。
2.2 软件分层:驱动层与应用层的边界
驱动层对外只暴露四个函数:ds18b20_init、ds18b20_read_temp、tm1637_display_temp、usart_send_frame。应用层完全不知道单总线怎么拉电平,也不关心 TM1637 的段码怎么排,它只调用这四个函数并处理返回值。这条边界立住了,调试单总线的时候直接把tm1637_display_temp换成空实现,码管不动但采集照跑,问题范围立刻缩小一半。反过来,显示模块单独调试时,温度值可以写死成常量,不用等传感器。
分层的另一个好处是便于答辩讲清楚结构。老师通常会问「你这个代码怎么组织的」,能说清三层职责和四个接口,比堆一堆全局变量要有说服力得多。驱动层的函数最好带返回值表示成功或失败,比如ds18b20_read_temp返回 0 表示读到有效温度、非 0 表示 CRC 校验失败或器件无响应,应用层据此决定是显示上一次有效值还是显示----,避免病态数据刷屏。
2.3 主循环调度:轮询、定时与串口事件的配合
主循环不做复杂调度,用一个软件定时器节拍配合状态机就够了。节拍源可以是 SysTick 每 1ms 中断一次,主循环里累加计数,每到 750ms 触发一次温度转换读取,每到 200ms 刷新一次显示,串口发送跟随温度更新事件一起走。DS18B20 12 位分辨率转换时间最长 750ms,刷新周期设成 1s 更稳妥,显示刷新可以快一些让数字看起来不呆板。
// 主循环骨架:节拍驱动,避免阻塞式延时拖垮响应 volatile uint32_t g_tick_ms = 0; void SysTick_Handler(void) { g_tick_ms++; } int main(void) { uint32_t last_sample = 0, last_display = 0; int16_t temp_x10 = 0; board_init(); // 时钟、GPIO、USART、SysTick 一起初始化 ds18b20_init(); tm1637_init(); while (1) { uint32_t now = g_tick_ms; // 每 1s 采样一次,采样成功才更新显示缓存 if (now - last_sample >= 1000) { last_sample = now; if (ds18b20_read_temp(&temp_x10) == 0) { usart_send_temp(temp_x10); // 上报成功读到的值 } } // 显示每 200ms 刷新一次,读失败时显示上次有效值 if (now - last_display >= 200) { last_display = now; tm1637_display_temp(temp_x10); } } }这段骨架的关键点有三个。g_tick_ms用volatile修饰,防止编译器把循环里的读优化掉;时间比较用无符号减法now - last_sample,即使计数溢出也能正确算出间隔;采样和显示分开计时,采样慢不会拖慢显示刷新。实际项目里如果把delay_ms(750)直接塞进主循环,串口接收就会错过上位机下发的命令,这是课堂上很常见的翻车点。
3. DS18B20 单总线驱动:从复位脉冲到温度换算的完整时序
DS18B20 用的是 1-Wire 协议,一根线完成供电(寄生模式)和双向通信,代价是所有时序都得主控自己精确控制。整个交互流程固定:复位 → 存在脉冲 → 跳过 ROM(0xCC)→ 启动转换(0x44)→ 等待 → 复位 → 跳过 ROM → 读暂存器(0xBE)→ 读两个字节 → CRC 校验。每一步都卡在微秒级窗口里,延时函数不准,后面全乱。
3.1 单总线时序的微秒级实现
STM32F030 没有 DWT 周期计数器,微秒延时通常用两种方式:SysTick 轮询或者空循环校准。空循环简单但受编译优化等级影响,-O0和-O2出来的延时能差好几倍,所以正式工程里我更倾向用 SysTick 做基准。
// 基于 SysTick 的微秒延时,主频 48MHz,1us = 48 个周期 void delay_us(uint32_t us) { uint32_t start = SysTick->VAL; uint32_t ticks = us * (SystemCoreClock / 1000000); uint32_t reload = SysTick->LOAD + 1; // SysTick 向下计数,用差值判断已经过去多少 while (1) { uint32_t now = SysTick->VAL; uint32_t elapsed = (start >= now) ? (start - now) : (start + reload - now); if (elapsed >= ticks) break; } }参数说明:us是要延时的微秒数;SystemCoreClock是当前系统主频,48MHz 时每微秒 48 个计数;reload是 SysTick 的重装载值。注意这个函数要求 SysTick 已经启动并且中断间隔大于单次延时,否则计数会绕回导致死循环。实际用的时候一次延时不要超过 SysTick 的重装周期,比如 1ms,超过就拆成多次调用。DS18B20 单步延时最长也就 750ms 的转换等待,那一步建议用前面主循环的节拍去等,别用delay_us硬扛。
3.2 ROM 命令与功能命令的调用顺序
单总线上挂一个器件时,跳过 ROM 命令0xCC是效率最高的做法,省掉 64 位地址匹配。启动转换0x44之后传感器开始内部 ADC,12 位分辨率需要 750ms。读完温度后如果想验证数据可靠,可以读暂存器的第 9 字节做 CRC8 校验,多项式是x^8 + x^5 + x^4 + 1,反射实现对应0x8C。
// 复位并检测存在脉冲,返回 1 表示总线上有器件应答 uint8_t ds18b20_reset(void) { uint8_t presence; DQ_OUT(); // 切输出 DQ_LOW(); // 拉低总线 delay_us(480); // 复位脉冲 >= 480us DQ_HIGH(); // 释放,由上拉电阻拉高 delay_us(60); // 等 15~60us 后采样存在脉冲 DQ_IN(); // 切输入 presence = DQ_READ(); delay_us(420); // 等剩余时隙走完,防止下一次操作撞车 return (presence == 0) ? 1 : 0; // 低电平表示有器件 }写字节和读字节是单总线最容易写错的地方。写 0 要拉低整个时隙至少 60us,写 1 只需拉低 1~2us 后立刻释放,读的时候主控先拉低 1~2us 发起读时隙,然后在 15us 内采样,采样完再等 45us 左右收尾。下面两段代码按这个窗口写:
// 写一个字节,LSB 优先 void ds18b20_write_byte(uint8_t data) { for (uint8_t i = 0; i < 8; i++) { DQ_OUT(); DQ_LOW(); if (data & 0x01) { delay_us(2); // 写 1:短低脉冲后释放 DQ_HIGH(); delay_us(60); // 补足 60us 时隙 } else { delay_us(60); // 写 0:拉低整个时隙 DQ_HIGH(); delay_us(2); } data >>= 1; } } // 读一个字节,LSB 优先 uint8_t ds18b20_read_byte(void) { uint8_t data = 0; for (uint8_t i = 0; i < 8; i++) { data >>= 1; DQ_OUT(); DQ_LOW(); delay_us(2); // 发起读时隙 DQ_HIGH(); DQ_IN(); delay_us(10); // 15us 内采样 if (DQ_READ()) data |= 0x80; delay_us(50); // 收尾,凑满 60us } return data; }这两个函数里的延时数字不是随便写的,都对应数据手册里的时序窗口。把delay_us(10)改成 20 就可能在采样点之后才读,读出来全是 1;把写 0 的 60us 缩短,从机可能识别成写 1。调不通的时候第一件事是拿逻辑分析仪抓波形,比对复位脉冲宽度和读时隙采样点,比盲改代码快得多。
3.3 温度寄存器的读取、符号扩展与 CRC 校验
读回来的两个字节组成 16 位有符号数,低 4 位是小数部分,每格 0.0625℃。负温度要用补码处理,常见做法是先判断符号位再取绝对值,避免直接用整数除法把负数算错。
// 返回 0 表示成功,temp_x10 输出单位为 0.1℃ 的温度 int ds18b20_read_temp(int16_t *temp_x10) { uint8_t buf[9]; if (!ds18b20_reset()) return -1; ds18b20_write_byte(0xCC); // 跳过 ROM ds18b20_write_byte(0x44); // 启动转换 delay_us(750); // 12 位分辨率转换时间 if (!ds18b20_reset()) return -2; ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器 for (uint8_t i = 0; i < 9; i++) { buf[i] = ds18b20_read_byte(); } if (ds18b20_crc8(buf, 8) != buf[8]) return -3; // CRC 校验失败 int16_t raw = ((int16_t)buf[1] << 8) | buf[0]; // 0.0625℃/LSB,乘以 10 换成 0.1℃ 便于显示 *temp_x10 = (int16_t)((int32_t)raw * 10 / 16); return 0; }这段代码里raw * 10 / 16利用了 0.0625 等于 1/16 的整数关系,避免浮点运算,对 M0 核很友好。CRC 校验那一步不能省,单总线在长线或者干扰环境下偶尔会读出错值,不校验直接显示就会出现「数字乱跳」的玄学现象。返回值设计成不同的负数码,应用层能区分是无应答、CRC 错还是别的问题,调试时看一眼就定位。
4. TM1637 显示与 USART 通信:显存映射与协议帧设计
显示和通信这两个模块本身不难,难在细节对齐。TM1637 不是标准 I2C,起始和停止信号的定义跟 I2C 不一样,网上抄来的 I2C 代码直接套会失败。USART 虽然外设成熟,但帧格式设计不好,上位机解析时就容易错位。这两块要一起讲,因为它们共享同一个「数据表示」问题:温度值怎么变成数码管能显示的段码,又怎么变成串口能传的字节流。
4.1 TM1637 两线时序与段码表
TM1637 的协议核心是:CLK 高电平时 DIO 下降沿为起始,CLK 高电平时 DIO 上升沿为停止,数据在 CLK 上升沿被写入,读 ACK 时主控释放 DIO 由从机拉低。亮度控制和显存地址通过命令字设置,显示数据写入地址0xC0开始的四个字节。段码要按实际数码管共阴共阳来定,常见共阳段码表是:
// 共阳数码管 0~9 段码,位序 dp g f e d c b a static const uint8_t seg_table[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; // 显示带一位小数的温度,例如 23.5 显示为 "23.5" void tm1637_display_temp(int16_t temp_x10) { uint8_t buf[4]; uint16_t v = (temp_x10 < 0) ? (uint16_t)(-temp_x10) : (uint16_t)temp_x10; buf[0] = (temp_x10 < 0) ? 0x40 : 0x00; // 负号用 g 段 buf[1] = seg_table[(v / 100) % 10]; buf[2] = seg_table[(v / 10) % 10] | 0x80; // 点亮小数点 buf[3] = seg_table[v % 10]; tm1637_write_regs(buf, 4); }seg_table里每一位对应一段 LED,位序要和硬件接线一致,接反了显示出来就是乱码。小数点位是最高位0x80,叠加在对应位的段码上。温度超过 99.9℃ 时最高位会溢出,通常的做法是显示EEE或者只保留两位整数,这属于业务规则,要在需求阶段就定下来。
4.2 四位显存映射与刷新策略
TM1637 内部有 6 字节显存,地址0xC0~0xC5,前四个对应四位数字。刷新时整块写比逐位写稳,因为逐位写会在中间状态出现「半截数字」。起始信号 → 写地址命令0x40自动地址递增 → 起始 → 写0xC0→ 连写四字节 → 停止,这是最常用的写序列。亮度用0x88 | level设置,level 取 0~7。
// 起始信号:CLK 高电平期间 DIO 由高变低 void tm1637_start(void) { DIO_HIGH(); CLK_HIGH(); delay_us(2); DIO_LOW(); delay_us(2); CLK_LOW(); } // 写一个字节并读 ACK,返回 0 表示从机应答 uint8_t tm1637_write_byte(uint8_t data) { uint8_t ack; for (uint8_t i = 0; i < 8; i++) { CLK_LOW(); if (data & 0x01) DIO_HIGH(); else DIO_LOW(); delay_us(2); CLK_HIGH(); // 上升沿写入 delay_us(2); data >>= 1; } CLK_LOW(); DIO_HIGH(); // 释放 DIO 等 ACK delay_us(2); CLK_HIGH(); delay_us(2); ack = DIO_READ(); CLK_LOW(); return ack; }刷新频率不要太高,200ms 一次对肉眼已经连续,写太频繁反而增加总线占用。主循环里显示刷新和温度采样分开计时,正是为了避免采样卡顿导致显示闪烁。这段代码里 ACK 读回来是 0 才算成功,如果一直是 1,说明从机没响应,先检查上拉电阻和电源。
4.3 USART 帧格式与中断收发
串口上报建议用固定帧头、命令字、数据、CRC、帧尾的结构,上位机按状态机解析,比纯文本printf更抗干扰。帧长控制在 8 字节以内,波特率 115200,8N1。
#pragma pack(push, 1) typedef struct { uint8_t head; // 0xA5 帧头 uint8_t cmd; // 0x01 温度上报 int16_t temp_x10; // 单位 0.1℃ uint8_t crc8; // 前面所有字节的 CRC8 uint8_t tail; // 0x5A 帧尾 } temp_frame_t; #pragma pack(pop) // 发送一帧温度数据 void usart_send_temp(int16_t temp_x10) { temp_frame_t f; f.head = 0xA5; f.cmd = 0x01; f.temp_x10 = temp_x10; f.crc8 = crc8_calc((uint8_t *)&f, 4); f.tail = 0x5A; for (uint8_t i = 0; i < sizeof(f); i++) { while (!(USART1->ISR & USART_ISR_TXE)); // 等发送寄存器空 USART1->TDR = ((uint8_t *)&f)[i]; } }这里用#pragma pack(1)保证结构体不填充,帧长固定 6 字节。CRC8 覆盖帧头到数据三字节,上位机校验不过就丢帧重同步。接收方向如果用中断,要注意在USART1_IRQHandler里读RDR清标志,只读ISR不清标志会导致中断反复触发,这是 USART 调试中的经典坑。发送用轮询等TXE就够,数据量小不值得上 DMA。
5. 避坑与排查:单总线、两线时序和串口最容易翻车的几个点
这套组合的难点不在功能本身,而在三类模块各自的边界条件。调试阶段遇到的「偶尔正常、偶尔不正常」几乎都落在这几个地方,按现象、原因、解决三步过一遍,能省掉大量无头苍蝇式的试错。
5.1 时序与延时类问题
现象:DS18B20 有时候能读到温度,有时候读回0x0550(85℃,上电默认值)或者 CRC 失败;换一块板子就完全读不到。原因:延时函数用空循环实现,换编译优化等级或者换主频后实际延时变了,复位脉冲宽度和读时隙采样点全部偏移。解决:延时统一走 SysTick 或定时器,不依赖编译器;把复位脉冲和读时隙的波形用逻辑分析仪抓出来,对齐数据手册的窗口再调参数。TM1637 那边同理,delay_us(2)在某些主频下实际可能是 0.5us 或 5us,ACK 采样点就会错。
现象:码管显示的数字偶尔闪一下变成乱码,过一会儿又恢复。原因:显示刷新和温度采样共用主循环,采样里放了 750ms 的硬延时,刷新节拍被拉长,TM1637 在两次写之间时序被拉歪。解决:把转换等待交给节拍计数,主循环不阻塞;刷新整块写显存,不逐位写。
5.2 电气与硬件类问题
现象:DS18B20 单独接能读,加上 TM1637 之后单总线频繁无应答。原因:单总线 DQ 的上拉电阻阻值偏大或者没接,总线容性负载一上来,上升沿变缓,存在脉冲被淹没;另一种情况是 DS18B20 用寄生供电,转换期间电流不够。解决:DQ 固定接 4.7k 上拉到 3.3V,长线降到 2.2k 试;尽量用外部供电的三线接法,VDD 直接接 3.3V,寄生模式只在引脚极紧张时用。TM1637 的 CLK/DIO 也需要上拉,很多模块板上自带,自己搭电路时别忘了。
现象:串口能发不能收,或者收到的数据全是 0xFF。原因:TX/RX 接反是最常见的,另一类是电平不匹配,主控 3.3V 直接接 5V 的 USB-TTL 模块,RX 端被拉在高电平。解决:交叉接线 TX 接对方 RX;用万用表量空闲时 TX 是否为高电平;USB-TTL 选 3.3V 电平版本,或者在 RX 上加电平转换。
5.3 通信协议与中断类问题
现象:上位机收到的帧偶尔错位,CRC 一直失败。原因:帧没有做转义或者长度保护,数据里恰好出现0xA5就被当成了新帧头;接收中断里只读了一个字节就返回,剩余字节把下一帧冲掉。解决:接收端用状态机按帧头、长度、CRC 逐字节推进,CRC 不过就丢弃重同步;中断里一次性把RDR读空,或者用空闲中断配合 DMA 收整帧。
现象:程序跑一段时间后死机,或者串口中断再也进不去。原因:中断优先级配置冲突,SysTick 和 USART 抢占优先级设成一样,长延时里被串口中断打断,时序被破坏;或者中断服务函数里做了耗时操作。解决:SysTick 优先级设最低,USART 接收中断设高一级,中断里只做标志位和缓冲搬运,解析放到主循环。这类问题用调试器看中断挂起位最快,别靠猜。
6. 验收与答辩:怎么证明三个模块都「正常」
到了验收环节,很多人栽在「功能能跑但说不清楚」上。老师或者评审看的不是你演示一遍温度变化,而是你能不能证明每个模块在异常条件下也有可预期的行为。我一般会准备一张验收表,把正常路径和异常路径都列出来,逐条演示或者说明预期结果。
| 验收项 | 操作 | 预期结果 | 判定依据 |
|---|---|---|---|
| 单总线识别 | 上电初始化 | 复位后检测到存在脉冲 | ds18b20_reset返回 1 |
| 温度准确性 | 冰水混合物、室温两点的对比 | 误差在 ±0.5℃ 内 | 与标准温度计比对 |
| CRC 保护 | 拔掉 DQ 再插回 | 显示保持上次有效值,不出现乱码 | 返回值非 0 时走降级逻辑 |
| 显示完整性 | 温度从 0 变到 99.9 | 四位全亮无缺段,小数点位置正确 | 目视加段码表核对 |
| 串口帧格式 | 上位机按协议解析 | 帧头帧尾对齐,CRC 通过率 100% | 抓包工具逐帧检查 |
| 连续运行 | 通电 2 小时 | 无死机、无显示漂移 | 串口日志时间戳连续 |
答辩时演示顺序也有讲究。先讲架构图,三层四模块一句话说清;再演示正常采集,让数字从室温跳一下;然后主动制造一个异常,比如拔掉传感器,展示降级显示而不是死机;最后打开串口助手展示帧结构和 CRC 校验。主动暴露异常并给出处理方案,比只演示成功路径更能体现工程思维。老师常追问「你这个延时怎么保证准确」,答「SysTick 基准加逻辑分析仪校准」,比答「我试出来的」要稳。
有个细节值得提前准备:把关键波形截图存进答辩 PPT,复位脉冲、读时隙、TM1637 起始信号各一张,配上时间轴标注。评审看到波形就知道你是真调过时序,不是抄来能跑就交。我带的项目里,凡是准备了波形截图的组,答辩时被追问技术细节的压力都小很多。
从那以后我每次做带时序协议的项目,都强制自己先用逻辑分析仪把最基础的复位和读写时隙抓一遍,确认波形对了再往上堆业务逻辑,省下的返工时间远比调试本身多。希望这套拆解能帮到正在搭温度采集终端的你,把三个模块一次跑通。
本文还有配套的精品资源,点击获取