ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32驱动OLED与DS18B20:实现环境温度与时间显示

STM32驱动OLED与DS18B20:实现环境温度与时间显示 简介面向51单片机入门开发者这份基于OLED显示屏与DS18B20温度传感器的工程资源完整演示了环境温度与时间的实时显示方案可作为单片机课程设计或物联网入门的实战参考。项目采用C语言编写涉及GPIO控制、DS18B20一线总线通信以及OLED的I2C驱动代码中包含主程序、IIC通信、延时函数和温度处理等模块配合Keil工程文件与HEX烧录文件可直接在开发板上运行验证便于对照学习。压缩包共31个文件主要涵盖.c/.h源码、编译中间产物.obj/.lst、工程配置.uvproj以及汇编启动文件STARTUP.A51等整体仅107KB目录组织清晰可快速找到所需模块。已有2343人学习参考对于理解DS18B20的温度数据读写与解析、OLED屏幕的初始化与刷屏逻辑以及单片机工程从编译到下载的完整流程都具有直接的参考价值。1. 项目功能与整体方案拆解收到这个标题的时候我第一反应是这又是一个经典到不能再经典的嵌入式入门组合。OLED显示屏加DS18B20温度传感器配合实时时钟在128x64的小屏幕上同时显示环境温度和当前时间做桌面小摆件、机箱温度监控面板、实验室环境监测器都非常合适。我当初做这个项目目的很直白一是把HAL库驱动OLED这套流程彻底跑通二是把DS18B20这条单总线的时序彻底搞明白三是给手头开发板加一个真正可用的“显示输出终端”。先说方案选型。温度传感器这端DS18B20几乎是绕不开的选择。它的优势在于单总线通信只需要一根数据线就能完成双向数据传输而且测量范围覆盖-55℃到125℃在常规环境监测场景下绰绰有余12位分辨率下精度可以达到0.0625℃。相比之下DHT11虽然也是入门常用但温度分辨率只有1℃采样周期还长达1秒以上做温度计明显力不从心。DS18B20唯一的门槛是时序要求严格好在搞清楚之后并不复杂这篇文章会把我调试过程中踩过的坑全部标出来。显示端选择OLED而不是LCD1602理由很简单。LCD1602需要背光供电、对比度调节显示汉字还要自己搞字库芯片8根数据线加3根控制线就把GPIO占去大半。而SSD1306驱动的OLED屏I2C接口版本只需要SDA和SCL两根线屏幕本身自发光、对比度高、视角广128x64的像素分辨率虽然不算大但显示两行大字加上一行小字绰绰有余。从练手的角度讲I2C通信本身也是嵌入式开发的高频技能点借着这个项目一并掌握很划算。时间来源这端标配套路是用STM32内部RTC。这里要提醒的是RTC需要外部32.768kHz晶振精准度取决于晶振本身的精度和布局普通贴片晶振一天下来差个几秒很正常。如果做的是长时间运行的桌面时钟这种误差是可以接受的如果要求更高再考虑外部RTC芯片或者NTP校时这个后面会展开说。整体架构一句话总结就是主控通过单总线协议周期读取DS18B20的温度数据通过I2C协议把温度和RTC时间显示到OLED屏上屏幕刷新由主循环里的定时逻辑控制。硬件的接线量非常少软件的核心难点集中在DS18B20的时序和OLED的字模显示机制上正好都是值得反复揣摩的技术点。2. 硬件连接与初始化细节2.1 引脚分配与接线表我做这个项目用的是STM32F103C8T6最小系统板这是一块在DIY圈子里流通量非常大的开发板资源够用、价格便宜资料也全。OLED选择0.96寸I2C接口版本DS18B20选择TO-92封装的三脚直插版。接线方案如下模块引脚STM32引脚备注OLED SCL时钟线PB6I2C1_SCL复用开漏需外部上拉OLED SDA数据线PB7I2C1_SDA复用开漏需外部上拉OLED VCC电源正3.3V部分模块支持5V注意看丝印OLED GND电源地GND共地必须DS18B20 DQ数据线PA0推挽输出/上拉输入切换DS18B20 VDD电源正3.3V寄生供电可省但强烈建议外部供电DS18B20 GND电源地GND共地必须强调一个细节DS18B20的数据线DQ一定要接一个4.7kΩ上拉电阻到VCC。这个电阻是单总线协议正常工作的重要前提因为单总线的空闲状态是高电平所有通信都是把总线拉低再释放来实现的。如果漏了这颗电阻DS18B20会表现为“有时候能读到温度有时候读不到”或者读到的数据一直是85℃这个固定值——85℃是DS18B20上电复位后暂存器里的默认温度值如果你看到85.00℃却没有报错大概率就是总线通信根本没建立成功。OLED模块的上拉电阻大多数成品板在板上已经集成不需要额外处理。2.2 供电方式与共地问题供电这块有两个容易踩的坑。第一个是OLED屏的电压等级0.96寸的SSD1306模块虽然绝大多数都能在3.3V到5V之间工作但不同批次板载稳压电路不一样。有些模块标了5V供电板上有稳压芯片这种情况下接5V和3.3V都能工作有些模块没标那就按照SSD1306控制器的VDD范围老老实实接3.3V。接错电压虽然大概率不会烧板但可能出现屏幕对比度异常、显示乱码等稀奇古怪的问题。第二个坑是电源共地。STM32开发板如果用USB供电OLED和DS18B20的电源也从USB的5V转3.3V那路取那么所有模块的地必须连到同一个GND。做实验时面包板上一排GND引脚全给它连上不要图省事只连信号线不连地线。没有共地的情况下I2C和单总线的电平参考点不一致信号容易出现误码表现就是显示乱跳、温度值随机漂移。DS18B20的供电我坚持用外部供电而不是寄生供电。寄生供电模式理论上可以省一根VDD线只需要DQ和GND两根线靠数据线上的高电平给内部电容充电维持工作但前提是转换温度期间总线必须保持高电平程序控制上多一重限制新手阶段没必要给自己加戏。外部供电的方式下DS18B20的VDD和GND直接接电源DQ只管通信逻辑清晰很多。3. OLED显示驱动的关键实现3.1 SSD1306控制器的显示原理很多新手第一次拿到OLED模块会下意识认为它可以像LCD1602那样直接发送字符编码就能显示文字这是个误会。SSD1306内部有一块名为GDDRAM的显存区域一共128x64 bit对应屏幕上的128x64个像素点。我们需要做的就是把希望显示的图案逐bit地写入这片显存控制器会自动把显存内容刷新到屏幕上。这个过程可以这样理解OLED屏本质上是一张128列64行的点阵纸我们每次显示内容都是在纸上打点SSD1306的作用就是维护这张纸上的每个点该亮还是不该亮。所以驱动OLED的第一步不是写“显示文字”的函数而是写一个最底层的“打点”函数然后在打点函数的基础上做字符、汉字和数字的渲染。有了打点的思路之后中文字符的显示逻辑就顺理成章了。屏幕上任意位置显示一个16x16的汉字就是把16x16点阵的字模数据逐行展开根据字模中每个bit是1还是0决定对应像素是否点亮。字模的获取方式有两种一种是用取模软件比如PCtoLCD2002把汉字从字库转成C语言数组另一种是直接用网上现成的GB2312汉字字库数组。我推荐第一种因为取模软件可以自己控制点阵大小、取模方式逐行还是逐列、高位在前还是低位在前和代码里的打点逻辑对应起来更容易。I2C驱动OLED时核心操作是向SSD1306发送命令和数据。命令的标识是控制字节0x00数据的标识是控制字节0x40。整个通信流程就是起始条件、设备地址写位、控制字节、数据字节、停止条件不断重复。设备地址常见是0x3C最后一位是读写控制位实际发送地址是0x78也有少数模块是0x3D如果屏幕没有反应先用I2C扫描程序确认地址这是排查OLED不显示的第一个步骤。3.2 HAL库下的OLED核心代码使用STM32CubeMX把I2C1初始化好之后OLED驱动的核心是封装两个基础函数写命令和写数据。我习惯用HAL_I2C_Mem_Write这个接口来简化流程它相当于把起始条件、地址、控制字节、数据发送、停止条件全部打包了。核心代码如下void OLED_WriteCmd(uint8_t cmd) { uint8_t buffer[2] {0x00, cmd}; HAL_I2C_Mem_Write(hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, buffer, 1, 100); } void OLED_WriteData(uint8_t data) { uint8_t buffer[2] {0x40, data}; HAL_I2C_Mem_Write(hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, buffer, 1, 100); }这里设备地址填0x78是因为0x3C左移一位的结果HAL库的接口会自动把8位地址的最后一位用作读写标志所以我们在代码里写左移后的值。控制字节通过Mem_Write的MemAddress参数传入OLED_WriteCmd把0x00作为控制字节OLED_WriteData把0x40作为控制字节语义很清晰。在这两个函数之上还需要一个初始化序列这个序列是SSD1306数据手册里规定的寄存器配置作用是把屏幕从默认的上电状态切换到我们需要的显示模式。我用的是常规配置关闭显示、设置显示时钟分频、设置复用率、设置显示偏移、设置起始行、设置列地址映射、设置COM扫描方向、设置COM引脚硬件配置、设置对比度、设置预充电周期、设置VCOMH电平、开启显示。这些命令值基本是固定的照抄即可但如果遇到屏幕显示偏移、画面翻转的问题需要改的是列地址映射0xA1改成0xA0和COM扫描方向0xC8改成0xC0这两个位置。显示数字的方式我封装了一个极简的数字渲染函数。OLED不像printf那样可以直接格式化输出我们需要把整数的每一位拆出来然后查表找到对应的16x8点阵字模数据逐列输出到显存对应位置。温度值往往带有一位小数处理方式是把温度乘以10转成整数然后分别提取十位、个位、小数位再添上小数点符号的字符完整拼出类似“25.4℃”的显示。3.3 I2C总线的稳定性问题HAL库的硬件I2C在STM32F1系列上有一处经典问题总线错误后模块会卡在一个异常状态后续通信全部超时。表现为程序跑着跑着屏幕突然不刷新了复位之后又能正常一段时间再随机复发。排查思路通常是这样的先确认总线上有没有设备地址冲突再检查上拉电阻阻值是否合适4.7k到10k都是常见范围最后检查代码里对I2C忙状态的恢复处理。我在驱动里加了一个简单的总线恢复机制每次通信前先检查I2C的BUSY标志如果异常就调用HAL_I2C_DeInit重新初始化I2C外设同时把SCL和SDA引脚手动翻转几次释放可能被外部设备拉死的总线。void I2C_Bus_Recovery(void) { if (__HAL_I2C_GET_FLAG(hi2c1, I2C_FLAG_BUSY)) { HAL_I2C_DeInit(hi2c1); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); for (int i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); delay_us(5); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); delay_us(5); } HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); MX_I2C1_Init(); } }这段代码的思路是对SCL做9个脉冲信号让可能处于错误状态的外部I2C设备复位其内部状态机然后再重新初始化I2C外设。实际测试下来这个机制对总线异常恢复非常有效。不过在项目里我仍然建议把I2C时钟频率设置为100kHz而不是400kHzOLED屏对速度要求不高100kHz下信号完整度好得多排查问题也更省心。4. DS18B20温度采集与时序分析4.1 单总线协议的工作机制DS18B20使用的单总线协议是一种半双工、低速通信协议所有设备共享一根数据线通信时由主机控制总线从机只能被动响应。协议的基本通信单位是时隙也就是一个bit的传输过程读写一bit都需要主机在总线上制造特定的电平变化模式DS18B20根据这些电平变化模式来识别是读还是写、写的是0还是1。读温度和读取数据之前主机必须发一个复位脉冲DS18B20收到复位脉冲后会拉低总线一段时间输出一个存在脉冲作为应答。这个过程相当于主机敲门问“有人在吗”DS18B20拉低总线回答“我在”。如果总线上没有应答信号后面的一切操作都不会有结果这也是判断传感器是否正常连接的最快方法。通信过程中的命令序列是固定的复位、发送ROM命令、发送功能命令、读写数据。因为我的项目里只挂了一个DS18B20所以ROM命令用跳过ROM0xCC即可不需要读取64位序列号。功能命令里最关键的是两个启动温度转换0x44和读取暂存器0xBE。启动转换之后需要等待转换完成12位分辨率下转换时间是750ms这个数值在算刷新率时必须考虑进来。4.2 时序参数与延时实现我在调试过程中最大的一个体会是DS18B20的时序要求并没有想象中那么苛刻但延时的精度必须够用。STM32F103主频72MHzHAL_Delay是毫秒级延时无法满足微秒级时序要求必须额外实现微秒延时。最简易的做法是使用SysTick的计数器循环等待或者直接用for循环空转粗略延时。我的做法是初始化时读取SysTick的LOAD值和当前VAL值计算每个微秒对应的计数值然后在延时函数里循环读取VAL直到差值达到目标。实测这种延时方式在DS18B20时序上非常稳。核心的时序参数整理如下时序操作持续时间说明复位脉冲480us以上主机拉低总线存在脉冲检测60-240usDS18B20应答写0时隙60-120us拉低总线保持全程写1时隙1-15us拉低然后释放拉低后快速释放读时隙1-15us拉低然后释放采样点在第15us附近时隙间隔1us以上两次写/读之间读时序的采样点位置是可以根据经验调整的关键参数。数据手册要求主机在拉低总线后释放然后在15us左右读取总线电平。我在代码里把读采样前的位置卡在10到12us之间实测成功率最高这个值是经过多次试验得到的经验区间不同主频和不同GPIO翻转速度下的最优值会有差异如果你的板子和我不一样建议先用逻辑分析仪或者示波器看一眼实际波形再调整。4.3 温度读取核心代码基于上面的分析我封装的DS18B20驱动核心如下uint8_t DS18B20_Reset(void) { uint8_t presence 1; DS18B20_DQ_LOW(); delay_us(500); DS18B20_DQ_HIGH(); delay_us(30); DS18B20_DQ_INPUT(); if (HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN) 0) { presence 0; } delay_us(450); DS18B20_DQ_OUTPUT(); DS18B20_DQ_HIGH(); return presence; } void DS18B20_WriteBit(uint8_t bit) { DS18B20_DQ_OUTPUT(); DS18B20_DQ_LOW(); delay_us(2); if (bit) { DS18B20_DQ_HIGH(); } delay_us(60); DS18B20_DQ_HIGH(); } uint8_t DS18B20_ReadBit(void) { uint8_t bit 0; DS18B20_DQ_OUTPUT(); DS18B20_DQ_LOW(); delay_us(2); DS18B20_DQ_HIGH(); DS18B20_DQ_INPUT(); delay_us(10); bit HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN); delay_us(50); DS18B20_DQ_OUTPUT(); DS18B20_DQ_HIGH(); return bit; } float DS18B20_ReadTemp(void) { uint8_t tempL, tempH; int16_t tempRaw; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); delay_ms(750); DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); tempL DS18B20_ReadByte(); tempH DS18B20_ReadByte(); tempRaw (tempH 8) | tempL; return (float)tempRaw * 0.0625f; }这里有一个常见的坑读取到的原始温度数据是16位有符号数负数温度比如零下10摄氏度在补码表示下直接强转float再乘0.0625会得到错误结果。必须先转成int16_t类型再计算。我第一次写的时候用的是uint16_t冬天测试零下温度时数据显示乱了很久才排查到这个问题。另外读出来的数据建议做一层合理性过滤。DS18B20在通信异常时读出的数据可能是0或0xFFFF如果直接显示会给用户一个荒谬的温度值。我通常在主循环里加上限幅判断超过-55到125这个范围的数据直接丢弃保持上一次的有效值这个处理在长时间运行的设备上非常有必要。5. 时间获取与显示布局实践5.1 STM32内部RTC的配置要点时间来源我选的是STM32F103内部RTC。F1系列的RTC是一个独立的定时器由32.768kHz的外部低速晶振驱动通过分频产生1Hz的秒脉冲秒计数寄存器在每次秒脉冲到达时自动加一。配置过程在STM32CubeMX里非常简单勾选RTC、设置异步预分频和同步预分频即可但实际上手时有几个注意点。第一F1的RTC和备份寄存器由VBAT引脚供电在只使用主电源的情况下需要在代码里使能PWR和BKP时钟并且调用HAL_PWR_EnableBkUpAccess()函数开启后备域访问权限。如果漏了这一步RTC寄存器写不进去时间设置会静默失败具体表现就是屏幕上时间永远是上电默认值。第二上电后需要判断RTC是否处于初始化状态。判断方式通常是读取备份寄存器里自己写的标志位比如在备份寄存器0里写入0xA5A5作为“已初始化”标记。每次上电先检查这个标记如果没写过就执行时间设置写过了就直接读取时间。这个标记法在RTC应用里是标准操作。void RTC_Time_Init(void) { RTC_TimeTypeDef sTime {0}; RTC_DateTypeDef sDate {0}; if (HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR1) ! 0xA5A5) { sTime.Hours 12; sTime.Minutes 0; sTime.Seconds 0; HAL_RTC_SetTime(hrtc, RTC_FORMAT_BIN, sTime, RTC_ALARM_A); HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, 0xA5A5); } }第三RTC晶振的振荡是否正常可以通过HAL_RTC_PollForConfig的返回值来判断。如果返回HAL_TIMEOUT说明外部32.768kHz晶振没有起振这时候时间不走或者走时异常优先检查晶振的两个负载电容是否焊接以及C1/C2电容的容值是否在6到12.5pF范围内。这个坑遇到过一次当时排查了很久后来发现是晶振虚焊导致的时间不走。5.2 屏幕布局与刷新策略128x64的分辨率布局方案我调整了几轮最终用的是三行结构顶部一行显示日期和星期中间两行用大号数字显示时间时:分:秒底部一行显示温度和温度单位。OLED的点阵字模中16x8的ASCII字符和16x16的汉字是常用规格但是16x8的数字显示时间不够醒目我专门用取模软件做了16x32的超大数字字模显示效果一下子提升了一个档次看起来像电子钟而不是调试信息输出。布局定义可以用一个简单的宏来管理#define LINE1_Y 0 // 日期星期 #define LINE2_Y 24 // 时间(大) #define LINE3_Y 56 // 温度刷新策略上有一个非常重要的原则不要整屏刷新。SSD1306的显存是128x64字节整屏重写一次需要发送1024个字节数据虽然I2C在100kHz下也就几十毫秒但这个时间在显示秒级变化时会导致文字闪烁同时还会占用大量CPU时间影响DS18B20的时序。正确的做法是只更新发生变化的部分秒变化时只更新时间区域温度变化时只更新温度区域。我的做法是在主循环里维护一个显示标志每个功能模块检测到数据变化后只把对应的坐标区域当作脏区处理这样屏幕稳定无闪烁。主循环的逻辑大概是每秒刷新一次时间每2秒读取一次DS18B20并更新温度。DS18B20单次转换需要750ms2秒的读取周期既保证了温度有充分的时间完成转换又不会因为频繁读取而影响时序稳定性。while (1) { if (s_1s_flag) { s_1s_flag 0; RTC_GetTime(); OLED_ShowTime(); } if (s_2s_flag) { s_2s_flag 0; float temp DS18B20_ReadTemp(); if (temp -55 temp 125) { last_temp temp; } OLED_ShowTemp(last_temp); } }这段主循环的好处是结构清晰后面的功能扩展比如加湿度显示、加闹钟提醒都只需要增加对应的定时标志和显示函数即可不需要改动底层驱动。6. 常见问题与排查技巧实录做这个项目的过程中遇到不少奇怪问题我把最典型的几个整理成一张速查表方便对照排查现象可能原因排查与解决OLED完全不亮I2C地址错误或接线错误先用I2C扫描程序确认设备地址再核对SCL/SDA是否接反OLED偶尔花屏I2C总线被干扰卡死加总线恢复机制检查上拉电阻降低I2C时钟到100kHz温度显示85℃DS18B20通信失败检查DQ引脚4.7k上拉电阻复位函数是否读到存在脉冲温度值跳动剧烈供电不稳或时序采样点偏差确认DS18B20外部供电用示波器看读写时序调整延时时间不走32.768kHz晶振没起振检查晶振焊接和负载电容查看RTC配置返回值只有温度没有时间RTC初始化失败使能PWR和BKP时钟调用HAL_PWR_EnableBkUpAccess其中一个比较值得展开的是温度值跳动的问题。有一次我读到的温度值在25℃和26℃之间快速不定向跳动排查了很久。最后发现问题出在DS18B20的数据线过长且和电源线绑在一起DS18B20转换温度时电流变化在传输线上产生了干扰。把线缆分开、线长缩短到20cm以内后问题消失。这个经验说明DS18B20虽然时序不算特别敏感但对物理布线还是有基本要求的。另一个经验是关于显示刷新顺序的。OLED在更新显存时如果正赶上写入点阵数据过程中有另一路任务插进来读DS18B20DS18B20的写时序会被打断进而导致数据错误。我最初在主循环里是“先显示温度再读温度”结果发现温度偶尔会读取失败后来调整为“先读温度再显示温度”并且读温度期间禁止其他任务打断问题就不再出现了。对于没有用RTOS的项目这个顺序问题的本质是临界区保护理解了这一点后续接入更多传感器时就不会踩同样的坑。最后补充一个提高开发效率的小技巧因为OLED驱动和DS18B20驱动都是标准的模块化代码建议把它们从项目里抽取成独立的.c/.h文件一方面方便后续其他项目复用另一方面也方便做单元测试。我在调试DS18B20时序时就是单独写了裸机测试代码把每次复位、读写、转换的状态通过串口打印出来确认所有时序参数都正确之后再把它集成到OLED显示的项目里。这个“先独立验证再集成”的习惯帮我节省了大量联调时间强烈建议新手养成。本文还有配套的精品资源点击获取
返回列表