STM32用IIC驱动SHT20温湿度传感器并在OLED上显示
简介这是一份IIC驱动0.96寸OLED显示SHT20温湿度数据的完整工程Demo面向STM32嵌入式开发者和物联网爱好者演示了从底层IIC通信到传感器数据解析、OLED界面刷新的全流程。压缩包内共149个文件约3.45MB主要包含C/H源码、Keil工程配置文件以及hex、axf等可烧录固件和编译中间文件工程目录清晰便于直接打开、编译和下载验证。Demo实现了IIC初始化、OLED初始化、SHT20温度湿度读取、数据格式处理和动态显示更新等关键模块同时包括IIC时序配置、SHT20命令读取与CRC校验解析、温湿度单位换算以及通信异常处理代码结构完整可作为实际项目改造的基础。目前已有847人学习下载适合用来快速掌握IIC总线协议、OLED显示驱动以及数字温湿度传感器的典型应用。1. 用IIC把SHT20的温湿度搬到0.96寸OLED上如果你手头有一颗SHT20、一块0.96寸SSD1306 OLED又不想在串口助手里盯着十六进制数据发呆那么这个IIC驱动的DEMO就是你需要的起点。它做的事情很直白通过IIC总线读取SHT20的温湿度原始值经过公式换算成十进制温湿度再用字模的方式显示在OLED屏幕上。整条链路只有两根信号线SCL、SDA加上电源地却把传感器采集、数据解析、显示驱动一次跑通。这个DEMO适合刚接触IIC协议的人也适合要用STM32或ESP32快速验证传感器和显示模块的工程师。我先讲明白SHT20的IIC行为再给可复现的代码和参数最后把最常见的坑列出来。2. SHT20的IIC时序与温湿度换算先把协议层吃透SHT20是Sensirion的数字温湿度传感器出厂前校准过走标准IICI2C接口。它区别于DHT11的关键在于SHT20的数据读取是主控主动发起测量命令然后等待一段时间再读取结果测量过程中传感器会拉低SDA来表示忙主控需要支持时钟拉伸clock stretching或者用延时来解决。很多初学者在这里翻车——用IIC读DHT11的思路去读SHT20结果永远读到0xFF。2.1 SHT20的设备地址和寄存器命令SHT20默认7位从机地址是0x40二进制1000000加上读写位后写地址为0x80读地址为0x81。注意不同厂家的模块可能把地址引脚ADDR接了不同电位如果地址不对可以通过跳线改成0x41。建议初始化时写一条软件复位命令0xFE让传感器回到已知状态。常用命令如下命令含义典型使用场景0xFE软件复位上电后调用复位寄存器0xE3读温度无时钟拉伸常用主机等待延时0xF3读湿度无时钟拉伸常用主机等待延时0xF5读湿度有时钟拉伸传感器拉低SDA直到完成0xE0读用户寄存器检查分辨率、加热选项IIC通信协议里一次测量流程是主机发START - 写地址写位0x80- 发命令字节 - 等待测量时间- 发START - 写地址读位0x81- 读取数据高字节 - 读取数据低字节 - 读取CRC可选- STOP。如果你用的是HAL库HAL_I2C_Mem_Read这类函数并不直接适用因为SHT20的命令流程是“先写后读”且中间有延时更推荐用HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive分步做。2.1.1 为什么等待时间不能拍脑袋SHT20在分辨率默认RH 12位T 14位下典型转换时间温度是58ms湿度是22ms最差情况下温度85ms、湿度29ms。很多人用HAL_Delay(50)后读数据运气好能读到但温度在低温或电压波动时转换时间变长就会在读到的数据里出现0x...的无效值。我一般会延时100ms以上或者用传感器提供的测量状态位来判断。在无时钟拉伸模式下最安全的做法是延时到100ms既不影响显示刷新也不容易读到旧数据。2.2 从原始码到温湿度的换算公式读回来的数据是16位无符号整型高字节在前。温度和湿度的换算公式在SHT20数据手册里写得很清楚温度T -46.85 175.72 * (raw / 65535)湿度补偿后RH -6 125 * (raw / 65535)实际有效范围0~100%RH。若算出来小于0取0大于100取100。在C语言里注意raw是uint16_t做除法前先转成float否则整数除法的结果是0。不少人在这一步得到固定值比如温度总是-46.85就是因为忘了转浮点。2.3 一个最小可用的SHT20读取函数下面给出一个基于STM32 HAL库的读取函数适配CubeMX生成的IIC初始化代码。这里不依赖中断或DMA逻辑最简单。#define SHT20_ADDR_W 0x80 #define SHT20_ADDR_R 0x81 #define CMD_READ_TEMP_NO_STRETCH 0xE3 #define CMD_READ_HUM_NO_STRETCH 0xF3 uint8_t sht20_read_measure(uint8_t cmd, float *result) { uint8_t buf[3]; *result 0.0f; // 发送测量命令 HAL_StatusTypeDef status HAL_I2C_Master_Transmit(hi2c1, SHT20_ADDR_W, cmd, 1, 100); if (status ! HAL_OK) { return 0; // 发送失败检查地址与连线 } // 等待转换完成温度建议100ms湿度建议50ms以上 HAL_Delay(100); // 读取2字节数据1字节CRC第三个字节忽略 status HAL_I2C_Master_Receive(hi2c1, SHT20_ADDR_R, buf, 3, 100); if (status ! HAL_OK) { return 0; } uint16_t raw ((uint16_t)buf[0] 8) | buf[1]; raw 0xFFFC; // 清除低2位状态位 if (cmd CMD_READ_TEMP_NO_STRETCH) { *result -46.85f 175.72f * ((float)raw / 65535.0f); } else { *result -6.0f 125.0f * ((float)raw / 65535.0f); if (*result 100.0f) *result 100.0f; if (*result 0.0f) *result 0.0f; } return 1; }逻辑说明cmd决定是温度命令还是湿度命令测量期间用HAL_Delay(100)保守等待读回来的低两位是状态位00表示OK所以要 0xFFFC清掉。最后按手册公式换算。如果你用的不是HAL换成寄存器操作或LL库也一样IIC时序不变。提示如果你的IIC总线还挂载了其他设备比如OLED注意每次通信完成后要释放总线。在HAL库中HAL_I2C_Master_Transmit返回前会发送STOP所以只要没有中断嵌套问题总线是干净的。3. 0.96寸OLED的SSD1306驱动从初始化到显示温湿度0.96寸OLED绝大多数是SSD1306驱动芯片分辨率128x64IIC接口的7位地址通常为0x3C写地址0x78。这款屏的内存是一个1KB的GRAM每一bit对应一个像素。控制它需要的不是帧缓冲而是逐页每页8像素高写入数据。驱动核心就两件事初始化序列和显存刷新策略。3.1 初始化序列的取舍网上流传的SSD1306初始化代码很多多数是从u8g2或Adafruit移植过来的但有些指令是冗余的。我常用的最小初始化序列如下IIC模式下命令前面要加0x00控制字节数据前面加0x40static void ssd1306_write_cmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, 0x78, buf, 2, 100); } static void ssd1306_write_data(uint8_t data) { uint8_t buf[2] {0x40, data}; HAL_I2C_Master_Transmit(hi2c1, 0x78, buf, 2, 100); } void ssd1306_init(void) { HAL_Delay(10); ssd1306_write_cmd(0xAE); // 关闭显示 ssd1306_write_cmd(0x20); // 设置内存地址模式 ssd1306_write_cmd(0x00); // 水平寻址模式 ssd1306_write_cmd(0xB0); // 设置页地址为0 ssd1306_write_cmd(0xC0); // 扫描方向从左上角开始 ssd1306_write_cmd(0xA8); // 设置多路复用比 ssd1306_write_cmd(0x3F); // 64行 ssd1306_write_cmd(0xA1); // 列地址重映射左右翻转 ssd1306_write_cmd(0xDA); // 设置COM引脚硬件配置 ssd1306_write_cmd(0x12); // 常见值 ssd1306_write_cmd(0x81); // 对比度 ssd1306_write_cmd(0x7F); // 中等亮度 ssd1306_write_cmd(0xA4); // 显示RAM内容 ssd1306_write_cmd(0xA6); // 正常点亮非反色 ssd1306_write_cmd(0xAF); // 开启显示 HAL_Delay(10); }参数说明0xA1和0xC0控制显示方向如果你的屏出厂镜像或者是0.91寸版本可能需要改成0xA0和0xC8。0x81后续值越大屏幕越亮。0x20后的0x00是水平寻址模式适合连续刷一行的字模。3.1.1 为什么OLED要用页模式SSD1306的GRAM被分成8页每页8行像素。水平寻址模式下写数据时列地址自动加1从第0页第0列开始写写满128个字节后自动跳到下一页。这个特性让显示字符非常方便一个8x16像素的字符正好是2页内16行的内容。如果你的驱动代码里出现“字符上下两半错位”多半是页地址没有正确切换。3.2 显示温湿度的字模渲染一般做法是内置ASCII字库每个字符用const uint8_t数组表示8x16点阵。比如字符‘A’的字模有16个字节前8字节是上半部分后8字节是下半部分。显示一个字符串时遍历每个字符在页P写前8字节在页P1写后8字节。void ssd1306_set_cursor(uint8_t page, uint8_t x) { ssd1306_write_cmd(0xB0 page); // 设置页 ssd1306_write_cmd(0x00 (x 0x0F)); // 列低4位 ssd1306_write_cmd(0x10 ((x 4) 0x0F)); // 列高4位 } void ssd1306_show_char(uint8_t page, uint8_t x, char c) { const uint8_t *font ascii_8x16[(uint8_t)c - 0x20]; ssd1306_set_cursor(page, x); for (int i 0; i 8; i) { ssd1306_write_data(font[i]); // 上半部分 } ssd1306_set_cursor(page 1, x); for (int i 8; i 16; i) { ssd1306_write_data(font[i]); // 下半部分 } }这里要注意IIC每次传输只能发一个字节数据效率很低。想提速可以用HAL_I2C_Master_Transmit一次发送多个字节把连续的一段数据帧合并成一包。比如显示一个完整的8x16字符可以构造一个17字节的缓存0x40 16个数据一次发出去这样IC总线上少了很多START/STOP实测显示刷新率能提高一半以上。4. 把SHT20和OLED串成一个DEMOSTM32 HAL库完整流程前面两章分别讲了SHT20的读取和OLED的驱动现在把它们整合进一个实际可运行的DEMO里。这里以STM32F103和STM32CubeMX为例梳理整个流程其他芯片和开发环境思路一致。4.1 CubeMX的IIC配置与引脚分配在CubeMX中把I2C1勾选为I2C模式速率为Standard Mode100kHz或Fast Mode400kHz。SHT20和OLED都挂在这个总线上遵循IIC通信协议所以不需要额外片选。一个值得注意的参数是上拉电阻IIC的SCL和SDA是开漏结构必须接上拉。很多开发板上已经有4.7kΩ的外部上拉如果你的板子是飞线的建议在数据手册推荐的基础上选用2.2kΩ~4.7kΩ。上拉太大会导致边沿过缓太小题材功耗高且容易拉不动。引脚分配上I2C1默认在PB6SCL和PB7SDA。OLED的VCC接3.3VSHT20的VDD也接3.3V。注意SHT20如果工作电压高于3.6V会损坏所以别接到5V。生成代码后在main.c的while(1)里循环读取并显示while (1) { float temp, hum; if (sht20_read_measure(CMD_READ_TEMP_NO_STRETCH, temp)) { // 格式化温度字符串保留一位小数 char t_buf[16]; snprintf(t_buf, sizeof(t_buf), T:%.1f C, temp); ssd1306_clear(); ssd1306_show_string(0, 0, t_buf); } if (sht20_read_measure(CMD_READ_HUM_NO_STRETCH, hum)) { char h_buf[16]; snprintf(h_buf, sizeof(h_buf), H:%.1f %%, hum); ssd1306_show_string(2, 0, h_buf); } HAL_Delay(1000); // 每秒刷新一次 }逻辑说明每次刷新先清屏再分别显示温度和湿度。读取间隔1秒避免传感器加热效应导致湿度值漂移。ssd1306_show_string底层会调用前面写的字符渲染函数页参数为0和2让温湿度分别显示在第0页和第2页。4.1.1 格式化输出的坑snprintf重定向到显示时需要保证单片机固件里实现了_write或putc之类的字符输出函数否则编译能过但运行时无法显示。更推荐的做法是自己写一个整数转字符串函数避免依赖庞大的vsnprintf库。比如void float_to_str(char *buf, int value_1decimal) { buf[0] (value_1decimal / 10) % 10 0; // 整数位 buf[1] .; buf[2] value_1decimal % 10 0; // 小数位 buf[3] \0; }调用方式float_to_str(buf, (int)(temp * 10))这样可以省下几十KB的ROM占用尤其在使用标准库的GCC工具链时效果明显。4.2 软复位与上电顺序优化SHT20上电后最好先执行0xFE软复位等待50ms再开始测量。OLED也需要初始化时序稳定后再刷显存。建议在main()初始化外设后先调用ssd1306_init()再调用sht20_soft_reset()最后进入主循环。顺序错了会出现第一次测量异常或OLED花屏。DEMO里还必须考虑IIC总线死锁的情况。常见死锁发生在读取SHT20时传感器完成测量前主机抢先发起读操作此时总线可能被拉死。解决办法是读取失败后翻转一下SCL并发送9个时钟脉冲让总线恢复空闲。这在单片机里可以通过软件模拟IIC实现或者在HAL中调用HAL_I2C_DeInit再重新初始化。5. 验证DEMO的4个关键步骤与排错技巧最后分享几个让DEMO稳定运行的小技巧按下面的顺序验证能快速定位是接线问题、地址问题还是数据处理问题。先检查地址用IIC扫描例程扫出0x40和0x3C两个地址。如果只扫到0x3C说明SHT20没工作检查供电和SDA/SCL是否接反。如果地址全无很可能是上拉电阻缺失。飞线时我一般用4.7kΩ上拉到VCC不要用内部上拉因为内部上拉电流太小在100kHz模式下不够驱动总线。再看转换数据把读取的原始raw值通过串口打印出来。温度原始值在室温25℃下大约在21000左右湿度在50%RH时大约在23000左右。如果读到的raw是0xFF或0x0000说明传感器没有应答或IIC读时序不对。这里要检查时间参数主机发送命令后HAL_Delay(100)只适用于无时钟拉伸模式若你用了0xF5命令就必须等待传感器拉低SDA结束不能用固定延时。第三是显示层验证OLED上电后如果屏幕有背光但无字符先看是否有数据写入。可以用一个简单的测试函数直接给整屏写0xAA即全部像素交替点亮确认GRAM写入正常。如果能看到棋盘格说明OLED驱动没问题问题在字模或页地址计算。0.96寸OLED的IIC地址有的模块是0x3D需要跳线或改代码。在HAL_I2C_Master_Transmit里把地址从0x78改成0x7A尝试一下。最后一个技巧是如何提高显示刷新率。一次IIC写单字节数据需要约0.3ms显示一帧128x64的内容需要1024字节如果逐字节写就是307ms这显然不可用。我会把整个显存映射进一个uint8_t buffer[8][128]的数组所有文字写入buffer然后一次性刷到屏幕上。刷新函数如下void ssd1306_refresh(void) { for (uint8_t page 0; page 8; page) { ssd1306_write_cmd(0xB0 page); ssd1306_write_cmd(0x00); ssd1306_write_cmd(0x10); // 先发控制字节再连续发128个数据 uint8_t frame[129]; frame[0] 0x40; memcpy(frame[1], buffer[page][0], 128); HAL_I2C_Master_Transmit(hi2c1, 0x78, frame, 129, 200); } }这个函数把每页的128字节打包成一个大包一次IIC传输完成一页刷新。这样整屏刷新时间可以降到20ms以内对DEMO来说完全够用。以后想加动画或者动态曲线也不用担心总线瓶颈。到这里SHT20温湿度数据就能稳定、清晰地显示在0.96寸OLED上了。你可以把采样周期改短也可以增加一个按键翻页显示历史最大值和最小值这套IIC驱动框架足够支撑。本文还有配套的精品资源点击获取