ARTICLE DETAIL

资讯详情

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

STM32温湿度监测项目实战:从GPIO、I2C到传感器驱动的嵌入式开发全流程

STM32温湿度监测项目实战:从GPIO、I2C到传感器驱动的嵌入式开发全流程 简介本资源是一套基于STM32F10x系列微控制器的嵌入式综合实践项目面向嵌入式初学者与课程设计者解决温湿度数据采集与本地可视化显示的核心需求。项目完整实现DHT11/DHT22传感器数据读取、STM32 ADC或GPIO单总线驱动、7脚SH1106 OLEDI²C/SPI接口动态刷新显示等功能覆盖硬件连接、外设配置GPIO/ADC/I²C/USART/TIM、固件库调用及Keil uVision5工程构建全流程。压缩包共141个文件含37个头文件h、34个源码文件c——涵盖STM32标准外设库如stm32f10x_adc.c、stm32f10x_i2c.c、OLED驱动、传感器通信协议解析及主应用逻辑另有编译中间文件o/d/crf、调试配置dbgconf、工程配置uvprojx/uvoptx及自动化脚本keilkilll.bat结构清晰便于理解工程组织与编译机制。已有3195人学习下载提供可直接编译运行的完整代码框架、README使用说明及典型硬件连接参考助读者快速掌握传感器MCU显示终端的协同开发能力。1. 项目概述一个经典的嵌入式入门实战如果你刚开始接触STM32或者想找一个能串联起GPIO、I2C通信、传感器数据采集和显示的综合小项目来练手那么这个“STM32 温湿度传感器 OLED显示”的组合绝对是你的不二之选。它不像点个LED灯那么简单也不至于复杂到让人望而却步正好卡在能让你充分动手、又能看到直观成果的那个甜点上。简单来说这个项目的核心就是让STM32微控制器读取温湿度传感器比如常见的DHT11的数据然后通过一个0.96寸的7脚OLED屏幕把这些数据实时、美观地显示出来。整个过程涉及了硬件引脚连接、通信协议驱动I2C或SPI取决于你的OLED模块、传感器数据协议解析以及最终的屏幕绘图逻辑。完成它你不仅能获得一个可以放在桌头显示环境温湿度的小玩意儿更重要的是你能系统地走一遍嵌入式开发中“感知-处理-显示”的完整链路这对理解任何更复杂的物联网IoT设备都大有裨益。我之所以推荐这个项目是因为它的要素非常典型一个主控STM32一个传感器输入一个人机交互输出。市面上大量的开发板教程和开源库都支持这些模块意味着你踩坑时能找到丰富的解决方案。接下来我会基于最常见的硬件组合STM32F103C8T6核心板、DHT11、SSD1306驱动的0.96寸OLED和HAL库带你从硬件连接到软件实现完整地走一遍流程并分享那些教程里通常不会细说的“坑点”和调试技巧。2. 硬件选型与核心思路解析2.1 为什么是这些硬件STM32F103C8T6Blue Pill核心板这是性价比之王也是STM32生态中资源最丰富的一款。它基于ARM Cortex-M3内核拥有足够的GPIO、定时器和通信接口USART、I2C、SPI来完成我们这个项目。其HAL库和标准库资料浩如烟海社区支持极好非常适合学习和原型开发。DHT11温湿度传感器这是一个单总线1-Wire数字传感器。选择它主要是因为其简单、廉价且足够用于室内环境监测。它直接输出数字信号省去了STM32内部ADC模数转换器的配置对于初学者更友好。不过要注意它的精度湿度±5%RH温度±2℃和响应速度对于高要求场合可能不够但对于学习项目完全足够。0.96寸OLEDSSD1306驱动7脚OLED是“有机发光二极管”的简称。这种屏幕自发光对比度高显示黑色时几乎不耗电视觉效果好。7脚模块通常支持I2C和SPI两种通信模式通过模块背面的电阻焊接选择。I2C只需2根数据线SCL SDA节省IO口SPI速度更快。对于显示温湿度这种刷新率要求不高的场景I2C是更简单、更通用的选择也是本项目的推荐方式。2.2 整体通信架构设计项目的核心是通信协议。STM32作为大脑需要与两个外设“对话”。与DHT11的“单线对话”DHT11采用单总线协议。这意味着数据发送和接收都通过同一根数据线DQ依靠严格的高低压电平持续时间来区分数据位0或1和通信阶段开始信号、响应信号。这要求STM32必须能够精确地控制引脚输出高低电平并更精确地读取输入电平的持续时间。通常我们会用一个通用GPIO引脚并配合微秒级延时函数来实现时序模拟。因为时序要求严格微秒级直接使用HAL库的HAL_Delay()毫秒级是不行的需要自己实现或使用定时器。与OLED的“I2C对话”我们选择I2C模式驱动OLED。I2C是一种同步、串行、多主从的通信总线只需要两根线串行时钟线SCL和串行数据线SDA。STM32作为主机MasterOLED屏幕上的SSD1306驱动芯片作为从机Slave。STM32通过I2C总线向SSD1306发送命令和数据控制其点亮哪些像素。HAL库提供了完善的I2C发送/接收函数我们只需要关注如何组织这些命令和数据来显示我们想要的字符或图形。注意在连接硬件前务必确认你的OLED模块是I2C模式。检查模块背面是否有标识或电阻焊点。通常将电阻焊接到标有“I2C”的焊盘上或者断开“CS”引脚如果是SPI的片选信号的连接即可设置为I2C模式。2.3 硬件连接清单与原理下面是最简洁的接线表。假设使用STM32F103C8T6的默认引脚功能STM32引脚连接至功能说明备注3.3VOLED VCC, DHT11 VCC电源正极务必接3.3V接5V可能烧毁OLEDGNDOLED GND, DHT11 GND电源地共地至关重要PB6OLED SCLI2C1时钟线需在CubeMX中配置为I2C1_SCLPB7OLED SDAI2C1数据线需在CubeMX中配置为I2C1_SDAPA1DHT11 DATA单总线数据线可任选一个GPIO需配置为上拉输入/推挽输出接线原理与避坑指南电源OLED和DHT11都工作于3.3V。千万不要将OLED的VCC接到开发板的5V引脚上极大概率会损坏屏幕。上拉电阻I2C总线SCL和SDA需要上拉电阻通常4.7kΩ或10kΩ到3.3V以确保总线空闲时为高电平。好消息是很多OLED模块已经内置了这些上拉电阻。如果你的模块没有或者通信不稳定需要在STM32的PB6和PB7引脚上分别外接上拉电阻到3.3V。DHT11数据线DHT11的数据线DQ在空闲时需要保持高电平。虽然STM32的GPIO可以配置为内部上拉但为了信号稳定建议在PA1和3.3V之间外接一个4.7kΩ~10kΩ的上拉电阻。这是很多初学者忽略导致读取失败的原因。3. 软件工程搭建与驱动层实现3.1 使用STM32CubeMX进行基础配置STM32CubeMX是ST官方提供的图形化配置工具能极大简化引脚、时钟和外设的初始化工作。创建项目与选型打开CubeMX选择STM32F103C8Tx创建工程。系统核心SYS在“SYS”选项卡中将“Debug”改为“Serial Wire”。这非常重要否则烧录一次程序后SWD调试接口可能被禁用导致无法再次下载程序。时钟RCC在“RCC”选项卡中将高速外部时钟HSE设置为“Crystal/Ceramic Resonator”。我们的核心板通常搭载了8MHz的外部晶振。时钟树配置进入“Clock Configuration”标签页。将输入频率设为8MHz然后通过PLL倍频将系统时钟SYSCLK设置为72MHz。这是F103系列在3.3V下的典型最高工作频率性能足够。GPIO配置找到PA1将其设置为“GPIO_Output”。我们先将其用作输出发送开始信号给DHT11。实际上PA1需要在输出和输入模式间动态切换。这部分更精细的控制我们留在代码中实现这里先设为输出。I2C配置找到I2C1将模式设置为“I2C”。将PB6和PB7自动映射为I2C1_SCL和I2C1_SDA。在参数设置中I2C速度模式选择“Standard Mode”100kHz即可。对于OLED显示这个速度绰绰有余。生成代码在“Project Manager”中设置好工程名称、路径、IDE如MDK-ARM V5然后生成代码。CubeMX会生成一个完整的Keil工程或其他IDE工程其中main.c、i2c.c、gpio.c等文件的初始化代码都已就绪。3.2 实现微秒级延时函数由于DHT11的时序要求精确到微秒级别而HAL库的HAL_Delay()是基于系统滴答定时器Systick的毫秒级延时我们需要一个更精确的延时。通常有两种方法方法一使用定时器精准但占用硬件资源配置一个基本定时器如TIM2使其每1微秒产生一次更新中断或计数。但为了一个延时函数占用一个定时器对于简单项目有些浪费。方法二使用空指令循环NOP进行近似延时简单常用这是更常见的做法。通过分析在72MHz系统时钟下执行一条空指令__NOP()所需的时间约1/72MHz ≈ 13.9纳秒我们可以编写一个循环来“消耗”时间。// 在 main.c 或 自定义的 delay.c 文件中 /** * brief 微秒级延时函数近似值 * param us: 需要延时的微秒数 * note 基于72MHz系统时钟通过循环空指令实现。不同优化等级会影响精度仅适用于时序要求不极端严格的场合如DHT11。 */ void delay_us(uint32_t us) { // 此参数需要根据实际芯片主频进行校准 // 72MHz下粗略经验值循环一次约消耗 0.125us (8个NOP/us) // 因此 us * 8 作为循环次数。你需要通过示波器或逻辑分析仪精确校准这个系数。 uint32_t delay us * 8; while(delay--) { __NOP(); // 执行一条空操作指令 } }实操心得这个delay_us函数的参数us * 8中的8是一个经验系数它会因编译器优化等级-O0, -O1, -O2的不同而产生巨大差异。最靠谱的校准方法是写一段代码让一个GPIO引脚每隔100us翻转一次然后用逻辑分析仪或示波器测量实际周期反过来调整这个系数。对于DHT11误差在几十微秒内通常可以接受。3.3 编写DHT11单总线驱动DHT11的通信流程分为三步主机STM32发送开始信号 - DHT11响应 - DHT11发送40位数据。我们需要一个GPIO引脚在输出模式发送开始信号和输入模式读取响应和数据之间切换。// dht11.h #ifndef __DHT11_H #define __DHT11_H #include main.h // 定义DHT11数据线连接的GPIO端口和引脚 #define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_1 // 函数声明 void DHT11_GPIO_Init(void); uint8_t DHT11_Read_Data(uint8_t *temperature, uint8_t *humidity); #endif// dht11.c #include dht11.h #include delay.h // 包含你自己实现的 delay_us 函数 extern TIM_HandleTypeDef htim2; // 如果你用定时器实现延时需要声明 /** * brief 设置DHT11数据线引脚为输出模式推挽输出 */ static void DHT11_GPIO_SetOutput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); } /** * brief 设置DHT11数据线引脚为输入模式上拉输入 */ static void DHT11_GPIO_SetInput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; // 输入模式 GPIO_InitStruct.Pull GPIO_PULLUP; // 内部上拉替代外部电阻 HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); } /** * brief 主机发送开始信号 * 拉低至少18ms然后拉高20-40us等待DHT11响应。 */ static void DHT11_Start(void) { DHT11_GPIO_SetOutput(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); // 拉低 delay_us(18000); // 拉低18ms DHT11手册要求至少18ms HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); // 拉高 delay_us(30); // 拉高20-40us DHT11_GPIO_SetInput(); // 切换为输入模式准备读取响应 } /** * brief 等待DHT11的响应信号 * retval 0: 响应成功 1: 响应超时失败 */ static uint8_t DHT11_Wait_Response(void) { uint32_t timeout 10000; // 超时计数器防止死循环 // 等待DHT11将总线拉低响应信号开始 while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if(--timeout 0) return 1; delay_us(1); } timeout 10000; // 等待DHT11拉低结束约80us while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET) { if(--timeout 0) return 1; delay_us(1); } timeout 10000; // 等待DHT11拉高结束约80us while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if(--timeout 0) return 1; delay_us(1); } return 0; // 响应成功 } /** * brief 从单总线上读取一个比特bit * retval 读取到的比特值0或1 * note DHT11发送0拉低50us后拉高26-28us * DHT11发送1拉低50us后拉高70us * 通过判断高电平持续时间来区分0和1。 */ static uint8_t DHT11_Read_Bit(void) { uint32_t timeout 10000; uint8_t bitval 0; // 等待低电平开始每个比特都以50us低电平开始 while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if(--timeout 0) return 0xFF; // 错误 delay_us(1); } delay_us(40); // 跳过开始的50us低电平等待到高电平阶段 // 检测高电平持续时间 if(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { // 如果是高电平延时30us后再检测 delay_us(30); // 如果30us后还是高电平说明高电平持续时间长是比特1 if(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) bitval 1; else bitval 0; // 否则是比特0 } // 等待这个比特的高电平结束 while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { delay_us(1); } return bitval; } /** * brief 从单总线上读取一个字节byte * retval 读取到的字节数据 */ static uint8_t DHT11_Read_Byte(void) { uint8_t i, data 0; for(i0; i8; i) { data 1; // 左移一位为新的比特腾出位置 data | DHT11_Read_Bit(); // 读取比特并拼接到data上 } return data; } /** * brief 读取DHT11的温湿度数据主函数 * param temperature: 指向存储温度值的变量整数部分单位℃ * param humidity: 指向存储湿度值的变量整数部分单位%RH * retval 0: 读取成功 1: 读取失败校验和错误或超时 */ uint8_t DHT11_Read_Data(uint8_t *temperature, uint8_t *humidity) { uint8_t buf[5] {0}; uint8_t i, checksum; DHT11_Start(); if(DHT11_Wait_Response() ! 0) return 1; // 响应失败 // 连续读取5个字节湿度整数、湿度小数、温度整数、温度小数、校验和 for(i0; i5; i) { buf[i] DHT11_Read_Byte(); } // 切换回输出模式释放总线拉高 DHT11_GPIO_SetOutput(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); // 计算校验和前四个字节相加的低8位 checksum buf[0] buf[1] buf[2] buf[3]; if(checksum ! buf[4]) return 1; // 校验和错误 // DHT11小数部分通常为0我们只取整数部分 *humidity buf[0]; *temperature buf[2]; return 0; // 读取成功 }关键点解析与避坑模式切换DHT11_GPIO_SetOutput和DHT11_GPIO_SetInput是关键。必须在发送开始信号前设为输出在等待响应和读取数据前设为输入。时序精度delay_us的准确性直接影响读取成功率。如果读取经常失败第一个要怀疑的就是延时函数。超时处理DHT11_Wait_Response和DHT11_Read_Bit函数中的while循环都加入了超时判断timeout。这是极其重要的防卡死机制。没有它一旦DHT11损坏或接触不良程序就会永远卡在等待循环里。校验和DHT11发送的第5个字节是前4个字节的和。每次读取后必须验证这是判断数据是否被干扰或读取过程是否出错的重要依据。3.4 编写OLEDSSD1306I2C驱动OLED驱动本质上就是通过I2C总线向SSD1306芯片发送两类东西命令控制屏幕开关、对比度、扫描方式等和数据要显示的像素点阵信息。SSD1306的I2C地址通常是0x78写地址或0x7A读地址很少用。发送一帧数据的格式是[I2C地址字节] [控制字节] [数据字节1] [数据字节2] ...。其中控制字节决定了后续字节是命令还是数据。// oled.h #ifndef __OLED_H #define __OLED_H #include main.h #include i2c.h // 使用HAL I2C函数 #define OLED_I2C_ADDR 0x78 // SSD1306的I2C写地址 #define OLED_WIDTH 128 #define OLED_HEIGHT 64 // 函数声明 void OLED_Init(void); void OLED_Clear(void); void OLED_ShowChar(uint8_t x, uint8_t y, char chr, uint8_t size); void OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size); void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size); void OLED_Refresh(void); // 刷新整个显存到屏幕 #endif// oled.c #include oled.h #include font.h // 需要包含字库数组例如ASCII字符的8x16点阵 // OLED显存共128*64/8 1024字节。每一位代表一个像素点1亮0灭。 static uint8_t OLED_GRAM[128][8]; // 二维数组方便按页8像素高为一页操作 /** * brief 通过I2C向OLED发送一个命令 * param cmd: 要发送的命令字节 */ static void OLED_Write_Cmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; // 控制字节0x00表示后续是命令 HAL_I2C_Master_Transmit(hi2c1, OLED_I2C_ADDR, buf, 2, 100); } /** * brief 通过I2C向OLED发送数据 * param data: 要发送的数据字节数组 * param len: 数据长度 */ static void OLED_Write_Data(uint8_t *data, uint16_t len) { // 方法先发送一个控制字节0x40然后连续发送数据 HAL_I2C_Mem_Write(hi2c1, OLED_I2C_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, data, len, 100); } /** * brief OLED初始化序列 * 按照SSD1306数据手册的推荐初始化流程设置显示开关、对比度、扫描方向等。 */ void OLED_Init(void) { HAL_Delay(100); // 上电后等待OLED稳定 OLED_Write_Cmd(0xAE); // 关闭显示 OLED_Write_Cmd(0xD5); // 设置显示时钟分频比/振荡器频率 OLED_Write_Cmd(0x80); OLED_Write_Cmd(0xA8); // 设置多路复用率 (64-1) OLED_Write_Cmd(0x3F); OLED_Write_Cmd(0xD3); // 设置显示偏移 OLED_Write_Cmd(0x00); OLED_Write_Cmd(0x40); // 设置显示起始行 OLED_Write_Cmd(0x8D); // 电荷泵设置 OLED_Write_Cmd(0x14); // 开启电荷泵 OLED_Write_Cmd(0x20); // 设置内存地址模式 OLED_Write_Cmd(0x00); // 水平地址模式 OLED_Write_Cmd(0xA1); // 段重映射设置 (0xA1左右反置0xA0正常) OLED_Write_Cmd(0xC8); // 扫描方向设置 (0xC8上下反置0xC0正常) OLED_Write_Cmd(0xDA); // 设置COM引脚硬件配置 OLED_Write_Cmd(0x12); OLED_Write_Cmd(0x81); // 设置对比度控制 OLED_Write_Cmd(0xCF); OLED_Write_Cmd(0xD9); // 设置预充电周期 OLED_Write_Cmd(0xF1); OLED_Write_Cmd(0xDB); // 设置VCOMH电压倍率 OLED_Write_Cmd(0x40); OLED_Write_Cmd(0xA4); // 设置整个显示开启/关闭 OLED_Write_Cmd(0xA6); // 设置正常/反色显示 (A6正常A7反色) OLED_Write_Cmd(0xAF); // 开启显示 OLED_Clear(); // 清屏 OLED_Refresh(); // 将清空后的显存刷新到屏幕 } /** * brief 清空OLED显存将所有像素点置0 */ void OLED_Clear(void) { uint8_t i, j; for(i0; i128; i) for(j0; j8; j) OLED_GRAM[i][j] 0x00; } /** * brief 设置光标位置像素级 * param x: 列坐标范围0~127 * param y: 页坐标每页8行范围0~7 */ static void OLED_Set_Pos(uint8_t x, uint8_t y) { OLED_Write_Cmd(0xB0 y); // 设置页地址 OLED_Write_Cmd(((x 0xF0) 4) | 0x10); // 设置列地址高4位 OLED_Write_Cmd(x 0x0F); // 设置列地址低4位 } /** * brief 将显存OLED_GRAM的内容全部刷新到OLED屏幕 */ void OLED_Refresh(void) { uint8_t i, j; for(j0; j8; j) // 遍历8页 { OLED_Set_Pos(0, j); // 设置到第j页第0列 OLED_Write_Data(OLED_GRAM[0] j * 128, 128); // 发送该页的128字节数据 } } /** * brief 在指定位置显示一个ASCII字符 * param x: 起始列 (0~127) * param y: 起始页 (0~7每页8行) * param chr: 要显示的字符 * param size: 字体大小 (1: 8x16, 2: 16x32需要对应字库) */ void OLED_ShowChar(uint8_t x, uint8_t y, char chr, uint8_t size) { uint8_t i, j, temp; uint8_t *pfont; // 指向字库数据的指针 // 根据字符和字体大小从字库数组中获取点阵数据首地址 // 假设字库数组名为 Font8x16每个字符16字节 if(size 1) { pfont (uint8_t*)Font8x16[(chr - ) * 16]; // ASCII字库从空格开始 for(i0; i16; i) // 字符高度16像素 { temp pfont[i]; for(j0; j8; j) // 字符宽度8像素 { if(temp 0x80) // 判断最高位是否为1 OLED_GRAM[x j][y i/8] | (1 (i%8)); // 置1点亮像素 else OLED_GRAM[x j][y i/8] ~(1 (i%8)); // 置0熄灭像素 temp 1; } } } // 可以扩展其他字体大小... } /** * brief 在指定位置显示字符串 */ void OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size) { while(*str ! \0) { OLED_ShowChar(x, y, *str, size); x 8; // 8x16字体每个字符占8列 str; if(x 120) // 换行判断 { x 0; y; } } } /** * brief 在指定位置显示数字 */ void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size) { char str[12]; sprintf(str, %*lu, len, num); // 格式化数字为固定长度字符串 OLED_ShowString(x, y, str, size); }驱动层核心逻辑显存GRAM我们在STM32的内存中开辟一个二维数组OLED_GRAM[128][8]用来模拟OLED屏幕的显存。这个数组的每一个bit对应屏幕上的一个像素点。我们所有画点、画线、显示字符的操作都是先修改这个数组。局部刷新 vs 全局刷新OLED_ShowChar等函数只修改OLED_GRAM并不立即发送给OLED。只有调用OLED_Refresh()时才会将整个OLED_GRAM通过I2C发送到屏幕。这种方式避免了频繁的I2C通信提高了效率。在温湿度显示中我们可以每秒调用一次OLED_Refresh来更新数据。字库你需要一个font.h文件里面包含字模数组。例如Font8x16[]它是一个二维数组存储了从空格开始的所有ASCII字符的8x16点阵数据。你可以用PCtoLCD2002等取模软件生成。4. 应用层逻辑整合与主程序实现驱动层完成后应用层逻辑就变得非常清晰了。我们只需要在主循环中定期读取传感器数据然后格式化并显示到OLED上。4.1 主程序流程图与逻辑初始化系统时钟、GPIO、I2C、OLED、DHT11GPIO。清屏并显示初始界面例如显示“Temp:”和“Humi:”的标签。进入主循环延时为了避免频繁读取导致DHT11发热或响应不及时每次读取间隔至少2秒。使用HAL_Delay(2000)。读取数据调用DHT11_Read_Data(temp, humi)。错误处理如果返回值为1失败可以在OLED上显示“Error”或保留上一次数据。更新显示将读取到的temp和humi整数转换成字符串调用OLED_ShowNum或OLED_ShowString函数在指定位置刷新数值。刷新屏幕调用OLED_Refresh()将更新后的显存发送到OLED。4.2 主函数代码示例// main.c #include main.h #include i2c.h #include gpio.h #include dht11.h #include oled.h I2C_HandleTypeDef hi2c1; int main(void) { uint8_t temperature 0; uint8_t humidity 0; uint8_t dht11_status 0; char disp_str[20]; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); // 初始化外设 OLED_Init(); DHT11_GPIO_Init(); // 初始化DHT11的GPIO设置为输出模式并拉高 // 显示静态标题 OLED_Clear(); OLED_ShowString(0, 0, STM32 DHT11 Demo, 1); OLED_ShowString(0, 2, Temp:, 1); // 第2页行16-31像素显示 OLED_ShowString(0, 4, Humi:, 1); // 第4页行32-47像素显示 OLED_Refresh(); while (1) { HAL_Delay(2000); // 每2秒读取一次 dht11_status DHT11_Read_Data(temperature, humidity); if(dht11_status 0) { // 读取成功更新温度显示 sprintf(disp_str, %d C, temperature); OLED_ShowString(40, 2, disp_str, 1); // 在“Temp:”后面显示 // 更新湿度显示 sprintf(disp_str, %d %%, humidity); // %% 表示一个%字符 OLED_ShowString(40, 4, disp_str, 1); // 在屏幕底部显示状态 OLED_ShowString(0, 6, Status: OK , 1); } else { // 读取失败 OLED_ShowString(0, 6, Status: ERROR, 1); // 可以选择不清除之前的温湿度数据 } // 刷新整个屏幕将GRAM数据发送到OLED OLED_Refresh(); } }4.3 显示效果优化与扩展基础的数值显示可能有些单调我们可以进行一些优化添加小数显示DHT11虽然小数位通常是0但我们可以预留位置。或者使用DHT22AM2302传感器它直接输出带一位小数的数据。绘制图标在“Temp:”和“Humi:”文字前可以绘制一个温度计和小水滴的图标增加美观度。这需要你事先制作好图标的点阵数组取模。显示历史曲线利用OLED的128x64像素可以绘制一个简单的温湿度历史曲线图。在GRAM中开辟一个区域作为曲线绘制区每次读取新数据后将旧数据点向左移动并在最右侧绘制新的数据点。添加单位如上述代码所示显示“C”和“%”符号。低功耗优化如果不要求实时显示可以让STM32进入休眠模式Stop Mode定时唤醒读取传感器并刷新屏幕可以极大降低功耗。5. 调试技巧与常见问题排查实录即使按照步骤操作第一次成功也往往伴随着一些调试过程。以下是几个最常见的“坑”和解决方法。5.1 DHT11读取始终失败这是最常见的问题症状是DHT11_Read_Data总是返回1超时或校验和错误。检查1电源和接线电压确保DHT11和OLED的VCC接的是3.3V不是5V。上拉电阻DHT11的数据线DQ必须接一个4.7kΩ - 10kΩ的上拉电阻到3.3V。STM32的内部上拉电阻约40kΩ可能强度不够导致高电平不稳定。共地确保STM32、DHT11、OLED三者的GND引脚连接在一起。检查2延时函数精度这是头号嫌疑犯。用逻辑分析仪或示波器检查DHT11_Start()函数中产生的开始信号低电平18ms高电平30us是否准确。如果没有仪器可以尝试调整delay_us函数中的系数。将us * 8改为us * 6或us * 10试试。不同主频和优化等级下这个系数差异很大。尝试关闭编译器的优化在Keil中设置为-O0看看是否能成功。如果关闭优化后成功说明就是延时精度问题。检查3GPIO模式切换时序在DHT11_Start()函数末尾切换到输入模式后是否立即进入了等待响应的循环中间不应有大的延时。确保在读取完数据后将GPIO设置回输出模式并拉高释放总线。检查4传感器本身DHT11是3.3V器件但有些模块兼容5V。如果接5V数据线输出高电平可能是5V长时间可能损坏STM32的3.3V GPIO。建议始终在3.3V下工作。换个新的DHT11试试。5.2 OLED屏幕不亮或显示乱码检查1电源和I2C地址电源再次确认VCC接3.3V。I2C地址尝试将OLED_I2C_ADDR从0x78改为0x7A。有些模块的地址可能是0x7A。上拉电阻如果屏幕完全无反应检查SCL和SDA线上是否有上拉电阻4.7kΩ到3.3V。如果没有请加上。检查2初始化序列OLED初始化命令序列很长且顺序有要求。确保OLED_Init()函数中的命令一个不少且0xAE关显示在开头0xAF开显示在最后。初始化后务必调用OLED_Clear()和OLED_Refresh()。检查3I2C通信在OLED_Write_Cmd和OLED_Write_Data函数中检查HAL_I2C_Master_Transmit的返回值。如果不是HAL_OK说明I2C通信失败。用逻辑分析仪连接SCL和SDA看初始化阶段是否有波形产生。如果没有检查CubeMX中I2C的配置是否正确引脚是否被其他功能占用。检查4显存与刷新显示乱码通常是GRAM数据错误或刷新位置不对。确保OLED_GRAM数组大小正确定义为[128][8]。在OLED_Refresh()函数中OLED_Set_Pos(0, j)和OLED_Write_Data(OLED_GRAM[0] j * 128, 128)这两行是关键它确保了将GRAM中每一页的128字节数据发送到屏幕对应的页地址。5.3 显示内容错位或重叠坐标计算OLED的坐标体系是以“页”为单位。一页是8行像素。OLED_ShowChar(x, y, chr, size)中的y是页坐标0~7x是列坐标0~127。字库匹配你使用的Font8x16字库必须是8像素宽、16像素高的。如果你显示函数里按8x16处理但字库数据是6x8的肯定会乱。刷新前未清除旧内容如果你在同一个位置显示变化的数字如从“25”变成“26”新数字“26”只有2个字符如果上次的“25”也是2个字符那没问题。但如果从“9”变成“10”新内容“10”会覆盖掉“9”和它后面一个字符的位置。如果后面那个位置原本有内容比如单位“C”就会被覆盖。解决方法有两种1) 在显示新数字前先用空格覆盖旧数字的整个区域2) 每次更新都重绘整个显示区域清屏再画虽然效率低但简单可靠。5.4 项目稳定性与抗干扰建议增加软件滤波对于温湿度数据可以连续读取3-5次然后取中值或平均值能有效滤除偶然的跳动。完善错误处理主循环中如果DHT11连续多次读取失败可以尝试重新初始化其GPIO或者让系统软复位。使用看门狗启用STM32的独立看门狗IWDG防止程序跑飞。在主循环中定期“喂狗”。如果因DHT11时序卡死等原因导致程序死循环看门狗会复位系统。电源去耦在STM32的3.3V和GND之间靠近芯片引脚处并联一个100nF和一个10uF的电容可以有效滤除电源噪声提高系统稳定性。通过以上步骤你应该能够成功搭建并运行这个STM32温湿度监测显示系统。这个项目虽然小但“麻雀虽小五脏俱全”它涵盖的硬件接口、通信协议、驱动编写和系统整合思想是嵌入式开发中最核心的部分。掌握了它你就为后续学习更复杂的传感器、显示屏、无线通信等打下了坚实的基础。本文还有配套的精品资源点击获取
返回列表