前言:
学会IIC驱动的原理,时序和代码实现
1.0 GPIO初始化
从原理图中可以看出IIC对应的时钟线和数据线在PB端口,因此需要初始化GPIOB的时钟,同时初始化GPIOB_PIN_6 | 7,的引脚,设置为开漏输出模式。
什么事开漏?为什么使用开漏?
GPIO_MODE_OUT_OD
是一个配置选项,用于设置微控制器 GPIO(General Purpose Input/Output)引脚的工作模式。在 GD32 系列微控制器中,这代表“开漏输出”(Open Drain Output)模式。
在开漏输出模式下,GPIO 引脚可以被驱动到低电平(接地),或者高阻态(不连接)。当引脚设置为高阻态时,它既不拉高也不拉低,因此外部电路可以将其电平拉高或保持不变。这种模式常用于需要多个设备共享同一信号线的场合,例如 I2C 总线,或是当输出级需要与不同电压级别的逻辑电平兼容时。
在开漏输出模式下,如果需要稳定的高电平输出,通常会在引脚外部接一个上拉电阻至电源。这样,在 GPIO 设置为高阻态时,上拉电阻会将引脚电平拉高;而在 GPIO 设置为低电平时,引脚则会被拉低至地电平。
GPIO初始化代码:
// GPIO初始化 static void GpioInit() { // rcu 初始化Gpio时钟 rcu_periph_clock_enable(RCU_GPIOB); // gpio 初始化 gpio_init ( GPIOB, GPIO_MODE_OUT_OD, GPIO_OSPEED_10MHZ, GPIO_PIN_6 | GPIO_PIN_7 ); } // 初始化GPIO void EepromDrvInit(void) { // GPIO初始化函数 GpioInit(); }
2.0 宏定义引脚
宏定义的作用是简化程序,防止程序出现魔鬼数字,本质是温文本展开,注意优先级问题
// 宏定义SCL 与 SDA
#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输出低电平
3.0 IIC 起始信号
起始信号:
/** ******************************************************************* * @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 }
4.0 IIC 停止信号
停止信号:
/** ******************************************************************* * @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总线结束信号 }
5.0 IIC 发送一个字节
发送字节是从高位开始发送的,一步一步的将数据移动出去,需要使用for循环,并且将数据按位左移。
发送字节:
/** ******************************************************************* * @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 } }
6.0 IIC 接收一个字节
接收字节:
在这段I²C读取字节的代码中,
byte++
的作用是在检测到 SDA(Serial Data)线上的数据位为1时,将当前正在构建的字节值的最低位设为1。具体来说,
byte <<= 1;
这一行将byte
的所有位都向左移动一位,腾出了最低位(最右边的位)。接下来,根据 SDA 线上的数据状态,byte
的最低位会被更新。如果GET_I2C_SDA()
返回非零值(即SDA线上的数据位为1),那么byte++
将会使byte
的值增加1,从而将最低位设为1。否则,如果GET_I2C_SDA()
返回0(即SDA线上的数据位为0),byte
的值保持不变,最低位仍然为0。/** ******************************************************************* * @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; // 返回读取到的数据 }
7.0 IIC 接收应答
接收应答:
/** ******************************************************************* * @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,表示接收应答成功 }
8.0 IIC 发送应答
发送应答:
/** ******************************************************************* * @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总线,很重要 }
9.0 IIC 发送非应答
/**
*******************************************************************
* @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
}
10.0 完整代码
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include "gd32f30x.h"
#include "delay.h"
#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输出低电平
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();
}
/**
*******************************************************************
* @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
}
/**
*******************************************************************
* @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总线结束信号
}
/**
*******************************************************************
* @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
}
}
/**
*******************************************************************
* @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; // 返回读取到的数据
}
/**
*******************************************************************
* @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
}
头文件
#ifndef _EEPROM_DRV_H_
#define _EEPROM_DRV_H_
void EepromDrvInit(void);
void EepromDrvTest(void);
#endif
后记:
进一步的完成连续发送和接收字节...