STM32F407/429软件I2C驱动AHT20
目录
引言
此前在STM32F407与429平台上使用软件I2C驱动AHT20温湿度传感器时,多次出现通信失败、数据读取异常的问题。排查后发现,核心原因是软件I2C的时序控制不精准,AHT20对启动信号、应答判断及延时时序要求严格,普通延时函数无法适配传感器规范;同时未正确执行传感器初始化校准流程,加上引脚浮空与电平切换延迟处理不当,导致通信异常。本次结合芯片主频特性优化时序延时,规范传感器初始化与数据读取流程,实现稳定可靠的软件I2C驱动,特此分享实操经验。
个人对于I2C的理解
I2C是一种简单的双线串行通信协议,它是半双工同步串行通信,它只有两根线,一根时钟线SCL,一根数据线SDA,仅靠SDA数据线和SCL时钟线即可实现多设备通信,由主机控制时钟与通信时序,从机被动响应。它采用应答机制保证传输可靠性,每发送一个字节都需要等待从机ACK,所以对时序精度要求较高。
它的优点:
首先时它占用的IO资源比较少,它只有两根线;其次,它支持多主机,多从机通信,可挂载多个设备,通过设备地址区分,即可实现不同设备的切换,扩展性比较强。最后是它有应答机制,它每个字节的参数都会有应答,从而实现数据稳定可靠。
它的缺点:
首先它是半双工通信,同一时间只能发送或接收,数据传输效率不够全双工高;其次,它的通信速率相对较低,仅支持标准模式100KHz,快速模式400KHz。对于要求通信较高的场景,它是不够用的。
软件I2C
软件I2C通过单片机GPIO模拟电平翻转与时序,无需硬件外设,适配性强,但对延时、起始停止信号、ACK检测的把控十分关键。像AHT20这类传感器对时序严格,时序偏差、电平切换不及时或初始化流程错误,都容易导致通信失败。合理控制延时、规范信号格式,才能保证稳定传输。
遇到的问题
首先是移植的问题。我们一般都是先学了F103的芯片来进行前期的学习,然后把相关代码移植到F407工程的时候,就发现了问题,模块不响应。查了很多资料才发现,原来是F103的主频跟F407的主频不一样,前者的主频是72Mhz,后者是168Mhz或者是更高的180mhz。所以得再驱动时序的部分加入几微秒延时。
第二个问题,把GPIO设置成推挽输出模式,这就导致在空闲状态下时钟线始终保持不了高电平的原因。而I2C协议规定,空闲状态下两根信号线默认是上拉状态,也就是处于高电平状态。所以为什么I2C协议需要加上拉电阻的原因?是因为I2C标准设备的引脚是开漏输出。只能主动拉低,不能主动拉高,所以必须在外部接上拉电阻才能保持高电平状态。所以要把GPIO设置成开漏上拉模式。因为开漏模式可以拉低电平,但当释放以后就处于浮空状态,再配上内部/外部的上拉电阻,强制把GPIO引脚只能有两种电平状态,也就是高电平或者低电平,再无第三种状态,比如浮空状态。
补充一点,I2C本质使用的是“线与”架构。怎么理解这个“线与”架构呢?其实可以把它们拆出来理解。“线”,最直观地理解就是信号线。“与”就是与运算的意思。“线”表示同在一个总线上。即I2C总线。I2C总线上可以同时挂载多个设备。与运算不是全1才是1吗,只要有一个为0,那它就是0。所以就叫做“线与”架构。那为什么不叫“线或”架构?因为或运算的规则是只要有一个为1,那该总线上就是1。而对于I2C总线上来说,如果都是1的话,那根本响应不了挂载的设备。所以只有“线与”架构,才适合I2C总线。
最后一个问题是在关键的信号处理中,没有能很好的拉低或拉高对应的数据线。在读取数据的时候,应该先移位,再读位。不然读出的数据都是0。
驱动代码
I2C.h
#ifndef __MYI2C_H
#define __MYI2C_H
#include "stm32f429xx.h"
#define SDA1_IN() {GPIOB->MODER &= ~(GPIO_MODER_MODE7); }
#define SDA1_OUT() {GPIOB->MODER &= ~(GPIO_MODER_MODE7); GPIOB->MODER |= 1 << GPIO_MODER_MODE7_Pos;}
void MyI2C1_Init(void);
void MyI2C1_Start(void);
void MyI2C1_Stop(void);
void MyI2C1_SendByte(uint8_t Byte);
void MyI2C1_SendAck(void);
void MyI2C1_SendNAck(void);
uint8_t MyI2C1_WaitAck(void);
uint8_t MyI2C1_ReceiveByte(uint8_t Ack);
#endif
I2C.c
#include "stm32f429xx.h"
#include "delay.h"
#include "myi2c.h"
#include "sys.h"
void MyI2C1_W_SCL(uint8_t BitValue)
{
if(BitValue)
{
GPIOB->ODR |= GPIO_ODR_ODR_6;
}
else
{
GPIOB->ODR &=~GPIO_ODR_ODR_6;
}
}
void MyI2C1_W_SDA(uint8_t BitValue)
{
if(BitValue)
{
GPIOB->ODR |= GPIO_ODR_ODR_7;
}
else
{
GPIOB->ODR &=~GPIO_ODR_ODR_7;
}
}
void MyI2C1_Init(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
GPIOB->MODER |= (GPIO_MODER_MODE6_0 | GPIO_MODER_MODE7_0);
GPIOB->MODER &= ~(GPIO_MODER_MODE6_1 | GPIO_MODER_MODE7_1);
// 开漏输出:置1
GPIOB->OTYPER |= (GPIO_OTYPER_OT6 | GPIO_OTYPER_OT7);
GPIOB->PUPDR &= ~(GPIO_PUPDR_PUPD6 | GPIO_PUPDR_PUPD7);
GPIOB->PUPDR |= (GPIO_PUPDR_PUPD6_0 | GPIO_PUPDR_PUPD7_0); // 上拉
GPIOB->ODR |= (GPIO_ODR_OD6 | GPIO_ODR_OD7);
}
/**
* @brief I2C 起始信号 (Start Condition)
* @note SCL高电平时,SDA从高变低
* 时序: SDA=1, SCL=1 -> SDA=0 -> SCL=0
*/
void MyI2C1_Start(void)
{
SDA1_OUT(); // SDA设为输出模式
MyI2C1_W_SDA(1); // SDA拉高
MyI2C1_W_SCL(1); // SCL拉高
delay_us(5); // 保持时间
MyI2C1_W_SDA(0); // SDA拉低,产生下降沿(起始信号)
delay_us(5); // 保持时间
MyI2C1_W_SCL(0); // SCL拉低,准备发送数据
}
/**
* @brief I2C 停止信号 (Stop Condition)
* @note SCL高电平时,SDA从低变高
* 时序: SCL=0, SDA=0 -> SCL=1 -> SDA=1
*/
void MyI2C1_Stop(void)
{
SDA1_OUT(); // SDA设为输出模式
MyI2C1_W_SCL(0); // 确保SCL为低
MyI2C1_W_SDA(0); // SDA拉低
delay_us(5); // 建立时间
MyI2C1_W_SCL(1); // SCL拉高
MyI2C1_W_SDA(1); // SDA拉高,产生上升沿(停止信号)
delay_us(5); // 保持时间
}
/**
* @brief 发送一个字节 (MSB first)
* @param Byte: 要发送的数据 (0x00~0xFF)
* @note 在SCL低电平时改变SDA,SCL高电平时从机采样
*/
void MyI2C1_SendByte(uint8_t Byte)
{
uint8_t i;
SDA1_OUT(); // SDA设为输出模式
for (i = 0; i < 8; i++) // 循环8次,发送8位数据
{
// 取最高位(bit7),右移7位得到0或1
MyI2C1_W_SDA((Byte & 0x80) >> 7);
Byte <<= 1; // 左移一位,下一位变成最高位
delay_us(3); // 数据建立时间
MyI2C1_W_SCL(1); // SCL拉高,从机读取SDA
delay_us(3); // 高电平保持时间
MyI2C1_W_SCL(0); // SCL拉低,准备发送下一位
delay_us(3); // 低电平保持时间
}
}
/**
* @brief 发送应答信号 (ACK)
* @note 在接收完一个字节后,主机拉低SDA表示继续接收
*/
void MyI2C1_SendAck(void)
{
MyI2C1_W_SCL(0); // SCL拉低,准备改变SDA
SDA1_OUT(); // SDA设为输出模式
MyI2C1_W_SDA(0); // SDA拉低(ACK信号)
delay_us(2); // 建立时间
MyI2C1_W_SCL(1); // SCL拉高,从机读取ACK
delay_us(2); // 保持时间
MyI2C1_W_SCL(0); // SCL拉低,完成应答周期
}
/**
* @brief 发送非应答信号 (NACK)
* @note 在接收完一个字节后,主机拉高SDA表示结束接收
*/
void MyI2C1_SendNAck(void)
{
MyI2C1_W_SCL(0); // SCL拉低,准备改变SDA
SDA1_OUT(); // SDA设为输出模式
MyI2C1_W_SDA(1); // SDA拉高(NACK信号)
delay_us(2); // 建立时间
MyI2C1_W_SCL(1); // SCL拉高,从机读取NACK
delay_us(2); // 保持时间
MyI2C1_W_SCL(0); // SCL拉低,完成应答周期
}
/**
* @brief 等待从机应答信号
* @retval 0: 接收到ACK,通信正常
* @retval 1: 超时未接收到ACK,通信失败,已发送停止信号
* @note 主机释放SDA后,读取SDA电平判断从机是否拉低SDA
*/
uint8_t MyI2C1_WaitAck(void)
{
uint8_t time = 0;
SDA1_IN();
delay_us(2);
MyI2C1_W_SCL(1);
delay_us(3);
while (PBin(7))
{
time++;
if (time > 250)
{
MyI2C1_Stop();
return 1;
}
}
MyI2C1_W_SCL(0);
delay_us(2);
return 0;
}
/**
* @brief 接收一个字节
* @param Ack: 应答控制
* 0 - 发送NACK,表示结束接收
* 1 - 发送ACK,表示继续接收
* @retval 接收到的数据 (0x00~0xFF)
* @note MSB first,接收完成后根据Ack参数发送应答/非应答
*/
uint8_t MyI2C1_ReceiveByte(uint8_t Ack)
{
uint8_t i;
uint8_t Receive = 0;
SDA1_IN();
for (i = 0; i < 8; i++)
{
MyI2C1_W_SCL(0); // 拉低时钟,从机放数据
delay_us(3);
MyI2C1_W_SCL(1); // 拉高时钟,读数据
delay_us(2);
Receive <<= 1;
if (PBin(7))
{
Receive |= 1;
}
delay_us(2);
}
MyI2C1_W_SCL(0);
delay_us(2);
if (Ack == 1)
MyI2C1_SendAck();
else
MyI2C1_SendNAck();
return Receive;
}
/***********************************************************/
AHT20.h
#ifndef __AHT20_H
#define __AHT20_H
#include "stm32f429xx.h"
void AHT20_INIT(void);
void AHT20(float *Temperature, float *Humidity);
#endif
AHT20.c
#include "stm32f429xx.h"
#include "delay.h"
#include "myi2c.h"
#define AHT20_ADDRESS 0x38 // 7位地址
extern float Temperature_Last,Humidity_Last;
void AHT20_INIT(void)
{
uint8_t readBuffer;
MyI2C1_Init();
delay_ms(100);
// 读状态字节
MyI2C1_Start();
MyI2C1_SendByte(AHT20_ADDRESS << 1 | 1); // 读
MyI2C1_WaitAck();
readBuffer = MyI2C1_ReceiveByte(0); // 读完 NACK
MyI2C1_Stop();
delay_ms(10);
if ((readBuffer & 0x08) == 0x00)
{
uint8_t sendBuffer[3] = {0xBE, 0x08, 0x00};
MyI2C1_Start();
MyI2C1_SendByte(AHT20_ADDRESS << 1 | 0); // 写
MyI2C1_WaitAck();
MyI2C1_SendByte(sendBuffer[0]);
MyI2C1_WaitAck();
MyI2C1_SendByte(sendBuffer[1]);
MyI2C1_WaitAck();
MyI2C1_SendByte(sendBuffer[2]);
MyI2C1_WaitAck();
MyI2C1_Stop();
}
}
void AHT20(float *Temperature, float *Humidity)
{
uint8_t sendBuffer[3] = {0xAC, 0x33, 0x00};
uint8_t readBuffer[6];
// 1. 触发测量
MyI2C1_Start();
MyI2C1_SendByte(AHT20_ADDRESS << 1 | 0);
MyI2C1_WaitAck();
MyI2C1_SendByte(sendBuffer[0]);
MyI2C1_WaitAck();
MyI2C1_SendByte(sendBuffer[1]);
MyI2C1_WaitAck();
MyI2C1_SendByte(sendBuffer[2]);
MyI2C1_WaitAck();
MyI2C1_Stop();
delay_ms(80); // 稍微延长一点,更稳
// 2. 读6字节
MyI2C1_Start();
MyI2C1_SendByte(AHT20_ADDRESS << 1 | 1);
MyI2C1_WaitAck();
readBuffer[0] = MyI2C1_ReceiveByte(1);
readBuffer[1] = MyI2C1_ReceiveByte(1);
readBuffer[2] = MyI2C1_ReceiveByte(1);
readBuffer[3] = MyI2C1_ReceiveByte(1);
readBuffer[4] = MyI2C1_ReceiveByte(1);
readBuffer[5] = MyI2C1_ReceiveByte(0);
MyI2C1_Stop();
if (readBuffer[0] & 0x80)
{
*Temperature = Temperature_Last;
*Humidity = Humidity_Last;
return;
}
uint32_t raw_humi = ((uint32_t)readBuffer[1] << 12)
| ((uint32_t)readBuffer[2] << 4)
| ((uint32_t)readBuffer[3] >> 4);
uint32_t raw_temp = ((uint32_t)(readBuffer[3] & 0x0F) << 16)
| ((uint32_t)readBuffer[4] << 8)
| (uint32_t)readBuffer[5];
*Humidity = (float)raw_humi * 100.0f / 1048576.0f;
*Temperature = (float)raw_temp * 200.0f / 1048576.0f - 50.0f;
}
总结
本文内容仅是个人看法,如有不足,欢迎指出!
更多推荐
所有评论(0)