STM32-软件模拟和硬件实现I²C

1. 软件模拟I²C

需求说明:我们向E2PROM写入一段数据,再读取出来,最后发送到串口,核对是否读写正确。

为了逻辑清晰,约定:在 发送起始信号后 ~ 发送结束信号前,使用完SCL后都将其拉低。

由于STM32时钟过快,EEPROM没有那么快的接收速度,所以我们选择EEPROM的100kHz传输速率。

因为GPIOB在APB2高速外设总线上,时钟频率达到72MHz,远超EEPROM能接受的100kHz,所以需要使用延时来延缓传输速率。故每次改变SDA或SCL都要延时10us。

1.1. 软件实现协议

1.1.1. 起始信号和结束信号

1.1.1.1. 起始信号

规定:当SCL为高电平时,SDA从高电平转变为低电平,则成功发送起始信号。

/**
 * @brief 发送起始信号
 *
 */
void I2C_Start(void)
{
    /* 1. 先将SDA和SCL拉高, 注意SDA先拉高 */
    SDA_1;
    SCL_1;
    I2C_DELAY;

    /* 2. 拉低SDA, 发送起始信号 */
    SDA_0;
    I2C_DELAY;

    /* 3. 遵守约定, 中间行为最后都将SCL拉低 */
    SCL_0;
    I2C_DELAY;
}

注意:一定要在SCL拉高前先拉高SDA,因为此时不知道SDA电平。若SDA此时为低电平,SCL先拉高,再拉高SDA,这样就先发送了一个结束信号,虽然对结果没什么影响,但会让程序时序混乱。

1.1.1.2. 结束信号

规定:当SCL为高电平时,SDA从低电平转变为高电平,则成功发送结束信号。

/**
 * @brief 发送结束信号
 *
 */
void I2C_Stop(void)
{
    /* 1. 按照约定SCL一定为0, 但SDA值未知, 故先将SDA拉低 */
    SDA_0;
    I2C_DELAY;

    /* 2. 拉高SCL, 等待SDA拉高发送结束信号 */
    SCL_1;
    I2C_DELAY;

    /* 3. 拉高SDA, 发送结束信号 */
    SDA_1;
    I2C_DELAY;
}

1.1.2. 响应 Ack或NAck

1.1.2.1. 响应Ack

以32为主视角,每当从机向我们发送完一个字节数据后,我们都需要响应一个ACK,即把SDA拉低。在响应完Ack后再释放SDA。

/**
 * @brief 响应对方Ack
 *
 */
void I2C_Ack(void)
{
    /* 1. 按照约定SCL一定为0, 将SDA拉低 */
    SDA_0;
    I2C_DELAY;

    /* 2. 将SCL拉高, 响应对方ACK */
    SCL_1;
    I2C_DELAY;

    /* 3. 响应结束, 按照约定拉低SCL */
    SCL_0;
    I2C_DELAY;

    /* 4. 释放SDA, 使其处于空闲状态 */
    SDA_1;
    I2C_DELAY;
}

注意:若最后不释放SDA,从机将无法向主机发送数据。将SDA拉高释放时,从机才能使用SDA向我们发送数据。SDA不释放代表占用。

1.1.2.2. 响应NAck

当接受数据失败或不想继续接受数据时,向对方响应NAck。

/**
 * @brief 响应对方NAck
 *
 */
void I2C_NAck(void)
{
    /* 1. 按照约定SCL一定为0, 将SDA拉高 */
    SDA_1;
    I2C_DELAY;

    /* 2. 将SCL拉高, 响应对方NACK */
    SCL_1;
    I2C_DELAY;

    /* 3. 响应结束, 按照约定拉低SCL */
    SCL_0;
    I2C_DELAY;
}

1.1.3. 等待对方响应

每发送完一个字节数据,都要等待对方响应Ack或NAck。

/**
 * @brief 等待对方响应[Ack or NAck]
 *
 */
