PN学堂GD32教程第11篇——I2C与EEPROM

代码仓库:

bash 复制代码
git clone https://gitee.com/jumayusi/unit11-gd32-iic.git

前言:IIC的基本知识这篇笔记不会再详细介绍。

11.2 EEPROM存储芯片AT24C02

这个分类并不严谨,FeRAM\ReRAM\MRAM都是已经在市场上量产的非易失性存储介质,FeRAM是现在AI存算一体的主流解决方案,这里不做过多展开了。

EEPROM的主要特点:

Flash的主要特点:

24C02不仅支持按字节写入,也支持按页写入。

11.3 11.3 软件模拟IIC驱动24C02-起始、停止、应答

引脚初始化

GPIO口模拟I2C的基石是用拉高IO口作为SET信号线,拉低IO口作为CLR信号线,因此我们可以直接用宏定义声明:

cpp 复制代码
#define GET_I2C_SDA() gpio_input_bit_get(GPIOB, GPIO_PIN_7) // 读取SDA端口
#define SET_I2C_SCL() gpio_bit_set(GPIOB, GPIO_PIN_6)   // 时钟线SCL输出高电平
#define CLR_I2C_SCL() gpio_bit_reset(GPIOB, GPIO_PIN_6) // 时钟线SCL输出低电平
#define SET_I2C_SDA() gpio_bit_set(GPIOB, GPIO_PIN_7)   // 数据线SDA输出高电平
#define CLR_I2C_SDA() gpio_bit_reset(GPIOB, GPIO_PIN_7) // 数据线SDA输出低电平

由于我们只使用了GPIO这一个peripheral,因此I2C的初始化就是:

cpp 复制代码
static void GpioInit(void) 
{
  rcu_periph_clock_enable(RCU_GPIOB);
  gpio_init(GPIOB, GPIO_MODE_OUT_OD, GPIO_OSPEED_10MHZ,
            GPIO_PIN_6 | GPIO_PIN_7);
}

void EepromDrvInit(void) 
{ 
  GpioInit(); 
}

I2CStart

下面实现I2C的起始信号:

代码示范:

cpp 复制代码
/**
*******************************************************************
* @function 产生IIC起始时序,准备发送或接收数据前必须由起始序列开始
* @param
* @return
* @brief    SCL为高电平时,SDA由高电平向低电平跳变,开始传输数据
*           生成下图所示的波形图,即为起始时序
*                1 2    3     4
*                    __________
*           SCL : __/          \_____
*                 ________
*           SDA :         \___________
*******************************************************************
*/
static void I2CStart(void) 
{
  SET_I2C_SDA(); // 1#数据线SDA输出高电平
  SET_I2C_SCL(); // 2#时钟线SCL输出高电平
  DelayNus(4);   // 延时4us
  CLR_I2C_SDA(); // 3#数据线SDA输出低电平
  DelayNus(4);   // 延时4us
  CLR_I2C_SCL(); // 4#时钟线SCL输出低电平,保持I2C的时钟线SCL为低电平,准备发送或接收数据
  DelayNus(4); // 延时4us
}

