ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

PN学堂GD32教程第11篇——I2C与EEPROM 代码仓库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信号线因此我们可以直接用宏定义声明#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的初始化就是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的起始信号代码示范/** ******************************************************************* * 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/** ******************************************************************* * 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代码示范/** ******************************************************************* * 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/** ******************************************************************* * 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/** ******************************************************************* * 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-读写多字节数据
返回列表