uint8_t I2C_Wait4Ack(void)
{
    uint16_t ack = 0;

    /* 1. 按照约定SCL一定为0, 先释放SDA */
    SDA_1;
    I2C_DELAY;

    /* 2. 拉高SCL锁定SDA, 读取对方响应结果 */
    SCL_1;
    I2C_DELAY;
    ack = READ_SDA;

    /* 3. 拉低SCL, 对方释放SDA */
    SCL_0;
    I2C_DELAY;

    /* 4. 若ack为0, 返回Ack, 若不为0, 返回NAck */
    return ((ack == 0) ? ACK : NACK);
}

1.1.4. 数据有效性

当SCL为高电平时,除起始信号和结束信号外,此时SDA的值锁定不能变化;当SCL为低电平时,SDA可以任意变化,以此来改变自身的电平。

当SCL为高电平时,发送端会将此时SDA的电平发送给接收方,即:若SDA此时为高点平,就发送一个高电平;若SDA此时为低电平,就发送一个低电平。

1.1.5. 单字节发送

/**
 * @brief 传输一个字节数据, 从高位开始
 *
 * @param data 传输的那一个字节数据
 */
void I2C_Transmit(uint8_t data)
{
    /* 按照约定, SCL此时一定为0, 所以可以直接改变SDA电平 */
    for (uint8_t i = 0; i < 8; ++i)
    {
        /* 根据data最高位的值, SDA输出对应的值 */
        if ((data & 0x80) != 0)
        {
            SDA_1;
        }
        else
        {
            SDA_0;
        }
        I2C_DELAY;

        /* 拉高SCL, 锁定SDA的值, 等待对方采样SDA */
        SCL_1;
        I2C_DELAY;

        /* 采样结束, 拉低SCL, 进行下一个字节的数据传输 */
        SCL_0;
        I2C_DELAY;

        /* data左移一位, 将下一位移动至最高位 */
        data <<= 1;
    }
}

1.1.6. 单字节接收

/**
 * @brief 接收一个字节数据
 *
 * @return uint8_t  将接收的数返回
 */
uint8_t I2C_Receive(void)
{
    uint8_t recv_data = 0; /* 存储接收的数据 */

    /* 释放SDA, 准备采样SDA值 */
    SDA_1;
    I2C_DELAY;
    
    for (uint8_t i = 0; i < 8; ++i)
    {
        /* 拉高SCL, 锁定SDA值, 对SDA进行采样 */
        SCL_1;
        I2C_DELAY;

        /* 对SDA进行采样 */
        recv_data <<= 1;
        if (READ_SDA != 0)
        {
            recv_data |= 0x01;
        }

        /* 拉低SCL, 等待接收下一字节数据 */
        SCL_0;
        I2C_DELAY;
    }

    /* 返回接收的数据 */
    return recv_data;
}

注意:要在接收数据前移动recv_data。若放在接收数据后移动,在最后一个bit接收完后,recv_data还会左移一位,导致接收的数据存储错误。

1.2. EEPROM读写

1.2.1. 获取设备地址

每个从机都有自己的地址,EEPROM作为STM32的从机,也有自己的地址。翻阅产品手册和PCB原理图可知其"读地址"为:0xA1,"写地址"为:0xA0。

EEPROM在使用I2C协议驱动时,需要对其进行初始化:

/**
 * @brief 初始化
 *
 */
void E2PROM_Init(void)
{
    I2C_Init();
}

1.2.2. 单字节写入

写入步骤:

  1. 发送起始信号;
  2. 发送待写入设备的地址;
  3. 指明从设备的哪个地址开始写;
  4. 写入数据,等待响应;
  5. 发送结束信号;
  6. 延时5ms等待EEPROM写入完成。
/**
 * @brief 往EEPROM中写入一个字节数据
 *
 * @param word_address  数据写入的地址
 * @param data          要写入的数据
 */
uint8_t E2PROM_WriteByte(uint8_t word_address, uint8_t data)
{
    uint8_t status = 0;

    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要写入数据的设备的地址 */
    I2C_Transmit(DEV_ADDR);
    status |= I2C_Wait4Ack();

    /* 3. 指明从设备中的哪个地址开始写 */
    I2C_Transmit(word_address);
    status |= I2C_Wait4Ack();

    /* 4. 开始写入数据 */
    I2C_Transmit(data);
    status |= I2C_Wait4Ack();

    /* 5. 发送结束信号 */
    I2C_Stop();

    /* 6. 等待EEPROM将数据写入 */
    Delay_ms(5);

    return status;
}

1.2.3. 单字节读取

读取步骤:

  1. 发送起始信号;
  2. 发送要读取数据的设备的地址[写],等待响应;
  3. 指明从设备中的哪个地址开始读,等待响应;
  4. 再次发送起始信号;
  5. 发送要读取数据的设备的地址[读],等待响应;
  6. 接收数据;
  7. 响应NACK, 停止读取;
  8. 发送结束信号
/**
 * @brief 读取EEPROM中的一个字节数据
 *
 * @param word_address  要读取数据的地址
 * @param p_data        将读到的数据存储在*p_data中
 */
uint8_t E2PROM_ReadByte(uint8_t word_address, uint8_t *p_data)
{
    uint8_t status = 0;
    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要读取数据的设备的地址[写] */
    I2C_Transmit(DEV_ADDR);
    status |= I2C_Wait4Ack();

    /* 3. 指明从设备中的哪个地址开始读 */
    I2C_Transmit(word_address);
    status |= I2C_Wait4Ack();

    /* 4. 发送起始信号 */
    I2C_Start();

    /* 5. 发送要读取数据的设备的地址[读] */
    I2C_Transmit(DEV_ADDR | 1);
    status |= I2C_Wait4Ack();

    /* 6. 接收数据 */
    *p_data = I2C_Receive();

    /* 7. 响应NACK, 停止读取 */
    I2C_NAck();

    /* 8. 发送结束信号 */
    I2C_Stop();

    return status;
}

1.2.4. 页写

和单字节写入类似,只是在写入数据时是写入多个字节。

/**
 * @brief 往EEPROM中写入多个字节数据
 *
 * @param word_address  数据写入的起始地址
 * @param datas         要写入的数据
 * @param size          要写入数据的长度
 * @return uint8_t      SUCCESS代表写入成功, FAIL代表写入失败
 */
uint8_t E2PROM_WritePage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
    uint8_t status = 0;

    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要写入数据的设备的地址 */
    I2C_Transmit(DEV_ADDR);
    status |= I2C_Wait4Ack();

    /* 3. 指明从设备中的哪个地址开始写 */
    I2C_Transmit(word_address);
    status |= I2C_Wait4Ack();

    /* 4. 开始写入数据 */
    for (uint8_t i = 0; i < size; ++i)
    {
        I2C_Transmit(datas[i]);
        status |= I2C_Wait4Ack();
    }

    /* 5. 发送结束信号 */
    I2C_Stop();

    /* 6. 等待EEPROM将数据写入 */
    Delay_ms(5);

    return status;
}

1.2.5. 页读

和单字节读取类似,只是只在最后一次读取后才返回NAck,其余均返回Ack。

/**
 * @brief 读取EEPROM中的多个字节数据
 *
 * @param word_address  数据读取的起始地址
 * @param datas         将读取的数据存储在此
 * @param size          要读取数据的长度
 * @return uint8_t      SUCCESS代表读取成功, FAIL代表读取失败
 */
uint8_t E2PROM_ReadPage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
    uint8_t status = 0;
    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要读取数据的设备的地址[写] */
    I2C_Transmit(DEV_ADDR);
    status |= I2C_Wait4Ack();

    /* 3. 指明从设备中的哪个地址开始读 */
    I2C_Transmit(word_address);
    status |= I2C_Wait4Ack();

    /* 4. 发送起始信号 */
    I2C_Start();

    /* 5. 发送要读取数据的设备的地址[读] */
    I2C_Transmit(DEV_ADDR | 1);
    status |= I2C_Wait4Ack();

    /* 6. 接收数据 */
    for (uint8_t i = 0; i < size - 1; ++i)
    {
        datas[i] = I2C_Receive();
        I2C_Ack();
    }
    datas[size - 1] = I2C_Receive();
    
    /* 7. 响应NACK, 停止读取 */
    I2C_NAck();

    /* 8. 发送结束信号 */
    I2C_Stop();

    return status;
}

1.2.6. 多页连写

static uint8_t s_record_i = 0; /* 记录多页写下次起始索引 */

/**
 * @brief 往EEPROM中写入多个字节数据
 *
 * @param word_address  数据写入的起始地址
 * @param datas         要写入的数据
 * @param size          要写入数据的长度
 * @return uint8_t      SUCCESS代表写入成功, FAIL代表写入失败
 */
uint8_t E2PROM_WritePage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
    uint8_t status = 0;                           /* 写入状态 */
    uint8_t low4bit_word = (word_address & 0x0F); /* 记录地址低4位 */

    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要写入数据的设备的地址 */
    I2C_Transmit(DEV_ADDR);
    status |= I2C_Wait4Ack();

    /* 3. 指明从设备中的哪个地址开始写 */
    I2C_Transmit(word_address);
    status |= I2C_Wait4Ack();

    /* 4. 开始写入数据 */
    uint8_t i;
    for (i = 0; i < size; ++i)
    {
        /* 当前页地址为16时代表此轮不准写入, 应在下一页写 */
        if (low4bit_word++ >= 16)
        {
            s_record_i += i;
            break;
        }

        I2C_Transmit(datas[s_record_i + i]);
        status |= I2C_Wait4Ack();
    }

    /* 5. 发送结束信号 */
    I2C_Stop();

    /* 6. 等待EEPROM将数据写入 */
    Delay_ms(5);

    /* 当前页写满, 跳转到下一页继续写 */
    if (low4bit_word >= 16)
    {
        uint8_t high4bit_page = word_address >> 4; /* 记录地址高4位 */
        E2PROM_WritePage((high4bit_page + 1) << 4, datas, size - i);
    }
    s_record_i = 0; /* 清除i记录标记 */

    return status;
}

2. 硬件实现I²C

2.1. 硬件图

EEPROM在板子上接的是I2C2,所以,为了使用STM32硬件实现协议,需要开启I²C时钟和对应GPIO寄存器的时钟。

2.2. 初始化

因为要使用到STM32的硬件I²C外设,所以需要开启对应I²C2的时钟;其次,I²C2的SCL和SDA使用的引脚是GPIOB中的PB10和PB11,所以也需要配置GPIOB的时钟。

RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;	/* 配置GPIOB时钟 */
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;	/* 配置I2C2时钟 */

因为我们使用到了PB10和PB11引脚,所以还需要配置它的工作模式,在芯片手册中已给出如何配置两个引脚工作模式:复用开漏输出。

GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);
GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11);

阅读芯片手册,可知要作为主机使用I²C外设,步骤如下:

第一步:在I2C_CR2寄存器中设定该模块的输入时钟,找到该寄存器说明:

因为I²C2在APB1低速外设总线上,所以FREQ[5:0]最大值只能设置为100100,可以选择合适的速率,这里选择36MHz。

I2C2->CR2 &= ~I2C_CR2_FREQ;						/* 先将后5位清零 */
I2C2->CR2 |= (I2C_CR2_FREQ_2 | I2C_CR2_FREQ_5); /* 输入时钟设置为36MHz */

第二步:配置时钟控制寄存器,找到该寄存器说明:

需要设置F/S位为0,使用其标准模式;CCR计算步骤如下图所示,值设置为180。

I2C2->CCR &= ~I2C_CCR_FS;		/* 选择标注模式的I²C */
I2C2->CCR &= ~I2C_CCR_CCR;		/* 将CCR先清零 */
I2C2->CCR |= 180;            	/* 配置时钟控制寄存器 */

第三步:配置上升时间寄存器,找到该寄存器的说明:

根据给出的公式:(1000ns / 一个时钟周期) + 1,可算出TRISE的值为37。上升时间就是一个上升沿要多长时间,即从低电平到高电平时间。

I2C2->TRISE = 37;            /* 配置上升时间寄存器 */

第四步:编程I2C_CR1寄存器启动外设,查看该寄存器说明:

首先需要设置SMBUS位,设置为I²C模式,再将PE设置为1,使能I²C模块。

I2C2->CR1 &= ~I2C_CR1_SMBUS; /* 设置为I²C模式 */
I2C2->CR1 |= I2C_CR1_PE;     /* 使能I²C模块 */

到此为止,所有的初始化完成,完整步骤如下所示:

/**
 * @brief 初始化
 *
 */
void I2C_Init(void)
{
    /* 1. 开启GPIOB、I²C2外设的时钟 */
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;

    /* 2. 配置GPIOB引脚工作模式: 复用开漏输出 */
    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);
    GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11);

    /* 3. 配置I²C2外设 */
    I2C2->CR2 &= ~I2C_CR2_FREQ;
    I2C2->CR2 |= (I2C_CR2_FREQ_2 | I2C_CR2_FREQ_5); /* 输入时钟设置为36MHz */
    I2C2->CCR &= ~(I2C_CCR_FS | I2C_CCR_CCR);
    I2C2->CCR |= 180;            /* 配置时钟控制寄存器 */
    I2C2->TRISE = 37;            /* 配置上升时间寄存器 */
    I2C2->CR1 &= ~I2C_CR1_SMBUS; /* 使用I²C */
    I2C2->CR1 |= I2C_CR1_PE;     /* 使能I²C */
}

2.3. 主发送器

以下时是发送数据的流程:

首先需要发送一个起始信号。要发送起始信号只需把I2C_CR1寄存器中的START位置为1即可,其在发出后会由硬件自动清除。

/**
 * @brief 发送起始信号
 *
 */
void I2C_Start(void)
{
    I2C2->CR1 |= I2C_CR1_START; /* 将START位置1 */
}

成功发送起始信号的标志是I2C_SR1寄存器中的SB被置1,此时会触发EV5事件,需要读SR1然后将地址写入DR寄存器清除该事件【SB被清除】,所以这一步可以集成在一个发送设备地址的函数中。清除事件就是将对应位清除,例如这里清除事件EV5就是清除SB。

/**
 * @brief 传输地址
 *
 * @param address 地址值
 */
void I2C_TransmitAddress(uint8_t address)
{
    /* 等待起始信号发送完成 */
    while ((I2C2->SR1 & I2C_SR1_SB) == 0)
    {
    }
    
    I2C2->DR = address; /* 此时起始信号以发送完毕, 发送设备地址, 同时清除EV5事件 */
}

在发送完地址后会触发EV6事件【ADDR被置1】,结合上下文,把EV6事件的代码放在传输地址函数里较合理,所以,完整的传输地址函数为:

/**
 * @brief 传输地址
 *
 * @param address 地址值
 */
void I2C_TransmitAddress(uint8_t address)
{
    /* 等待起始信号发送完成 */
    while ((I2C2->SR1 & I2C_SR1_SB) == 0)
    {
    }
    I2C2->DR = address; /* 此时起始信号以发送完毕, 发送设备地址, 同时清除EV5事件 */

    /* 等待地址传输完成 */
    while ((I2C2->SR1 & I2C_SR1_ADDR) == 0)
    {
    }
    I2C2->SR2; /* 清除EV6事件【清除ADDR】 */
}

在成功发送地址后,就可发送数据,在发送数据时,需要判断TxE位是否为1,只有在TxE为1时才能发送数据。且写入DR寄存器会将TxE清除,当收到从机响应后,TxE会被硬件置位。这也说明从机响应的Ack会被硬件自动接收判断。

/**
 * @brief 传输一个字节数据, 从高位开始
 *
 * @param data 传输的那一个字节数据
 */
void I2C_Transmit(uint8_t data)
{
    /* 等待数据寄存器为空 */
    while ((I2C2->SR1 & I2C_SR1_TXE) == 0)
    {
    }

    I2C2->DR = data;
}

若要发多个数据,重复调用I2C_Transmit()函数即可。若不再继续发送,就发送停止信号即可,但在发送结束信号前,需要确保最后一个数据发送完成。

所以,在发生结束信号前,需要判断TxE位和BTF位(数据发送成功时BTF会被置1)是否为1,只有当这两者都为1才表明数据全部发送完成。

