I2C通信
前言:
IIC( Inter-Integrated Circuit,or I2C)协议是由飞利浦半导体(现在的恩智浦半导体)开发,并于1982年发布的一种串行、半双工总线,主要用于近距离,低速的芯片之间的通信;I2C总线由两根双向的信号线,一根数据线SDA(serial data)用于收发数据,一根时钟线SCL(serial clock)L用于通信双方时钟的同步;I2C总线硬件结构简单,成本较低,因此在各领域得到了广泛的应用。
IC2原理:
I2C 总线,分别由SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成。
通信原理是通过对SCL和SDA线高低电平时序的控制,来 产生I2C总线协议所需要的信号进行数据的传递。在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。
I2C主要特征如下:
- IIC用2根信号线通信:串行数据线 SDA(数据传输)、串行时钟线 SCL(同步时钟信号);
- IIC总线上所有器件的SDA、SCL引脚输出驱动都为 开漏(OD) 结构,通过外接上拉电阻实现总线上所有节点SDA、SCL信号的线与逻辑关系;
- 总线上的所有设备通过软件寻址且具有唯一的地址(7位或10位)。7位“从机专用地址码”,其高4位为由生产厂家制定的设备类型地址,低3位为器件引脚定义地址(由使用者定义);10位地址不常见;
- 任何时刻都只存在简单的主从关系,按数据传输的方向,主机可以是主发送器或主接收器;
- 支持多主机。在总线上存在多个主机时,通过冲突检测和仲裁机制防止多个主机同时发起数据传输时存在的冲突;
- IIC总线上所有器件都具有“自动应答”功能,保证数据传输的正确性; 主机和从机的区别在于对SCL的发送权,只有主机才能发送SCL;
I2C物理拓扑如下:
I2C 总线的稳定通信,离不开合适的上拉电阻。它需要在速度、功耗和驱动能力之间取得平衡。下面是一个快速选型参考:
通信模式 | 速率 | 典型总线电容 | 推荐上拉电阻值 (Vcc=3.3V/5V) | 主要考虑因素 |
---|---|---|---|---|
标准模式 | ≤100 kbps | ≤400 pF | 4.7 kΩ 或 10 kΩ | 稳定性、功耗 |
快速模式 | ≤400 kbps | ≤200 pF | 2.2 kΩ 或 1.5 kΩ | 上升时间、速度要求 |
高速模式 | ≤3.4 Mbps | ≤100 pF | ≤1.5 kΩ | 严格的上升时间要求 |
低功耗设备 | 低速 | 较小 | 10 kΩ 或更大 | 最小化静态电流,延长电池寿命 |
多设备总线 | 依模式而定 | 较大 | 适当减小阻值(如用2.2kΩ) | 总线电容增大,需保证上升时间 |
常见值起步:
- 3.3V 系统:从 4.7kΩ 或 2.2kΩ 开始尝试。
- 5V 系统:从 4.7kΩ 或 10kΩ 开始尝试。
I2C协议层:
起始和结束:
I2C协议规定,总线上数据的传输必须以一个起始信号作为开始条件,以一个结束信号作为传输的停止条件。
起始和结束信号总是由主设备产生。
- 总线在空闲状态 时,SCL和SDA都保持着高电平,
- 当SCL为高而SDA由高到低的跳变,表示产生一个起始条件;
- 当SCL为高而SDA由低到高的跳变,表示产生一个 停止条件。
- 在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线;
- 而在停止条件产生后,本次数据传输的主从设备将释放总线,总线再次处于空闲状态。
数据有效性:
IIC 的数据读取动作都在 SCL为高 时产生,SCL为低时是数据改变的时期,无论SDA如何变化都不影响读取。所以,传输数据的过程中,当SCL为高时,数据应当保持稳定,避免数据的采集出错。
特性 | 描述 | 重要性/影响 |
---|---|---|
数据稳定 | SCL 高电平期间,SDA 上的数据必须保持稳定。 | 确保接收方在时钟上升沿能采样到正确数据。 |
数据变化时机 | SDA 上的数据状态只能在 SCL 为低电平时改变。 | 为下一个数据位的传输做准备,避免在数据有效期内产生干扰。 |
起始条件 | 当 SCL 为高电平时,SDA 从高电平跳变到低电平。 | 标志一次数据传输的开始,唤醒总线上的从设备。 |
停止条件 | 当 SCL 为高电平时,SDA 从低电平跳变到高电平。 | 标志一次数据传输的结束,总线进入空闲状态。 |
应答信号(ACK) | 接收设备在传输第9个时钟周期将SDA拉低。 | 确认已成功接收一个字节的数据。 |
非应答信号(NACK) | 接收设备在第9个时钟周期保持SDA为高。 | 表示未成功接收数据或希望结束传输。 |
高低电平表示:在 SCL 高电平期间,SDA 为高电平表示数据 '1',低电平表示数据 '0'
具体的取值过程如下:
SCL低电平不取数据,作为下一位数据的电平转换间隔
SCL高电平才会取数据
响应:
数据传输必须带响应,相关的响应SCL时钟脉冲由主机产生,在响应的时钟脉冲期间,发送器释放 SDA 线(输出高阻态使SDA线被上拉电阻拉高)。在响应的时钟脉冲期间,接收器必须将 SDA 线拉低,使它在这个时钟脉冲的高电平期间保持稳定的低电平。 必须考虑建立和保持时间。
I2C 协议中的应答信号(ACK) 和非应答信号(NACK) 是确保数据可靠传输的关键机制。每次传输完一个字节(8位数据)后,都会紧跟一个应答位,使得实际传输一帧数据需要9个时钟脉冲
特性 | 应答信号 (ACK) | 非应答信号 (NACK) |
---|---|---|
电平状态 | SDA 为低电平 | SDA 为高电平 |
产生时机 | 每传输完8位数据后的第9个时钟周期 | 每传输完8位数据后的第9个时钟周期 |
发送方 | 接收数据的一方(可能是主设备或从设备) | 接收数据的一方(可能是主设备或从设备) |
含义 | 成功接收字节,请求继续传输 | 未成功接收字节或希望终止传输 |
典型场景 | 从设备地址匹配、数据正确接收 | 从设备地址不匹配、接收方繁忙、主机读取数据时希望停止 |
对应的常见场景:
- 地址应答:主设备发送从设备地址后,地址匹配的从设备会回复 ACK,否则总线保持高电平(NACK)。
- 数据应答:成功接收一个数据字节后,接收方会回复 ACK。
- 读数据终止:主设备作为接收方从从设备读取数据时,在收到最后一个字节后,会发送 NACK 通知从设备结束发送,随后主设备产生停止信号。
遇到 NACK 通常表示通信出现了问题,可能的原因包括:
- 从设备地址错误:总线上无此地址的从设备。
- 从设备忙:从设备可能正在进行其他操作(如内部写入),无法及时响应。
- 硬件连接问题:如线路接触不良、上拉电阻不合适等。
- 协议错误:如时序不符合规范。
寻址:
I2C 总线通过独特的寻址机制实现主设备与特定从设备间的通信。每个从设备都有唯一地址,主设备通过发送该地址来选中目标从设备进行数据交换。
寻址分为7位寻址模式和10位寻址模式,具体区别如下
特性 | 7位寻址模式 | 10位寻址模式 |
---|---|---|
地址长度 | 7位 | 10位 |
地址空间 | 128个 (0x00 到 0x7F) | 1024个 (0x000 到 0x3FF) |
通信开销 | 较低 | 较高 (需两个字节传输地址) |
兼容性 | 广泛支持,最常用 | 与7位寻址兼容,可在同一总线上混合使用 |
典型应用 | 传感器、EEPROM、RTC等常见外设,设备数量适中的系统 | 复杂系统、大型传感器网络等需要连接大量设备的场合 |
地址字节结构 | 第一字节:高7位为地址,最低位 (LSB) 为 R/W 位 | 第一字节:高5位为 11110 ,随后2位为地址高两位,LSB为 R/W 位;第二字节:地址低8位 |
7位寻址模式如下:
在起始条件 S 后 ,发送了一个从机地址SLAVE ADDRESS, 这个地址共有 7 位,紧接着的第 8 位是数据方向位[R/W], 0 表示写,1表示读。接下来的一个bit是应答位NACK/ACK,当这个帧中前面8bits发送完后,接收端获得SDA控制权,此时接收设备应该在第9个时钟脉冲之前回复一个ACK(将SDA拉低)以表示接收正常,如果接收设备没有将SDA拉低,则说明接收设备可能没有收到数据(如寻址的设备不存在或设备忙)或无法解析收到的消息,如果是这样,则由master来决定如何处理(stop或repeated start condition)。
10位寻址模式如下:
两个寻址代码区别:
// 假设 device_address 存储了目标从设备的地址
if (device_address <= 0x7F) {// 使用7位地址模式send_7bit_address(device_address); // 发送一个字节,包含7位地址和R/W位
} else {// 使用10位地址模式send_10bit_address_first_byte(0xF0 | ((device_address >> 8) & 0x03)); // 发送第一个字节,固定前缀11110 + 地址高2位 + R/W位(通常先设为写)send_10bit_address_second_byte(device_address & 0xFF); // 发送第二个字节,地址低8位
}
数据传输:
数据传输主要分为如下两种
- 主机写-从机收,传输方向不变
- 主机读-从机发,传输方向改变
传输阶段 | 主导方 | 关键动作/信号 | 备注 |
---|---|---|---|
起始条件 | 主设备 | SCL为高电平时,SDA产生一个从高到低的下降沿。 | 标志一次传输的开始,唤醒所有从设备。 |
发送地址帧 | 主设备 | 发送7位或10位从设备地址 + 1位读写控制位(0写/1读)。 | 总线上所有从设备将自己的地址与此比较。 |
等待地址应答 | 从设备 | 地址匹配的从设备在第9个时钟周期将SDA拉低,返回ACK。地址不匹配或总线出问题时,SDA保持高电平,即NACK。 | |
数据传输 | 主/从设备 | 每个数据字节8位,高位(MSB)在前。每个字节后必须跟一个应答位(ACK或NACK)。 | 数据在SCL高电平时必须稳定,只能在SCL低电平时变化。 |
写操作:主设备发送数据,从设备每接收一个字节后回复ACK。 | |||
读操作:从设备发送数据,主设备每接收一个字节后回复ACK;主设备在收到最后一个字节后发送NACK以示读取结束。 | |||
时钟拉伸 | 从设备 | 从设备若未准备好,可在ACK周期后将SCL拉低,强制主设备等待,直到从设备释放SCL。 | 一种简单的流控制机制。 |
停止条件 | 主设备 | SCL为高电平时,SDA产生一个从低到高的上升沿。 | 标志一次传输的结束,总线进入空闲状态(SDA和SCL均保持高电平) |
主设备向从设备写数据:
- 主设备发送起始条件(Start)。
- 主设备发送从设备地址(7位或10位) + 写控制位(0)。此处需要注意,发送数据时,无法发送7bit数据,此处发送了7bit地址+1bit读写选择位,即发送7bit+R/W。最低位为1表示读,为0表示写。
- 从设备返回ACK。
- 主设备发送数据字节。
- 从设备每收到一个字节,回复一个ACK。
- 重复步骤4-5直至数据发送完毕。
- 主设备发送停止条件(Stop)。
主设备从从设备读数据:
主机需要先向从机发送一次信号,告诉从机”我要读取数据“,然后重开一次通信,等待从机主动返回数据。以示例来讲解,发送 “[1-Byte]开始字节(写) + [1-Byte]要读取的寄存器的地址”,之后结束通信,或者重开始,来进入到第二次通信中,先发送 [1-Byte]开始字节(读),然后等待读取从机发送过来的 [1-Byte]数据 即可。
- 主设备发送起始条件(Start)。
- 主设备发送从设备地址(7位或10位) + 读控制位(1)。
- 从设备返回ACK。
- 从设备发送数据字节。
- 主设备每收到一个字节,回复一个ACK(最后一个字节除外)。
- 主设备在收到最后一个字节后,回复NACK。
- 主设备发送停止条件(Stop)
单片机I2C通信:
这里以AHT20+BMP280 温湿度气压传感器模块为例
AHT20 模块:
首先查看AHT20传感器的芯片手册,重要的信息如下
上述可以知道设备地址为0x38,初始化命令0xBE,开始测量0xAC;软复位0xBA
对应的返回状态如下
对应的状态含义如下:
比特位(从高到低) | 含义 |
---|---|
7 | Busy 状态:1 表示传感器正忙,0 表示空闲。 |
6 | 当前工作模式:NOR;CYC;CMD |
5 | 保留位 |
4 | 保留位 |
3 | 校准使能位 (Calibration Enable):1 表示已校准,0 未校准。 |
2 | 保留位 |
1 | 保留位 |
0 | 数据就绪状态:1 表示数据可用,0 表示数据未就绪。 |
特性 | NOR 模式 (Normal Mode) | CYC 模式 (Cycle Mode) | CMD 模式 (Command Mode) |
---|---|---|---|
触发方式 | 需外部主机发送命令来触发单次测量 | 自动周期性地进行测量和休眠 | 通过发送特定命令进行模式切换或配置 |
功耗特点 | 休眠时功耗极低 (约 0.1μA),仅保持接口电路 | 休眠功耗略高于 NOR 模式 (约 0.2μA),周期性唤醒测量 | 取决于具体执行的命令和芯片状态 |
数据更新 | 测量完成后更新数据,之后进入休眠,数据保持不变直至下次测量 | 自动更新数据,用户可在任意时刻读取,但需注意数据是否为最新 | 依据所发送的命令决定 |
适用场景 | 对功耗敏感,采样率低,或需与主机同步操作的应用 | 需要持续自动监测,主机不想频繁发送命令的应用 | 用于配置芯片参数、切换模式等操作 |
工作流程 | 命令 → 测量 → 数据更新 → 休眠 → 等待下一个命令 | 休眠 → 自动唤醒 → 测量 → 数据更新 → 休眠 | 接收并执行特定命令(如寄存器读写、模式切换) |
代码如下:
#define ATH20_SLAVE_ADDRESS 0x38 /* I2C从机地址 *///****************************************
// 定义 AHT20 内部地址
//****************************************
#define INIT 0xBE //初始化
#define SoftReset 0xBA //软复位
#define StartTest 0xAC //开始测试
这里需要注意:0x38 = 0011 1000
但是这里需要左移一位,第八位为读取还是写入字符,即如下
写:0111 0001:0x71
读:0111 0000:0x70
详细的测量流程如下
对应代码步奏如下:
第一步初始化,首先等待40ms,刚开始上电的时候开始初始化,初始化使用0xBE命令进行初始化,然后使用ATH20_Read_Cal_Enable方法判断返回值,参数是0x08和0x00,判断返回值是否有效,如果有效则进入下一步,对模式的判断要求为已经校验的NOR模式
if((val & 0x68) == 0x08)
- 0x68 的二进制表示:
0110 1000
- 0x08 的二进制表示:
0000 1000
- 这意味着它关注(屏蔽)了第 7、6、5、3 位(从最高位 7 到最低位 0),但更具体地说,它主要检查第 6、5 和 3 位(因为二进制中为1的那些位)。
对应的第三位模式位;第五六位为模式位,要求为已经校验的NOR模式
//读取AHT10的状态寄存器
uint8_t ATH20_Read_Status(void)
{uint8_t Byte_first;Sensors_I2C_ReadRegister(ATH20_SLAVE_ADDRESS, 0x00, 1,&Byte_first);return Byte_first;
}uint8_t ATH20_Read_Cal_Enable(void)
{//ret = 0uint8_t val = 0;val = ATH20_Read_Status();//判断NOR模式和校准输出是否有效if((val & 0x68) == 0x08) return 1;elsereturn 0;
}uint8_t count;
uint8_t ATH20_Init(void)
{uint8_t tmp[10];SoftDelay_ms(40);tmp[0] = 0x08;tmp[1] = 0x00;//P0 口中断不使能Sensors_I2C_WriteRegister(ATH20_SLAVE_ADDRESS,INIT, 2, tmp); SoftDelay_ms(500);count = 0;//需要等待状态字status的Bit[3]=1时才去读数据。如果Bit[3]不等于1 ,//发软件复位0xBA给AHT10,再重新初始化AHT10,直至Bit[3]=1while(ATH20_Read_Cal_Enable() == 0){Sensors_I2C_WriteRegister(ATH20_SLAVE_ADDRESS,SoftReset, 0, tmp);SoftDelay_ms(200);Sensors_I2C_WriteRegister(ATH20_SLAVE_ADDRESS,INIT, 2, tmp);count++;if(count >= 10)return 0;SoftDelay_ms(500);}return 1;
}
第二步开始请求获取对应的值,首先发送0xAC命令对应0x33和0x00数据然后等待75ms后读取数据,返回的状态主要读取Bit[7],判断是不是0,如果是1则为设备还在测量则重复读取数据,然后就是读取数据,读取数据需要参考如下官方图
安装上面的分类可以分为如下
- Data[0]: 状态字(包含繁忙位和校准状态等信息)
- Data[1]: 湿度数据高字节
- Data[2]: 湿度数据中字节
- Data[3]: 湿度数据低字节(高4位)和温度数据高字节(低4位)
- Data[4]: 温度数据中字节
- Data[5]: 温度数据低字节
所以温度是20位,湿度为20位,总共为40位,对应的uint32位的RetuData存储温湿度数据,值来源为unit8位的Data原始数据
湿度为(Data[1]+Data[2]+Data[3])>> 4
温度为(Data[3]+Data[4]+Data[5])& 0xfffff
最后就是对获取的值进行换算,对应的换算公式如下
对应的换算
实际湿度值 =
(ct[0] * 100) / 1048576
- 公式含义:
湿度百分比 = (原始值 / 2^20) * 100%
- 示例:
ct[0] = 733473
, 则湿度 = (733473 * 100) / 1048576 ≈ 69.94%
- 公式含义:
实际温度值 =
(ct[1] * 200) / 1048576 - 50
- 公式含义:
温度摄氏度 = (原始值 / 2^20) * 200 - 50
- 示例:
ct[1] = 512000
, 则温度 = (512000 * 200) / 1048576 - 50 ≈ 47.65°C
- 公式含义:
对应的代码如下
//读取AHT10的温度和湿度数据
void ATH20_Read_CTdata(uint32_t *ct)
{uint32_t RetuData = 0;uint16_t cnt = 0;uint8_t Data[10];uint8_t tmp[10];tmp[0] = 0x33;tmp[1] = 0x00;//P0 口中断不使能Sensors_I2C_WriteRegister(ATH20_SLAVE_ADDRESS,StartTest, 2, tmp); //等待75msSoftDelay_ms(75);cnt = 0;//等待忙状态结束while(((ATH20_Read_Status()&0x80) == 0x80)){SoftDelay_ms(1);if(cnt++ >= 100){break;}}Sensors_I2C_ReadRegister(ATH20_SLAVE_ADDRESS, 0x00, 7,Data);RetuData = 0;RetuData = (RetuData|Data[1]) << 8;RetuData = (RetuData|Data[2]) << 8;RetuData = (RetuData|Data[3]);RetuData = RetuData >> 4;ct[0] = RetuData;RetuData = 0;RetuData = (RetuData|Data[3]) << 8;RetuData = (RetuData|Data[4]) << 8;RetuData = (RetuData|Data[5]);RetuData = RetuData&0xfffff;ct[1] = RetuData;
}int main(void)
{uint8_t ret = 0;float P,T,ALT;uint32_t CT_data[2];int c1,t1;uint8_t LED_Stat = 0;ret = ATH20_Init();if(ret == 0){printf("ATH20传感器初始化错误\n");while(1);}while(1){/* 读取 ATH20 传感器数据*/while(ATH20_Read_Cal_Enable() == 0){//如果为0再使能一次ATH20_Init();SoftDelay_ms(30);}//读取温度和湿度ATH20_Read_CTdata(CT_data); //计算得到湿度值(放大了10倍,如果c1=523,表示现在湿度为52.3%)c1 = CT_data[0] * 1000 / 1024 / 1024; //计算得到温度值(放大了10倍,如果t1=245,表示现在温度为24.5℃)t1 = CT_data[1] * 200 *10 / 1024 / 1024 - 500;}
}
BMP280模块:
BMP280主要是通过对寄存器的操作来对气压进行读取
主要的寄存器解释如下
寄存器地址 (Hex) | 寄存器名称 | 主要功能与位定义 | 读写类型 | 注意事项 |
---|---|---|---|---|
0xD0 | 芯片ID寄存器 | 存储芯片标识符,固定值为 0x58。用于验证通信和器件型号。 | 只读 | 上电后读取该寄存器,若值非0x58,应检查通信线路、地址或电源。 |
0xE0 | 复位寄存器 | 向该寄存器写入 0xB6 可使传感器执行软复位,恢复至上电初始状态。写入其他值无效。 | 只写 | 复位后需等待短暂时间(通常几毫秒)让传感器准备就绪。 |
0xF3 | 状态寄存器 | Bit3 (measuring):1表示正在测量,0表示测量完成。 Bit0 (im_update):1表示NVM(校准参数)正在复制,0表示复制完成。 | 只读 | 读取数据前,建议检查 Bit3 是否为0,以确保数据已就绪,避免读取到旧数据或未完成测量的数据。 |
0xF4 | 控制测量寄存器 | Bit7-5 (osrs_t):温度过采样设置(0-5对应关闭/1x/2x/4x/8x/16x)。 Bit4-2 (osrs_p):压力过采样设置(同上)。 Bit1-0 (mode):工作模式(00=休眠,01=强制模式,11=正常模式)。 | 读写 | 过采样率越高,数据精度越高,但转换时间和功耗也会增加。强制模式:单次测量后返回睡眠模式;正常模式:持续循环测量。 |
0xF5 | 配置寄存器 | Bit7-5 (t_sb):在正常模式下的待机时间(0-7对应0.5ms-4000ms)。 Bit4-2 (filter):IIR滤波器系数(0-4对应关闭/2/4/8/16)。 Bit0 (spi3w_en):SPI三线模式使能(I2C模式下保持0)。 | 读写 | IIR滤波器可平滑输出数据,减少短期波动。待机时间仅在正常模式下有效,决定了连续测量之间的间隔。 |
0xF7-0xF9 | 压力数据寄存器 | 存储20位原始压力测量值(ADC值)。需组合 0xF7 (MSB) 、0xF8 (LSB) 、0xF9 (XLSB) 。 | 只读 | 需按 `raw_data = (msb << 12) |
0xFA-0xFC | 温度数据寄存器 | 存储20位原始温度测量值(ADC值)。需组合 0xFA (MSB) 、0xFB (LSB) 、0xFC (XLSB) 。 | 只读 | 组合方式同压力数据。需使用校准参数补偿计算得到真实温度值(通常单位为℃)。 |
0x88-0x9F | 校准参数寄存器 | 存储26个出厂校准系数(只读),用于温度和压力数据的补偿计算: dig_T1 (uint16), dig_T2 (int16), dig_T3 (int16):温度补偿参数。 dig_P1 (uint16), dig_P2-P9 (int16):压力补偿参数。 | 只读 | 必须在初始化时读取并保存这些参数。后续每次读取的原始ADC值都必须使用这些参数通过特定算法计算,才能得到准确的物理量。 |
简单起见,先介绍下BMP280 的芯片识别
BMP280 的芯片 ID 寄存器地址为 0xD0。向该寄存器发起读取操作,成功通信后,BMP280 会返回一个字节的数据。这个值对于 BMP280 来说固定为 0x58。因此,只要成功读回 0x58,就表明你正在与一个 BMP280 通信,并且 I2C 通信基本正常。
#define BMP280_CHIP_ID_REG 0xD0 // 芯片ID寄存器地址
#define BMP280_EXPECTED_ID 0x58 // 期望的芯片ID值/*** @brief 检测BMP280是否存在* @param hi2c: I2C句柄* @param devAddr: BMP280的I2C设备地址 (0x76 or 0x77)* @retval HAL_OK: 存在且ID正确; HAL_ERROR: 失败*/
HAL_StatusTypeDef BMP280_CheckID(I2C_HandleTypeDef *hi2c, uint16_t devAddr)
{uint8_t chip_id = 0;// 尝试从芯片ID寄存器读取一个字节HAL_StatusTypeDef status = HAL_I2C_Mem_Read(hi2c, devAddr, BMP280_CHIP_ID_REG, I2C_MEMADD_SIZE_8BIT, &chip_id, 1, HAL_MAX_DELAY);if (status != HAL_OK) {return HAL_ERROR; // I2C通信失败}if (chip_id == BMP280_EXPECTED_ID) {return HAL_OK; // 芯片识别成功} else {return HAL_ERROR; // 读到的ID不是0x58}
}
完整HAL库代码:
输出float类型,首先要加入编译参数:-u_printf_float
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -fdata-sections -ffunction-sections -u _printf_float")
AHT20.h
#include <stdint.h>#ifndef AHT20BMP280_AHT20_H
#define AHT20BMP280_AHT20_H#define ATH20_SLAVE_ADDRESS 0x38 /* I2C从机地址 *///****************************************
// 定义 AHT20 内部地址
//****************************************
#define INIT 0xBE //初始化
#define SoftReset 0xBA //软复位
#define StartTest 0xAC //开始测试// 初始化
uint8_t AHT20_Init(void);
//查询cal enable位有没有使能
uint8_t AHT20_Read_Cal_Enable(void);
//读取AHT10的温度和湿度数据
uint8_t AHT20_Read_CTdata(uint32_t *ct);
// 读取AHT10的状态寄存器
uint8_t AHT20_Read_Status(void);#endif //AHT20BMP280_AHT20_H
AHT20.c
//
// Created by GalaxySpaceX on 2025/9/15.
//
#include "AHT20.h"#include "i2c.h"
#include "stm32f1xx_hal.h"
#include "usart.h"// 定义AHT20地址和命令
#define AHT10_SLAVE_ADDRESS (0x38 << 1) // HAL库要求左移1位[1,4] 0x38 << 1 = 0x70
#define AHT10_CMD_INIT 0xE1
#define AHT10_CMD_MEASURE 0xAC
#define AHT10_CMD_SOFTRESET 0xBA
#define AHT10_CMD_READSTATUS 0x71// 读取AHT20的状态寄存器
uint8_t AHT20_Read_Status(void)
{uint8_t status_byte;uint8_t read_cmd = AHT10_CMD_READSTATUS;// 先发送读取状态命令,然后读取1字节状态数据[1,4](@ref)HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, &read_cmd, 1, HAL_MAX_DELAY);HAL_I2C_Master_Receive(&hi2c1, AHT10_SLAVE_ADDRESS, &status_byte, 1, HAL_MAX_DELAY);return status_byte;
}uint8_t AHT20_Read_Cal_Enable(void)
{uint8_t val = AHT20_Read_Status();// 判断NOR模式和校准输出是否有效[1]if((val & 0x68) == 0x08)return 1;elsereturn 0;
}uint8_t AHT20_Init(void) {uint8_t init_cmd[3] = {AHT10_CMD_INIT, 0x08, 0x00}; // 初始化命令[1,4]uint8_t reset_cmd = AHT10_CMD_SOFTRESET; // 软复位命令uint8_t count = 0;HAL_Delay(40);// 发送初始化命令if (HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, init_cmd, 3, HAL_MAX_DELAY) != HAL_OK) {return 0;}HAL_Delay(500);while (AHT20_Read_Cal_Enable() == 0) {// 发送软复位命令HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, &reset_cmd, 1, HAL_MAX_DELAY);HAL_Delay(200);// 重新初始化HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, init_cmd, 3, HAL_MAX_DELAY);count++;// 尝试10次后失败if(count >= 10)return 0;HAL_Delay(500);}return 1;
}// 读取AHT20的温度和湿度数据
uint8_t AHT20_Read_CTdata(uint32_t *ct)
{uint8_t data[6];uint8_t measure_cmd[3] = {AHT10_CMD_MEASURE, 0x33, 0x00}; // 触发测量命令[1,4]uint16_t cnt = 0;uint32_t RetuData;// 发送触发测量命令if(HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, measure_cmd, 3, HAL_MAX_DELAY) != HAL_OK)return 0;// 等待75ms测量完成[1,4]HAL_Delay(80);// 等待忙状态结束while((AHT20_Read_Status() & 0x80) == 0x80){HAL_Delay(1);if(cnt++ >= 100)return 0; // 超时退出}// 读取6字节数据[1,4]if(HAL_I2C_Master_Receive(&hi2c1, AHT10_SLAVE_ADDRESS, data, 6, HAL_MAX_DELAY) != HAL_OK)return 0;// 检查状态位(bit7=0表示忙)[4]if(data[0] & 0x80)return 0;RetuData = 0;RetuData = (RetuData|data[1]) << 8;RetuData = (RetuData|data[2]) << 8;RetuData = (RetuData|data[3]);RetuData = RetuData >> 4;ct[0] = RetuData;RetuData = 0;RetuData = (RetuData|data[3]) << 8;RetuData = (RetuData|data[4]) << 8;RetuData = (RetuData|data[5]);RetuData = RetuData&0xfffff;ct[1] = RetuData;return 1;
}
BMP280.h
//
// Created by GalaxySpaceX on 2025/9/16.
//#ifndef AHT20BMP280_BMP280_H
#define AHT20BMP280_BMP280_H#include "stdint.h"
#include "stm32f1xx_hal.h" // 替换为HAL库头文件
#include <math.h>#define BMP280_SLAVE_ADDRESS 0x77 /* I2C´Ó»úµØÖ· *//*calibration parameters */
#define BMP280_DIG_T1_LSB_REG 0x88
#define BMP280_DIG_T1_MSB_REG 0x89
#define BMP280_DIG_T2_LSB_REG 0x8A
#define BMP280_DIG_T2_MSB_REG 0x8B
#define BMP280_DIG_T3_LSB_REG 0x8C
#define BMP280_DIG_T3_MSB_REG 0x8D
#define BMP280_DIG_P1_LSB_REG 0x8E
#define BMP280_DIG_P1_MSB_REG 0x8F
#define BMP280_DIG_P2_LSB_REG 0x90
#define BMP280_DIG_P2_MSB_REG 0x91
#define BMP280_DIG_P3_LSB_REG 0x92
#define BMP280_DIG_P3_MSB_REG 0x93
#define BMP280_DIG_P4_LSB_REG 0x94
#define BMP280_DIG_P4_MSB_REG 0x95
#define BMP280_DIG_P5_LSB_REG 0x96
#define BMP280_DIG_P5_MSB_REG 0x97
#define BMP280_DIG_P6_LSB_REG 0x98
#define BMP280_DIG_P6_MSB_REG 0x99
#define BMP280_DIG_P7_LSB_REG 0x9A
#define BMP280_DIG_P7_MSB_REG 0x9B
#define BMP280_DIG_P8_LSB_REG 0x9C
#define BMP280_DIG_P8_MSB_REG 0x9D
#define BMP280_DIG_P9_LSB_REG 0x9E
#define BMP280_DIG_P9_MSB_REG 0x9F#define BMP280_CHIPID_REG 0xD0 /*Chip ID Register */
#define BMP280_RESET_REG 0xE0 /*Softreset Register */
#define BMP280_STATUS_REG 0xF3 /*Status Register */
#define BMP280_CTRLMEAS_REG 0xF4 /*Ctrl Measure Register */
#define BMP280_CONFIG_REG 0xF5 /*Configuration Register */
#define BMP280_PRESSURE_MSB_REG 0xF7 /*Pressure MSB Register */
#define BMP280_PRESSURE_LSB_REG 0xF8 /*Pressure LSB Register */
#define BMP280_PRESSURE_XLSB_REG 0xF9 /*Pressure XLSB Register */
#define BMP280_TEMPERATURE_MSB_REG 0xFA /*Temperature MSB Reg */
#define BMP280_TEMPERATURE_LSB_REG 0xFB /*Temperature LSB Reg */
#define BMP280_TEMPERATURE_XLSB_REG 0xFC /*Temperature XLSB Reg */#define BMP280_SLEEP_MODE (0x00)
#define BMP280_FORCED_MODE (0x01)
#define BMP280_NORMAL_MODE (0x03)#define BMP280_TEMPERATURE_CALIB_DIG_T1_LSB_REG (0x88)
#define BMP280_PRESSURE_TEMPERATURE_CALIB_DATA_LENGTH (24)
#define BMP280_DATA_FRAME_SIZE (6)#define BMP280_OVERSAMP_SKIPPED (0x00)
#define BMP280_OVERSAMP_1X (0x01)
#define BMP280_OVERSAMP_2X (0x02)
#define BMP280_OVERSAMP_4X (0x03)
#define BMP280_OVERSAMP_8X (0x04)
#define BMP280_OVERSAMP_16X (0x05)/* 使用标准整数类型定义 */
typedef struct
{uint16_t dig_T1; /* calibration T1 data */int16_t dig_T2; /* calibration T2 data */int16_t dig_T3; /* calibration T3 data */uint16_t dig_P1; /* calibration P1 data */int16_t dig_P2; /* calibration P2 data */int16_t dig_P3; /* calibration P3 data */int16_t dig_P4; /* calibration P4 data */int16_t dig_P5; /* calibration P5 data */int16_t dig_P6; /* calibration P6 data */int16_t dig_P7; /* calibration P7 data */int16_t dig_P8; /* calibration P8 data */int16_t dig_P9; /* calibration P9 data */int32_t t_fine; /* calibration t_fine data */
} bmp280Calib;uint8_t BMP280_Init(void);
void BMP280GetData(float* pressure, float* temperature, float* asl);#endif //AHT20BMP280_BMP280_H
BMP280.c
//
// Created by GalaxySpaceX on 2025/9/16.
//
#include "BMP280.h"
#include "stdint.h"
#include "i2c.h"
#include "stm32f1xx_hal.h"
#include "math.h"static int32_t bmp280RawPressure = 0;
static int32_t bmp280RawTemperature = 0;#define BMP280_PRESSURE_OSR (BMP280_OVERSAMP_8X)
#define BMP280_TEMPERATURE_OSR (BMP280_OVERSAMP_16X)
#define BMP280_MODE (BMP280_PRESSURE_OSR << 2 | BMP280_TEMPERATURE_OSR << 5 | BMP280_NORMAL_MODE)bmp280Calib bmp280Cal;// HAL库I2C读写函数
static HAL_StatusTypeDef BMP280_ReadRegister(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t *data, uint16_t size)
{return HAL_I2C_Mem_Read(hi2c, BMP280_SLAVE_ADDRESS << 1, reg, I2C_MEMADD_SIZE_8BIT, data, size, 100);
}static HAL_StatusTypeDef BMP280_WriteRegister(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t *data, uint16_t size)
{return HAL_I2C_Mem_Write(hi2c, BMP280_SLAVE_ADDRESS << 1, reg, I2C_MEMADD_SIZE_8BIT, data, size, 100);
}uint8_t BMP280_Init(void)
{uint8_t bmp280_id;uint8_t tmp[10];HAL_StatusTypeDef status;// 读取芯片IDstatus = BMP280_ReadRegister(&hi2c1, BMP280_CHIPID_REG, &bmp280_id, 1);if(status != HAL_OK)return 0;// 读取校准参数status = BMP280_ReadRegister(&hi2c1, BMP280_DIG_T1_LSB_REG, (uint8_t *)&bmp280Cal, 24);if(status != HAL_OK)return 0;// 配置测量模式tmp[0] = BMP280_MODE;status = BMP280_WriteRegister(&hi2c1, BMP280_CTRLMEAS_REG, tmp, 1);if(status != HAL_OK)return 0;// 配置滤波器tmp[0] = (5 << 2);status = BMP280_WriteRegister(&hi2c1, BMP280_CONFIG_REG, tmp, 1);if(status != HAL_OK)return 0;return bmp280_id;
}static void BMP280GetPressure(void)
{uint8_t data[BMP280_DATA_FRAME_SIZE];// 读取传感器数据if(BMP280_ReadRegister(&hi2c1, BMP280_PRESSURE_MSB_REG, data, BMP280_DATA_FRAME_SIZE) == HAL_OK){bmp280RawPressure = (int32_t)((((uint32_t)(data[0])) << 12) | (((uint32_t)(data[1])) << 4) | ((uint32_t)data[2] >> 4));bmp280RawTemperature = (int32_t)((((uint32_t)(data[3])) << 12) | (((uint32_t)(data[4])) << 4) | ((uint32_t)data[5] >> 4));}
}// Returns temperature in DegC, resolution is 0.01 DegC. Output value of "5123" equals 51.23 DegC
// t_fine carries fine temperature as global value
// 温度补偿函数
static int32_t BMP280CompensateT(int32_t adcT)
{int32_t var1, var2, T;var1 = ((((adcT >> 3) - ((int32_t )bmp280Cal.dig_T1 << 1))) * ((int32_t )bmp280Cal.dig_T2)) >> 11;var2 = (((((adcT >> 4) - ((int32_t )bmp280Cal.dig_T1)) * ((adcT >> 4) - ((int32_t )bmp280Cal.dig_T1))) >> 12) * ((int32_t )bmp280Cal.dig_T3)) >> 14;bmp280Cal.t_fine = var1 + var2;T = (bmp280Cal.t_fine * 5 + 128) >> 8;return T;
}// Returns pressure in Pa as unsigned 32 bit integer in Q24.8 format (24 integer bits and 8 fractional bits).
// Output value of "24674867" represents 24674867/256 = 96386.2 Pa = 963.862 hPa
static uint32_t BMP280CompensateP(int32_t adcP)
{int64_t var1, var2, p;var1 = ((int64_t)bmp280Cal.t_fine) - 128000;var2 = var1 * var1 * (int64_t)bmp280Cal.dig_P6;var2 = var2 + ((var1*(int64_t)bmp280Cal.dig_P5) << 17);var2 = var2 + (((int64_t)bmp280Cal.dig_P4) << 35);var1 = ((var1 * var1 * (int64_t)bmp280Cal.dig_P3) >> 8) + ((var1 * (int64_t)bmp280Cal.dig_P2) << 12);var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)bmp280Cal.dig_P1) >> 33;if (var1 == 0)return 0;p = 1048576 - adcP;p = (((p << 31) - var2) * 3125) / var1;var1 = (((int64_t)bmp280Cal.dig_P9) * (p >> 13) * (p >> 13)) >> 25;var2 = (((int64_t)bmp280Cal.dig_P8) * p) >> 19;p = ((p + var1 + var2) >> 8) + (((int64_t)bmp280Cal.dig_P7) << 4);return (uint32_t)p;
}#define CONST_PF 0.1902630958 //(1/5.25588f) Pressure factor
#define FIX_TEMP 25 // Fixed Temperature. ASL is a function of pressure and temperature, but as the temperature changes so much (blow a little towards the flie and watch it drop 5 degrees) it corrupts the ASL estimates.// TLDR: Adjusting for temp changes does more harm than good.
/*** Converts pressure to altitude above sea level (ASL) in meters*/
// 压力转海拔函数
static float BMP280PressureToAltitude(float* pressure/*, float* groundPressure, float* groundTemp*/)
{if (*pressure > 0){return ((pow((1015.7f / *pressure), CONST_PF) - 1.0f) * (FIX_TEMP + 273.15f)) / 0.0065f;}else{return 0;}
}#define FILTER_NUM 5
#define FILTER_A 0.1f/*限幅平均滤波法*/
static void presssureFilter(float* in, float* out)
{static uint8_t i = 0;static float filter_buf[FILTER_NUM] = {0.0};double filter_sum = 0.0;uint8_t cnt = 0;float deta;if(filter_buf[i] == 0.0f){filter_buf[i] = *in;*out = *in;if(++i >= FILTER_NUM)i=0;}else{if(i)deta = *in-filter_buf[i - 1];elsedeta = *in-filter_buf[FILTER_NUM - 1];if(fabs(deta) < FILTER_A){filter_buf[i] = *in;if(++i >= FILTER_NUM)i = 0;}for(cnt = 0; cnt < FILTER_NUM; cnt++){filter_sum += filter_buf[cnt];}*out = filter_sum / FILTER_NUM;}
}void BMP280GetData(float* pressure, float* temperature, float* asl)
{static float t;static float p;BMP280GetPressure();t = BMP280CompensateT(bmp280RawTemperature) / 100.0;p = BMP280CompensateP(bmp280RawPressure) / 25600.0;presssureFilter(&p, pressure);*temperature = (float)t;/*单位度*/*asl = BMP280PressureToAltitude(pressure); /*转换成海拔*/
}
main.c
/* USER CODE BEGIN Header */
/********************************************************************************* @file : main.c* @brief : Main program body******************************************************************************* @attention** Copyright (c) 2025 STMicroelectronics.* All rights reserved.** This software is licensed under terms that can be found in the LICENSE file* in the root directory of this software component.* If no LICENSE file comes with this software, it is provided AS-IS.********************************************************************************/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"
#include "AHT20.h"
#include "BMP280.h"
#include "stdio.h"
#include "string.h"
#include "stdlib.h"/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes *//* USER CODE END Includes *//* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD *//* USER CODE END PTD *//* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD *//* USER CODE END PD *//* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM *//* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV *//* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP *//* USER CODE END PFP *//* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 *//* USER CODE END 0 *//*** @brief The application entry point.* @retval int*/
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_I2C1_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */uint8_t ret = 0;int humidity, temperature;char txData[100];uint32_t CT_data[2];int c1,t1;float P,T,ALT;int temp_int = (int)(T * 100); // 温度放大100倍int press_int = (int)(P * 100); // 压力放大100倍int alt_int = (int)ALT; // 海拔取整HAL_Delay(5000);HAL_UART_Transmit(&huart1, "start\r\n", strlen("start\r\n"), HAL_MAX_DELAY);ret = AHT20_Init();if(ret == 0){HAL_UART_Transmit(&huart1, "AHT20 Init Error\r\n", strlen("AHT20 Init Error\r\n"), HAL_MAX_DELAY);HAL_Delay(5000);while(1);}ret = BMP280_Init();if(ret != 0x58){HAL_UART_Transmit(&huart1, "BMP280 Init Error\r\n", strlen("BMP280 Init Error\r\n"), HAL_MAX_DELAY);HAL_Delay(5000);while(1);}/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */// 读取AHT20传感器数据if(AHT20_Read_Cal_Enable() == 0){HAL_UART_Transmit(&huart1, "AHT20_Init\r\n", strlen("AHT20_Init\r\n"), HAL_MAX_DELAY);AHT20_Init(); // 如果校准未使能,重新初始化HAL_Delay(30);}HAL_UART_Transmit(&huart1, "AHT20_Read_CTdata\r\n", strlen("AHT20_Read_CTdata\r\n"), HAL_MAX_DELAY);// 读取温湿度数据if(AHT20_Read_CTdata(CT_data) == 1){c1 = CT_data[0] * 1000 / 1024 / 1024; //计算得到湿度值(放大了10倍,如果c1=523,表示现在湿度为52.3%)t1 = CT_data[1] * 200 *10 / 1024 / 1024 - 500;//计算得到温度值(放大了10倍,如果t1=245,表示现在温度为24.5℃)//printf("湿度: %.2f%%, 温度: %.2f℃\r\n", humidity, temperature);int length = sprintf(txData, "temperature: %d.%d C, humidity: %d.%d %%\r\n", (t1/10), (t1%10), (c1/10), (c1%10));HAL_UART_Transmit(&huart1, (uint8_t*)txData, length, HAL_MAX_DELAY);}else{//printf("读取传感器数据失败\r\n");HAL_UART_Transmit(&huart1, "AHT20 Read Error\r\n", strlen("AHT20 Read Error\r\n"), HAL_MAX_DELAY);}/* 读取 BMP280 传感器数据*/BMP280GetData(&P, &T, &ALT);// 输出测量结果char buffer[100];int len = snprintf(buffer, sizeof(buffer),"Temp: %.2f C, Press: %.2f hPa, Alt: %.2f m\r\n",T, P, ALT);// int len = snprintf(buffer, sizeof(buffer),// "Temp: %d.%02d C, Press: %d.%02d hPa, Alt: %d m\r\n",// temp_int / 100, abs(temp_int % 100),// press_int / 100, abs(press_int % 100),// alt_int);HAL_UART_Transmit(&huart1, (uint8_t*)buffer, len, HAL_MAX_DELAY);HAL_Delay(10000); // 每秒读取一次/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}/*** @brief System Clock Configuration* @retval None*/
void SystemClock_Config(void)
{RCC_OscInitTypeDef RCC_OscInitStruct = {0};RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};/** Initializes the RCC Oscillators according to the specified parameters* in the RCC_OscInitTypeDef structure.*/RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;RCC_OscInitStruct.HSIState = RCC_HSI_ON;RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK){Error_Handler();}/** Initializes the CPU, AHB and APB buses clocks*/RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK){Error_Handler();}
}/* USER CODE BEGIN 4 *//* USER CODE END 4 *//*** @brief This function is executed in case of error occurrence.* @retval None*/
void Error_Handler(void)
{/* USER CODE BEGIN Error_Handler_Debug *//* User can add his own implementation to report the HAL error return state */__disable_irq();while (1){}/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/*** @brief Reports the name of the source file and the source line number* where the assert_param error has occurred.* @param file: pointer to the source file name* @param line: assert_param error line source number* @retval None*/
void assert_failed(uint8_t *file, uint32_t line)
{/* USER CODE BEGIN 6 *//* User can add his own implementation to report the file name and line number,ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) *//* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */