基于STM32与DS18B20的温控系统Proteus仿真设计与实现

发布时间:2026/9/17 5:45:11
基于STM32与DS18B20的温控系统Proteus仿真设计与实现 简介基于STM32微控制器的DS18B20温度采集与报警系统完整项目主要面向单片机初学者、课程设计学生以及电子竞赛备赛者。系统采用STM32作为主控芯片由DS18B20数字温度传感器采集环境温度并将温度值通过IIC通信协议实时显示在OLED液晶屏上。用户可以通过按键设置温度报警阈值当实测温度低于阈值时加热继电器吸合并启动加热当温度高于阈值时继电器断开停止加热从而形成一个简单实用的闭环温度控制方案。压缩包内共包含191个文件大小仅3.89MB包内以C语言源文件、H头文件、Keil工程配置、Proteus仿真工程及hex烧录文件为主同时还包括文本说明、图片与网页文档可以辅助理解项目结构与原理。已有783人学习使用具备较高的参考价值通过这套资料读者可以学习STM32的GPIO、定时器与中断配置掌握DS18B20单总线时序读取、OLED的I2C驱动开发、按键消抖与继电器驱动控制等关键技能同时借助Proteus仿真在无硬件条件下完成功能验证是一份适合入门到进阶的实用工程参考资料。1. 一个带继电器控制的温度监控系统为什么值得拆开环境温度监控在仓储、养殖、培养箱这类场景里最常见的实现并不是直接上 PLC而是一颗 STM32、一颗 DS18B20、一块小 OLED 加上两个按键。这个系统的价值不在硬件贵而在完整走通了「单总线传感器采集 → IIC 屏幕显示 → 按键参数设置 → 继电器开关控制」这条最常见的嵌入式控制链路。拿到这套基于 STM32 DS18B20 OLED 的 Proteus 仿真工程你可以直接在电脑上调试完整的温控逻辑不烧一片板子。适合刚学完 STM32 标准库、想自己动手做一个小系统的读者也适合准备课程设计的人参考整体架构。这套设计里最有嚼头、也最容易踩坑的是三块DS18B20 的单总线时序、OLED 的 IIC 显示流程、阈值控制里的回差处理。2. DS18B20 温度采集单总线时序和精度处理是分水岭DS18B20 是达拉斯半导体出品的单总线数字温度传感器测温范围 -55℃ 到 125℃12 位分辨率下温度分辨率是 0.0625℃。它只有一根数据线但这一根线上同时承载复位、存在检测、指令和数据的双向传输所有时序都由软件延时驱动。工程里经常看到有人把 DS18B20 读出来的数据直接拿去显示结果温度在 25℃ 附近乱跳或者一直是 85℃这基本就是没处理好时序和原始数据。2.1 初始化时序先做完整的复位和存在脉冲检测单总线上所有通信都从复位开始。主机将总线拉低至少 480μs然后释放总线DS18B20 检测到这个下降沿后会等待 15-60μs再拉低总线 60-240μs 作为存在脉冲。所以在驱动代码里初始化函数不仅要做 GPIO 配置还要有一套完整的复位检测逻辑。我把 GPIO 方向切换做成两个独立函数保证总线在输入输出之间切换时不会有额外电平毛刺。#define DS18B20_PORT GPIOB #define DS18B20_PIN GPIO_Pin_0 #define DS18B20_CLK RCC_APB2Periph_GPIOB static void DS18B20_Mode_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DS18B20_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DS18B20_PORT, GPIO_InitStructure); } static void DS18B20_Mode_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DS18B20_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(DS18B20_PORT, GPIO_InitStructure); }输入模式用GPIO_Mode_IPU而不是GPIO_Mode_IN_FLOATING这是关键。DS18B20 是开漏输出结构总线空闲时靠上拉电阻维持高电平浮空输入会读到不确定电平。STM32 内部上拉约 30-50kΩ配合较短的总线长度足够稳定但如果是真实电路且要外接长线建议在数据线上再并一个 4.7kΩ 外部上拉。以上这段代码配置了 STM32 的 GPIOB Pin 0 作为数据口把方向切换封装成两个静态函数后续所有读写时序都基于这两个函数。2.2 写时序与读时序控制采样点位置DS18B20 的写时序分为写“0”和写“1”。写“0”时主机拉低总线 60-120μs写“1”时主机拉低总线 1-15μs 后释放。读时序由主机拉低总线 1-15μs 后释放然后在释放后的 15μs 内读取总线电平。关键是读采样点必须在 15μs 窗口内完成太晚会读到下一个位周期的电平。static void DS18B20_Write_Bit(uint8_t bit) { DS18B20_Mode_Output(); DS18B20_PORT-BRR DS18B20_PIN; /* 拉低总线 */ delay_us(5); /* 稳定时间 */ if (bit) { DS18B20_PORT-BSRR DS18B20_PIN; /* 写1时释放总线 */ } delay_us(60); DS18B20_PORT-BSRR DS18B20_PIN; /* 结束位周期 */ } static uint8_t DS18B20_Read_Bit(void) { uint8_t val; DS18B20_Mode_Output(); DS18B20_PORT-BRR DS18B20_PIN; /* 启动读时序 */ delay_us(2); DS18B20_PORT-BSRR DS18B20_PIN; /* 释放总线 */ delay_us(8); DS18B20_Mode_Input(); delay_us(5); /* 采样点 */ if (DS18B20_PORT-IDR DS18B20_PIN) { val 1; } else { val 0; } delay_us(50); /* 补足读时隙 */ return val; }delay_us(5)、delay_us(8)、delay_us(5) 这几个参数组合起来把采样点卡在释放总线后约 13μs 的位置。不同主频下 delay_us 的精度不一样我在 STM32F103 72MHz 下用定时器做了微秒级延时直接用空循环的版本必须在 Keil 里开启优化等级时重新实测一次。写时序里第二个delay_us(60)保证位周期不小于 60μs因为 DS18B20 要求两次独立时隙之间至少有 1μs 恢复时间实际工程里统一按 70μs 左右的位周期处理。2.3 温度读取流程和负数处理一次完整的温度转换流程分三步复位并检测存在脉冲 → 发送 0xCC (Skip ROM) 0x44 (Convert T) 启动温度转换 → 等待转换完成后再次复位 → 发送 0xCC 0xBE (Read Scratchpad) 读取 16 位温度寄存器。下面这段代码实现了完整的读取流程并把原始数据换算成整数温度扩大 10 倍方便 OLED 显示一位小数。int16_t DS18B20_Read_Temp(void) { uint8_t low_byte, high_byte; int16_t temp_raw; uint8_t presence; int16_t temp_result; presence DS18B20_Reset(); if (presence 1) { return 0xFFFF; /* 无应答返回错误标志 */ } DS18B20_Write_Byte(0xCC); /* Skip ROM单总线只有一颗传感器时使用 */ DS18B20_Write_Byte(0x44); /* 启动温度转换 */ delay_ms(750); /* 12位精度转换时间最大750ms */ DS18B20_Reset(); DS18B20_Write_Byte(0xCC); DS18B20_Write_Byte(0xBE); /* 读取暂存器 */ low_byte DS18B20_Read_Byte(); high_byte DS18B20_Read_Byte(); temp_raw (high_byte 8) | low_byte; /* 温度分辨率 0.0625℃转换成扩大10倍的整数 */ if (temp_raw 0x8000) { /* 判断最高位处理零下温度 */ temp_raw ~temp_raw 1; temp_result (int16_t)(temp_raw * 625) / 100; /* 负温度 */ temp_result -temp_result; } else { temp_result (int16_t)(temp_raw * 625) / 100; } return temp_result; }我把温度换算成temp_result * 10的定点数而不是直接用浮点。原因有两点第一OLED 显示小数字符时浮点格式化sprintf在 STM32F103 上要占不少 Flash 和栈空间对小资源项目不划算第二后面做阈值比较时定点数可以直接做整数比较继电器控制逻辑更干净。temp_raw * 625 / 100这段是整个读温度逻辑的核心DS18B20 的 12 位原始数据 1 LSB 代表 0.0625℃乘以 625 再除以 100 之后得到十分之一度的整数。比如原始值 0x0191 表示 25.0625℃换算结果是 250也就是 25.0℃。2.4 复位函数的返回值决定上层逻辑复位函数除了拉低总线还负责判断 DS18B20 是否在线。如果存在脉冲没有出现上层就不要再做后续操作否则读到的一定是垃圾数据。下面是复位和存在检测的参考写法。uint8_t DS18B20_Reset(void) { uint8_t presence; DS18B20_Mode_Output(); DS18B20_PORT-BRR DS18B20_PIN; /* 拉低复位脉冲 */ delay_us(500); /* 480-960us 均可 */ DS18B20_PORT-BSRR DS18B20_PIN; /* 释放总线 */ delay_us(60); /* 等待存在脉冲窗口 */ DS18B20_Mode_Input(); delay_us(5); presence (DS18B20_PORT-IDR DS18B20_PIN) ? 1 : 0; /* 读到低电平表示存在 */ delay_us(500); return presence; }一个容易忽略的细节是存在脉冲结束后总线会被 DS18B20 释放回高电平所以函数末尾要留出足够时间让总线稳定再做下一次时序。我见过有人把delay_us(500)去掉连续调用 Reset 时就偶发失败。返回值presence 0表示检测到存在脉冲presence 1表示无响应这个取反逻辑在多种参考代码里有不同写法建议在开发板上先用逻辑分析仪确认一次。3. SSD1306 驱动的 OLED 显示用 IIC 把数据搬上屏幕这套系统里 OLED 是 0.96 寸、分辨率为 128x64、控制器是 SSD1306IIC 接口。控制器的从机地址由 SA0 引脚决定常见模块上 SA0 接地7 位地址是 0x3C加上写位后的字节是 0x78。IIC 通信本身不复杂但显示一个温度值需要弄清楚三个概念显存是 1KB 的显存按页划分字符本质上是从字模数组里取点阵。3.1 页寻址模式一个字节对应 8 个像素点SSD1306 的 128x64 显存被划分成 8 页每页 128 字节每字节表示该页 8 个像素的垂直方向点阵。页地址模式下写数据时先设置页地址0xB0-0xB7再设置列地址低 4 位 高 4 位然后连续写入数据每写一个字节列地址自动加 1。这解释了一个常见问题为什么显示字符串要按 8 像素高度设计因为字体点阵正好占满一页的 8 个像素行。#define OLED_ADDR 0x78 #define OLED_CMD 0x00 #define OLED_DATA 0x40 void OLED_Write_Byte(uint8_t dat, uint8_t cmd_or_data) { I2C_Start(); I2C_SendByte(OLED_ADDR); /* 0x78 写地址 */ if (cmd_or_data) { I2C_SendByte(OLED_DATA); /* 数据控制字节 */ } else { I2C_SendByte(OLED_CMD); /* 命令控制字节 */ } I2C_SendByte(dat); I2C_Stop(); } void OLED_SetPos(uint8_t page, uint8_t col) { OLED_Write_Byte(0xB0 page, OLED_CMD); /* 页地址 0x00-0x07 */ OLED_Write_Byte(((col 0xF0) 4) | 0x10, OLED_CMD); /* 列地址高4位 */ OLED_Write_Byte(col 0x0F, OLED_CMD); /* 列地址低4位 */ }I2C_SendByte这里是基于 STM32 标准库的软件模拟 IIC挂接在 GPIOB 的 PB8 (SCL)、PB9 (SDA) 上。多数 0.96 寸 OLED 模块在 IIC 模式下 SCL 约 100-400kHz 都能正常工作我一般让软件模拟 IIC 的速度落在 200kHz 附近足够刷新温度显示且抗干扰较好。3.2 初始化序列和字符显示SSD1306 上电后的初始化序列较长但固定核心命令包括关闭显示、设置显示时钟分频、设置复用率、设置对比度、设置页地址模式、打开电荷泵、开启显示。以 128x64 为例下面这张表列出初始化里值得留意的几个命令后续做屏幕闪烁或者降低功耗时你会经常改到这几条。命令数据作用0xAE / 0xAF-关闭/开启显示0x8D0x14打开内部 DC-DC 电荷泵0xC8-扫描方向从 COM63 到 COM00x810xCF设置对比度0x7F 是默认值0x200x02使用页地址模式初始化完成后清屏就是把 8 页、每页 128 字节全部写 0。显示一个字符的原理是从字模数组oled_font6x8[]中取出字符对应的 8 字节数据逐字节写到当前页。下面是一个最小化的字符显示函数。void OLED_ShowChar(uint8_t page, uint8_t col, char ch) { uint8_t i; OLED_SetPos(page, col); for (i 0; i 8; i) { OLED_Write_Byte(oled_font6x8[ch - ][i], OLED_DATA); } } void OLED_ShowSignedNum(uint8_t page, uint8_t col, int16_t temp_x10) { uint8_t i; uint8_t buf[8]; int16_t abs_temp temp_x10; if (temp_x10 0) { OLED_ShowChar(page, col, -); abs_temp -temp_x10; } else { OLED_ShowChar(page, col, ); } buf[0] abs_temp / 1000 % 10 0; /* 十位 */ buf[1] abs_temp / 100 % 10 0; /* 个位 */ buf[2] .; buf[3] abs_temp / 10 % 10 0; /* 十分位 */ buf[4] abs_temp % 10 0; /* 百分位 */ for (i 0; i 5; i) { OLED_ShowChar(page, col 1 i, buf[i]); } }显示函数本身不复杂但很多初学者会忽略 OLED 的写数据方向逐字节纵向排列字符数组的第 0 字节对应的是字符顶部像素因此取模工具里的“纵向取模、字节倒序”设置必须和显示函数匹配。我在实际排错中遇到过不少次屏幕显示成左右镜像或者上下颠倒基本都是取模方式和这里不匹配导致的。3.3 主界面布局和刷新策略OLED 的刷新策略直接决定了会不会闪烁。SSD1306 没有自动刷新机制主循环里反复全屏更新数据时如果同时更新整屏显存用软件模拟 IIC 在 72MHz 主频下大约需要 20ms 以上肉眼看不出明显闪烁但会拖慢主循环。更好的做法是只在温度值变化超过 0.1℃ 时才更新数字区域阈值和状态栏内容不变就不重绘。void Display_MainPage(int16_t cur_temp, int16_t set_temp, uint8_t heat_state) { static int16_t last_temp 0x7FFF; static uint8_t last_heat_state 0xFF; if (cur_temp ! last_temp) { OLED_ShowSignedNum(2, 20, cur_temp); /* 第2页列20 */ OLED_ShowString(2, 60, C); /* 单位符号 */ last_temp cur_temp; } if (heat_state ! last_heat_state) { OLED_ShowString(5, 10, heat_state ? HEAT ON : HEAT OFF); last_heat_state heat_state; } }这个函数的关键在于把“变化才刷新”变成默认行为。继电器状态切换频率很低温度变化也局限在零点几度静态局部变量记录上次值只有变化时才真正调用 OLED 写函数主循环可以空出来做按键扫描。4. 按键设置阈值与继电器控制加入滞回避免继电器抖动按键和继电器是这套系统里逻辑密度最高的部分。需求描述是按键设置阈值低于阈值加热继电器启动高于阈值停止加热。这里最容易犯的错误是不加回差直接把“低于阈值启动、高于阈值停止”写成互斥条件。实际温度在被控对象上有惯性继电器会在阈值附近高速切换触点寿命被大幅缩短。我在这套系统的控制逻辑里加入了约 1℃ 的滞回区间。4.1 按键功能划分与消抖状态机三个按键分别承担“进入设置/切换参数项”“加号”“减号”的职责。物理结构上按键接在 GPIO 和 GND 之间GPIO 初始化为上拉输入按下时读到低电平。按键消抖方案我选状态机而不是简单的 delay 消抖原因是一个温控系统的主循环里同时要做温度转换等待和 OLED 刷新如果每个按键都占 20ms 阻塞延时主循环响应会变差。#define KEY_SET GPIO_Pin_3 #define KEY_UP GPIO_Pin_4 #define KEY_DOWN GPIO_Pin_5 #define KEY_PORT GPIOA uint8_t Key_Scan(uint8_t mode) { static uint8_t key_up_flag 1; uint8_t key_val 0; if (mode 1) { key_up_flag 1; /* 连按模式 */ } if (key_up_flag (KEY_PORT-IDR KEY_SET) 0) { delay_ms(10); /* 软件消抖 */ if ((KEY_PORT-IDR KEY_SET) 0) { key_val 1; key_up_flag 0; } } else if ((KEY_PORT-IDR KEY_SET) ! 0) { key_up_flag 1; /* 按键释放恢复标志 */ } return key_val; }mode参数控制是否支持连续按键。设置阈值时希望按住加号键连续增加数值所以把mode设为 1每次按键触发后key_up_flag被清除只有释放后才会再次响应要支持连加效果则在按键按下期间周期性调用Key_Scan(1)第一个 10ms 消抖后进入长按累加逻辑。4.2 阈值范围与步进逻辑阈值本质上是一个int16_t类型的定点数单位是 0.1℃默认设 25.0℃。为了不让用户把阈值设置成超出 DS18B20 量程的荒谬值设置界面的加减操作要夹在两个边界内。这里用了一个比较实用的处理步进值动态变化短按一次加 0.1℃长按超过 1 秒后每 200ms 加 1℃这样既能细调也方便快速把阈值调到 30℃ 以上。#define TEMP_MIN_X10 (-550) /* -55.0℃ */ #define TEMP_MAX_X10 (1250) /* 125.0℃ */ void Threshold_Process(void) { uint8_t key; key Key_Scan(1); if (key 1) { /* 设置键 */ if (setup_mode 0) { setup_mode 1; /* 进入阈值设置 */ } else { setup_mode 0; /* 保存并退出 */ } } if (setup_mode 1) { if (Key_Scan(1)) { /* 加号键 */ set_temp_x10 1; if (set_temp_x10 TEMP_MAX_X10) { set_temp_x10 TEMP_MAX_X10; } } if (Key_Scan(1)) { /* 减号键 */ set_temp_x10 - 1; if (set_temp_x10 TEMP_MIN_X10) { set_temp_x10 TEMP_MIN_X10; } } } }用一个setup_mode标志区分运行模式和设置模式比用中断处理按键简单得多因为设置模式里所有按键逻辑都在主循环里串行执行不会打断 DS18B20 的转换过程。系统刚上电时默认进入运行模式用 OLED 屏幕能直接看到当前阈值配合按键进入修改状态。这里最关键的是设置过程中如果 DS18B20 正在转换温度不要中断或阻塞它否则读到的数据可能是乱的。4.3 带滞回的控制逻辑控制逻辑里的“低于阈值启动加热高于阈值停止加热”被我改成了“低于启动阈值启动高于停止阈值停止”中间 1℃ 的区间保持原来状态。这个策略在工业控制里叫作滞回比较控制。核心代码如下。#define HYSTERESIS_X10 10 /* 1.0℃ 滞回 */ void Heat_Control_Task(int16_t cur_temp_x10) { static uint8_t heat_state 0; if (setup_mode 1) { return; /* 设置阈值时不动作 */ } if (cur_temp_x10 0xFFFF) { heat_state 0; /* 传感器故障关闭加热 */ HEAT_RELAY_OFF(); return; } if (heat_state 0) { if (cur_temp_x10 set_temp_x10 - HYSTERESIS_X10) { heat_state 1; HEAT_RELAY_ON(); } } else if (heat_state 1) { if (cur_temp_x10 set_temp_x10 HYSTERESIS_X10) { heat_state 0; HEAT_RELAY_OFF(); } } }这套逻辑的含义是设定阈值 25.0℃系统在 24.0℃ 以下启动加热加热到 26.0℃ 以上才停止期间温度在 25℃ 附近波动时继电器保持现有状态杜绝抖动。继电器控制引脚这里没有直接驱动继电器线圈而是通过一个 NPN 三极管或 ULN2003 驱动STM32 GPIO 的输出能力只有约 20mA直接驱动 5V 继电器线圈是不够的。在 Proteus 仿真里可以直接用一个 LED 加上限流电阻代替继电器观察动作。5. Proteus 仿真搭建从零到能跑的调试细节Proteus 仿真这套设计时需要把 STM32、DS18B20、OLED、按键、LED 全部画在一个原理图里再把 Keil 编译生成的 HEX 文件烧进虚拟 MCU。这里有一个常见误区很多人用 Proteus 自带的默认晶振频率跑程序结果串口和 DS18B20 时序全错。仿真里也必须配置 8MHz 晶振并在 STM32 代码里使用标准库的SystemInit()把 PLL 倍频到 72MHz否则delay_us的时序全是按 72MHz 算的会完全乱掉。元件在 Proteus 里的搜索名分别是STM32F103C8、DS18B20、OLED部分版本在 Graphics 分类下叫 OLED 或 SSD1306、BUTTON、LED-RED、RES。OLED 元件放置后要注意引脚定义Proteus 里不同型号的 OLED 模型引脚排列差别很大有的直接把 VCC、GND、SCL、SDA 四个引脚排在一起有的还引出 DC 和 RESET。使用前双击元件确认引脚名称IIC 模式下必须把 DC 引脚接高电平有些模型内部已固定。编译配置是另一个高频报错点。Keil 里点击Options for Target → Output勾选Create HEX File生成后 Proteus 里双击 STM32 元件在 Program File 一栏选择对应的.hex文件。如果直接点 Proteus 的调试按钮报 “No STM32 target found”是因为 Keil 的调试器配置里选了 ST-Link 仿真器而 Proteus 仿真不需要也不经过调试器把 KeilDebug选项改为Use Simulator或者干脆只做编译输出 HEX不从 Keil 启动调试。关于 DS18B20 仿真Proteus 的模型行为和真实器件存在一个差异虚拟 DS18B20 的转换时间明显比真实芯片快但代码里的delay_ms(750)依然可以保留因为即使模型在 100ms 内就完成了转换后续Read Scratchpad依然能正确返回刚刚转换完的数据。模拟 IIC 总线上虚拟 OLED 对时钟极性和速率的要求比真实模块更严格如果 OLED 显示花屏把I2C_Delay从 5μs 增加到 10μs 一般能解决。Proteus 的调试资源里查看 IIC 波形时可以在 SCL 和 SDA 引脚上直接放虚拟示波器确认起始条件、地址字节和 ACK 响应是否正常。最后给一个验证技巧把 DS18B20 元件属性里的温度值改成 30.0℃阈值设置为 25.0℃观察继电器控制 LED 是否熄灭再把温度改到 20.0℃LED 被点亮。如果 LED 在 25℃ 附近闪烁说明回差逻辑没生效重点检查Heat_Control_Task里的滞回区间代码。仿真环境下没有物理热惯性这个验证方法能最直观地确认整条链路——温度采集、数据显示、阈值比较、输出控制——是否完全打通。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询