/**
 * @brief 发送结束信号
 *
 */
void I2C_TransmitStop(void)
{
    /* 等待最后一个数据发送结束, 再发送停止信号 */
    while ((I2C2->SR1 & I2C_SR1_BTF) == 0)
    {
    }

    I2C2->CR1 |= I2C_CR1_STOP;
}

2.4. 主接收器

以下时是接收数据的流程:

首先需要先发送一个起始信号,会触发EV5事件,当起始信号发送成功后,SB位被置1,此时读SR1,然后再将地址写入DR寄存器即可清除该事件【清除SB】。

在发出地址后,会触发EV6事件,当地址发送成功后,ADDR被置1,此时通过读SR1和SR2将清除该事件【清除ADDR】。

之后就可以接收数据,此时会触发EV7事件,当接收到数据时,RxNE会被置1,这时将DR寄存器的内容读取即可清除该事件【清除RxNE】,同时应该响应ACK,将ACK位设置为1。

/**
 * @brief 接收一个字节数据
 *
 * @return uint8_t  将接收的数返回
 */
uint8_t I2C_Receive(void)
{
    /* 等待数据寄存器非空 */
    while ((I2C2->SR1 & I2C_SR1_RXNE) == 0)
    {
    }

    return I2C2->DR;	/* 读取数据, 并且清除EV7事件 */
}

/**
 * @brief 响应对方Ack
 *
 */
void I2C_Ack(void)
{
    I2C2->CR1 |= I2C_CR1_ACK;
}

在接收最后一个数据之前,需要提前将NAck和停止信号发送,也就是说最后一个数据的读取应该在NAck和停止信号后面。这点同样适用于单字节接收,在接收数据前就应该将NAck和停止信号发送。

2.5. I²C协议驱动代码汇总

#ifndef __I2C_H__
#define __I2C_H__

#include "stm32f10x.h"
#include <stdio.h>

/**
 * @brief 初始化
 *
 */
void I2C_Init(void);

/**
 * @brief 发送起始信号
 *
 */
void I2C_Start(void);

/**
 * @brief 发送时结束信号
 *
 */
void I2C_TransmitStop(void);

/**
 * @brief 接收时结束信号
 *
 */
void I2C_ReceiveStop(void);

/**
 * @brief 响应对方Ack
 *
 */
void I2C_Ack(void);

/**
 * @brief 响应对方NAck
 *
 */
void I2C_NAck(void);

/**
 * @brief 传输地址
 *
 * @param address 地址值
 */
void I2C_TransmitAddress(uint8_t address);

/**
 * @brief 传输一个字节数据, 从高位开始
 *
 * @param data      要传输的那一个字节数据
 */
void I2C_Transmit(uint8_t data);

/**
 * @brief 接收一个字节数据
 *
 * @return uint8_t  将接收的数返回
 */
uint8_t I2C_Receive(void);

#endif
#include "i2c.h"

/**
 * @brief 初始化
 *
 */
void I2C_Init(void)
{
    /* 1. 开启GPIOB、I²C2外设的时钟 */
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;

    /* 2. 配置GPIOB引脚工作模式: 复用开漏输出 */
    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);
    GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11);

    /* 3. 配置I²C2外设 */
    I2C2->CR2 &= ~I2C_CR2_FREQ;
    I2C2->CR2 |= (I2C_CR2_FREQ_2 | I2C_CR2_FREQ_5); /* 输入时钟设置为36MHz */
    I2C2->CCR &= ~(I2C_CCR_FS | I2C_CCR_CCR);
    I2C2->CCR |= 180;            /* 配置时钟控制寄存器 */
    I2C2->TRISE = 37;            /* 配置上升时间寄存器 */
    I2C2->CR1 &= ~I2C_CR1_SMBUS; /* 使用I²C */
    I2C2->CR1 |= I2C_CR1_PE;     /* 使能I²C */
}

/**
 * @brief 发送起始信号
 *
 */
void I2C_Start(void)
{
    I2C2->CR1 |= I2C_CR1_START; /* 将START位置1 */
}

