ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F407与DS18B20温度报警系统实战:从单总线时序到OLED显示

STM32F407与DS18B20温度报警系统实战:从单总线时序到OLED显示 简介这是一份基于STM32F407的嵌入式温度监测报警工程使用DS18B20采集环境温度搭配4针0.96寸OLED显示屏实时刷新数据并借助RTC时钟模块附加当前日期显示适合正在学习STM32 GPIO、定时器、I2C/SPI通信及OLED驱动开发的初中级读者。工程实现了温度超出设定范围即触发声光报警的完整逻辑代码结构清晰且在STM32F4系列芯片上具备良好的可移植性。压缩包共210个文件体积约7.05MB其中包含51个h头文件与49个c源文件以及Keil工程配置文件uvprojx/uvoptx、编译生成的hex/axf/map烧录文件、链接脚本、启动汇编文件等打开工程即可查看或直接编译烧录验证。目前已有2844人学习下载。资料基于作者多次测试稳定的代码整理而成可在此基础上方便地扩展上位机通信、数据记录、多路传感器采集等功能是快速上手STM32F4外设驱动与裸机开发的实用素材。1. 基于STM32F407的DS18B20温度报警系统先从需求倒推方案接了个小需求给冷柜做一个温度监控开机就能在屏幕上看到当前温度超限就响铃屏幕角落再放一行日期。多数人第一反应是买现成的温控模块但要改阈值、改显示内容就得动厂家固件反而更费事。用 STM32F407 最小系统板、DS18B20 和一块 4 针 0.96 寸 OLED整套物料成本最低程序也能完全自己掌控。这也是 STM32 温度传感器报警系统这类项目最常见的组合F407 主频高、外设全DS18B20 测温简单OLED 显示直观。这个标题看起来简单真正动手时三个坎绕不过去DS18B20 的单总线时序在 HAL 库下怎么写才稳定、OLED 的 I2C 驱动是走硬件外设还是软件模拟、报警逻辑怎么避免在阈值附近反复触发。下面把这三条线拆开讲最后给一个可以直接上板的 main 框架。手上有 STM32F407 开发板和 0.96 寸 OLED 显示模块的话跟着步骤走一遍两小时内能跑出第一版。2. 用 STM32F407 写 DS18B20 时序单总线到底怎么拉DS18B20 的核心是单总线协议数据线只有一根既要传数据又要传时序参考。理解它用不着把数据手册背下来抓住复位、写时隙、读时隙这三组动作就够了。2.1 单总线协议的核心复位、读时隙、写时隙主机和 DS18B20 之间通过拉低总线的时间长短来区分信号。平时总线被上拉电阻拉到 3.3V器件不动作时是高电平任何一方要通信就把总线拉低。每次读取温度前必须做一次复位握手目的是让 DS18B20 知道主机要开始一轮操作。2.1.1 复位与存在脉冲上电后先看器件在不在复位时序分四步主机把总线拉低 480us 以上然后释放DS18B20 检测到这个下降沿后等 15 到 60us主动把总线拉低 60 到 240us主机在释放总线后的 60us 左右去读电平读到低说明器件在线读到高说明总线上没有 DS18B20。uint8_t DS18B20_Reset(void) { uint8_t pulse; DS18B20_LOW; // 主机拉低总线 delay_us(480); // 至少 480us常用 500us DS18B20_HIGH; // 释放总线等待器件响应 delay_us(70); // 60us 到 240us 之间采样 pulse DS18B20_READ; // 读回 0存在1未检测到 delay_us(410); // 补足整个复位时隙 return pulse; }引脚宏DS18B20_LOW、DS18B20_HIGH、DS18B20_READ对应 PA0 的写低、写高和读操作。pulse读取的是 IDR 寄存器即使引脚配置成开漏输出只要释放总线后外部上拉生效读到的电平就是真实的总线状态。这里返回1表示异常能检测到器件时返回0。2.1.2 写时隙和读时隙的 1us 差写时隙最小 60us。写 0 是主机拉低总线并保持 60us写 1 是主机拉低 1 到 15us 后立刻释放让上拉电阻把总线拉高。读时隙同样由主机发起拉低 1 到 15us 后释放然后主机在 15us 内采样电平。DS18B20 会在主机释放后立刻把总线拉低或保持高所以采样点不能太靠后超过 15us 容易读到 1。关键时序参数可以收成一张表调试时对照着查时序主机动作DS18B20 动作时间要求复位拉低 480us 后释放拉低 60~240us采样点在释放后 60us 附近写 0拉低并保持等待整个时隙 60~120us写 1拉低 1~15us 后释放等待采样窗口在 15~60us读 0/1拉低 1~15us 后释放15us 内拉低或保持高主机在第 15us 采样温度转换等待内部转换12 位分辨率最大 750ms2.2 用 HAL 库模拟单总线GPIO 开漏输出与延时HAL 库的HAL_GPIO_WritePin和HAL_GPIO_ReadPin本身不慢但HAL_Delay最小单位是 1ms没法用在 15us 的采样窗口上。常见做法是用 DWT 实现微秒延时或者用普通 for 循环粗略卡时间。DWT 更准确而且不占用定时器。2.2.1 引脚初始化为什么必须开漏DS18B20 数据线推荐配成开漏输出。开漏模式下写 1 相当于释放总线引脚被外部上拉拉高写 0 才是真正拉低。这样主机和 DS18B20 能同时操作同一条线不会出现推挽输出时两边打架的情况。void DS18B20_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出写1释放 GPIO_InitStruct.Pull GPIO_PULLUP; // 内部上拉配合外部4.7k GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); }GPIO_PULLUP只是兜底板上最好再加一个 4.7k 上拉到 3.3V。测量时用示波器看 DS18B20 数据脚如果低电平拖尾明显通常是上拉偏小或走线太长。2.2.2 1us 延时用 DWT 而不是 HAL_DelayDWT 是 Cortex-M4 内核自带的调试计数器168MHz 下一个 tick 约 6ns做微秒延时足够。static void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); }SystemCoreClock默认是 168MHz计算出的ticks就是延时对应的周期数。注意 DWT 计数器是 32 位的DWT-CYCCNT - start用无符号减法处理回绕所以长时间运行也不用担心溢出。2.3 读温度完整流程与分辨率配置单根总线上只挂一个 DS18B20 时流程固定为复位、跳过 ROM、启动温度转换、等待、再复位、跳过 ROM、读暂存器。2.3.1 跳过 ROM 读单个 DS18B20 的时序顺序float DS18B20_ReadTemperature(void) { uint8_t buf[9]; if (DS18B20_Reset()) return -100.0f; // 未检测到器件 DS18B20_WriteByte(0xCC); // 跳过ROM单点使用 DS18B20_WriteByte(0x44); // 启动温度转换 HAL_Delay(750); // 12位分辨率最长750ms if (DS18B20_Reset()) return -100.0f; DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器从第0字节开始 for (uint8_t i 0; i 9; i) { buf[i] DS18B20_ReadByte(); // 前2字节是温度第9字节CRC } int16_t raw ((int16_t)buf[1] 8) | buf[0]; return raw * 0.0625f; // 12位分辨率LSB0.0625度 }0xCC跳过 ROM 适用于单点测温省去匹配 64 位序列号。启动转换命令是0x44它会让 DS18B20 进入转换状态转换期间数据线保持低电平用HAL_Delay(750)是为了避免死等也简化了超时处理。读暂存器用0xBE前两个字节分别是温度低字节和高字节第 9 个字节是 CRC如果需要更严谨的校验可以按数据手册的 CRC 多项式算一遍再判断。2.3.2 12 位分辨率下的转换时间表DS18B20 默认 12 位分辨率最小刻度 0.0625 度。也可以把分辨率改低来换速度写入配置寄存器0x4E后需要把告警高低字节一起补齐。分辨率转换时间温度精度适用场景9 位93.75ms0.5 度快速巡检10 位187.5ms0.25 度一般监控11 位375ms0.125 度温控要求较高12 位750ms0.0625 度默认按这个值写延时如果发现读温度偶发出现 85 度那是 DS18B20 上电时的固化值不是真实温度程序里可以先读一次丢弃或者对连续两次数值做合理性判断。3. 0.96 寸 OLED 显示模块的 HAL 库驱动从模拟 I2C 到汉字4 针 0.96 寸 OLED 显示模块用的是 SSD1306 驱动芯片接口是 I2C。和 DS18B20 的单总线不同OLED 只需要主机单向写数据逻辑上简单很多。3.1 SSD1306 寻址模式与 4 针接线的对应关系SSD1306 的 128x64 显存分成 8 页每页 8 个像素高。写数据时有两种常用寻址模式页寻址和水平寻址。页寻址下写完一列自动加 1到 127 后回到开头适合逐行显示水平寻址适合整屏填充。之后显示的日期、温度都是用页寻址模式来做代码直观。4 针 OLED 的引脚定义固定GND、VCC、SCL、SDA。VCC 接 3.3VSCL 接 PB6SDA 接 PB7正好是 STM32F407 的 I2C1 引脚位置。不过 F407 的硬件 I2C 对时序要求严格加上 OLED 走线稍长就容易出现偶发死锁实际项目里很多人直接用模拟 I2C也就是用两个 GPIO 手写时序。模拟 I2C 的优点是随便换引脚、不用排查外设状态寄存器调试门槛低不少。3.2 用 HAL 库模拟 I2C 点亮 OLED 的最小代码模拟 I2C 时SCL 用推挽输出SDA 配成开漏输出。这样 SDA 释放时是被上拉拉高的主机也能读回从机的 ACK不需要切换方向。3.2.1 起始停止与 ACK 处理static void I2C_Start(void) { OLED_SDA_HIGH; OLED_SCL_HIGH; delay_us(5); OLED_SDA_LOW; // SCL高电平期间SDA拉低产生起始条件 delay_us(5); OLED_SCL_LOW; } static void I2C_Stop(void) { OLED_SDA_LOW; OLED_SCL_HIGH; delay_us(5); OLED_SDA_HIGH; // SCL高电平期间SDA拉高产生停止条件 delay_us(5); } static uint8_t I2C_WaitAck(void) { OLED_SDA_HIGH; // 释放SDA等从机拉低 delay_us(3); OLED_SCL_HIGH; delay_us(3); uint8_t ack OLED_SDA_READ; // 低电平表示从机已应答 OLED_SCL_LOW; delay_us(3); return ack; } static void I2C_SendByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { OLED_SCL_LOW; if (dat 0x80) OLED_SDA_HIGH; else OLED_SDA_LOW; delay_us(2); OLED_SCL_HIGH; // SCL上升沿从机采样 delay_us(2); dat 1; } OLED_SCL_LOW; }起始条件和停止条件都要求在 SCL 为高时切换 SDA。发送字节时先准备 SDA 数据再拉高 SCL让从机在上升沿采样。I2C_WaitAck如果返回非 0说明 OLED 没有响应常见原因是地址写错或者 SDA 没接上拉。3.2.2 初始化命令序列SSD1306 上电后默认处于关闭显示状态必须先发初始化命令再点亮。核心命令如下命令参数作用0xAE / 0xAF无关闭 / 打开显示0xD50x80显示时钟分频0xA80x3F复用比 1/640x8D0x14开启电荷泵必须设0x200x02页寻址模式0x810xCF对比度0xA1 / 0xC8无段重映射与扫描方向0xDA0x12COM 引脚硬件配置void OLED_Init(void) { delay_us(100); // 上电等待 OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); OLED_WriteCmd(0x40); // 显示起始行0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 电荷泵开 OLED_WriteCmd(0x20); OLED_WriteCmd(0x02); // 页寻址 OLED_WriteCmd(0xA1); OLED_WriteCmd(0xC8); OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); OLED_WriteCmd(0xA4); OLED_WriteCmd(0xA6); OLED_WriteCmd(0xAF); // 打开显示 }这里OLED_WriteCmd发的是控制字节0x00加命令本身OLED_WriteData发的是控制字节0x40加数据。凡是0xAF之后屏幕还黑着优先查电荷泵0x8D和0x14有没有配上。3.3 显示温度与日期8x16 字符和 16x16 中文字模OLED 本身没有字库所有字形都要以点阵数据形式写进显存。做报警系统至少要准备两套字模ASCII 的 8x16 字符和汉字的 16x16 点阵。3.3.1 ASCII 字模的打点逻辑void OLED_ShowChar(uint8_t x, uint8_t page, char ch) { const uint8_t *src asc2_1616[(ch - )][0]; OLED_SetCursor(x, page); for (uint8_t i 0; i 16; i) { OLED_WriteData(src[i]); // 每一列一个字节共16列 } }asc2_1616是一个 95 行、每行 16 字节的字模表用取模软件从标准 ASCII 字库生成。OLED_SetCursor内部会发送页地址和列地址。显示一行字符串时每显示一个字符后x 8到 120 左右自动换行。3.3.2 OLED 显示汉字的取模参数汉字宽度是 16 像素一个 16x16 字形占 32 字节。取模软件生成时配置为“逐行式、阴码、列行式”得到的就是从左上角开始、每行两个字节、共 16 行的数据。显示时上半页写前 16 字节下半页写后 16 字节。void OLED_ShowCN16(uint8_t x, uint8_t page, uint8_t index) { OLED_SetCursor(x, page); // 上半页 for (uint8_t i 0; i 16; i) { OLED_WriteData(font16x16[index][i]); } OLED_SetCursor(x, page 1); // 下半页 for (uint8_t i 16; i 32; i) { OLED_WriteData(font16x16[index][i]); } }font16x16是按固定顺序排列的汉字点阵数组比如前 20 个位置分别对应“温度报警系统正常异常”之类的字符。128x64 的屏一页最多放 8 个 16x16 汉字一行放 4 个比较舒服。OLED 显示汉字的关键是把取模方式和数据排列对齐排不对就会出现字上下两半错位。4. 报警阈值、滞回与日期显示的软件实现温度读出来、日期显示出来后剩下就是把报警逻辑做对。这个环节最容易出现的是蜂鸣器在阈值附近疯狂抖动。4.1 阈值判断与滞回为什么蜂鸣器会临界抖动如果判断写成“温度大于 30 度就响小于 30 度就停”那么温度在 29.9 和 30.1 之间波动时报警信号会不停翻转继电器触点很快烧掉。解决手段是滞回进条件用高阈值出条件用低阈值中间留一个缓冲区。4.1.1 滞回参数设置表以高温报警为例合理的一组参数是参数值说明高温报警阈值30.0 度超过后进入报警高温恢复阈值29.0 度低于该值才解除报警低温报警阈值5.0 度低于后进入报警低温恢复阈值6.0 度高于该值才解除报警温差点1.0 度滞回带宽度4.1.2 区间判断代码typedef struct { float high_threshold; // 高温报警点 float low_threshold; // 低温报警点 float hysteresis; // 滞回带宽 uint8_t alarm; // 当前报警状态 } Alarm_t; void Alarm_Update(Alarm_t *a, float temp) { if (temp a-high_threshold || temp a-low_threshold) { a-alarm 1; // 进入报警区 } else if (temp a-high_threshold - a-hysteresis temp a-low_threshold a-hysteresis) { a-alarm 0; // 回到安全区才解除 } }判断逻辑强调区间而不是单点。温度从 29.5 升到 30.2alarm置 1之后必须回落到 29.0 以下才清零中间的 29.3、29.8 都不会触发。低温度报警同理需要回升到低阈值加滞回带后才能解除。这样写还有个好处不会出现两条独立判断互相覆盖的 bug。4.2 报警动作执行与状态回显报警状态的消费方通常有三个蜂鸣器、OLED 顶部的状态文字、串口日志。蜂鸣器接到一个 GPIO 上用推挽输出驱动主动蜂鸣器高电平响如果接的是继电器模块要留意继电器线圈需要三极管或光耦驱动不能直接挂单片机引脚上。void Alarm_Execute(Alarm_t *a) { HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, a-alarm ? GPIO_PIN_SET : GPIO_PIN_RESET); if (a-alarm) { OLED_ShowCN16(0, 4, 0); // 显示“报警”两个汉字 OLED_ShowCN16(32, 4, 1); } else { OLED_ShowCN16(0, 4, 2); // 显示“正常” OLED_ShowCN16(32, 4, 3); } }实际项目里蜂鸣器不宜一直响常见做法是alarm为真时让蜂鸣器按 1Hz 占空比响。这个可以放到主循环里用HAL_GetTick()做不影响温度采集的节拍。4.3 日期显示软 RTC 的实现与边界标题里“附带日期显示”有两种实现路径。第一种是 STM32F407 内部 RTC需要外接 32768Hz 晶振和备份电池第二种是软件 RTC用一个结构体保存年月日时分秒在主循环里每秒加 1。开发板上不一定贴了 RTC 晶振软 RTC 更稳妥。4.3.1 闰年与月份天数日期不能止步于秒数累加月份天数和闰年必须处理。闰年规则是能被 4 整除但不能被 100 整除或者能被 400 整除。const uint8_t MonthDays[] {31,28,31,30,31,30,31,31,30,31,30,31}; uint8_t IsLeapYear(uint16_t year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); } void SoftRTC_Tick(SoftRTC_t *rtc) { rtc-second; if (rtc-second 60) return; rtc-second 0; rtc-minute; if (rtc-minute 60) return; rtc-minute 0; rtc-hour; if (rtc-hour 24) return; rtc-hour 0; rtc-day; uint8_t max_day MonthDays[rtc-month - 1]; if (rtc-month 2 IsLeapYear(rtc-year)) max_day 29; if (rtc-day max_day) return; rtc-day 1; rtc-month; if (rtc-month 12) { rtc-month 1; rtc-year; } }这个函数放在 1ms 或 10ms 的主循环里用标志位触发每秒调用一次。注意格式化和显示时year用完整四位年号OLED 上用%04d-%02d-%02d输出。4.3.2 校准初始时间软 RTC 掉电不保持上电时需要一个初始时间。最简单的方式是在代码里写死一个默认值再留一个串口命令或两个按键来调整。串口方式更省 IOSoftRTC_Init(rtc, 2025, 1, 7, 12, 0, 0); // 每年发布前改一次如果要做到掉电后日期不丢可以把秒数写入 STM32F407 的备份寄存器需要时再读回来备份域由 VBAT 供电主电源掉了也能维持。这是软 RTC 和内部 RTC 之间的折中方案。5. 串起来之后main 函数轮询框架与三点现场调试技巧把 DS18B20 读取、OLED 刷新、报警判断、日期累加放进同一个 main最难的是节奏控制因为 DS18B20 每次转换要 750msOLED 刷新又不能用阻塞延时拖住温度采集。5.1 2 秒刷新节奏的 main 逻辑int main(void) { HAL_Init(); SystemClock_Config(); // 168MHz DWT_Delay_Init(); DS18B20_GPIO_Init(); OLED_Init(); OLED_Clear(); SoftRTC_t rtc {2025, 1, 7, 12, 0, 0}; Alarm_t alarm {30.0f, 5.0f, 1.0f, 0}; uint32_t next_read HAL_GetTick(); uint32_t next_rtc HAL_GetTick(); while (1) { if (HAL_GetTick() next_read) { float temp DS18B20_ReadTemperature(); // 内部阻塞750ms if (temp -55.0f temp 125.0f) { Alarm_Update(alarm, temp); Alarm_Execute(alarm); OLED_ShowTemperature(temp); } else { OLED_ShowString(0, 4, Sensor Err); } next_read 2000; // 固定2秒周期 } if (HAL_GetTick() next_rtc) { SoftRTC_Tick(rtc); OLED_ShowDate(rtc); next_rtc 1000; } } }时间轴很清晰温度读取周期是 2 秒因为读函数内部已经等了 750ms按 2 秒一轮感官上很舒服。日期每秒刷一次温度刷新和日期刷新互不阻塞。OLED 刷新时先把内容画到内存缓存再整体刷一次显存可以避免屏上出现半行残影。5.2 三个调试技巧从时序到阈值验证第一招DS18B20 读不到数据时别急着改代码用逻辑分析仪抓数据脚。复位后能不能看到一个明显的存在脉冲写时序里每个时隙是否保持 60us一眼就能定位是连接问题还是延时问题。没有逻辑分析仪时可以先用 GPIO 翻转法把 DS18B20 的上拉脚接到一个空闲引脚然后点灯看状态。第二招OLED 不亮时不要只查接线。先看 SDA 地址对不对SSD1306 的 I2C 地址是 0x3C左移一位后是 0x78再确认发送控制字节时用的到底是 0x00 还是 0x40命令和数据混了屏幕会花。把初始化命令逐条发进串口打印配合示波器看 SCL 上有没有波形很快能判断是初始化序列问题还是时序太慢。第三招验证报警逻辑用“反向调阈值”。运行状态下把high_threshold调成当前温度减 1 度蜂鸣器应该立刻响再把阈值调回当前温度加 2 度确认报警解除。接着测试滞回让温度缓慢穿过阈值看报警翻转是否干脆。这个测试应该放在温度稳定时做不要在空调出风口旁边测否则会被气流干扰。5.3 把 OLED 刷新率压下去的功耗技巧系统稳定之后OLED 的刷新率是可以优化的。温度显示小数点后一位每秒刷两次就够了日期只在变化时刷新秒钟显示可以只更新最后两位不要每次都整屏重写。把 OLED 刷新的目标区域缩小到变化行发送的数据量会少一半以上。如果整机要省电还可以在不需要显示时让 OLED 进入休眠模式发送0xAE关掉显示再送0x8D和0x10关闭电荷泵需要看时再按初始化序列恢复。加上 DS18B20 的读取周期从 2 秒拉长到 30 秒整机静态工作电流能从十几毫安降到个位数毫安这在电池供电的温度记录场景里足够撑一个晚上。本文还有配套的精品资源点击获取
返回列表