12. 基于TI MSPM0G3507开发板的I2C协议详解与SHT20温湿度传感器驱动实战
最近在做一个环境监测的小项目,用到了TI的MSPM0G3507开发板来读取SHT20温湿度传感器的数据。很多刚开始接触嵌入式或者准备电赛的朋友都问过我,I2C协议到底怎么用?软件模拟和硬件I2C有什么区别?今天我就结合这个实战项目,把I2C从原理到代码实现,手把手给大家讲清楚。
咱们这篇教程会从I2C协议的基础讲起,然后带你用MSPM0G3507的GPIO引脚模拟I2C时序(也就是软件I2C),最后驱动SHT20传感器读取温湿度数据。整个过程我会把原理、配置、代码和常见问题都讲明白,保证你跟着做一遍就能掌握。
1. I2C协议到底是什么?
1.1 两线制的“聊天”协议
I2C(Inter-Integrated Circuit)也叫IIC,你可以把它想象成设备之间“聊天”的一种方式。它最大的特点就是简单——只需要两根线就能让多个设备互相通信。
这两根线分别是:
- SDA(Serial Data Line):数据线,用来传输实际的数据
- SCL(Serial Clock Line):时钟线,用来同步数据传输的节奏
在I2C总线上,设备有主从之分。主设备就像是聊天群里的群主,负责发起对话、控制聊天节奏;从设备就是群成员,只能等群主@自己才能说话。一个主设备可以同时和多个从设备通信,每个从设备都有一个唯一的“电话号码”(地址),主设备通过这个地址来指定要和谁通信。
1.2 硬件上的门道:为什么需要上拉电阻?
I2C在硬件实现上有个很重要的特点:它用的是开漏输出模式。这是什么意思呢?
你可以把开漏输出想象成一个只能“拉低”不能“推高”的开关。当设备要输出低电平时,它就把开关闭合,把线拉到地(0V);当要输出高电平时,它就把开关断开,让线“悬空”。
注意:开漏输出本身不能产生高电平,所以必须在SDA和SCL两根线上都接一个上拉电阻(通常2.2kΩ到10kΩ),这样当所有设备都不拉低时,电阻就把线拉到了高电平。
为什么要这么设计?主要有三个原因:
- 防止信号打架:如果多个设备同时用推挽模式输出,一个输出高一个输出低,就直接短路了
- 避免干扰:开漏模式下,只有拉低操作,不会出现多个高电平叠加干扰
- 实现“线与”:任何设备都能把总线拉低,方便实现多主机仲裁
I2C有不同的速度模式,咱们常用的有:
- 标准模式:100 kbps(每秒10万位)
- 快速模式:400 kbps
- 高速模式:更快,但用得少
速度越快,能用的线就越短。标准模式大概能到1米,快速模式就只有0.3米左右了。
1.3 软件I2C vs 硬件I2C:怎么选?
这是很多初学者困惑的地方,我简单对比一下:
软件I2C:
- 原理:用普通的GPIO引脚,通过程序控制引脚电平变化来模拟I2C时序
- 优点:不挑芯片,只要有GPIO就能用;引脚可以随便选,灵活
- 缺点:占用CPU资源,时序要靠延时函数控制,速度慢且不稳定
- 适用场景:低速通信、引脚资源紧张、芯片没有硬件I2C模块
硬件I2C:
- 原理:芯片内部有专门的I2C硬件模块,自动处理时序
- 优点:不占用CPU,通信稳定可靠,速度有保障
- 缺点:必须用芯片指定的引脚,不是所有芯片都有
- 适用场景:高速稳定通信、需要DMA传输、多任务系统
MSPM0G3507其实是有硬件I2C模块的,支持100k、400k、1Mbps三种速率,还有8字节的FIFO(数据缓冲区)。但今天咱们为了彻底理解I2C原理,先用软件方式实现一遍。
2. 手把手搭建MSPM0G3507的I2C工程
2.1 工程准备与引脚配置
首先,咱们需要一个干净的工程模板。如果你用的是TI的SDK,找到G3507的空白工程(empty工程),复制到SDK根目录下,然后用Keil打开。
提示:工程必须放在SDK根目录,否则编译时会找不到相关文件。
打开工程后,咱们要配置两个引脚作为I2C的SDA和SCL。根据原文,我们选择:
- SDA:PA0引脚
- SCL:PA1引脚
在Keil里打开empty.syscfg文件,然后会弹出SYSCONFIG的图形化配置界面。这里我们需要做两件事:
配置GPIO引脚
- 点击ADD添加GPIO配置组
- 把PA0和PA1都配置为GPIO功能
- 注意:软件I2C不需要配置为I2C外设模式,就用普通的GPIO就行
配置串口
- 因为我们要通过串口把温湿度数据打印到电脑上查看
- 选择任意一个UART引脚(比如PA2、PA3)配置为UART功能
配置完成后保存,Keil会自动生成ti_msp_dl_config.h文件,里面就有我们刚才配置的引脚定义。
2.2 工程文件管理
为了让代码结构清晰,咱们在工程里新建一个BSP文件夹(Board Support Package,板级支持包),所有自己写的驱动文件都放这里。
在BSP文件夹下创建两个文件:
bsp_i2c.c- I2C驱动实现文件bsp_i2c.h- I2C驱动头文件
然后在Keil的工程管理窗口:
- 新建一个工程分组,命名为
BSP - 把
bsp_i2c.c添加到这个分组 - 在工程设置里添加
BSP文件夹到头文件搜索路径
注意:如果你把工程名从
empty改成了别的名字,记得更新头文件路径,把empty改成你的新工程名。
3. 用GPIO模拟I2C时序
3.1 引脚操作的宏定义
软件I2C的核心就是控制GPIO引脚的电平。为了代码清晰易读,咱们先定义一组宏:
// bsp_i2c.h #ifndef __BSP_I2C_H__ #define __BSP_I2C_H__ #include "ti_msp_dl_config.h" #include "stdio.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和SCL引脚输出高低电平 #define SDA(x) ( (x) ? (DL_GPIO_setPins(I2C_PORT,I2C_SDA_PIN)) : (DL_GPIO_clearPins(I2C_PORT,I2C_SDA_PIN)) ) #define SCL(x) ( (x) ? (DL_GPIO_setPins(I2C_PORT,I2C_SCL_PIN)) : (DL_GPIO_clearPins(I2C_PORT,I2C_SCL_PIN)) ) #endif这几个宏是软件I2C的基石:
SDA_OUT()和SDA_IN()用于切换SDA引脚的方向(输出或输入)SDA_GET()用于读取SDA引脚的电平状态SDA(x)和SCL(x)用于设置引脚电平,x为1输出高,为0输出低
3.2 I2C的四大基本时序
I2C通信有四个最基本的时序信号,咱们一个一个来实现。
1. 起始信号(Start)起始信号告诉所有设备:“我要开始通信了”。它的时序是:SCL为高电平时,SDA从高变低。
void IIC_Start(void) { SDA_OUT(); // SDA设置为输出模式 SCL(0); // 先确保SCL为低 SDA(1); // SDA拉高 SCL(1); // SCL拉高 delay_us(5); // 保持一段时间 SDA(0); // SDA从高变低,产生起始信号 delay_us(5); SCL(0); // SCL拉低,为后续数据传输做准备 delay_us(5); }2. 停止信号(Stop)停止信号表示:“通信结束”。时序是:SCL为高电平时,SDA从低变高。
void IIC_Stop(void) { SDA_OUT(); SCL(0); // 先确保SCL为低 SDA(0); // SDA拉低 SCL(1); // SCL拉高 delay_us(5); SDA(1); // SDA从低变高,产生停止信号 delay_us(5); }3. 发送一个字节发送数据时,每个bit都是在SCL为低电平时改变SDA,在SCL为高电平时保持稳定。
void IIC_Send_Byte(uint8_t dat) { int i = 0; SDA_OUT(); SCL(0); // 确保SCL为低 // 从最高位开始,依次发送8个bit for(i = 0; i < 8; i++) { // 取出最高位(0x80是1000 0000) SDA((dat & 0x80) >> 7); delay_us(1); SCL(1); // SCL拉高,从机在这时采样数据 delay_us(5); SCL(0); // SCL拉低,准备发送下一位 delay_us(5); dat <<= 1; // 左移一位,准备发送下一位 } }4. 接收一个字节接收数据时,主机控制SCL产生时钟,从机在SCL为高时把数据放到SDA上。
unsigned char IIC_Read_Byte(void) { unsigned char i, receive = 0; SDA_IN(); // SDA设置为输入模式 for(i = 0; i < 8; i++) { SCL(0); delay_us(5); SCL(1); // SCL拉高,从机在这时放置数据 delay_us(5); receive <<= 1; // 左移一位,为接收新bit腾出位置 if(SDA_GET()) // 读取SDA电平 { receive |= 1; // 如果为高,最低位置1 } SCL(0); // SCL拉低,准备接收下一位 } return receive; }3.3 应答机制:确认收到
I2C有个很重要的机制叫应答(ACK)。每传输完一个字节(8位数据),接收方都要在第9个时钟周期给出应答信号:
- 应答(ACK):接收方把SDA拉低,表示“我收到了”
- 非应答(NACK):接收方保持SDA为高,表示“我没收到”或“别发了”
主机发送应答/非应答:
void IIC_Send_Ack(uint8_t ack) { SDA_OUT(); SCL(0); SDA(0); // 先拉低,准备发送应答 delay_us(5); if(!ack) SDA(0); // ack=0,发送应答(低电平) else SDA(1); // ack=1,发送非应答(高电平) SCL(1); // SCL拉高,从机在这时采样应答信号 delay_us(5); SCL(0); SDA(0); // 恢复SDA为低 }主机等待从机应答:
uint8_t IIC_Wait_Ack(void) { char ack = 0; unsigned char ack_flag = 10; // 超时计数 SDA_IN(); // SDA设置为输入,准备读取从机应答 SDA(1); // 释放SDA线(加上拉电阻会拉高) delay_us(5); SCL(1); // SCL拉高 delay_us(5); // 等待从机拉低SDA(应答) while((SDA_GET() == 1) && (ack_flag)) { ack_flag--; delay_us(5); } if(ack_flag <= 0) // 超时,没有收到应答 { IIC_Stop(); // 发送停止信号 return 1; // 返回1表示无应答 } else // 收到了应答 { SCL(0); // SCL拉低 SDA_OUT(); // SDA改回输出模式 } return ack; // 返回0表示有应答 }这里有个细节要注意:等待应答时,主机要先释放SDA线(设置为输入模式),让从机有机会拉低它。如果从机不拉低,SDA会被上拉电阻拉到高电平,这就是非应答。
4. 驱动SHT20温湿度传感器
4.1 SHT20传感器简介
SHT20是一款数字式温湿度传感器,精度很高:
- 温度精度:±0.3℃
- 湿度精度:±3%RH
- 工作电压:2.1~3.6V(正好适合咱们的3.3V开发板)
- 通信接口:I2C
它的I2C地址是0x80(写操作)或0x81(读操作)。这里解释一下:I2C地址一般是7位,SHT20的7位地址是0b1000_000(0x40)。但实际发送时,我们要把读写位加上:
- 写操作:地址左移1位,最低位写0 → 0x80
- 读操作:地址左移1位,最低位写1 → 0x81
4.2 硬件连接
接线非常简单,只有4根线:
| SHT20引脚 | 开发板引脚 |
|---|---|
| SCL | PA1 |
| SDA | PA0 |
| GND | GND |
| VCC | 3V3 |
注意:SHT20的工作电压是2.1~3.6V,一定要接3.3V,不要接5V!
4.3 SHT20的通信流程
SHT20支持两种工作模式,咱们用的是非主机模式(No Hold Master)。这种模式下,传感器测量时不会占用I2C总线,主机可以干别的事。
测量命令:
- 测量温度:0xF3
- 测量湿度:0xF5
读取数据的完整流程,我画了个示意图帮你理解:
开始通信: 1. 主机发送起始信号 2. 主机发送器件地址+写(0x80) 3. 从机应答 4. 主机发送测量命令(0xF3或0xF5) 5. 从机应答 等待测量完成(需要延时一段时间) 读取数据: 6. 主机发送起始信号(重复起始) 7. 主机发送器件地址+读(0x81) 8. 从机应答 9. 主机读取数据高字节 10. 主机发送应答 11. 主机读取数据低字节 12. 主机发送应答 13. 主机读取校验字节(可选) 14. 主机发送非应答 15. 主机发送停止信号4.4 完整的SHT20驱动函数
把上面的流程用代码实现:
float SHT20_Read(uint8_t regaddr) { unsigned char data_H = 0; // 数据高8位 unsigned char data_L = 0; // 数据低8位 float temp = 0; // 第一步:开始通信,发送器件地址(写) IIC_Start(); IIC_Send_Byte(0x80 | 0); // 0x80是写地址 if(IIC_Wait_Ack() == 1) // 等待应答 { printf("error -1\r\n"); // 无应答,可能是接线问题 return 0; } // 第二步:发送测量命令 IIC_Send_Byte(regaddr); // 0xF3测温度,0xF5测湿度 if(IIC_Wait_Ack() == 1) { printf("error -2\r\n"); return 0; } // 第三步:等待测量完成,然后发送读地址 do { delay_us(10); // 给传感器一点测量时间 IIC_Start(); // 重复起始信号 IIC_Send_Byte(0x80 | 1); // 0x81是读地址 } while(IIC_Wait_Ack() == 1); // 如果传感器没准备好,会返回非应答,需要重试 delay_us(20); // 再等一小会儿 // 第四步:读取数据 data_H = IIC_Read_Byte(); // 读取高8位 IIC_Send_Ack(0); // 发送应答,告诉传感器继续发 data_L = IIC_Read_Byte(); // 读取低8位 IIC_Send_Ack(1); // 发送非应答,告诉传感器别发了 IIC_Stop(); // 发送停止信号,结束通信 // 第五步:数据处理 // 数据低字节的最后两位是状态位,需要清零 uint16_t dat = (data_H << 8) | data_L; dat &= ~(0x03); // 清除最后两位状态位 // 第六步:数据换算 if(regaddr == 0xf3) // 温度数据 { // 温度转换公式:T = -46.85 + 175.72 * (S(T) / 65536) temp = (dat / 65536.0) * 175.72 - 46.85; } if(regaddr == 0xf5) // 湿度数据 { // 湿度转换公式:RH = -6 + 125 * (S(RH) / 65536) temp = (dat / 65536.0) * 125.0 - 6; } return temp; }这里有几个关键点需要注意:
- 状态位清除:SHT20返回的16位数据中,最低两位是状态位(bit0无意义,bit1表示测量类型),计算前要清除
- 数据换算:传感器返回的是原始数据,需要按公式转换成实际值
- 浮点运算:公式里有除法和小数,记得用浮点变量存储结果
4.5 主函数调用
最后,在main函数里调用我们的驱动函数:
#include "ti_msp_dl_config.h" #include "stdio.h" #include "bsp_i2c.h" #define delay_ms(X) delay_cycles((CPUCLK_FREQ/1000)*(X)) #define T_ADDR 0xf3 // 温度命令 #define PH_ADDR 0xf5 // 湿度命令 int main(void) { SYSCFG_DL_init(); // 系统初始化 printf("SHT20 Start!!\r\n"); while(1) { float TEMP = SHT20_Read(T_ADDR); // 读取温度 float PH = SHT20_Read(PH_ADDR); // 读取湿度 printf("Temp = %.2f C\r\n", TEMP); printf("Humi = %.2f %%RH\r\n", PH); printf("\n"); delay_ms(1000); // 每秒读取一次 } }重要提示:printf输出浮点数需要开启microLIB库,并在工程设置里调整堆栈大小。具体操作:Options for Target → Target → 勾选"Use MicroLIB",然后调整Stack Size为0x800(2KB)。
5. 调试技巧与常见问题
5.1 如果通信失败怎么办?
- 检查接线:这是最常见的问题,确保SDA、SCL、GND、VCC四根线都接对了
- 检查上拉电阻:软件I2C必须接上拉电阻(4.7kΩ或10kΩ),否则SDA和SCL无法被拉高
- 用示波器看波形:如果有条件,用示波器看SDA和SCL的波形,检查时序是否符合要求
- 调整延时时间:不同MCU速度不同,可能需要调整
delay_us()的时间 - 检查地址:确认传感器地址是否正确,SHT20是0x80(写)/0x81(读)
5.2 软件I2C的时序优化
软件I2C的时序全靠延时函数控制,如果发现通信不稳定:
- 缩短延时:如果MCU速度快,可以减小
delay_us()的参数 - 增加超时判断:在等待应答的地方增加超时机制,避免死等
- 禁用中断:在关键时序操作期间禁用中断,避免被打断
5.3 从软件I2C迁移到硬件I2C
当你掌握了软件I2C的原理后,可以尝试用MSPM0G3507自带的硬件I2C模块。硬件I2C更稳定,不占用CPU,而且支持DMA传输。迁移时主要改三部分:
- 引脚配置改为I2C功能模式
- 初始化硬件I2C外设
- 调用TI提供的I2C驱动库函数
不过说实话,对于初学者来说,先搞懂软件I2C的原理非常重要。我见过很多直接调库的同学,一旦通信出问题就完全不知道怎么调试。自己实现一遍软件I2C,你对时序的理解会深刻很多。
好了,关于MSPM0G3507的I2C协议和SHT20驱动就讲到这里。实际做项目时,你可能还会遇到I2C总线冲突、从设备无响应、数据校验错误等问题,但只要你理解了基本原理,这些都能通过逻辑分析仪或示波器找到原因。