/**
 * @brief 发送结束信号
 *
 */
void I2C_TransmitStop(void)
{
    /* 等待最后一个数据发送结束, 再发送停止信号 */
    while ((I2C2->SR1 & I2C_SR1_BTF) == 0)
    {
    }

    I2C2->CR1 |= I2C_CR1_STOP;
}

/**
 * @brief 接收时结束信号
 *
 */
void I2C_ReceiveStop(void)
{
    I2C2->CR1 |= I2C_CR1_STOP;
}

/**
 * @brief 响应对方Ack
 *
 */
void I2C_Ack(void)
{
    I2C2->CR1 |= I2C_CR1_ACK;
}

/**
 * @brief 响应对方NAck
 *
 */
void I2C_NAck(void)
{
    I2C2->CR1 &= ~I2C_CR1_ACK;
}

/**
 * @brief 传输地址
 *
 * @param address 地址值
 */
void I2C_TransmitAddress(uint8_t address)
{
    /* 等待起始信号发送完成 */
    while ((I2C2->SR1 & I2C_SR1_SB) == 0)
    {
    }
    I2C2->DR = address; /* 此时起始信号以发送完毕, 发送设备地址 */

    /* 等待地址传输完成 */
    while ((I2C2->SR1 & I2C_SR1_ADDR) == 0)
    {
    }

    I2C2->SR2; /* 清除EV事件 */
}

/**
 * @brief 传输一个字节数据, 从高位开始
 *
 * @param data 传输的那一个字节数据
 */
void I2C_Transmit(uint8_t data)
{
    /* 等待数据寄存器为空 */
    while ((I2C2->SR1 & I2C_SR1_TXE) == 0)
    {
    }

    I2C2->DR = data;
}

/**
 * @brief 接收一个字节数据
 *
 * @return uint8_t  将接收的数返回
 */
uint8_t I2C_Receive(void)
{
    /* 等待数据寄存器非空 */
    while ((I2C2->SR1 & I2C_SR1_RXNE) == 0)
    {
    }

    return I2C2->DR;
}

2.6. EEPROM代码改写

在软件模拟基础上改写即可。

#include "e2prom.h"

/**
 * @brief 初始化
 *
 */
void E2PROM_Init(void)
{
    I2C_Init();
}

/**
 * @brief 往EEPROM中写入一个字节数据
 *
 * @param word_address  数据写入的地址
 * @param data          要写入的数据
 */
void E2PROM_WriteByte(uint8_t word_address, uint8_t data)
{
    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要写入数据的设备的地址 */
    I2C_TransmitAddress(DEV_ADDR);

    /* 3. 指明从设备中的哪个地址开始写 */
    I2C_Transmit(word_address);

    /* 4. 开始写入数据 */
    I2C_Transmit(data);

    /* 5. 发送结束信号 */
    I2C_TransmitStop();
    /* 6. 等待EEPROM将数据写入 */
    Delay_ms(5);
}

/**
 * @brief 读取EEPROM中的一个字节数据
 *
 * @param word_address  要读取数据的地址
 * @param p_data        将读到的数据存储在*p_data中
 */
void E2PROM_ReadByte(uint8_t word_address, uint8_t *p_data)
{
    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要读取数据的设备的地址[写] */
    I2C_TransmitAddress(DEV_ADDR);

    /* 3. 指明从设备中的哪个地址开始读 */
    I2C_Transmit(word_address);

    /* 4. 发送起始信号 */
    I2C_Start();

    /* 5. 发送要读取数据的设备的地址[读] */
    I2C_TransmitAddress(DEV_ADDR | 1);

    /* 6. 响应NACK, 停止读取 */
    I2C_NAck();

    /* 7. 发送结束信号 */
    I2C_ReceiveStop();

    /* 8. 接收数据 */
    *p_data = I2C_Receive();
}

/**
 * @brief 往EEPROM中写入多个字节数据
 *
 * @param word_address  数据写入的起始地址
 * @param datas         要写入的数据
 * @param size          要写入数据的长度
 */
