SHT30温湿度传感器在TI MSPM0G3507开发板上的I2C驱动移植与精度测试最近在做一个环境监测的小项目需要用到高精度的温湿度传感器SHT30是个不错的选择。正好手头有TI的MSPM0G3507开发板就想着把SHT30的驱动移植上去。整个过程踩了一些坑也积累了一些经验今天就把从硬件连接到软件驱动再到数据验证的完整过程分享给大家特别适合正在准备电赛或者刚接触MSPM0系列MCU的朋友。1. 准备工作认识你的传感器在动手写代码之前咱们得先搞清楚手头的“家伙事儿”到底是个啥。SHT30是Sensirion公司推出的一款高精度、低功耗的数字温湿度传感器它通过I2C接口和单片机通信用起来挺方便的。核心参数一览参数规格工作电压2.4V - 5.5V工作电流0.2μA - 1500μA温度测量范围-40℃ ~ 125℃温度测量精度±0.3℃湿度测量范围0%RH ~ 100%RH湿度测量精度±2%RH通信接口I2C引脚数量4 Pin (VCC, GND, SCL, SDA)从参数看它的精度相当不错而且功耗很低非常适合电池供电的便携式设备。我用的模块是GY-SHT31-D原理图上已经通过一个下拉电阻将ADDR引脚接地了这意味着它的I2C设备地址是0x44。这里有个细节要注意在I2C通信中我们实际使用的7位地址是0x44但在发送地址字节时需要左移一位0x44 1空出最低位来表示读写操作0为写1为读。2. 理解SHT30的两种工作模式SHT30有两种测量模式理解它们对正确使用传感器很重要。单次测量模式就像它的名字你发一次测量命令它才采集一次数据。每次读取会得到2个字节的温度原始值和2个字节的湿度原始值每个值后面还跟着一个CRC校验字节用于验证数据在传输过程中有没有出错。在这个模式下还可以选择是否启用“时钟拉伸”Clock Stretching。简单说时钟拉伸就是传感器在忙的时候可以主动把I2C时钟线拉低让主机等待直到它准备好数据。对于MSPM0这种有硬件I2C的MCU用时钟拉伸会方便些但咱们这次用的是软件模拟I2C所以选择时钟不拉伸的模式。周期测量模式在这个模式下传感器会按照你设定的频率0.5, 1, 2, 4, 10次/秒自动进行测量并把结果存到内部的寄存器里。主机随时可以发一个读取命令0xE000去把数据读出来。读完之后寄存器就清空了直到下一次测量完成才会写入新数据。这种模式适合需要周期性、快速读取数据的场景。注意如果设置周期测量模式的频率过高比如10次/秒传感器可能会因为自身发热而影响测量精度这点在数据手册里有明确提醒。3. 硬件连接与引脚配置硬件连接很简单模块就4根线。我选择用MSPM0G3507开发板上的PA0和PA1这两个GPIO来软件模拟I2C的SDA和SCL。传感器模块引脚开发板连接引脚说明VCC3V3接3.3V电源GNDGND接地SDAPA0I2C数据线SCLPA1I2C时钟线连接好硬件后需要在SysConfig图形化配置工具里把这两个引脚配置成GPIO功能。具体步骤是在CCS工程里双击empty.syscfg文件打开配置界面。在“Peripherals”里找到并添加“GPIO”。在GPIO配置中将PA0和PA1都配置为“Digital Output Pin”数字输出引脚。虽然SDA有时是输入但软件模拟时我们先按输出初始化后续在代码里动态切换方向。按CtrlS保存配置然后编译工程。这时工具会自动生成ti_msp_dl_config.h文件里面包含了我们对引脚的宏定义。这个文件又被board.h包含了所以我们后续在代码里直接包含board.h就行非常方便。4. 手把手编写软件模拟I2C驱动因为想更清晰地理解I2C时序也为了代码的移植性我决定用软件模拟Bit-Banging的方式来实现I2C通信。我们在工程里新建一个BSP板级支持包文件夹里面创建bsp_sht30.c和bsp_sht30.h两个文件。4.1 宏定义与引脚控制首先在头文件bsp_sht30.h里我们要定义好控制PA0和PA1引脚高低电平以及方向切换的宏。这些宏会用到SysConfig为我们生成的引脚定义。#ifndef __BSP_SHT30_H__ #define __BSP_SHT30_H__ #include board.h extern double Temperature, Humidity; // 用于存储计算后的温湿度值 // SDA引脚设置为输入模式读取数据时用 #define SDA_IN() { DL_GPIO_initDigitalInput(IIC_Software_SDA_IOMUX); } // SDA引脚设置为输出模式发送数据时用 #define SDA_OUT() { DL_GPIO_initDigitalOutput(IIC_Software_SDA_IOMUX); \ DL_GPIO_enableOutput(IIC_Software_PORT, IIC_Software_SDA_PIN); \ } // 控制SCL和SDA引脚输出高低电平 #define SCL(BIT) ( BIT ? DL_GPIO_setPins(IIC_Software_PORT,IIC_Software_SCL_PIN) : DL_GPIO_clearPins(IIC_Software_PORT,IIC_Software_SCL_PIN) ) #define SDA(BIT) ( BIT ? DL_GPIO_setPins(IIC_Software_PORT,IIC_Software_SDA_PIN) : DL_GPIO_clearPins(IIC_Software_PORT,IIC_Software_SDA_PIN) ) // 读取SDA引脚的电平状态 #define SDA_GET() ( ( DL_GPIO_readPins( IIC_Software_PORT, IIC_Software_SDA_PIN ) IIC_Software_SDA_PIN ) ? 1 : 0 ) char SHT30_Read(uint16_t dat); // 主要的读取函数声明 #endif4.2 构建I2C基础时序函数I2C通信有一套严格的时序咱们先把几个最基础的时序函数写好它们是所有通信的基石。这里我加了一些微秒级的延时delay_us()来满足时序要求具体延时时间需要根据你的主频调整。#include bsp_sht30.h #include stdio.h double Temperature 0.0, Humidity 0.0; // 全局变量存放结果 /** * brief 产生I2C起始信号 * 时序SCL高电平时SDA产生一个下降沿。 */ void IIC_Start(void) { SDA_OUT(); // 确保SDA为输出模式 SCL(1); // SCL拉高 SDA(1); // SDA先拉高 delay_us(5); SDA(0); // SDA拉低产生下降沿 delay_us(5); SCL(0); // SCL拉低准备传输数据 } /** * brief 产生I2C停止信号 * 时序SCL高电平时SDA产生一个上升沿。 */ void IIC_Stop(void) { SDA_OUT(); SCL(0); // 先确保SCL为低 SDA(0); // SDA拉低 delay_us(5); SCL(1); // SCL拉高 delay_us(5); SDA(1); // SDA拉高产生上升沿 delay_us(5); } /** * brief 主机发送应答(ACK)或非应答(NACK)信号 * param ack 0:发送应答(ACK) 1:发送非应答(NACK) * 时序在SCL低电平期间改变SDA然后产生一个SCL脉冲。 */ void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); SDA(0); // 先默认拉低 delay_us(5); if(!ack) SDA(0); // 发送ACKSDA保持低 else SDA(1); // 发送NACKSDA拉高 SCL(1); // 产生一个时钟脉冲从机在这个上升沿采样SDA delay_us(5); SCL(0); SDA(1); // 释放SDA线 } /** * brief 主机等待从机SHT30的应答信号 * return 0:收到应答 1:超时未收到应答通信失败 * 时序主机释放SDA设置为输入然后在SCL高电平期间检测SDA是否为低。 */ unsigned char I2C_WaitAck(void) { char ack 0; unsigned char ack_flag 10; // 超时计数 SCL(0); SDA(1); // 主机先释放SDA线 SDA_IN(); // 将SDA设置为输入模式准备读取 SCL(1); // 拉高SCL从机应在此期间将SDA拉低 // 循环检测SDA是否被拉低同时进行超时判断 while( (SDA_GET()1) ( ack_flag ) ) { ack_flag--; delay_us(5); } if( ack_flag 0 ) // 超时未收到应答 { IIC_Stop(); // 发送停止信号结束本次通信 return 1; } else // 收到应答 { SCL(0); SDA_OUT(); // 将SDA切回输出模式为后续发送数据做准备 } return ack; }4.3 实现字节读写函数有了基础时序就可以实现最核心的发送和接收一个字节的函数了。/** * brief I2C发送一个字节数据 * param dat 要发送的数据 * 时序高位(MSB)先发。在SCL低电平时改变SDA在SCL高电平时保持SDA稳定。 */ void Send_Byte(u8 dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟开始数据传输 for( i 0; i 8; i ) { // 取出最高位右移7位后结果非0即1直接赋值给SDA SDA( (dat 0x80) 7 ); delay_us(1); SCL(1); // 拉高时钟从机采样数据位 delay_us(5); SCL(0); // 拉低时钟准备发送下一位 delay_us(5); dat1; // 数据左移准备发送下一位 } } /** * brief I2C读取一个字节数据 * return 读取到的数据 * 时序主机释放SDA输入在SCL高电平期间读取SDA状态。 */ unsigned char Read_Byte(void) { unsigned char i, receive 0; SDA_IN(); // SDA设置为输入读取从机数据 for(i0;i8;i ) { SCL(0); delay_us(5); SCL(1); // 拉高时钟此时数据有效 delay_us(5); receive1; // 左移为接收新数据位腾出空间 if( SDA_GET() ) // 读取SDA引脚状态 { receive|1; // 如果为高则将最低位置1 } delay_us(5); } SCL(0); // 拉低时钟结束本次字节读取 return receive; }4.4 编写SHT30的通信与读取函数基础打好了现在来写和SHT30传感器对话的函数。这里我实现了周期测量模式。/** * brief 向SHT30发送命令用于设置周期测量模式 * param dat 16位的命令字如0x2130代表每秒1次高重复性测量 * return 0成功其他值为失败阶段用于调试 */ char SHT31_Write_mode(uint16_t dat) { IIC_Start(); // 发送设备地址写位 (0x44 1) | 0 Send_Byte((0X44 1) | 0 ); if( I2C_WaitAck() 1 ) return 1; // 地址无应答 // 发送命令高8位 Send_Byte((dat 8 ) ); if( I2C_WaitAck() 1 ) return 2; // 数据无应答 // 发送命令低8位 Send_Byte(dat 0xff ); if( I2C_WaitAck() 1 ) return 3; // 数据无应答 // IIC_Stop(); // 设置命令后可以不发停止信号保持总线 return 0; } /** * brief CRC8校验函数用于验证SHT30数据的正确性 * param data 待校验数据的指针 * param len 数据长度字节数 * return 计算出的CRC8校验值 * SHT30使用多项式0x31 (x^8 x^5 x^4 1)进行CRC校验。 */ unsigned char crc8(const unsigned char *data, int len) { const unsigned char POLYNOMIAL 0x31; unsigned char crc 0xFF; int j, i; for (j0; jlen; j) { crc ^ *data; // 与数据字节异或 for ( i 0; i 8; i ) { // 判断最高位进行移位和异或 crc ( crc 0x80 ) ? (crc 1) ^ POLYNOMIAL : (crc 1); } } return crc; } /** * brief 读取SHT30的温湿度数据主函数 * param dat 读取命令。周期模式用0xE000单次模式用0x2C06等。 * return 0读取成功其他值为失败原因 */ char SHT30_Read(uint16_t dat) { uint16_t i 0; unsigned char buff[6] {0}; // 存放6字节原始数据温高、温低、温CRC、湿高、湿低、湿CRC uint16_t data_16 0; // 第一步设置传感器为周期测量模式每秒1次高重复性 // 如果使用单次模式将这行代码注释掉即可。 SHT31_Write_mode(0x2130); // 第二步发送“读取数据”命令 IIC_Start(); Send_Byte( (0x441) | 0); // 地址写 if( I2C_WaitAck() 1 ) return 1; Send_Byte((dat 8 )); // 发送命令高字节 (0xE0) if( I2C_WaitAck() 1 ) return 2; Send_Byte( dat 0xff ); // 发送命令低字节 (0x00) if( I2C_WaitAck() 1 ) return 3; // 第三步等待传感器准备数据防止读取到旧数据或无效数据 // 通过不断尝试启动读操作直到收到应答来判断数据是否就绪 do { i; if( i 20 ) return 4; // 超时退出 delay_ms(2); // 等待一段时间再尝试 IIC_Start(); Send_Byte((0X44 1) | 1 ); // 地址读 } while(I2C_WaitAck() 1); // 如果从机无应答NACK说明数据未就绪继续循环 // 第四步连续读取6字节数据温度、湿度及它们的CRC buff[0] Read_Byte(); // 温度高8位 IIC_Send_Ack(0); // 发送ACK要求继续发送 buff[1] Read_Byte(); // 温度低8位 IIC_Send_Ack(0); // 发送ACK buff[2] Read_Byte(); // 温度CRC IIC_Send_Ack(0); // 发送ACK buff[3] Read_Byte(); // 湿度高8位 IIC_Send_Ack(0); // 发送ACK buff[4] Read_Byte(); // 湿度低8位 IIC_Send_Ack(0); // 发送ACK buff[5] Read_Byte(); // 湿度CRC IIC_Send_Ack(1); // 发送NACK表示读取结束 IIC_Stop(); // 发送停止信号 // 第五步进行CRC校验并计算实际温湿度值 if( (crc8(buff,2) buff[2]) ( crc8(buff3,2) buff[5]) ) { // CRC校验通过计算温度公式来自数据手册 data_16 (buff[0]8) | buff[1]; Temperature (data_16/65535.0)*175.0 - 45.0; // 计算湿度 data_16 0; data_16 (buff[3]8) | buff[4]; Humidity (data_16/65535.0) * 100.0; return 0; // 读取成功 } else { lc_printf(Read ERROR!! CRC Check Failed.\r\n); } return 5; // CRC校验失败 }5. 上机测试与精度验证代码写好了最后一步就是下载到板子上跑起来看看。在主函数里我们初始化系统后在一个循环里不断读取并打印数据。#include ti_msp_dl_config.h #include board.h #include bsp_sht30.h int main(void) { SYSCFG_DL_init(); // 初始化系统时钟、外设等 lc_printf(SHT30 Demo Start....\r\n); while (1) { // 调用读取函数参数0xE000是周期模式下的读取命令 if(SHT30_Read(0xe000) 0) { lc_printf(Temperature %.2f C\r\n, Temperature); lc_printf(Humidity %.2f %%RH\r\n, Humidity); } else { lc_printf(Read sensor failed!\r\n); } delay_ms(1000); // 每秒读取一次 } }上电后打开串口调试助手应该能看到每秒输出一次的温湿度数据。你可以对着家里的温湿度计或者手机上的天气APP查看室内数据做个粗略对比。在我的测试中SHT30的数据非常稳定精度也完全符合手册上标称的±0.3℃和±2%RH完全能满足大多数环境监测项目的需求。几个调试时可能遇到的坑没有数据或全是0首先检查硬件连接尤其是VCC和GND是否接反。然后用逻辑分析仪或者示波器抓一下PA0和PA1上的波形看看I2C的起始、地址、数据波形是否正常。CRC校验失败大概率是I2C时序有问题数据在传输中出错了。重点检查Read_Byte和Send_Byte函数中的延时delay_us()是否合适SCL高电平的时间是否足够传感器稳定数据。读数跳变很大检查电源是否稳定。传感器对电源噪声比较敏感可以在VCC和GND之间加一个0.1uF的滤波电容。另外确保传感器没有受到热源比如MCU、LDO的直接热辐射。整个移植过程就是这样。软件模拟I2C虽然比硬件I2C慢但好处是引脚可以任意指定代码也清晰易懂。如果你对速度有要求也可以参考这个流程改用MSPM0G3507的硬件I2C外设来实现通信的稳定性会更好。希望这篇教程能帮你顺利搞定SHT30