STM32F103驱动SHT30温湿度传感器并OLED显示:从I2C到CRC校验完整实战

发布时间:2026/9/9 15:59:46

STM32F103驱动SHT30温湿度传感器并OLED显示:从I2C到CRC校验完整实战 简介这是一份基于STM32F1系列微控制器的温湿度监测工程源码使用SHT30传感器采集环境数据并通过0.96寸OLED屏实时显示。工程面向正点原子mini板开发环境也适合需要学习I2C外设与传感器驱动移植的嵌入式开发者代码已整理成可直接编译的Keil工程包含I2C底层驱动、SHT30读写与校验、SSD1306显示驱动以及轮流显示温度湿度的用户界面。资源包共200个文件以h头文件、c源文件为主辅以uvprojx工程配置、hex烧录文件与编译生成的map、lst文件压缩包大小约4.89MB解压后即可打开使用。已有2441人学习查看适合需要快速参考驱动实现或在此基础上扩展应用的用户。通过这份源码可以系统理解STM32的I2C通信时序、温湿度数据转换和OLED点阵显示原理对提升嵌入式驱动开发能力有实际帮助代码在正点原子mini板上验证可用注释清晰便于学习与二次开发。 最早接触温湿度测控这块时我也是从DHT11走过来的便宜、教程多、网上随便一搜就是大把例程。但用过一段时间后你会明显感到它那几个老毛病有多烦人精度勉强、时序敏感、温漂明显而且每篇例程写出来的时序节奏都不太一样换个引脚换个芯片就得重新调。后来项目里换成了SHT30配合STM32F103和一块OLED屏做本地显示整个体验完全不一样——I2C接口挂上去就通数据稳定还有CRC校验调试起来省心太多了。这篇文章就是把这一套“STM32F1 SHT30 OLED”方案完整拆开讲。从硬件选型、I2C时序、数据读取、OLED驱动到源码工程组织最后再把我在实测中踩过的坑一并列出来。适合已经会点STM32基础、想跳过DHT11直接上数字温湿度传感器的读者也适合手里正好有一块F103核心板、想快速跑通一个带显示的温湿度计项目的朋友。文末的源码逻辑可以直接抄进自己的工程里先跑通再按需改。1. 为什么是SHT30而不是DHT11选型对比与接线先说选型这件事。很多新手朋友以为DHT11和SHT30只是精度不同其实它们的接口协议和开发体验完全不在一个层级上。DHT11走的是单总线时序MCU需要把引脚配置成开漏输出然后手动拉低拉高一个bit一个bit地抠时序温湿度数据全靠GPIO电平翻转来模拟对定时器延时精度要求很高。如果用的是主频不准的内部RC振荡器或者编译器优化等级一变读取结果就可能变成乱码。而SHT30走的是标准I2C协议你只需要把从机地址和寄存器/命令字发过去数据就规规矩矩地回来开发门槛低很多。从性能参数上对比就更直观对比项DHT11SHT30温度精度±2℃±0.3℃典型值湿度精度±5%RH±2%RH接口单总线GPIO模拟时序I2C最大1MHz数据校验简单8位校验CRC-8可靠性高典型价格1~3元6~12元测量分辨率8bit温度 / 8bit湿度16bit / 16bit多花几块钱换来的不仅仅是精度提升更重要的是省掉了那些让人头大的时序调优工作。I2C协议有标准可循出错也好排查用逻辑分析仪一目了然。接线方面我这套方案用的是STM32F103C8T6SHT30和OLED都挂在同一个I2C总线上。SHT30的VDD接3.3VGND接地SDA和SCL分别接PB7和PB6。OLED的SDA/SCL同样接在PB7/PB6上VCC接3.3V如果模块丝印标注支持5V供电你也可以接5V但建议统一用3.3V尤其SHT30必须3.3V它的绝对最大供电电压是3.6V直接上5V会烧芯片。注意SHT30的ADDR引脚是地址选择脚。ADDR接地时I2C从机地址是0x44ADDR接VDD时地址是0x45。PCB设计上通常默认拉低所以市面上多数模块都是0x44。如果你的模块读不到数据第一件事就是量一下ADDR引脚的电位。I2C总线还需要上拉电阻。STM32核心板或者传感器模块内部一般已经带了4.7k或10k上拉直接能用。如果是自己做的板子记得在SDA和SCL上分别焊一个4.7k左右的电阻到3.3V否则总线信号上不去通信会时好时坏。2. 读SHT30数据的完整I2C链路命令、时序与CRCSHT30的I2C通信不像普通EEPROM那样先发寄存器地址再读写它用的是“命令字”体系。核心是单片机先往传感器写两个字节的命令传感器执行完测量之后再把测量结果读回来。以单次测量、高重复性模式为例最常用的命令是0x2C 0x06。这里稍微解释一下命令字含义0x2C代表单次测量模式且不使用时钟延展clock stretching0x06代表高重复性另外还有0x0D中重复性和0x10低重复性。重复性越高内部测量时间越长噪声越低对应的转换时间大约在12.5ms到15ms之间。单片机端的主流程并不复杂uint8_t sht30_write_cmd(uint16_t cmd) { uint8_t buf[2]; buf[0] cmd 8; buf[1] cmd 0xFF; return i2c_write_bytes(SHT30_ADDR, buf, 2); } uint8_t sht30_read_data(float *temp, float *humi) { uint8_t buf[6]; uint16_t rawTemp, rawHumi; if (sht30_write_cmd(0x2C06) ! 0) { return 1; } delay_ms(20); // 等待转换完成 if (i2c_read_bytes(SHT30_ADDR, buf, 6) ! 0) { return 2; } // 检查CRC if (crc8(buf, 2) ! buf[2] || crc8(buf 3, 2) ! buf[5]) { return 3; } rawTemp (buf[0] 8) | buf[1]; rawHumi (buf[3] 8) | buf[4]; *temp -45.0f 175.0f * rawTemp / 65535.0f; *humi 100.0f * rawHumi / 65535.0f; return 0; }SHT30返回的6个字节结构很固定第1~2字节是温度原始值16bit第3字节是温度CRC校验值第4~5字节是湿度原始值第6字节是湿度CRC校验值。转换公式用了datasheet里给的线性映射温度转换成实际摄氏度是-45 175 * rawTemp / 65535湿度转换是100 * rawHumi / 65535。很多网上拿来的例程是不做CRC校验的直接读6个字节就拿来算。传感器本身在正常环境下几乎不会出错但只要总线受干扰或者接线长了以后信号边沿变差偶尔就会冒出个异常大值显示“-40℃”或“0%RH”。加了CRC校验之后读到的每一帧数据都是经过完整性验证的可靠性完全不一样。CRC8的实现也不复杂多项式是0x31初值0xFFuint8_t crc8(const uint8_t *data, uint8_t len) { uint8_t crc 0xFF; for (uint8_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x80) { crc (crc 1) ^ 0x31; } else { crc 1; } } } return crc; }如果你用的是STM32的硬件I2C外设读回来的流程是相同的只是把i2c_write_bytes和i2c_read_bytes换成HAL库的HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive也就是在发送时带上从机地址和数据长度在接收时从总线上直接拿6字节数据。区别只是底层驱动上层这套命令和CRC逻辑可以完全复用。3. OLED显示模块SSD1306初始化与字符串输出OLED部分用的是最常见的SSD1306驱动、128x64分辨率的I2C接口屏。地址大多数是0x3C也有少数模块是0x3D可以在模块背面丝印或者淘宝详情页里确认一下。如果两者都试了依然没有反应就用I2C扫描程序扫一遍总线看看实际枚举出来的从机地址是什么。SSD1306上电后并不会自动显示内容必须先发送一串初始化命令。初始化序列其实有固定套路我在工程里用的是这么一组static void oled_init_cmd(void) { uint8_t cmds[] { 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF }; for (uint8_t i 0; i sizeof(cmds); i) { oled_write_cmd(cmds[i]); } }这里面最容易漏掉的是0x8D 0x14。0x8D是电荷泵开关命令0x14表示开启内部DC-DC升压。如果少了这两条OLED内部升压电路不工作屏幕有背光亮但怎么刷都刷不出像素。我遇到过太多次初学者问“为什么我的OLED上电亮了但什么都显示不出来”十有八九就是这里的问题。然后是字符显示。我不太建议在128x64的小屏上用全屏帧缓冲方式去刷新太浪费内存而且刷新速度还慢。更实用的做法是“页地址模式”把屏幕分成8个page每个page是8像素高的横条定位到某个page和column之后直接把字模数据写进去即可。ASCII字库用6x8或8x16都行我工程里用的是8x16显示更清晰一些温度湿度信息量不大一屏完全放得下。温度湿度数据的显示需要做一次浮点到字符串的转换。嵌入式开发里我不太喜欢在STM32F1这种小芯片上频繁用浮点格式化所以更推荐把整数部分和小数部分拆开拼字符串float temp, humi; uint8_t temp_int, temp_dec, humi_int, humi_dec; char line[16]; sht30_read_data(temp, humi); temp_int (uint8_t)temp; temp_dec (uint8_t)((temp - temp_int) * 10); humi_int (uint8_t)humi; humi_dec (uint8_t)((humi - humi_int) * 10); sprintf(line, T:%d.%d C, temp_int, temp_dec); oled_show_string(0, 0, line); sprintf(line, H:%d.%d %%, humi_int, humi_dec); oled_show_string(0, 2, line);OLED屏上显示速度很快而且SHT30数据半小时不刷也能保持显示不需要像数码管那样动态刷新所以这个方案做静态显示非常合适。如果你有显示中文的需求要先把汉字取模生成字库数组再写一个按二维坐标绘制字模的函数。这部分代码量会大些但原理就是把8x16的英文字模扩展成16x16的中文字模。4. 让源码一次跑通的工程组织初始化顺序与主循环设计很多人拿到例程第一反应是“编译完直接烧”结果发现什么都显示不出来然后就卡住了。其实多数问题不是代码本身有问题而是工程文件的组织方式不匹配或者芯片型号、启动文件选错了。这里我把这套源码的工程结构和我推荐的初始化顺序完整说一下。工程上我用的是标准外设库3.5 Keil MDK5芯片选择STM32F103C8启动文件是startup_stm32f10x_md.s。目录里核心文件就这些main.c系统时钟配置、引脚初始化、主循环逻辑sht30.c/sht30.hSHT30命令、CRC、数据读取封装oled.c/oled.hSSD1306驱动、字库、字符串绘制myi2c.c/myi2c.h模拟I2C底层时序也可替换成硬件I2C/HAL如果你用的是HAL库文件结构几乎一样只需要把myi2c.c替换成对应的HAL I2C调用即可。之前提到的HAL_I2C_Mem_Write和HAL_I2C_Mem_Read在这里就能派上用场。在CubeMX里把I2C1的SCL/SDA配置成PB6/PB7速率设为100kHz然后直接在sht30.c里调用HAL函数剩下的业务逻辑一行不用改。初始化顺序是这套东西能不能一次跑起来的关键我的经验是严格按下面的顺序来int main(void) { Delay_Init(); // 1. 延时函数必须先就绪 OLED_Init(); // 2. 初始化OLED OLED_Clear(); // 3. 清屏 SHT30_Init(); // 4. 初始化SHT30这里实际上只是确认通信 delay_ms(20); // 5. 给传感器上电稳定时间 while (1) { float temp 0, humi 0; uint8_t err SHT30_ReadData(temp, humi); if (err 0) { OLED_ShowTempHumi(temp, humi); } else { OLED_ShowError(); } delay_ms(2000); // 2秒刷新一次 } }主循环里2秒刷一次是我实际调出来的平衡值。SHT30单次测量模式下每次读取必然触发一次测量间隔太短传感器转换时间不够容易读到旧数据间隔太长又没必要。2秒钟人眼感觉不到卡顿功耗也低如果以后做锂电池版本这个刷新率对续航很友好。还有个小细节SHT30_Init()里我并没有做什么神秘的初始化命令只是发一条软复位或者直接读一次数据做握手确认传感器在线。真正重要的是主循环里封装错误处理——读不到数据时不要让程序卡死而是显示一个ERR提示这样调试的时候至少能知道是通信断了还是传感器没上电。5. 实测翻车记录NACK、花屏、跳数的排查思路这套方案虽然简单但我在实际调试中还是踩了几个坑每一个都很有代表性写出来给大家避一避。第一个坑SHT30读回来全是NACK。查这个问题的链路非常固定先用万用表量SDA/SCL的对地电压正常空闲状态应该都是3.3V左右如果其中一根是0V那基本就是上拉电阻缺失或者引脚配置成了推挽输出而不是开漏。再看ADDR引脚如果模块上默认接了上拉电阻你的代码却按0x44去访问从机根本不会应答。最后用逻辑分析仪抓波形看主机发送地址之后有没有ACK位。这三个方向能覆盖95%以上的NACK问题。第二个坑OLED屏幕亮着但没有任何字形。出现这种问题先不要急着怀疑字库代码八九成是初始化命令里少了电荷泵那两条。前面提到的0x8D 0x14是开启DC-DC升压的开关很多淘宝卖家给的例程里偏偏漏了这两条或者顺序不对。另外如果OLED模块上有RES引脚初始化之前要先做一次硬件复位RES拉低50ms再拉高50ms然后再发初始化命令。模块上电瞬间若没有外部复位时序SSD1306内部状态可能不稳定也会导致后续命令无效。第三个坑温度偶尔跳成-40℃或者湿度显示0%。这个现象基本就是CRC没做导致的。数据在I2C总线上传输时如果线长超过20cm或者旁边有大电流电机、继电器干扰偶发错一个bit是很正常的事。做上CRC校验后错误帧直接丢弃并在下一轮重新读取显示就不会再乱跳。我强烈不建议省略这一步。第四个坑显示乱码或者汉字是反的。这种情况通常不是通信问题而是字模取模方式选错了。取模软件里常见的设置有“逐行式”“逐列式”“低位在前/高位在前”等选项SSD1306的水平寻址模式下字模数据是按列从上到下、从左到右扫的所以取模时一般要选“列行式”或“纵向取模高位在前”。我把显示ASCII字母和汉字的函数分开了ASCII用oled_show_string汉字用oled_show_chinese分别对应不同的字模格式避免混用出错。第五个坑I2C总线卡死程序跑飞。模拟I2C最大的问题就是如果主从设备时序不合很容易卡在某个while循环里等不到应答。我的解决方法是给等待ACK的循环加超时计数比如while (... ) timeout如果超时就直接返回错误而不是死等。硬件I2C也有类似问题标准库的I2C事件等待也会卡死HAL库版本相对好一些因为有超时机制。6. 进阶玩法中断刷新、周期测量模式与后续扩展基础版本跑通之后你会开始想它还能做什么。这里分享几个我已经验证过的扩展方向。第一个是中断刷新。不要在定时器中断函数里直接做I2C读取和OLED刷新因为I2C通信耗时不稳定OLED刷新更是要几百微秒到几毫秒放在中断里会严重影响系统实时性。正确做法是定时器中断只置一个flag 1主循环检测到标志位后再去做读取和刷新。这样哪怕I2C通信偶尔出点幺蛾子也只是主循环慢一点中断响应时间完全不受影响。第二个是SHT30的周期测量模式。SHT30支持自动周期测量比如每秒测量10次MCU可以只发一条配置命令然后每隔一段时间直接读最新结果不用再每次触发单次测量。这种模式适合需要更高数据更新率、又不希望频繁操作I2C的场景。代价是传感器内部一直在测量休眠电流比单次测量模式高。如果做电池供电的低功耗设备我更推荐单次测量模式MCU休眠前发一条命令唤醒测量醒来后读结果平均电流能压得比较低。第三个是把数据送出去。OLED只承担本地显示功能如果项目需要远程监控可以用串口把温度和湿度以JSON或CSV格式发出去接到ESP8266/ESP32或者USB转串口模块上这样就能把SHT30变成一个小型环境数据采集节点。注意SHT30的I2C地址冲突问题如果同一总线上要挂多个SHT30就得把ADDR引脚分别接GND和VDD一个用0x44一个用0x45。最后再分享一个我个人的调试习惯每次拿到一个新的传感器模块我都会先用一个I2C总线扫描程序扫一遍看看实际识别到的从机地址和手册里写的一不一样然后再上正式例程。这一步真的能省掉大量“为什么读不到数据”的排查时间。这套STM32F1 SHT30 OLED的方案我在三个不同型号的F103板子上跑过除了引脚映射需要微调之外核心代码一次都没改过稳定性是经过验证的。你可以直接把这份源码烧进板子里先跑通再根据你的需求去改刷新率、通讯方式或者显示布局。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/9 15:54:46

STM32+可控硅零点检测:白炽灯无级调光实战解析

简介:一套基于STM32的白炽灯亮度调节完整工程,面向嵌入式开发者和智能照明设计人员。资源以零点检测与可控硅(Triac)控制为核心,展示如何借助定时器PWM输出、GPIO中断和软件调度,实现交流负载的连续调光&am…

2026/9/9 15:54:46

Agent时代CPU为何成为核心调度中枢

1. 这不是CPU的“复仇”,而是算力分工的范式重写最近在几个AI工程群和硬件技术论坛里,反复看到一句被刷屏的话:“GPU主导了大模型时代,而Agent会让CPU翻身?”——它像一句口号,更像一个信号弹。但说实话&am…

2026/9/9 15:54:46

Codex+Simulink助力大学生方程式八字绕环仿真搭建全流程实践

赛季初拿到规则书,八字绕环这个项目几乎是每个大学生方程式车队最早开工的仿真任务。场地几何简单、工况相对固定,但真要把它仿真做明白,牵扯到车辆动力学建模、轮胎模型、路径跟随策略、求解器设置一大堆事。今年我尝试把 Codex 拉进这套工作…

2026/9/9 17:04:57

高校题库管理与自动组卷系统全解析:从数据库建模到算法实现

高校题库管理与自动组卷系统,听起来是个课程设计级别的题目,真要做起来其实水挺深。它不只是存题、抽题那么简单,牵扯到权限体系、题目标签、组卷策略、试卷导出、考试流程管理,甚至还要考虑并发和扩展性。更关键的是,…

2026/9/9 17:04:57

机器学习特征工程实战:从数据清洗到特征选择的完整指南

在机器学习实践中,特征工程(Feature Engineering)经常被误认为是只属于“数据处理”的琐碎工作。实际上,它决定了模型能从数据中看到什么规律,也直接影响最终效果能到多高。在 CampusX 的 100 天机器学习计划中&#x…

2026/9/9 17:04:57

高维数据下的维度灾难:原理、实验验证与工程降维策略

高维数据为什么难学?这是机器学习里最容易被忽视、却几乎处处存在的坑:维度灾难(Curse of Dimensionality)。很多在线课程,包括 CampusX 系列的机器学习讲解,都会把这个问题放在比较靠前的位置,…

2026/9/9 17:04:57

【计算机JAVA毕业设计案例】基于SpringBoot的现代农业信息管理系统的设计与实现 基于SpringBoot的智慧农业综合信息管理平台的设计与实现(程序+文档+讲解+定制)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

2026/9/9 16:59:57

个人微信API接口如何处理好友申请?从收到请求到业务判断

好友申请处理的核心不在"通过"这个动作,而在"判断要不要通过"这条决策链。按判断逻辑分三层,从粗到细。 一、来源判断——这个申请从哪来 第一层判断看申请来源。好友事件回调里带来源信息:搜索微信号加的、手机通讯录…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/9 0:00:48

MHS模型硬件标准:让大模型像调用软件一样控制物理设备

让Claude真正看着显微镜说“这个细胞形态不太对”,或者让大模型自己调一版机械臂的运动轨迹,这事儿听上去已经很接近科幻片了。但你真上手试一次就会发现,模型不缺智商,缺的是一个能插进显微镜、机械臂、激光控制器里的“通用插座…

2026/9/9 0:00:48

AI五大核心方向详解:从机器学习到大模型,零基础转行选哪条?

会有人告诉我,他想转行学AI,但打开招聘网站一看直接傻眼:机器学习、深度学习、自然语言处理、计算机视觉、大模型应用……满屏都是这些词,好像每个都会一点,又好像每个都离自己很远。还有人上来就问“学Python还是学Ja…

2026/9/9 0:00:49

从50行最小循环到生产级AI引擎:工程化改造全解析

直接说干货。这一章我写的不是那种"hello world跑通某个模型"的教程,而是把AI引擎当做一个真正要上线、要被人调用、要扛流量的系统来聊。从最初只有50行的最小循环,到能够承载生产流量的AI引擎,中间差的不是代码量,而是…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码