目录

引言

个人对于I2C的理解

 它的优点:

 它的缺点:

软件I2C

遇到的问题

驱动代码

I2C.h

I2C.c

AHT20.h

AHT20.c

总结


引言

此前在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;
}

总结

本文内容仅是个人看法,如有不足,欢迎指出!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