STM32+DS18B20按键报警系统Proteus仿真设计与实现

发布时间:2026/9/17 16:29:37
STM32+DS18B20按键报警系统Proteus仿真设计与实现 简介基于STM32与DS18B20的温度监控报警仿真项目面向单片机初学者及嵌入式课程设计人员解决温度采集、OLED显示与阈值控制联调的常见问题。系统以STM32为主控DS18B20采集环境温度通过IIC接口驱动OLED实时显示并支持按键设定上下限阈值当温度低于阈值时加热继电器吸合高于阈值时断开形成完整的闭环控制逻辑。压缩包共191个文件包含43个.h头文件、39个.c源码、Proteus仿真工程.pdsprj/.pdsbak、Keil工程配置.uvprojx/.uvoptx以及可直接烧录的.hex文件等整体约3.89MB目录结构清晰方便按模块查阅与二次开发。已有783人学习浏览适合用来对照学习STM32外设初始化、DS18B20时序驱动、OLED的IIC通信及继电器控制程序的编写思路。1. 直接用STM32DS18B20OLED按键报警的Proteus仿真项目先分清硬件关系很多嵌入式初学者拿到“基于STM32-DS18B20-按键设置报警阀值-OLED显示-proteus仿真”这串名字第一反应是“这又是一个把所有外设堆在一起的课设”。实际上这套系统拆开看就是三层DS18B20负责温度采集STM32负责读传感器、处理按键和比较阈值OLED显示温度与设置界面报警输出可以挂在LED或蜂鸣器上。题里的“阀值”是错别字工程上叫“阈值”。Proteus仿真的价值在于不需要买开发板也能把STM32的GPIO时序、按键状态机和I2C OLED显示串起来跑通。这套方案适合做单片机课设、毕业设计预研也适合想从51过渡到STM32的开发者。要注意的是Proteus里的DS18B20和OLED模型跟实物存在差异时序参数不能照搬后面会专门讲怎么调。2. DS18B20单总线时序解析与STM32驱动封装2.1 DS18B20复位和存在脉冲的时序要求DS18B20是Dallas单总线器件所有通信都由主机发起。每次读写之前都要先复位主机把总线拉低至少480us然后释放等待器件拉低60-240us表示存在。在STM32里通常用GPIO开漏模式配合外部上拉电阻在Proteus仿真里则要注意总线浮空问题。下面是常见的时序参数表来自数据手册仿真和实物都适用参数最小典型最大单位复位低电平时间480960960us存在脉冲低电平60120240us写0时隙低电平时间6090120us写1时隙低电平时间1615us读时隙低电平时间1615us时隙总时间6090120us注意复位低电平和写0低电平都要求不低于60us很多网上代码用了“延时xx微秒”但在Proteus仿真里由于模型时钟频率和指令周期不同容易出现时序偏差。建议直接使用DWT计数器做微秒延时而不是简单的软件空循环。DWT是Cortex-M内核里的调试监视跟踪单元它能提供不受中断影响的us级延时这在时序敏感的1-Wire通信上很关键。2.2 STM32用HAL库GPIO模拟单总线的读写函数这里以STM32F103C8为例用PB0作为单总线数据脚。HAL库的GPIO配置为开漏输出外部上拉4.7k。在Proteus仿真里如果没有外加上拉电阻可以配置内部上拉但为了贴近实物最好在原理图上放一个上拉电阻。核心代码分四部分复位、写位、读位、读取温度。先看复位函数#define DS18B20_PORT GPIOB #define DS18B20_PIN GPIO_PIN_0 static void DS18B20_DelayUs(uint32_t us) { // 使用DWT计数器实现1us精度延时不依赖SysTick CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while (DWT-CYCCNT - start ticks); } static uint8_t DS18B20_Reset(void) { uint8_t presence 0; GPIO_InitTypeDef gpio {0}; // 配置为开漏输出并拉低 gpio.Pin DS18B20_PIN; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(DS18B20_PORT, gpio); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); DS18B20_DelayUs(500); // 释放总线配置为输入模式等待存在脉冲 HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(DS18B20_PORT, gpio); DS18B20_DelayUs(70); presence HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN); // 等待剩余存在脉冲时间再切回输出模式 DS18B20_DelayUs(410); gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(DS18B20_PORT, gpio); return presence; // 存在脉冲应为0若为1则未检测到器件 }这段代码的关键点在于复位时的500us低电平和释放后70us处读存在脉冲。如果读到0表示器件存在读到1表示没检测到。HAL_GPIO_Init在每次模式切换时会有额外开销虽然会引入几百ns延迟但在Proteus仿真里通常还能容忍。如果要求更精确可以直接操作GPIO的CRL/CRH寄存器。写位和读位是1-Wire通信的核心。写位时写1和写0的时序都以拉低总线开始但释放时间不同读位时主机拉低总线2us后释放然后在6-9us窗口内采样电平。具体实现如下void DS18B20_WriteBit(uint8_t bit) { HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); if (bit) { DS18B20_DelayUs(6); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); DS18B20_DelayUs(64); } else { DS18B20_DelayUs(60); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); DS18B20_DelayUs(10); } } uint8_t DS18B20_ReadBit(void) { uint8_t bit 0; GPIO_InitTypeDef gpio {0}; // 拉低总线然后切换输入模式 HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); DS18B20_DelayUs(2); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); DS18B20_DelayUs(6); gpio.Pin DS18B20_PIN; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(DS18B20_PORT, gpio); bit HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN); // 恢复输出模式并等待时隙结束 gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(DS18B20_PORT, gpio); DS18B20_DelayUs(60); return bit; }读位时如果采样太早读到的可能是总线释放后的高电平太晚又可能读到下一时隙。这里选择拉低2us、释放6us后采样落在数据手册要求的15us窗口内这种写法在Proteus和实物上都能稳定工作。读写字节只需要循环8次把位拼起来即可。启动温度转换和读取结果static void DS18B20_WriteByte(uint8_t byte) { for (int i 0; i 8; i) { DS18B20_WriteBit(byte 0x01); byte 1; } } static uint8_t DS18B20_ReadByte(void) { uint8_t byte 0; for (int i 0; i 8; i) { byte 1; if (DS18B20_ReadBit()) { byte | 0x80; } } return byte; } void DS18B20_Start(void) { DS18B20_Reset(); DS18B20_WriteByte(0xCC); // Skip ROM跳过ROM匹配 DS18B20_WriteByte(0x44); // Start conversion启动温度转换 } float DS18B20_ReadTemp(void) { uint8_t low 0, high 0; int16_t raw 0; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // Read scratchpad读取暂存寄存器 low DS18B20_ReadByte(); high DS18B20_ReadByte(); raw (high 8) | low; return raw * 0.0625f; // 12位分辨率LSB对应0.0625度 }0xCC是跳过ROM指令适用于单点测温0x44是启动转换指令温度转换最长需要750ms0xBE是读取暂存寄存器的指令。温度计算结果乘以0.0625是因为默认12位分辨率下最低有效的bit代表0.0625摄氏度。在Proteus仿真中如果温度显示固定为85度往往就是复位时序不满足存在脉冲没读到。2.3 Proteus中DS18B20仿真对时序的影响Proteus的DS18B20模型支持1-Wire时序但有几个细节要注意。第一模型对复位低电平的最小时间要求比实物高实测要拉低600us左右才稳定第二读时隙的采样点必须靠近15us否则容易读到上次的电平第三仿真默认的STM32晶振频率要和keil工程里的代码定义一致否则微秒延时偏差会让温度一直读为85度。我的做法是在Proteus原理图里给DS18B20加一个4.7k上拉电阻到3.3V同时把STM32的PLL配置从8MHz外部晶振改成8MHz确保时钟树正确。这样读出来的温度值才能和实物趋势一致。另外Proteus的STM32模型对开漏输出的行为模拟得比较粗糙。如果发现总线电平始终为高可以在DS18B20的D Q脚和GND之间再并联一个0.1uF电容用来抑制仿真中的数字噪声这也能让存在脉冲更稳定。很多同学在Proteus里仿真DS18B20失败不是程序问题而是模型参数和环境配置不对。3. 按键设置报警阈值的状态机与掉电保存3.1 按键去抖与长按短按识别按键在这个项目里不只是简单的电平读取因为要设置报警阈值需要区分短按、长按和连续步进。常见做法是用10ms定时器扫描按键端口连续读到3次稳定电平才算有效。下面是一个适合STM32HAL库的按键扫描代码片段typedef enum { KEY_RELEASED, KEY_PRESSED, KEY_LONG_PRESSED } KeyState; KeyState KEY_Scan(void) { static uint8_t key_buf 0xFF; static uint32_t press_time 0; uint8_t key_level HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); // PA0接按键低电平有效 key_buf (key_buf 1) | (key_level 0x01); key_buf (key_buf 1) | (key_level 0x01); // 连续读两次实际为8位移位 if ((key_buf 0xFF) 0x00) { // 连续8次低电平认为按键已稳定按下 if (press_time 0) { press_time HAL_GetTick(); } if (HAL_GetTick() - press_time 1000) { press_time HAL_GetTick(); return KEY_LONG_PRESSED; } return KEY_PRESSED; } else { key_buf 0xFF; press_time 0; return KEY_RELEASED; } }这个代码用了8次移位取连续低电平的方式去抖比单纯延时方式响应快。注意这里PA0配置为下拉输入按键另一端接3.3V按下为高电平代码里也用高电平当作有效。长按返回KEY_LONG_PRESSED在阈值设置界面里可以用长按快速加减短按则每次加1或减1。这里的HAL_GetTick()默认是1ms节拍刚好适合做超时判断。3.2 阈值设置菜单状态机设计报警阈值一般有上限和下限两个值简单实现可以用三个状态正常显示、设置上限、设置下限。用一个枚举定义状态按键KEY1切换状态KEY2增加阈值KEY3减小阈值。状态机切换表如下当前状态按键KEY1按键KEY2按键KEY3NORMAL进入SET_HIGH无动作无动作SET_HIGH进入SET_LOW高阈值1高阈值-1SET_LOW返回NORMAL低阈值1低阈值-1阈值调整步进默认是1度但长按KEY2或KEY3时每100ms连续加减适合从25度调到80度的场景。这里要注意上限阈值必须大于下限阈值否则在判断报警逻辑时会出现矛盾。我在状态机里加入了一个矫正逻辑当设置高阈值时如果当前值小于低阈值就把低阈值强制设置为高阈值减1同理设置低阈值时也做反向校验。这样能避免用户在Proteus仿真里把阈值设成不可用数值。状态机的核心代码不复杂重点在于每次按键事件只处理一次不能长按自动重复触发导致阈值跳动太快。所以KEY_Scan的返回值只在跳变沿有效长按则单独用一个计数器控制步进速度。实际测试中10ms扫描周期加300ms长按初始延迟再100ms重复手感最接近实物。3.3 阈值用STM32内部Flash模拟EEPROM保存阈值参数不能每次上电都从25度默认值开始需要使用STM32内部Flash模拟EEPROM。STM32F103的Flash按页组织一页1KB。这个项目只需要保存4个字节直接写到最后一页比如主Flash的127页。写入前必须先擦除整页然后HAL库写数。下面是保存阈值的代码#define FLASH_USER_PAGE 127 // 使用Flash最后一个1KB页 #define FLASH_BASE_ADDR 0x0801FC00 // 最后一页起始地址 void SaveThreshold(uint8_t high, uint8_t low) { FLASH_EraseInitTypeDef erase {0}; uint32_t page_err 0; uint64_t data 0; data ((uint64_t)high 8) | low; HAL_FLASH_Unlock(); erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress FLASH_BASE_ADDR; erase.NbPages 1; HAL_FLASHEx_Erase(erase, page_err); HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, FLASH_BASE_ADDR, data); HAL_FLASH_Lock(); }这里用64位双字写入STM32的Flash编程最小单位是双字所以即使只用2个字节也要按8字节对齐。读取时直接通过指针强转uint16_t read_val 0; uint64_t *p (uint64_t *)FLASH_BASE_ADDR; read_val (uint16_t)(*p 0x00FF);注意Flash页擦除会把这个页里所有数据清零如果项目里还有其他需要保存的参数最好提前规划好布局。另外Flash擦写寿命约1万次仿真阶段频繁调试阈值会消耗擦写次数但Proteus仿真不会真正磨损芯片只有实物长期跑才需要考虑磨损均衡。每次上电初始化时先读取Flash里的值然后判断是否在合理范围内比如高阈值在30-90之间、低阈值在10-60之间超出范围就重新初始化为默认值。4. OLED显示与Proteus仿真联调4.1 HAL库下I2C驱动OLED屏幕的全流程这个项目里OLED用的是I2C接口常见的是0.96寸128x64屏驱动芯片为SSD1306。在STM32CubeMX中把I2C1配置为快速模式速率400kHz引脚PB8-PB9。OLED的I2C地址由SA0引脚决定通常为0x787位地址0x3C左移一位。HAL库驱动的关键是写命令和写数据的函数#define OLED_ADDR 0x78 static void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 20); } static void OLED_WriteData(uint8_t data) { uint8_t buf[2] {0x40, data}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 20); }第一个字节是控制字节0x00表示后面跟命令0x40表示后面跟显存数据。每次写入都需要发送完整两字节不能拆开。OLED初始化序列很长主要是设置显示开关、时钟分频、对比度、寻址模式等。核心设置是寻址模式设为页寻址每页128列。显示温度数值时需要把数字转成字符串再通过字库点阵写入显存。显示一帧内容的流程是先选择一个页地址再设置列地址低高位然后连续写入该页128字节数据。如果显示内容需要动态刷新通常的做法是维护一个128x8的显存数组每次修改数组后整屏刷新。但整屏刷新会占用不少I2C时间在温度显示这类低速场景里完全够用。Proteus仿真中I2C速度可以降到100kHz避免模型响应不过来。4.2 温度、阈值和报警状态的界面布局OLED界面可以分三行第一行显示当前温度第二行显示上限阈值和下限阈值第三行显示报警状态和设置菜单指示。比如第一行Temp:25.5C第二行H:30 L:10第三行Alarm:Off在设置状态下用一个小箭头标记当前正在修改的项。这个界面不需要复杂的GUI库用字库提取工具生成ASCII字符点阵即可。下面是在主循环中刷新的逻辑char temp_str[16]; sprintf(temp_str, Temp:%.1fC, temp_value); OLED_ShowString(0, 0, temp_str); sprintf(temp_str, H:%d L:%d, high_th, low_th); OLED_ShowString(8, 2, temp_str); sprintf(temp_str, Alarm:%s, alarm_flag ? On : Off); OLED_ShowString(16, 4, temp_str);这里的OLED_ShowString需要自己实现核心是从字库里取出每一列的字节写入显存。注意在Proteus仿真里OLED模型刷新速度比实物慢如果调用I2C的延时太短屏幕会出现花屏。一个简单的规避方法是每次写入命令后加一个100us的延时或者在I2C初始化时把通信超时调到100ms。如果屏幕只亮不显示内容优先检查OLED地址和复位引脚是否与原理图一致。4.3 Proteus仿真里OLED与DS18B20联调的3个坑第一个坑是Proteus的OLED模型需要正确选择型号有的版本里只有“OLED 128x64 I2C”而驱动芯片不一定兼容SSD1306软件显示乱码。第二个坑是I2C总线上拉电阻Proteus默认不加上拉STM32的I2C开漏输出会导致通信失败必须在原理图里给PB8和PB9各加一个4.7k电阻。第三个坑是仿真中DS18B20的温度变化很慢而OLED刷新又频繁看起来温度像卡住了一样。其实可以在Proteus里双击DS18B20模型修改温度步进值Temperature Step把默认的1度改成0.1度这样温度变化会平滑很多方便测试报警阈值是否生效。这三个坑如果不在原理图阶段就规避程序写得再好也跑不起来。我在Proteus仿真时习惯把DS18B20的4.7k上拉、I2C的4.7k上拉、OLED的电源去耦电容都画上虽然仿真不需要这些电容但对后面移植实物有参考价值。OLED的VCC和GND也要接到3.3V和地不要在仿真里接到5VSSD1306的耐压范围是3.3V接5V会烧坏。5. 报警阈值边界与仿真误差的验证技巧5.1 用Proteus虚拟示波器抓取单总线时序调试DS18B20最有效的手段是看单总线波形Proteus的虚拟示波器可以接到PB0上但1-Wire时序只有几十微秒设置示波器时基必须调到50us/格以内。复位波形应该看到一长一短两个低电平脉冲第一个是主机拉低500us第二个是DS18B20回应的存在脉冲。如果只有主机脉冲而看不到存在脉冲说明复位时序太短或上拉电阻没起作用。这个验证方法在实物调试时同样适用只是需要把示波器探头换成逻辑分析仪。5.2 阈值临界值测试矩阵报警逻辑不能只测一个温度点建议做一个测试矩阵覆盖三种边界情况温度等于阈值、温度低于阈值、温度高于阈值。在实际调试中把报警阈值上限设为30度、下限设为10度然后通过修改Proteus里DS18B20的温度值观察OLED上Alarm状态和LED是否变化。测试时要注意DS18B20的分辨率是0.0625度浮点比较不能直接用严格等于改为(temp_value 0.05f) high_th这样的容错比较。仿真通过后建议把阈值上下限校验逻辑保留防止实物上按键抖动导致阈值越界这个校验代码在任何ST32项目中都能复用。本文还有配套的精品资源点击获取