12. 基于TI MSPM0L系列MCU的软件I2C驱动SHT20温湿度传感器实战
基于TI MSPM0L系列MCU的软件I2C驱动SHT20温湿度传感器实战最近在做一个环境监测的小项目需要用到温湿度传感器。SHT20这个传感器精度不错而且用的是I2C接口接线简单。不过我手头这块TI的MSPM0L开发板硬件I2C资源比较紧张或者我想用更灵活的引脚所以决定用软件来模拟I2C协议。今天我就把从零开始用软件I2C驱动SHT20的整个过程包括踩过的坑和调试心得完整地分享给大家。如果你是刚开始接触MSPM0或者I2C通信的新手跟着这篇教程一步步走一定能成功点亮你的传感器。1. 项目准备认识我们的“主角”在动手写代码之前咱们得先搞清楚要用的两个核心部件MSPM0L微控制器和SHT20传感器。1.1 SHT20温湿度传感器简介SHT20是一款非常常用的数字式温湿度传感器。它内部采用电容式测量技术精度高而且稳定。最关键的是它通过标准的I2C接口和我们单片机通信只需要两根线SCL时钟线和SDA数据线大大简化了硬件连接。它的主要性能参数如下参数规格工作电压2.1V ~ 3.6V工作电流0.1uA ~ 1000uA非常省电温度测量范围-40℃ ~ 125℃温度精度±0.3℃湿度测量范围0% ~ 100% RH湿度精度±3% RH通信接口I2C引脚数量4个 (VCC, GND, SDA, SCL)注意SHT20的供电电压是2.1V到3.6V我们开发板上的3.3V输出正好合适直接连接即可。1.2 I2C协议快速入门I2C协议是一种非常经典的串行通信总线它的最大优点就是省线。只需要两根线SDA和SCL就能连接多个从设备比如多个传感器。你可以把它想象成一条电话线SDA和一条打拍子的线SCL。主设备我们的MCU控制着拍子SCL的节奏所有设备都听着这个拍子在特定的节拍上通过电话线SDA说话或听话。主设备通过呼叫每个从设备的“电话号码”设备地址来指定和谁通信。这里有几个关键点对后续写软件驱动至关重要开漏输出I2C总线上的SDA和SCL线所有设备都必须配置为“开漏输出”模式。简单理解就是设备只能把线“拉低”输出0而不能主动“推高”输出1。总线的高电平是靠外接的上拉电阻通常4.7kΩ或10kΩ实现的。这样做是为了防止多个设备同时输出高电平和低电平时造成短路。起始与停止信号通信的开始和结束有专门的信号。起始信号在SCL为高电平期间SDA线产生一个从高到低的下降沿。停止信号在SCL为高电平期间SDA线产生一个从低到高的上升沿。应答机制每传输完一个字节8位数据接收方都需要在下一个时钟脉冲发送一个应答信号ACK将SDA拉低。如果没有应答NACKSDA保持高说明传输可能出了问题。1.3 硬件连接接线非常简单一共四根线SHT20传感器引脚连接到 MSPM0L 开发板VCC3.3V 电源引脚GNDGND 地引脚SDAPA1 (我们用作数据线)SCLPA0 (我们用作时钟线)提示SHT20模块上通常已经集成了必需的上拉电阻在SDA和SCL到VCC之间所以咱们的开发板不需要额外焊接电阻了。如果你的模块没有需要在SDA和SCL线上各接一个4.7kΩ到10kΩ的电阻到3.3V。2. 软件I2C驱动层实现硬件接好了接下来就是重头戏用MSPM0L的GPIO口通过软件模拟出I2C的时序。我们会在工程里创建两个文件bsp_sht20.c和bsp_sht20.h把所有的I2C底层操作和传感器操作都封装在这里。2.1 引脚宏定义与初始化首先在头文件bsp_sht20.h里我们要定义好操作SDA和SCL引脚的各种宏。这些宏会让后面的代码看起来更清晰也更容易修改。#ifndef __BSP_SHT20_H__ #define __BSP_SHT20_H__ #include board.h // 设置SDA引脚为输出模式主机要发送数据时 #define SDA_OUT() { \ DL_GPIO_initDigitalOutput(I2C_SDA_IOMUX); \ DL_GPIO_setPins(I2C_PORT, I2C_SDA_PIN); \ DL_GPIO_enableOutput(I2C_PORT, I2C_SDA_PIN); \ } // 设置SDA引脚为输入模式主机要接收数据时 #define SDA_IN() { DL_GPIO_initDigitalInput(I2C_SDA_IOMUX); } // 读取SDA引脚当前的输入电平是高还是低 #define SDA_GET() ( ( ( DL_GPIO_readPins(I2C_PORT,I2C_SDA_PIN) I2C_SDA_PIN ) 0 ) ? 1 : 0 ) // 控制SDA引脚输出高电平(1)或低电平(0) #define SDA(x) ( (x) ? (DL_GPIO_setPins(I2C_PORT,I2C_SDA_PIN)) : (DL_GPIO_clearPins(I2C_PORT,I2C_SDA_PIN)) ) // 控制SCL引脚输出高电平(1)或低电平(0) #define SCL(x) ( (x) ? (DL_GPIO_setPins(I2C_PORT,I2C_SCL_PIN)) : (DL_GPIO_clearPins(I2C_PORT,I2C_SCL_PIN)) ) // 温湿度读取函数声明 float SHT20_Read(unsigned char regaddr); #endif这里用到了TI驱动库DriverLib的函数来操作GPIO。I2C_SDA_IOMUX、I2C_PORT这些宏应该在board.h或ti_msp_dl_config.h中定义好了它们对应着我们硬件连接中使用的PA1和PA0引脚。2.2 核心时序函数编写现在我们在bsp_sht20.c文件中实现I2C通信最基础的几个函数起始、停止、发送一个字节、接收一个字节、等待应答和发送应答。起始信号 (IIC_Start)void IIC_Start(void) { SDA_OUT(); // 先把SDA设置为输出模式因为我们要控制它 SCL(1); // 确保SCL一开始是高电平 SDA(1); // SDA也是高电平 delay_us(5); // 短暂保持让信号稳定 SDA(0); // 在SCL高电平期间SDA产生一个下降沿这就是起始信号 delay_us(5); SCL(0); // 拉低SCL为后续传输数据位做准备 delay_us(5); }停止信号 (IIC_Stop)void IIC_Stop(void) { SDA_OUT(); SCL(0); // 先确保SCL是低电平 SDA(0); // 拉低SDA delay_us(5); SCL(1); // 将SCL拉高 delay_us(5); SDA(1); // 在SCL高电平期间SDA产生一个上升沿停止通信 delay_us(5); }发送一个字节 (IIC_Write)发送数据时是从最高位MSB开始一位一位地发送。void IIC_Write(uint8_t data) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟线准备开始传输数据位 for( i 0; i 8; i ) { // 取出数据的最高位第7位并设置到SDA线上 SDA( (data 0x80) 7 ); delay_us(2); // 数据建立时间 data 1; // 数据左移为发送下一位做准备 delay_us(6); SCL(1); // 将SCL拉高此时从机可以读取SDA上的数据 delay_us(4); // 时钟高电平保持时间 SCL(0); // 拉低SCL完成一位数据的传输 delay_us(4); } }接收一个字节 (IIC_Read)接收数据时也是在SCL的上升沿读取SDA线上的数据。uint8_t IIC_Read(void) { unsigned char i, receive 0; SDA_IN(); // 非常重要接收数据前要把SDA设置为输入模式释放总线 for(i0; i8; i ) { SCL(0); delay_us(5); SCL(1); // 产生一个时钟上升沿 delay_us(5); receive 1; // 左移一位为新来的数据位腾出位置 if( SDA_GET() ) // 读取此时SDA线上的电平 { receive | 1; // 如果是高电平就把最低位置1 } delay_us(5); } SCL(0); // 读完8位后将SCL拉低 return receive; }等待应答 (IIC_Wait_Ack)发送完一个字节比如地址或命令后主设备需要释放SDA线设置为输入并检查从设备是否在第九个时钟脉冲拉低了SDA即发送ACK。uint8_t IIC_Wait_Ack(void) { char ack 0; unsigned char ack_flag 10; // 超时计数 SDA_IN(); // 释放SDA线设置为输入准备读取从机的应答 SDA(1); // 通过上拉电阻确保SDA为高开漏模式下设置输出1实际上是释放 delay_us(5); SCL(1); // 产生第9个时钟脉冲 delay_us(5); // 循环等待SDA被从机拉低ACK信号 while( (SDA_GET() 1) ( ack_flag ) ) { ack_flag--; delay_us(5); } if( ack_flag 0 ) // 超时了没等到ACK { IIC_Stop(); // 发送停止信号结束本次通信 return 1; // 返回1表示无应答失败 } else // 成功收到ACK { SCL(0); // 拉低SCL SDA_OUT(); // 将SDA重新设置为输出为后续发送数据做准备 } return 0; // 返回0表示有应答成功 }发送应答 (IIC_Send_Ack)当主设备作为接收方读完一个字节后需要告诉从设备是否还要继续发送下一个字节。发送ACK(0)表示“请继续”发送NACK(1)表示“够了停止发送”。void IIC_Send_Ack(uint8_t ack) { SDA_OUT(); SCL(0); SDA(0); // 先拉低准备产生ACK或NACK信号 delay_us(5); if(!ack) SDA(0); // 如果ack0发送ACK保持低电平 else SDA(1); // 如果ack1发送NACK拉高电平 SCL(1); // 产生一个时钟脉冲从机在这个脉冲期间读取ACK/NACK delay_us(5); SCL(0); SDA(1); // 释放SDA线 }3. SHT20传感器应用层驱动底层时序函数准备好了现在我们来编写针对SHT20传感器的具体读写函数。3.1 了解SHT20的通信规则设备地址SHT20的7位I2C地址是0x40二进制1000000。在I2C通信帧中地址后面会跟一个读写位。所以写操作发送(0x40 1) | 0 0x80读操作发送(0x40 1) | 1 0x81测量命令SHT20支持“主机保持”和“非主机保持”两种模式。我们常用的是“非主机保持”模式这样在传感器测量时MCU还能干点别的比如处理其他I2C设备。触发温度测量0xF3触发湿度测量0xF5数据读取发送测量命令后需要等待一段时间让传感器完成测量SHT20很快微秒级。然后重新发起读操作读取两个字节的数据高字节在前和一个字节的CRC校验码本教程为简化先忽略CRC校验。3.2 封装读取函数我们将读取温湿度的流程封装成一个函数SHT20_Read通过传入不同的命令字来区分是读温度还是湿度。float SHT20_Read(uint8_t regaddr) { unsigned char data_H 0; // 数据高字节 unsigned char data_L 0; // 数据低字节 float result 0; // 步骤1: 发送起始信号 器件地址(写) IIC_Start(); IIC_Write(0x80); // 写地址 0x40 1 if( IIC_Wait_Ack() 1 ) { printf(Error: No ACK after address write\r\n); return -999; // 返回一个错误值 } // 步骤2: 发送测量命令温度0xF3 或 湿度0xF5 IIC_Write(regaddr); if( IIC_Wait_Ack() 1 ) { printf(Error: No ACK after command write\r\n); return -999; } // 步骤3: 等待测量完成并发送起始信号 器件地址(读) // 这里用一个do-while循环直到传感器应答测量完成 do { delay_us(100); // 等待传感器转换时间根据数据手册这里给个稍长的值 IIC_Start(); IIC_Write(0x81); // 读地址 0x40 1 | 1 } while( IIC_Wait_Ack() 1 ); // 如果没应答NACK说明没准备好循环重试 delay_us(20); // 短暂延时 // 步骤4: 读取两个字节的数据和校验字节 data_H IIC_Read(); // 读高字节 IIC_Send_Ack(0); // 发送ACK请求下一个字节 data_L IIC_Read(); // 读低字节 IIC_Send_Ack(1); // 发送NACK表示不再需要数据忽略CRC字节 IIC_Stop(); // 发送停止信号结束通信 // 步骤5: 数据整合与换算 // SHT20返回的14位数据存放在高14位低2位是状态位需要清零 unsigned short raw_data (data_H 8) | data_L; raw_data ~0x0003; // 清除最低两位状态位 // 根据不同的命令进行数据换算 if( regaddr 0xf3 ) // 温度 { // 公式: T -46.85 175.72 * (raw_data / 2^16) result (raw_data / 65536.0) * 175.72 - 46.85; } if( regaddr 0xf5 ) // 湿度 { // 公式: RH -6 125.0 * (raw_data / 2^16) result (raw_data / 65536.0) * 125.0 - 6; if (result 100) result 100; // 湿度最大100% if (result 0) result 0; // 湿度最小0% } return result; }4. 主程序调用与实验最后我们在主函数里调用这个驱动就能读取温湿度了。#include board.h #include stdio.h #include bsp_sht20.h #define T_CMD 0xf3 // 读取温度命令 #define H_CMD 0xf5 // 读取湿度命令 int main(void) { // 开发板初始化时钟、串口等 board_init(); printf(SHT20 Demo Start!\r\n); while(1) { // 读取温度值 float temperature SHT20_Read(T_CMD); // 读取湿度值 float humidity SHT20_Read(H_CMD); // 通过串口打印结果保留两位小数 printf(Temperature: %.2f C\r\n, temperature); printf(Humidity: %.2f %%RH\r\n, humidity); printf(-----------------\r\n); // 每隔1秒读取一次 delay_ms(1000); } }将代码编译下载到MSPM0L开发板打开串口调试助手波特率根据你的board_init配置通常是115200应该就能看到周期打印出来的温湿度数据了。5. 调试心得与常见问题第一次做软件I2C很容易在时序上出问题。这里分享几个我调试时遇到的坑时序延时是关键delay_us()里的微秒延时数值不是随便写的。太快了传感器跟不上太慢了通信效率低。我代码里给的延时几个微秒对于SHT20在标准模式100kHz下是足够的。如果通信不稳定可以适当加大delay_us(5)这样的延时。SDA方向切换这是最容易出错的地方发送数据时SDA必须是输出模式接收数据前一定要先切换成输入模式。SDA_OUT()和SDA_IN()这两个宏就是干这个的务必在正确的地方调用。ACK等待超时在IIC_Wait_Ack()函数里我加了一个超时判断ack_flag。如果从设备一直不回应程序不能死等要超时退出并发送停止信号否则总线会被卡死。上拉电阻确保SDA和SCL线上有上拉电阻通常4.7kΩ到10kΩ。模块自带最好如果没有一定要自己加上否则总线无法拉高通信必然失败。电源与地线确保传感器供电稳定3.3V并且和MCU共地。地线连接不良是很多诡异问题的根源。好了整个基于软件I2C驱动SHT20的流程就是这样。虽然软件模拟比硬件I2C麻烦一点但它不占用硬件外设引脚可以任意分配在引脚紧张或者需要兼容不同硬件时非常有用。希望这篇教程能帮你顺利打通MSPM0的I2C通信之路。