(1# & 2#)我们知道,I2C在总线空闲时期SCL和SDA都需要保持高电平(硬件实现上SCL/SDA两个引脚都接上了上拉电阻),但为了避免IO口处于不确定状态,我们需要先操作两个IO拉高。

tHD;STA是I2C能够正常建立通信的一个重要指标,它的中文全称叫做起始保持时间,飞利浦在制定IIC规则时约束这个时间典型值为4us,我们实际写代码时必须参考芯片的时序约束,3V3的Vio下只需要0.25us,我们这里规定4us是绰绰有余。

(3#)同样的,在完成SDA由高拉低的操作之后,我们同样提供4us的相位冗余,这是为了满足tHD;STA。

(4#)最后拉低SCL电平,因为正常通信时SDA只可以在SCL低电平时发生跳变,这是为了数据传输做准备。

I2CStop

cpp 复制代码
/**
*******************************************************************
* @function 产生IIC停止时序
* @param
* @return
* @brief    SCL为高电平时,SDA由低电平向高电平跳变,结束传输数据
*          生成下图所示的波形图,即为停止时序
*                1 2   3  4
*                       _______________
*          SCL : ______/
*                __        ____________
*          SDA:    \______/
*******************************************************************
*/
static void I2CStop(void) 
{
  CLR_I2C_SDA(); // 2#数据线SDA输出低电平
  DelayNus(4);   // 延时4us
  SET_I2C_SCL(); // 3#时钟线SCL输出高电平
  DelayNus(4);
  SET_I2C_SDA(); // 4#数据线SDA输出高电平,发送I2C总线结束信号
}

就不介绍的那么细了,非常推荐初学者用示波器实操一下加深印象。

I2CSendByte

代码示范:

cpp 复制代码
/**
*******************************************************************
* @function 发送一字节,数据从高位开始发送出去
* @param    byte
* @return
* @brief    下面是具体的时序图
*                1 2     3      4
*                         ______
*           SCL: ________/      \______
*                ______________________
*           SDA: \\\___________________
*******************************************************************
*/
static void I2CSendByte(uint8_t byte) 
{
  for (uint8_t i = 0; i < 8; i++) // 循环8次,从高到低取出字节的8个位
  {
    if ((byte & 0x80)) // 2#取出字节最高位,并判断为'0'还是'1',从而做出相应的操作
    {
      SET_I2C_SDA(); // 数据线SDA输出高电平,数据位为'1'
    } else {
      CLR_I2C_SDA(); // 数据线SDA输出低电平,数据位为'0'
    }

    byte <<= 1; // 左移一位,次高位移到最高位

    DelayNus(4);   // 延时4us
    SET_I2C_SCL(); // 3#时钟线SCL输出高电平
    DelayNus(4);   // 延时4us
    CLR_I2C_SCL(); // 4#时钟线SCL输出低电平
    DelayNus(4);   // 延时4us
  }
}

其实很好理解,核心就是参考SCL的电平,**低电平改变数据,高电平采样数据。**在时钟信号实现跳变之前,把SDA要输出的0/1塞到总线上。

I2CReadByte

cpp 复制代码
/**
*******************************************************************
* @function 读取一字节数据
* @param
* @return   读取的字节
* @brief    下面是具体的时序图
*                       ______
*           SCL: ______/      \___
*                ____________________
*           SDA: \\\\______________\\\
*******************************************************************
*/
static uint8_t I2CReadByte(void) 
{
  uint8_t byte = 0;               // byte用来存放接收的数据
  SET_I2C_SDA();                  // 释放SDA
  for (uint8_t i = 0; i < 8; i++) // 循环8次,从高到低读取字节的8个位
  {
    SET_I2C_SCL(); // 时钟线SCL输出高电平
    DelayNus(4);   // 延时4us
    byte <<= 1;    // 左移一位,空出新的最低位

    if (GET_I2C_SDA()) // 读取数据线SDA的数据位
    {
      byte++; // 在SCL的上升沿后,数据已经稳定,因此可以取该数据,存入最低位
    }
    CLR_I2C_SCL(); // 时钟线SCL输出低电平
    DelayNus(4);   // 延时4us
  }

  return byte; // 返回读取到的数据
}

和上文加粗字体一样,主机(MCU)先释放总线,然后控制SCL切换到高电平之后读取SDA上从机发来的数据。循环内最后切换SCL到低电平并且钳住4us保证Tlow的时序完整性。

I2CWaitAck/I2CSendAck/I2CSendNack

cpp 复制代码
/**
*******************************************************************
* @function 等待接收端的应答信号
* @param
* @return   1,接收应答失败;0,接收应答成功
* @brief    当SDA拉低后,表示接收到ACK信号,然后,拉低SCL,
*           此处表示发送端收到接收端的ACK
*                _______|____
*           SCL:        |    \_________
*                _______|
*           SDA:         \_____________
*******************************************************************
*/
static bool I2CWaitAck(void) 
{
  uint8_t errTimes = 0;

  SET_I2C_SDA(); // 释放SDA总线,很重要
  DelayNus(4);   // 延时4us

  SET_I2C_SCL(); // 时钟线SCL输出高电平
  DelayNus(4);   // 延时4us

  while (GET_I2C_SDA()) // 读回来的数据如果是高电平,即接收端没有应答
  {
    errTimes++; // 计数器加1

    if (errTimes > 250) // 如果超过250次,则判断为接收端出现故障,因此发送结束信号
    {
      I2CStop();    // 产生一个停止信号
      return false; // 返回值为1,表示没有收到应答信号
    }
  }

  CLR_I2C_SCL(); // 表示已收到应答信号,时钟线SCL输出低电平
  DelayNus(4);   // 延时4us

  return true; // 返回值为0,表示接收应答成功
}

/**
*******************************************************************
* @function 发送应答信号
* @param
* @return
* @brief    下面是具体的时序图
*                 1 2     3      4      5
*                         ______
*           SCL: ________/      \____________
*                __                     ______
*           SDA:   \___________________/
*******************************************************************
*/
void I2CSendAck(void)
{
  CLR_I2C_SDA(); // 2#数据线SDA输出低电平
  DelayNus(4);   // 延时4us
  SET_I2C_SCL(); // 3#时钟线SCL输出高电平,在SCL上升沿前就要把SDA拉低,为应答信号
  DelayNus(4);   // 延时4us
  CLR_I2C_SCL(); // 4#时钟线SCL输出低电平
  DelayNus(4);   // 延时4us
  SET_I2C_SDA(); // 5#数据线SDA输出高电平,释放SDA总线,很重要
}

/**
*******************************************************************
* @function 发送非应答信号
* @param
* @return
* @brief    下面是具体的时序图
*               1 2     3      4
*                        ______
*          SCL: ________/      \______
*               __ ___________________
*          SDA: __/
*******************************************************************
*/
void I2CSendNack(void) 
{
  SET_I2C_SDA(); // 2#数据线SDA输出高电平
  DelayNus(4);   // 延时4us
  SET_I2C_SCL(); // 3#时钟线SCL输出高电平,在SCL上升沿前就要把SDA拉高,为非应答信号
  DelayNus(4);   // 延时4us
  CLR_I2C_SCL(); // 4#时钟线SCL输出低电平
  DelayNus(4);   // 延时4us
}

在 I2C 里,第 9 个时钟(每个字节 8 位之后的应答位)是收/发双方唯一的握手机制。谁接收数据,谁就在第 9 个时钟给出应答。所以主机(MCU)处于不同数据收发方向时,用的是不同函数接口:

主机发数据(写 EEPROM)→ 从机应答 → 主机用 I2CWaitAck();

主机收数据(读 EEPROM)→ 主机应答 → 主机用 I2CSendAck() / I2CSendNack()。

11.4 软件模拟IIC驱动24C02-读写多字节数据

相关推荐
阿钱真强道1 小时前
27 嵌入式操作系统 | C 网关:串口 ⇄ UDP 转发(先用 UDP 当上位机替身)
c语言·单片机·网关·udp·uloop
沐欣工作室_lvyiyi2 小时前
基于STM32的便捷式智能气象仪设计(论文+源码)
stm32·单片机·嵌入式硬件·单片机毕业设计·电子信息工程毕业设计·河北工程技术学院·廊坊师范学院
iCxhust2 小时前
STM32F103C8T6 CAN 通讯
stm32·单片机·嵌入式硬件
铅笔小新z2 小时前
【stm32】SPI Flash 驱动开发
驱动开发·stm32·嵌入式硬件
老王爱玩车2 小时前
深入理解指针6
c语言·开发语言·学习
苏辰澈2 小时前
C++自己的族谱规则:继承详解上
c语言·c++·visualstudio
haliu3 小时前
【FHE 同态加密】我们如何实现同态加密推理(十六):未完成清单与已知边界(同态加密推理的精度、性能与安全)
人工智能·嵌入式·c·fhe·推理引擎·c11·边缘推理·同态加密推理
Logic1015 小时前
C语言/数据结构字符串题解:按字符类型重排(数字→字母→符号)——三趟扫描稳定分区
c语言·数据结构·字符串·时间复杂度·计数排序·算法题·ascii
Logic10111 小时前
C语言/数据结构位运算题解:异或XOR找出学习计划中的“独特学习时间“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质