STM32F407驱动DS18B20温度采集与串口屏显示实战解析
简介面向嵌入式初学者与STM32开发人员这份DEMO例程演示了在STM32F407上读写DS18B20温度传感器并通过串口屏实时显示温度数据帮助解决单总线驱动编写、设备序列号匹配读取以及HMI指令发送等常见问题。例程基于HAL库编写主循环调用DS18B20_Init检测传感器、DS18B20_ReadId读取64位序列号、DS18B20_GetTemp_MatchRom获取指定器件温度再通过HMI_value_setting向串口屏页面控件发送赋值指令逻辑清晰便于直接移植或二次开发。压缩包共663个文件包含大量.c/.h源码文件、IAR与Keil工程配置文件.ewp/.uvprojx、链接脚本.icf、编译批处理、hex烧录文件及docx说明文档整体7.81MB工程结构完整。已有1700余人学习浏览可作为课程设计、毕业设计或项目预研的参考模板。1. 这块 407 板子读温度难点不在 DS18B20 而在时序和帧尾拿到这份 YS-F4Pro 的 DEMO第一眼看到main里那个while(DS18B20_Init())循环很多人会误以为最难的是单总线复位。其实 DS18B20 的驱动网上抄得到真正会被卡住的是两个地方一是 STM32F407 用 HAL 库做 GPIO 翻转时时延精度被系统时钟和编译器优化吃掉二是淘晶驰串口屏的指令帧必须以 0xFF 0xFF 0xFF 结尾而例程里HMI_value_setting函数末尾那三行HMI_USARTx-DR0xFF才是整个显示链路能不能刷出数的关键。这套代码的价值在于它把「单总线时序 ROM 匹配读取 串口屏指令封帧」串成了一个完整闭环适合正在做多点测温、或者第一次把传感器数据送上串口屏的人拆开研究。下面从驱动层、应用层、排错和编译体积四个角度逐段说清楚。2. DS18B20 单总线驱动GPIO 模式切换与微秒级延时是命门2.1 为什么 STM32F407 上不能用HAL_Delay做时序DS18B20 是单总线器件所有通信都靠一根 DQ 线上的高低电平时序完成。总线复位要求主机拉低至少 480µs然后释放并等待 60µs读时序要在 15µs 内采样写 1 时序和写 0 时序的宽度差只有几十微秒。HAL_Delay基于 SysTick 中断最小粒度是 1ms而且中断嵌套时会阻塞完全没法满足 15µs 级别的边沿要求。void DS18B20_Rst(void) { DS18B20_Mode_Output(); /* PA9 配置为推挽输出 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET); delay_us(500); /* 拉低 500µs略长于规格的 480µs */ DS18B20_Mode_Input(); /* 切回输入模式释放总线 */ delay_us(60); }这段代码里的关键动作是DS18B20_Mode_Output()和DS18B20_Mode_Input()。单总线是开漏架构主机释放总线后靠上拉电阻把电平拉高但 STM32 的 GPIO 如果一直保持推挽输出高电平从设备拉低时会产生短路风险所以必须在读写时序之间切换 GPIO 方向。延时函数delay_us在例程里用的是 DWT 或 SysTick 的微秒级忙等循环。使用 DWT 的好处是它不受中断影响也不占用 SysTick 中断在SystemClock_Config之后直接使能CoreDebug-DEMCR和DWT-CYCCNT即可。提示把delay_us实现为for循环空转的版本也能跑但编译器开-O2优化后循环会被部分折叠导致时序缩短到不可用状态。建议用 DWT 计数器实现并把延时函数放在单独的.c文件里能避免这类诡异问题。2.2 写时序和读时序的位操作细节DS18B20 的写时序分为写 0 和写 1区别在于拉低总线后的释放时间不同。写 0 需要全程拉低至少 60µs写 1 则是在拉低 1~15µs 后释放总线。读时序则是主机先拉低 1µs然后释放总线再在 15µs 内读取 DQ 电平。void DS18B20_WriteBit(uint8_t bit) { if (bit) { DS18B20_Mode_Output(); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET); delay_us(6); /* 拉低 6µs 后释放 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_SET); DS18B20_Mode_Input(); delay_us(60); } else { DS18B20_Mode_Output(); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET); delay_us(60); /* 写 0 要维持低电平 60µs */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_SET); DS18B20_Mode_Input(); } }写 1 时拉低 6µs 再释放正好落在规格书的 1~15µs 窗口内。写 0 时保持 60µs确保从设备采样时读到确定的低电平。注意DS18B20_Mode_Input()之后没有立刻拉高 GPIO因为输入模式下引脚呈高阻总线由上拉电阻恢复高电平。读时序的采样窗口只有 15µs所以读位函数要先把 GPIO 配成输出并拉低delay_us(2)再切回输入模式然后立刻读取引脚电平。整个过程必须在一个函数里完成中间不能穿插其他代码否则时序就乱了uint8_t DS18B20_ReadBit(void) { uint8_t bit; DS18B20_Mode_Output(); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET); delay_us(2); DS18B20_Mode_Input(); delay_us(5); /* 等待从设备驱动总线 */ bit HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9); delay_us(60); return bit; }delay_us(2)和delay_us(5)合起来是 7µs在读时序要求的 15µs 窗口内。这里如果把 GPIO 配置成开漏输出而不是切换输入/输出模式时序同样成立但要注意上拉电阻的阻值不能太大否则电平恢复速度变慢高速连续读位时容易误码。2.3 ROM 匹配读取与跳过 ROM 的取舍例程里main函数先调用DS18B20_ReadId(DS18B20ID)读取 64 位 ROM 序列号随后在while(1)里每次读温度都用DS18B20_GetTemp_MatchRom(DS18B20ID)而不是常见的DS18B20_GetTemp_SkipRom()。float DS18B20_GetTemp_MatchRom(uint8_t *ROM) { uint8_t tempL, tempH, i; float temperature; DS18B20_Rst(); DS18B20_WriteByte(0x55); /* 匹配 ROM 指令 */ for (i 0; i 8; i) { DS18B20_WriteByte(ROM[i]); /* 逐字节发送序列号 */ } DS18B20_WriteByte(0x44); /* 启动温度转换 */ DS18B20_Rst(); DS18B20_WriteByte(0x55); for (i 0; i 8; i) { DS18B20_WriteByte(ROM[i]); } DS18B20_WriteByte(0xBE); /* 读取暂存器 */ tempL DS18B20_ReadByte(); tempH DS18B20_ReadByte(); temperature (tempH 8 | tempL) * 0.0625; return temperature; }0x55匹配 ROM 指令后面必须紧跟完整的 8 字节序列号主机会在总线上寻址到唯一器件。跳过 ROM 指令0xCC只适用于单器件场景一旦总线上挂了两颗以上 DS18B20两路输出就会在总线上短路读到的是所有从设备数据的与逻辑结果温度值完全不可信。这段代码里值得注意的另一个点0x44启动转换指令发出后没有等待转换完成而是直接DS18B20_Rst()复位总线再重新发匹配指令读暂存器。因为DS18B20_Rst()本身会拉低总线 500µs这个时间足够 12 位精度的转换完成省去了轮询忙标志的代码。3. 串口屏显示链路淘晶驰指令格式与0xFF 0xFF 0xFF帧尾3.1 指令封帧的完整流程淘晶驰串口屏使用类文本指令协议数据通过 UART 发送每条指令以三个连续的0xFF作为帧结束符。例程里的HMI_value_setting函数正是围绕这个协议展开void HMI_value_setting(const char *val_str, uint32_t value) { uint8_t tmp_str[30] {0}; uint8_t i; sprintf((char *)tmp_str, %s%d, val_str, value); for (i 0; i strlen((char *)tmp_str); i) { HMI_USARTx-DR tmp_str[i]; while (__HAL_UART_GET_FLAG(husartx_HMI, UART_FLAG_TXE) RESET); } HMI_USARTx-DR 0xFF; while (__HAL_UART_GET_FLAG(husartx_HMI, UART_FLAG_TXE) RESET); HMI_USARTx-DR 0xFF; while (__HAL_UART_GET_FLAG(husartx_HMI, UART_FLAG_TXE) RESET); HMI_USARTx-DR 0xFF; while (__HAL_UART_GET_FLAG(husartx_HMI, UART_FLAG_TXE) RESET); }函数签名里的第一个参数val_str是屏幕页面上控件的变量名路径比如page1.gross.val表示第一页里名为gross的控件的val属性。第二个参数value是实际写入的数值。这是直接操作数据寄存器HMI_USARTx-DR而非调用HAL_UART_Transmit的写法。直接操作寄存器配合TXE标志位轮询可以避免 HAL 库在HAL_UART_Transmit里对超时参数的额外校验不过在 CPU 主频 168MHz 下这点性能差异微乎其微。真正需要留意的是sprintf之后没有手动清空tmp_str末尾残留数据的处理如果前一次格式化后的字符串比当前的长理论上会有旧数据残留。当然sprintf会在写入的字符串末尾自动追加\0所以strlen截断后实际发送的内容是安全的。3.2 浮点数显示为什么要乘 10串口屏控件大多数只支持 16 位或 32 位整型属性直接传浮点数会被截断成 0。例程里用temperature*10把小数位搬到整数部分屏幕上再通过控件的小数位设置显示成一位小数temperature DS18B20_GetTemp_MatchRom(DS18B20ID); HMI_value_setting(page1.gross.val, (uint32_t)(temperature * 10));temperature * 10的结果是浮点数强转成uint32_t时小数部分直接舍弃。如果温度是 25.375℃乘 10 后是 253.75转整型变成 253屏幕显示 25.3℃。这里DS18B20_GetTemp_MatchRom返回的temperature实际上已经把原始 12 位 ADC 值乘以 0.0625 转成了摄氏度所以乘 10 后精度等价于 0.1℃。要想保留两位小数就乘 100但要注意uint32_t的表示范围温度达到 42949672.95℃ 才会溢出工业场景根本不会触及。3.3 串口初始化两路 UART 的分工例程里初始化了两个串口MX_DEBUG_USART_Init是调试串口用于向 PC 端打印日志HMI_USARTx_Init是屏幕串口专门发指令。这种分工在多传感器系统里很常见把「人机交互」和「调试信息」隔离到不同串口避免指令日志混在一起干扰屏的解析。static void MX_DEBUG_USART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; HAL_UART_Init(huart1); }淘晶驰串口屏出厂默认波特率通常是 9600也有型号是 115200需要和屏上的配置保持一致。HMI_USARTx_Init里的波特率如果和屏端不匹配现象是屏幕偶尔闪一下变量值但立刻变回旧值这是串口帧错位导致的解析失败。检查时先确认屏的波特率参数再对照例程里的huartx_HMI.Init.BaudRate。4. 编译环境与工程依赖删冗余文件时别误伤 DSP 库4.1arm_common_tables.c是什么例程源码包里出现arm_common_tables.c、arm_dct4_init_f32.c、arm_rfft_init_f32.c这一串文件它们是 ARM 官方 CMSIS-DSP 库的源码文件。项目工程里被裁剪后的 DSP 库源码会直接参与编译而不是链接预编译的.lib文件。文件作用为什么出现在工程里arm_common_tables.cFFT 蝶形运算的旋转因子查找表arm_rfft_f32依赖它计算系数arm_dct4_init_f32.cDCT-IV 变换的初始化系数被 FFT 相关的变换函数间接引用arm_rfft_init_f32.c实数 FFT 初始化函数提供arm_rfft_fast_init_f32等接口arm_dct4_init_q15.c/q31.c定点 DCT 系数如果只用 float 可以整个删掉这套 DEMO 里stm32f4xx_hal_cryp_ex.c是加密外设的扩展驱动和温度采集无关属于 CubeMX 自动生成的 HAL 库文件。源码包里有它说明工程是用 STM32CubeMX 生成的模板改的初始外设勾选时把 CRYP加密处理器模块也勾上了。4.2 两个批处理文件的作用YS-F4Pro.YS-F4Pro.cspy.bat是 IAR 的调试器命令行启动脚本由 IAR Embedded Workbench 自动生成内容通常是调用cspy工具加载调试配置。这个文件只在 IDE 里调试时用到手动删除不影响编译产物。删除编译信息文件(缩小体积方便备份).bat是个实用脚本核心逻辑是删除.axf、.hex、.map、.o等编译中间文件和调试信息保留.c、.h和工程文件。YS-F4Pro.axf是 ARM 可执行文件包含完整的调试符号表体积往往在几 MB 到几十 MB 不等备份源码时删掉它能显著缩小压缩包体积。提示批量删除*.o文件后重新编译时 Keil 或 IAR 会全量重编。如果电脑性能一般编译时长可能从十几秒膨胀到几分钟。建议只在备份前执行清理平时开发保留这些中间文件能加速增量编译。4.3 重新编译时的链接错误排查直接打开工程编译常见的报错是undefined symbol指向arm_common_tables.c里的某个数组。这通常是因为 DSP 库源码文件被误删或者工程文件里的路径指向不存在。解决方案是把缺失的.c文件从原始固件包里复制回工程目录再在 IAR 的Options - C/C Compiler - Preprocessor里确认Include路径包含 DSP 库的Include文件夹。例程的.axf文件还在说明这个 DEMO 已经被成功编译链接过所以源码本身没有语法级的问题。如果自己搭工程时去掉 DSP 库要注意arm_math.h头文件里有些宏定义依赖编译器的__FPU_PRESENT和__FPU_USEDF407 的 FPU 没开启时浮点运算会被强制走软件模拟温度计算会慢一个数量级但不会报错。5. 把这套 DEMO 改成多点测温ROM 遍历、校验和与负数处理5.1 总线上挂多颗 DS18B20 时的遍历策略DS18B20_ReadId只能读出一颗器件的序列号如果总线上挂了三颗需要先发送0x33读 ROM 指令让所有从设备同时响应然后按位仲裁。仲裁过程需要逐位读取每位读两次当读到的两次电平不同说明有从设备在此位产生冲突。例程里没有实现这个算法只做了单器件读取。一个更快但受限的替代方案是每次系统上电时用跳过 ROM 指令读一次温度如果总线上只挂了一颗跳过 ROM 就能工作如果挂了多颗就先发0xCC和0x44同时启动所有器件转换再通过遍历搜索逐颗读取。搜索从 ROM 的最高位开始维护一个冲突位栈每次遇到冲突就强制置 0记录置 1 的分支用于下一次搜索。uint8_t DS18B20_SearchROM(uint8_t *romID) { uint8_t discrepancy 0, lastDiscrepancy 0, romByte 0, romBit 0; uint8_t readBit, readBit2, conflict; DS18B20_Rst(); DS18B20_WriteByte(0xF0); /* 搜索 ROM 指令 */ for (romByte 0; romByte 8; romByte) { for (romBit 0; romBit 8; romBit) { readBit DS18B20_ReadBit(); readBit2 DS18B20_ReadBit(); if (readBit readBit2) { return 0; /* 无器件应答结束搜索 */ } if (readBit ! readBit2) { conflict 1; /* 位有冲突 */ } else { conflict 0; /* 所有器件该位一致 */ } /* 按 lastDiscrepancy 决定写入值具体策略参见单总线搜索算法 */ } } return 1; }这段代码的骨架展示了搜索 ROM 的基本结构完整的搜索算法还需要lastDiscrepancy和分支回溯逻辑。实际工程中如果单总线挂载数量不超过 3 颗更省事的做法是手动记录每颗传感器的序列号在代码里维护一个数组存所有 ROM省去每次都跑搜索算法的开销。5.2 ROM 校验和的快速验证每颗 DS18B20 的 64 位 ROM 最后 8 位是 CRC 校验码验证方式是把前 7 个字节按特定多项式逐位计算得到的结果应当和读到的第 8 字节一致。例程里DS18B20_ReadId已经读出了完整序列号可以在上电自检时打印出来人工和传感器上印刷的序列号比对。如果发现读回来的序列号每次上电都不一样往往是 GPIO 上拉电阻太大或delay_us时序偏慢导致有位被误读。读出的序列号正常但温度值固定在 85℃这是新手最容易撞上的现象。85℃ 是 DS18B20 上电复位的默认寄存器值如果代码没等到转换完成就读取暂存器就会拿到这个假温度。例程通过DS18B20_Rst()后的 500µs 拉低时间恰好覆盖了转换窗口如果你把DS18B20_Rst的延时改成 100µs这个 85℃ 的坑就会重新出现。5.3 零下温度的整数化处理例程里temperature * 10后强转uint32_t温度高于 0℃ 没问题但低于 0℃ 时乘法结果是负数强制转换成无符号整型会变成一个很大的正数屏幕上会跳出乱码。处理负数场景要把数据类型换成int32_tint32_t display_value (int32_t)(temperature * 10); if (display_value 0) { display_value -display_value; HMI_value_setting(page1.gross.val, (uint32_t)(0x10000 - display_value)); }淘晶驰屏的整型属性支持有符号显示直接把(int32_t)(temperature * 10)传给HMI_value_setting也可以。不过例程里HMI_value_setting的形参是uint32_t传负数进来会做隐式转换所以要么改函数签名为int32_t要么在调用处先做符号判断。整型变量int32_t的范围是 -2147483648 到 2147483647DS18B20 的测量范围是 -55℃ 到 125℃乘 10 后是 -550 到 1250远在安全范围内可以放心用。把这个值直接发给串口屏后屏端控件如果需要显示小数点就把控件的vscope或格式属性设为 1 位小数否则看到的就是缩放了 10 倍的整数。本文还有配套的精品资源点击获取