void E2PROM_WritePage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要写入数据的设备的地址 */
    I2C_TransmitAddress(DEV_ADDR);

    /* 3. 指明从设备中的哪个地址开始写 */
    I2C_Transmit(word_address);

    /* 4. 开始写入数据 */
    for (uint8_t i = 0; i < size; ++i)
    {
        I2C_Transmit(datas[i]);
    }

    /* 5. 发送结束信号 */
    I2C_TransmitStop();

    /* 6. 等待EEPROM将数据写入 */
    Delay_ms(5);
}

/**
 * @brief 读取EEPROM中的多个字节数据
 *
 * @param word_address  数据读取的起始地址
 * @param datas         将读取的数据存储在此
 * @param size          要读取数据的长度
 */
void E2PROM_ReadPage(uint8_t word_address, uint8_t datas[], uint8_t size)
{
    /* 1. 发送起始信号 */
    I2C_Start();

    /* 2. 发送要读取数据的设备的地址[写] */
    I2C_TransmitAddress(DEV_ADDR);

    /* 3. 指明从设备中的哪个地址开始读 */
    I2C_Transmit(word_address);

    /* 4. 发送起始信号 */
    I2C_Start();

    /* 5. 发送要读取数据的设备的地址[读] */
    I2C_TransmitAddress(DEV_ADDR | 1);

    /* 6. 接收数据 */
    for (uint8_t i = 0; i < size - 1; ++i)
    {
        I2C_Ack();
        datas[i] = I2C_Receive();
    }
    /* 7. 响应NACK, 停止读取 */
    I2C_NAck();

    /* 8. 发送结束信号 */
    I2C_ReceiveStop();

    datas[size - 1] = I2C_Receive();
}

为什么响应ACK和NAck都要在接收数据前呢?

在进行STM32微控制器与SMI9541气压传感器之间的通信时,由于硬件I²C资源可能已被占用或者微控制器不支持足够数量的I²C接口,软件模拟I²C协议便成为一种可行的解决方案。要实现这一目标,首先需要了解STM32的GPIO配置,以及如何通过软件控制这些GPIO模拟出I²C的时序和通信流程。 参考资源链接:[STM32通过软件IIC读取SMI9541气压传感器数据](https://wenku.csdn.net/doc/1y5pti895r) 具体实现步骤包括初始化I²C所需的GPIO引脚,包括SDA和SCL线,设置为开漏模式,并配置上拉电阻。在STM32上,可以使用HAL库函数或者直接操作寄存器来实现这一点。例如,使用`HAL_GPIO_Init()`函数来配置GPIO模式和上拉/下拉设置,确保与I²C协议的要求一致。 接下来,需要编写一系列函数来模拟I²C协议的行为。这包括产生起始条件、停止条件、发送字节、接收字节以及应答位的处理。每个操作都需要严格遵守I²C时序要求,以确保与SMI9541传感器通信的可靠性。 在软件模拟的过程中,可以通过`HAL_Delay()`函数来实现必要的时序延迟,确保在发送或接收数据之间有适当的等待时间。例如,在发送数据之前,通常需要等待一段时间以确保传感器已经准备好接收数据。 一旦建立了通信,就需要根据SMI9541气压传感器的技术手册来构造相应的I²C通信协议。这包括设置正确的设备地址,发送读写命令,以及处理返回的数据。数据通常以二进制格式返回,需要根据传感器手册中提供的数据格式进行解析,将其转换为可读的气压值。 最后,为了提高软件模拟I²C的效率和可靠性,可以通过DMA(直接内存访问)和中断来优化数据传输过程。这可以减少CPU的负担,并提高数据处理的速度。 实现软件模拟I²C与SMI9541气压传感器通信是一个综合性的项目,涉及到嵌入式软件开发的多个方面,包括GPIO控制、I²C协议的理解、以及数据处理等。通过这个项目,开发者不仅可以掌握软件模拟I²C的技巧,还能够深入理解传感器的数据读取和解析过程。 参考资源链接:[STM32通过软件IIC读取SMI9541气压传感器数据](https://wenku.csdn.net/doc/1y5pti895r)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值