STM32F407驱动DS18B20与TFT GUI:从单点测温到曲线显示
发布时间:2026/10/20 4:52:14 锦皓数字建站

简介这份资源面向嵌入式开发初学者与物联网实践者围绕STM32F407微控制器与DS18B20数字温度传感器的结合应用解决单总线通信时序配置与温度数据可视化显示的问题。项目通过GPIO开漏输出模拟One-Wire协议完成DS18B20初始化、命令发送与温度读取再将数据经SPI或I2C送至TFT屏配合ILI9341等GUI库绘制数字仪表盘或温度计样式界面可作为智能家居与IoT测温节点的参考案例。压缩包共248个文件约10.96MB以h头文件、c源文件为主辅以o、d、crf等编译中间文件另含uvprojx工程、axf可执行映像与map映射文件Library、Project、User、Listing、Output等目录分别承载驱动库、工程配置、用户代码与编译产物结构完整便于二次开发。目前已有1006人学习下载适合希望掌握单总线协议、HAL库驱动与TFT GUI显示的中级开发者参考。1. 从一块“不务正业”的板子说起STM32F407 驱动 DS18B20 到底能玩出什么花很多人第一次拿到 STM32F407 开发板跑完点灯和串口打印就不知道该干嘛了。手头正好有个 DS18B20 温度传感器单总线、三根线、便宜耐造看起来是个练手的好目标。但如果你只把它接到板子上读个温度值发到串口那确实有点浪费这块带 FPU、主频能跑到 168MHz 的 M4 芯片。这份资源有意思的地方在于它把 STM32F407、DS18B20 和 TFT 屏、GUI 放在了一起——温度采集只是起点真正要跑通的是“传感器采集 → 数据处理 → 屏幕刷新”这条完整链路。适合已经会点灯、会串口想找一个能把外设驱动、时序控制和显示交互串起来的练手项目的开发者。下面我就按拿到源码后实际拆解的顺序把这份资源讲清楚。2. 项目定位与技术选型为什么是 F407 DS18B20 TFT GUI 这套组合2.1 三个组件各自承担什么角色先把这个项目的骨架说清楚。STM32F407 是主控负责跑主循环、调度采集和刷新DS18B20 是单总线温度传感器负责把环境温度转成数字量TFT 屏加 GUI 是输出端把温度值、趋势或者状态用图形化方式呈现出来。三者不是简单堆叠而是有明确的数据流向F407 通过 GPIO 模拟单总线时序读 DS18B20拿到原始温度后做换算和滤波再交给 GUI 层去刷新界面。选 F407 而不是 F103常见做法是看中它的主频和浮点单元。DS18B20 本身对算力要求不高但一旦上了 TFT 屏做 GUI 刷新尤其是带缓存的屏或者需要做简单动画、曲线绘制时F103 的 72MHz 就会有点吃力。F407 的 168MHz 加上硬件 FPU做温度换算和屏幕坐标计算会从容很多。DS18B20 这边选它的理由很直接单总线协议只需要一个 GPIO不需要额外 ADC 或者 I2C 外设接线简单成本低而且测温范围 -55 到 125 摄氏度精度在常温下能到 ±0.5做环境监测足够用。TFT GUI 这一层是这份资源的亮点。很多 DS18B20 的例程止步于串口打印但这份资源把显示做进去了。GUI 不一定是 emWin 或者 LVGL 这种大块头也可能是一个轻量的自绘界面核心是把温度值映射成屏幕上的数字、图标或者曲线。这一步跨过去项目就从“传感器实验”变成了“有交互的嵌入式小系统”。2.2 目录结构长什么样拿到压缩包解压后目录结构通常能反映作者的工程组织习惯。这类 STM32 项目常见做法是分成几个大块启动文件与内核相关、外设驱动、中间件、用户应用、以及屏幕相关的底层驱动。下面是一个典型的目录骨架我按实际拆包经验整理出来stm32f407_ds18b20_tft/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── stm32f4xx_hal_conf.h │ │ └── stm32f4xx_it.h │ └── Src/ │ ├── main.c │ ├── stm32f4xx_hal_msp.c │ ├── stm32f4xx_it.c │ └── system_stm32f4xx.c ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ │ └── CMSIS/ ├── BSP/ │ ├── ds18b20.c │ ├── ds18b20.h │ ├── tft.c │ ├── tft.h │ ├── gui.c │ └── gui.h ├── App/ │ ├── app_temp.c │ └── app_temp.h └── MDK-ARM/ 或 STM32CubeIDE/ └── 工程文件这个结构里Core是 CubeMX 生成的框架代码Drivers是 HAL 库和 CMSISBSP放板级支持包也就是 DS18B20、TFT 和 GUI 的驱动App放应用逻辑。把驱动和应用分开的好处是换屏幕或者换传感器时只需要动 BSP 层App 层的逻辑不用大改。我一般也会建议把 DS18B20 的时序函数和业务逻辑分开时序函数只负责“读 bit / 写 bit / 复位”业务层负责“发命令、读暂存器、算温度”。2.3 关键参数与引脚分配DS18B20 用单总线只需要一根 GPIO 加一个上拉电阻。常见接法是数据线接 MCU 的某个 GPIO比如 PA0 或者 PG9然后在数据线和 3.3V 之间挂一个 4.7k 的上拉电阻。TFT 屏这边要看具体型号如果是 SPI 接口的 ILI9341 或者 ST7789通常需要 SCK、MOSI、CS、DC、RST 这几根线再加上背光控制。下面这张表是我从这类项目里整理出来的典型引脚分配实际以你手上的原理图为准功能外设典型引脚备注DS18B20 数据GPIO 输入输出PA0需 4.7k 上拉TFT SPI SCKSPI1PA5时钟TFT SPI MOSISPI1PA7数据TFT CSGPIO 输出PA4片选TFT DCGPIO 输出PA3数据/命令TFT RSTGPIO 输出PA2复位TFT BLKGPIO 输出PA1背光可 PWM引脚不是死的CubeMX 里可以重映射但改完之后 BSP 层的宏定义要跟着改。我见过有人改了引脚但忘了改ds18b20.h里的DS18B20_GPIO_PORT结果读出来一直是 85 度查了半天以为是传感器坏了其实是引脚没对上。3. 跑起来之前环境搭建与工程配置的实操步骤3.1 工具链选择与工程打开这份资源大概率带的是 Keil MDK 或者 STM32CubeIDE 的工程。如果你用 Keil需要装好 MDK-ARM 和对应的 STM32F4 器件包如果用 CubeIDE直接 File → Open Projects from File System 选到工程目录就行。我一般会先确认一件事工程里的 HAL 库版本和你的器件包是否匹配。有时候作者用的是旧版 HAL你本地装了新版编译会报一堆重定义错误。遇到这种情况要么装对应版本的器件包要么把 Drivers 目录下的 HAL 库替换成工程自带的。打开工程后先别急着编译。看一眼main.c里的SystemClock_Config确认主频是不是配到了 168MHz。F407 的外部晶振常见是 8MHz经过 PLL 倍频到 168MHz。如果晶振频率不对串口波特率会偏DS18B20 的延时也会跟着偏读出来的温度就会飘。下面是一段典型的时钟配置代码我加了注释说明关键参数void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; // 使能 HSE晶振为 8MHz RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; // PLLM8, PLLN336, PLLP2 - 8/8*336/2 168MHz RCC_OscInitStruct.PLL.PLLM 8; RCC_OscInitStruct.PLL.PLLN 336; RCC_OscInitStruct.PLL.PLLP RCC_PLLP_DIV2; RCC_OscInitStruct.PLL.PLLQ 7; HAL_RCC_OscConfig(RCC_OscInitStruct); // AHB168M, APB142M, APB284M RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV4; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV2; HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_5); }这段代码里PLLM、PLLN、PLLP三个参数决定了最终主频。公式是HSE / PLLM * PLLN / PLLP。如果你的板子晶振不是 8MHz比如是 25MHz那这三个值都要改否则主频就不是 168MHz。FLASH_LATENCY_5是 168MHz 下需要的等待周期设少了会跑飞。改完时钟后HAL_RCC_GetHCLKFreq()可以打印出来确认一下。3.2 DS18B20 单总线时序的初始化DS18B20 的难点不在协议本身而在时序。单总线对延时很敏感复位脉冲要拉低至少 480 微秒然后释放等待 15 到 60 微秒后检测存在脉冲。写 0 和写 1 的区别在于拉低后的释放时间写 0 要拉低 60 微秒以上写 1 则在 15 微秒内释放。读时序也是类似主机拉低 1 微秒后释放然后在 15 微秒内采样。下面这段是 DS18B20 的复位和读写 bit 的典型实现延时用HAL_Delay或者us级延时函数// 微秒级延时基于 DWT 或者定时器实现 static void ds18b20_delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t cycles us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) cycles); } // 复位 DS18B20返回 0 表示存在1 表示不存在 uint8_t ds18b20_reset(void) { uint8_t presence; DS18B20_OUTPUT_MODE(); DS18B20_LOW(); ds18b20_delay_us(480); // 拉低至少 480us DS18B20_HIGH(); DS18B20_INPUT_MODE(); ds18b20_delay_us(60); // 等待 15-60us presence DS18B20_READ_PIN(); // 读存在脉冲低电平有效 ds18b20_delay_us(420); // 等待复位完成 return presence; } // 写一个字节 void ds18b20_write_byte(uint8_t data) { for (uint8_t i 0; i 8; i) { DS18B20_OUTPUT_MODE(); DS18B20_LOW(); if (data 0x01) { ds18b20_delay_us(2); // 写 1拉低 2us 后释放 DS18B20_HIGH(); ds18b20_delay_us(60); } else { ds18b20_delay_us(60); // 写 0拉低 60us DS18B20_HIGH(); ds18b20_delay_us(2); } data 1; } }这里的关键参数是延时时间。ds18b20_delay_us的精度直接决定通信成败。用HAL_Delay只能到毫秒级不够用所以常见做法是用 DWT 的周期计数器或者一个定时器来做微秒延时。DWT 需要先使能CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk;和DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;。如果读出来一直是 0 或者 85先查延时函数准不准再查上拉电阻有没有接。3.3 TFT 屏与 GUI 的初始化顺序TFT 屏的初始化顺序不能乱。一般是先初始化 SPI 外设再初始化屏的 GPIOCS、DC、RST、BLK然后拉复位、发初始化命令序列最后开背光。GUI 层要在屏初始化完成之后再初始化否则画出来的东西看不见。下面是一个典型的初始化流程void bsp_init(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); tft_init(); // 屏复位、发命令、设方向 tft_fill_screen(BLACK); gui_init(); // 初始化 GUI 缓冲、字体、颜色 ds18b20_init(); // 配置 DS18B20 引脚 }tft_init里面会发一系列命令比如0x11退出睡眠、0x36设扫描方向、0x3A设像素格式。这些命令值跟屏的型号有关ILI9341 和 ST7789 就不一样。如果你烧进去屏幕白屏或者花屏先确认屏的型号和驱动 IC再对一下初始化命令。GUI 层这边如果用的是轻量自绘通常就是一个gui_draw_temp(float temp)函数把温度值转成字符串再调用tft_draw_string画到指定坐标。4. 核心模块拆解从单总线时序到屏幕刷新的完整链路4.1 DS18B20 温度读取与换算DS18B20 读温度的标准流程是复位 → 跳过 ROM0xCC→ 启动转换0x44→ 等待转换完成 → 复位 → 跳过 ROM → 读暂存器0xBE→ 读两个字节 → 换算。转换时间取决于分辨率12 位分辨率下最长 750 毫秒。如果你在主循环里直接等 750 毫秒屏幕刷新会卡顿所以常见做法是用状态机或者定时器来分步执行。下面这段是读温度的核心代码float ds18b20_read_temp(void) { uint8_t temp_l, temp_h; int16_t raw; float result; ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳过 ROM ds18b20_write_byte(0x44); // 启动温度转换 // 这里可以等 750ms或者用状态机分步 HAL_Delay(750); 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; // DS18B20 低 4 位是小数精度 0.0625 result raw * 0.0625f; return result; }raw是有符号 16 位整数高 5 位是符号位低 4 位是小数。0.0625是 1/16因为 12 位分辨率下最小刻度是 0.0625 摄氏度。如果读出来是 85.0说明复位或者读时序有问题传感器没真正响应。如果读出来是 -127通常是上拉电阻没接或者引脚配置错了。我一般会在换算之后加一个简单的滑动平均滤波比如连续读 5 次去掉最大最小再取平均避免屏幕上的数字跳得太厉害。4.2 GUI 层的温度显示与刷新策略GUI 层要做的事情不复杂但要做好也不容易。最基本的是把浮点温度转成字符串比如25.6 C然后画到屏幕上。如果要做曲线就需要维护一个温度数组每次新数据进来就左移然后重绘曲线区域。刷新策略上全屏刷新太慢常见做法是只刷新数值变化的区域或者用双缓冲。下面是一个简单的 GUI 绘制函数void gui_update_temp(float temp) { char buf[16]; // 格式化温度保留一位小数 snprintf(buf, sizeof(buf), %.1f C, temp); // 只刷新数值区域避免全屏重绘 tft_fill_rect(TEMP_X, TEMP_Y, TEMP_W, TEMP_H, BLACK); tft_draw_string(TEMP_X, TEMP_Y, buf, WHITE, BLACK, 24); // 如果要做趋势更新曲线缓冲区 gui_push_temp_to_curve(temp); gui_draw_curve(); }TEMP_X、TEMP_Y是数值显示区域的左上角坐标TEMP_W、TEMP_H是区域宽高。先填充背景色再画字可以避免旧数字残留。tft_draw_string的最后一个参数是字体大小24 号字在 240x320 的屏上比较合适。曲线部分gui_push_temp_to_curve把新温度压入数组gui_draw_curve遍历数组画点连线。如果屏的刷新率不够可以把曲线更新频率降到每秒一次数值刷新保持每秒两次。4.3 主循环的任务调度主循环里要做的事情有三件读温度、更新 GUI、处理其他任务比如串口输出、按键扫描。如果全部串行执行读温度的 750 毫秒等待会让界面卡住。常见做法是用一个软件定时器或者HAL_GetTick()来做非阻塞调度。下面是一个典型的主循环结构uint32_t last_temp_tick 0; uint32_t last_gui_tick 0; float current_temp 0.0f; while (1) { uint32_t now HAL_GetTick(); // 每 1 秒启动一次温度转换 if (now - last_temp_tick 1000) { last_temp_tick now; ds18b20_start_convert(); // 只发转换命令不等待 } // 转换完成后读温度这里简化处理 if (ds18b20_is_conversion_done()) { current_temp ds18b20_read_temp_no_wait(); } // 每 500 毫秒刷新一次 GUI if (now - last_gui_tick 500) { last_gui_tick now; gui_update_temp(current_temp); } // 其他任务 app_uart_report(current_temp); }这里的关键是把“启动转换”和“读温度”分开。ds18b20_start_convert只发 0x44 命令就返回不等 750 毫秒。ds18b20_is_conversion_done可以通过定时器判断时间是否到了或者读总线状态。这样主循环就不会被阻塞GUI 刷新和串口输出都能正常跑。如果你觉得状态机麻烦也可以用一个定时器中断来触发转换和读取主循环只负责显示。5. 避坑与常见问题DS18B20 读 85 度、TFT 白屏这些坑怎么填5.1 读出来一直是 85.0 摄氏度现象串口或者屏幕上温度值固定显示 85.0换传感器也一样。原因85.0 是 DS18B20 的上电默认值说明 MCU 根本没有成功读到转换后的数据。最常见的是复位时序不对或者 GPIO 方向切换没做对。单总线在输出和输入之间切换时如果忘了把引脚设成输入模式读到的永远是高电平。解决先查ds18b20_reset的返回值如果一直返回 1说明没检测到存在脉冲。用示波器或者逻辑分析仪看数据线复位脉冲应该被拉低 480 微秒以上。如果没有仪器就把延时函数单独拿出来测用示波器看一个 500 微秒的方波准不准。另外确认上拉电阻是 4.7k 而不是 10k10k 在长导线下可能拉不起来。5.2 TFT 屏白屏或者花屏现象烧录后屏幕全白或者显示乱码、颜色不对。原因白屏通常是初始化命令没发对或者复位时序不对。花屏多半是像素格式设错了比如屏是 RGB565 但你按 RGB888 发数据。还有一种是 SPI 时钟太快ILI9341 在 168MHz 主频下 SPI 分频后可能超过它支持的 10MHz 上限。解决先确认屏的驱动 IC 型号对着数据手册检查初始化命令序列。SPI 波特率先降到 5MHz 试试如果能显示再往上加。颜色不对就查0x3A命令的参数RGB565 是0x55。还有一点容易忽略CS 和 DC 的初始电平如果 CS 一直拉低命令和数据会混在一起。5.3 温度值跳动厉害现象温度在 25.1 和 25.8 之间反复跳屏幕上的数字一直闪。原因DS18B20 本身精度是 0.0625但电源噪声或者导线太长会引入干扰。另外如果每次读之前没有做滤波原始值就会跳。解决加滑动平均滤波连续读 5 次去掉最大最小再取平均。电源端并一个 0.1uF 的电容。如果导线超过一米考虑用屏蔽线或者降低上拉电阻到 3.3k。GUI 刷新的时候如果温度变化小于 0.2 度就不更新显示也能减少视觉上的跳动。5.4 主循环卡死屏幕不刷新现象程序跑起来后屏幕一直停在初始界面温度也不更新。原因HAL_Delay(750)在等待温度转换时把整个主循环阻塞了。如果 GUI 刷新也在主循环里就会被一起卡住。解决把温度转换改成非阻塞状态机或者用定时器中断来触发。如果不想改架构至少把HAL_Delay换成HAL_GetTick轮询在等待期间去刷新屏幕。我一般会在等待转换的 750 毫秒里插入几次 GUI 刷新让界面保持响应。5.5 编译报错找不到 HAL 库文件现象换了一台电脑或者换了 IDE编译时报stm32f4xx_hal.h找不到。原因工程里的头文件路径是绝对路径或者 HAL 库版本和器件包不匹配。解决在 IDE 的设置里把Drivers/STM32F4xx_HAL_Driver/Inc和Drivers/CMSIS/Include加到包含路径。如果是 CubeIDE右键工程 → Properties → C/C General → Paths and Symbols。还不行就把工程自带的 Drivers 目录整个替换掉本地的库确保版本一致。6. 二次开发可以怎么改从单点测温到多点组网和曲线记录这份资源跑通之后最自然的改造方向是把它从“单点测温”扩展成“多点测温 数据记录 曲线显示”。DS18B20 的单总线本身支持挂多个传感器每个传感器有唯一的 64 位 ROM 地址。你可以先跑一个搜索 ROM 的命令0xF0把总线上的所有地址读出来然后针对每个地址分别发转换和读取命令。这样一根 GPIO 就能挂 5 到 10 个传感器成本极低。改造的时候我一般会先把ds18b20.c里的单设备函数改成带 ROM 地址参数的版本。比如ds18b20_read_temp_by_rom(uint8_t *rom)内部把0xCC跳过 ROM 换成0x55匹配 ROM然后发 8 字节地址。搜索 ROM 的算法稍微复杂一点涉及位碰撞检测但网上有现成的实现可以参考。改完之后App 层维护一个传感器列表每个传感器有自己的温度值和刷新时间。GUI 这边可以加一个曲线记录功能。用一个环形缓冲区存最近 60 个温度点每 10 秒采一个然后画成折线图。屏幕分辨率如果是 240x320可以留出 200x100 的区域画曲线横轴是时间纵轴是温度范围。下面是一个简单的曲线绘制思路#define CURVE_POINTS 60 static float temp_history[CURVE_POINTS]; static uint8_t history_index 0; void gui_push_temp_to_curve(float temp) { temp_history[history_index] temp; history_index (history_index 1) % CURVE_POINTS; } void gui_draw_curve(void) { // 先清空曲线区域 tft_fill_rect(CURVE_X, CURVE_Y, CURVE_W, CURVE_H, BLACK); // 找到最大最小值做归一化 float min_t temp_history[0], max_t temp_history[0]; for (int i 1; i CURVE_POINTS; i) { if (temp_history[i] min_t) min_t temp_history[i]; if (temp_history[i] max_t) max_t temp_history[i]; } if (max_t - min_t 1.0f) { max_t min_t 1.0f; } // 防止除零 // 逐点画线 for (int i 0; i CURVE_POINTS - 1; i) { int idx1 (history_index i) % CURVE_POINTS; int idx2 (history_index i 1) % CURVE_POINTS; int x1 CURVE_X i * CURVE_W / CURVE_POINTS; int x2 CURVE_X (i 1) * CURVE_W / CURVE_POINTS; int y1 CURVE_Y CURVE_H - (int)((temp_history[idx1] - min_t) / (max_t - min_t) * CURVE_H); int y2 CURVE_Y CURVE_H - (int)((temp_history[idx2] - min_t) / (max_t - min_t) * CURVE_H); tft_draw_line(x1, y1, x2, y2, GREEN); } }这段代码里temp_history是环形缓冲区history_index指向最新数据。画曲线之前先找最大最小值做归一化避免温度波动小的时候曲线变成一条直线。tft_draw_line是画线函数如果屏的驱动里没有可以用 Bresenham 算法自己写一个。归一化的时候注意max_t - min_t可能为零加一个最小范围保护。另一个改造方向是加数据记录。F407 有 Flash 和 SDIO可以接一张 SD 卡把温度数据按 CSV 格式存进去。这样设备离线跑一天回来把卡拔出来就能看历史数据。SD 卡这边用 FatFs 文件系统CubeMX 里可以直接勾选。存数据的时候注意不要频繁写可以攒够 100 条再一次性写入减少文件系统开销。最后说一个我自己的习惯每次改完 DS18B20 的时序或者换引脚我都会先用逻辑分析仪抓一遍波形确认复位脉冲、写时隙、读时隙都在规格范围内再去看温度值。因为单总线这东西时序差一点就全盘皆输光看代码是看不出来的。从那以后我每次调单总线外设都强制走一遍示波器省得在“为什么读出来是 85 度”上浪费时间。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。