STM32F103驱动DS18B20温度传感器:从GPIO模拟单总线到串口显示
简介:这份资源面向正在学习STM32嵌入式开发、需要动手实践外设驱动的初学者与进阶者,围绕野火STM32F103指南者开发板,提供一套可直接编译运行的简易温度读取工程。工程基于标准外设库搭建,涉及ADC采样、定时器、RCC时钟配置、串口通信等模块,适合作为温度传感器采集实验的参考模板,也可用于课程设计或自学练手。压缩包共366个文件,约17.18MB,包含52个C源文件与58个头文件构成的核心驱动代码,54个编译中间文件与54个crf文件、52个o文件记录构建过程,另有hex与axf固件、map映射文件、Keil工程文件(uvprojx、uvoptx、sln)以及少量图片与说明文档,目录结构完整,便于直接导入IDE调试。目前已有51人学习下载。读者可从中获得一套结构清晰的STM32温度采集工程范例,理解ADC与定时器协同工作的实现方式,并借助现成工程快速验证硬件、排查配置问题,节省从零搭建环境的时间。
1. 从一块吃灰的指南者板子说起:温度读取到底难在哪
手头有一块野火 STM32F103 指南者开发板,配套的例程跑通了 LED 和串口,但真到自己动手接一个温度传感器把数值读出来,很多人会卡在几个具体的地方:传感器选型是 DS18B20 还是 LM75,单总线时序怎么用 HAL 库写,读出来的数据为什么一直是 85 或者 -127,串口打印的中文为什么乱码。这个标题指向的就是一个最小闭环——用 STM32F103 把温度读出来,通过串口或者屏幕看到数值。它不涉及复杂的 RTOS 或网络协议,但覆盖了 GPIO 模拟时序、定时器延时、串口收发、数据手册阅读这几个嵌入式入门必须迈过去的坎。适合刚拿到开发板、想从点灯进阶到读传感器的人,也适合需要快速验证温度采集方案、不想在环境搭建上耗太久的熟手。下面按我实际调试的顺序,把选型、接线、代码、参数和翻车点一次讲清楚。
2. 温度传感器选型与 STM32F103 的硬件对接
2.1 DS18B20 和 LM75 的取舍逻辑
常见做法是优先选 DS18B20,原因很直接:单总线协议只占一个 GPIO,测温范围 -55 到 +125 摄氏度,精度在 -10 到 +85 区间是 ±0.5 摄氏度,足够覆盖室温到一般工业场景。野火指南者板子上有现成的 DS18B20 接口,通常是接在 PG11 或者 PA0 这类普通 IO 上,具体看板子丝印。LM75 是 I2C 接口,精度 ±2 摄氏度,优势是读取速度快、不需要严格时序,但需要两根线加地址配置,对于只读一个温度点的需求来说反而更麻烦。
选 DS18B20 的另一个理由是资料多,时序图在数据手册里画得很清楚,用 GPIO 模拟也能跑通。代价是它对延时敏感,微秒级的时序偏差就会导致读回全 1 或者全 0。如果你用的是 HAL 库,HAL_Delay 只能到毫秒,必须自己写微秒延时,或者用定时器做。这一点在后面避坑章节会展开。
2.2 硬件接线与上拉电阻
DS18B20 的引脚定义:面对扁平一面,左脚是 GND,中间是 DQ,右脚是 VDD。指南者板子上一般已经焊好了 4.7k 上拉电阻,DQ 直接连到 GPIO。如果你是自己用杜邦线接传感器,务必在 DQ 和 VDD 之间加一个 4.7k 到 10k 的电阻,否则读出来的数据会随机跳变。供电方式选寄生电源还是外部电源,建议用外部电源,也就是三根线都接,稳定性好很多。
接线确认后,先用万用表量一下 DQ 对 GND 的电压,空闲状态应该是 3.3V。如果只有 0V 或者 1V 左右,检查上拉电阻和焊接。STM32F103 的 IO 是 3.3V 电平,DS18B20 可以工作在 3.0V 到 5.5V,所以直接连没问题,不需要电平转换。这一点和某些 5V 传感器不同,别被网上那些 5V 转 3.3V 电路带偏。
2.3 工程模板与时钟配置
用 Keil MDK 或者 STM32CubeMX 生成工程都可以。我一般用 CubeMX 把时钟树配到 72MHz,GPIO 选推挽输出和浮空输入切换,串口 USART1 配 115200 波特率。生成代码后,在 main.c 里加自己的延时函数和 DS18B20 驱动。如果你用的是野火官方例程,注意它的延时函数可能基于 SysTick,和 HAL 库的 HAL_Delay 有冲突,需要统一。
提示:CubeMX 生成的工程默认用 HAL_Delay,精度是 1ms。DS18B20 需要 1us、10us、60us 级别的延时,必须另外实现。可以用 DWT 或者 TIM 做微秒延时,下面代码块里给出一种基于 SysTick 的简单写法。
3. 用 GPIO 模拟单总线:复位、写位、读位的代码实现
3.1 微秒延时函数的实现
先解决延时问题。下面这个函数用 SysTick 的当前值做忙等,不依赖中断,实测在 72MHz 下误差在 1us 以内。注意变量类型用 uint32_t,避免溢出。
// 基于 SysTick 的微秒延时,72MHz 主频 void delay_us(uint32_t us) { uint32_t start = SysTick->VAL; uint32_t ticks = us * 72; // 72MHz 下 1us = 72 个 tick uint32_t cnt = 0; while (cnt < ticks) { uint32_t now = SysTick->VAL; if (now <= start) { cnt += start - now; } else { cnt += start + (SysTick->LOAD - now); } start = now; } }逻辑说明:SysTick 是递减计数器,LOAD 寄存器存重装载值。每次读 VAL,计算两次之间的差值,累加到 cnt。当 cnt 超过目标 tick 数就退出。参数 us 就是需要延时的微秒数,ticks 是换算后的 tick 总数。这个函数在中断里调用也没问题,因为它不关中断。
3.2 复位脉冲与存在检测
DS18B20 每次通信前必须复位,拉低 DQ 至少 480us,然后释放,等待 15 到 60us 后检测存在脉冲。代码如下:
#define DS18B20_PIN GPIO_PIN_11 #define DS18B20_PORT GPIOG // 复位并检测存在脉冲,返回 0 表示存在,1 表示不存在 uint8_t ds18b20_reset(void) { uint8_t presence; // 主机拉低 480us HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(480); // 释放总线,等待 15-60us HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); delay_us(60); // 读取从机拉低的存在脉冲 presence = HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN); delay_us(420); // 等待复位完成 return presence; }参数说明:480us 是数据手册规定的最小复位低电平时间,60us 是等待从机响应的时间窗口。presence 为 0 表示 DS18B20 拉低了总线,说明器件在线。如果返回 1,检查接线和上拉电阻。注意这里用的是 HAL_GPIO_ReadPin,读之前 GPIO 模式要设成输入,但 HAL 库的 ReadPin 不要求切换模式,因为 STM32 的 GPIO 在开漏或推挽输出下也能读回引脚电平,不过为了规范,建议在初始化时把 DQ 配成开漏输出,这样读写都方便。
3.3 写一个字节和读一个字节
写位和读位是单总线最核心的时序。写 0 是拉低 60us 后释放,写 1 是拉低 1us 后释放再等 60us。读位是主机拉低 1us 后释放,然后在 15us 内采样。
// 写一个字节,data 的每一位从 LSB 开始 void ds18b20_write_byte(uint8_t data) { for (int i = 0; i < 8; i++) { if (data & 0x01) { // 写 1:拉低 1us,释放,延时 60us HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(1); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); delay_us(60); } else { // 写 0:拉低 60us,释放,延时 1us HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(60); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); delay_us(1); } data >>= 1; } } // 读一个字节,返回读到的数据 uint8_t ds18b20_read_byte(void) { uint8_t data = 0; for (int i = 0; i < 8; i++) { data >>= 1; // 主机拉低 1us 启动读时隙 HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(1); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); delay_us(10); // 等待从机输出稳定 if (HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN)) { data |= 0x80; } delay_us(50); // 完成整个读时隙 } return data; }逻辑说明:写字节时,每个 bit 先判断是 1 还是 0,然后按对应时序拉低和释放。读字节时,每次先右移一位,然后启动读时隙,采样到高电平就把最高位置 1。参数上,1us 和 60us 是数据手册给的典型值,实际调试时如果读出来全是 0xFF 或者 0x00,优先检查 delay_us 的精度。
3.4 完整的温度读取流程
一次温度转换需要发 0xCC 跳过 ROM,发 0x44 启动转换,延时 750ms 以上,再发 0xCC 和 0xBE 读暂存器,读两个字节拼成 16 位有符号数,乘以 0.0625 得到摄氏度。
float ds18b20_read_temp(void) { uint8_t temp_l, temp_h; int16_t raw; float temp; ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳过 ROM ds18b20_write_byte(0x44); // 启动温度转换 HAL_Delay(750); // 等待转换完成,12 位精度需要 750ms ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器 temp_l = ds18b20_read_byte(); temp_h = ds18b20_read_byte(); raw = (temp_h << 8) | temp_l; temp = raw * 0.0625f; return temp; }参数说明:0xCC 是跳过 ROM 命令,适合总线上只有一个器件的情况。0x44 启动转换,0xBE 读暂存器。raw 是有符号 16 位,负温度时高字节为 0xFF,直接乘 0.0625 也能得到负值。HAL_Delay(750) 是保守值,实际 12 位精度最大转换时间 750ms,如果设成 9 位精度可以缩短到 94ms,但需要改配置寄存器,初学不建议动。
4. 串口打印与 OLED 显示:把温度值送出来
4.1 重定向 printf 到 USART1
读出来的浮点数要能看到,最省事的是重定向 printf。在 Keil 里勾选 Use MicroLIB,然后实现 fputc。
#include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100); return ch; }逻辑说明:printf 最终会调用 fputc 输出每个字符,这里把字符通过 HAL_UART_Transmit 发到 USART1。参数 100 是超时时间,单位毫秒。注意 huart1 是 CubeMX 生成的句柄名,如果你用的是 USART2 就改成 huart2。勾选 MicroLIB 后,printf 不支持浮点数,需要改成整数打印,比如printf("temp: %d.%d\r\n", (int)temp, (int)(temp*10)%10);。
4.2 用 OLED 显示温度
如果手头有 0.96 寸 I2C OLED,可以把温度显示在屏幕上。SSD1306 驱动网上很多,移植时注意 I2C 地址通常是 0x78 或 0x7A。显示浮点数先转成字符串,再调用 OLED_ShowString。刷新频率不要太高,200ms 一次足够,否则 I2C 占用时间太长会影响 DS18B20 的时序。
注意:DS18B20 的时序对中断敏感。如果串口接收中断或者 SysTick 中断在读写过程中打断超过 10us,可能导致读回数据错误。调试时可以先关掉全局中断,读完之后再开。
4.3 数据校验与异常值处理
读回 85.0 是上电默认值,说明转换命令没发出去或者复位失败。读回 -127.0 说明读到的两个字节都是 0xFF,通常是总线一直高电平,传感器没响应。处理办法是在 ds18b20_read_temp 里加判断,如果 raw 等于 0x0550 或者 0xFF00 就返回一个错误码,上层决定是否重试。重试三次都失败就报传感器离线。
5. 避坑与排查:DS18B20 读温的五个血泪教训
5.1 现象:温度一直是 85.0,复位检测返回 1
原因:复位脉冲拉低时间不够,或者 delay_us 实际延时远小于设定值。常见于用 HAL_Delay 代替微秒延时,HAL_Delay(1) 实际是 1ms,但复位需要 480us,拉低太久反而让传感器进入不确定状态。另一个原因是 GPIO 速度配置太低,推挽输出在 2MHz 下上升沿太缓。
解决:用示波器看 DQ 波形,确认拉低时间在 480us 到 960us 之间。把 GPIO 速度设成 High 或者 Very High。delay_us 用 SysTick 或 DWT 实现,不要用 HAL_Delay。
5.2 现象:读回数据偶尔正确,偶尔变成 0 或 4095
原因:读写过程中被中断打断。SysTick 中断默认 1ms 一次,如果 delay_us 忙等时来了中断,实际延时会被拉长,导致读时隙采样点偏移。串口接收中断也会造成类似问题。
解决:在 ds18b20_reset、write_byte、read_byte 这几个函数里,进入时关全局中断,退出时开。或者把 DS18B20 的读写放到定时器中断里,用状态机实现,避免忙等。
5.3 现象:串口打印中文乱码,英文和数字正常
原因:Keil 默认编码是 GBK,串口助手如果设成 UTF-8 就会乱码。STM32 本身不处理编码,它只发字节。你的源文件里中文字符串按 GBK 编码存成两个字节,串口助手按 UTF-8 解析就显示乱码。
解决:把串口助手编码改成 GBK,或者把源文件转成 UTF-8 并在工程设置里加--locale=english。更彻底的办法是打印英文,避免编码问题。
5.4 现象:OLED 显示正常,但 DS18B20 读数变慢或出错
原因:OLED 的 I2C 刷新占用大量 CPU 时间,如果刷新和 DS18B20 转换在同一个循环里,I2C 传输期间可能错过 DS18B20 的读时隙。特别是用软件 I2C 时,延时函数被 OLED 驱动占用。
解决:把 DS18B20 转换和 OLED 刷新分时处理。启动转换后先刷新 OLED,等 750ms 到了再读温度。或者用硬件 I2C 加 DMA,减少 CPU 占用。
5.5 现象:换一块板子或者换一个 DS18B20 就读不到
原因:不同批次的 DS18B20 时序参数有细微差异,尤其是山寨器件。上拉电阻阻值不对也会导致某些器件能读某些不能读。另外,如果传感器线太长,分布电容会让上升沿变缓。
解决:上拉电阻用 4.7k,不要用 10k。线长控制在 20cm 以内。如果还不行,把 delay_us 的 1us 改成 2us,60us 改成 70us,牺牲一点速度换稳定性。
6. 进阶技巧:用定时器捕获单总线时序与低功耗读取
6.1 用 TIM 输入捕获自动测量存在脉冲
前面用 GPIO 模拟时序,CPU 全程忙等,效率低。更工程化的做法是用一个定时器做输入捕获,另一个定时器做微秒延时。具体思路:把 DQ 接到 TIM 的某个通道,配置成输入捕获模式,上升沿和下降沿都触发。复位后启动捕获,记录从机拉低和释放的时间戳,自动判断存在脉冲宽度是否在 15us 到 60us 之间。这样不需要在代码里写死延时,换传感器也不用改参数。
配置步骤:CubeMX 里选 TIM2 通道 1 为输入捕获,预分频器设成 71,这样计数频率是 1MHz,一个 tick 就是 1us。开启捕获中断,在中断里读 CCR 寄存器,计算两次捕获的差值。如果差值在 15 到 60 之间,置一个标志位表示传感器在线。
6.2 低功耗间歇读取
如果这个温度节点是电池供电,DS18B20 的 750ms 转换期间可以让 STM32 进入 Stop 模式。用 RTC 或者独立看门狗定时唤醒,醒来后读温度,通过串口发出去,然后继续睡。实测平均电流可以从 20mA 降到 2mA 以下。注意 Stop 模式下 SysTick 停止,delay_us 不能用,需要换成 RTC 唤醒或者外部中断。
6.3 多传感器挂同一条总线
DS18B20 的 ROM 是 64 位,每个器件唯一。如果总线上挂多个,就不能用 0xCC 跳过 ROM,要先发 0x33 读 ROM 命令,用搜索算法枚举所有器件地址,然后逐个匹配。搜索算法用二叉树遍历,代码量比单器件大不少。如果只是测两三个点,更简单的办法是用多个 GPIO 分别接,每个 GPIO 一个传感器,代码复用单器件的逻辑。
6.4 验证读取精度的土办法
拿一个标准温度计放在旁边,同时读 DS18B20 的值,记录十组数据。如果偏差在 ±0.5 摄氏度以内,说明时序和换算都正确。如果偏差固定,比如总是高 2 度,检查是不是把华氏度当摄氏度了,或者 raw 的符号位处理错了。如果偏差随机跳动,回到避坑章节查中断和上拉电阻。
我自己调试时习惯在 ds18b20_read_temp 里加一句printf("raw: %d\r\n", raw);,先把原始值打出来,确认 raw 在室温下大约是 25 度对应 400 左右,再去看浮点换算。这样能把时序问题和换算问题分开定位。希望帮到你。
本文还有配套的精品资源,点击获取