在上一篇博客中已经讲过 I2C 总线通信协议, 本文讲述 I2C 总线协议的软件模拟实现方法
1. 简述
所谓的 I2C 总线协议的软件模拟实现方法, 就是用软件控制 GPIO 的输入输出和高低电平变化, 来模拟 I2C 总线通讯过程中 SCLSDA 的电平变化来实现的
2. I2C 总线的封装
每个处理器对应的 GPIO 操作都有差异, 即使是同一款处理器, 不同的人也会有不同的 GPIO 封装风格, 就以我个人习惯用的 GPIO 方法为例来进行讲解我习惯上将 GPIO 的组和位封装为一个结构体, 这样使用方便, 看起来也更直观
- typedef struct {
- unsigned char group;
- unsigned char bit;
- } gpio_t;
将 I2C 总线中使用的 SCL 和 SDA 的 GPIO 进一步进行封装
- typedef struct {
- gpio_t scl;
- gpio_t sda;
- } i2c_gpio_t;
将 I2C 总线软件模拟部分当做驱动程序中的一个模块来使用, 定义一个结构体来封装 I2C 模块中的一些全局变量, 如: GPIO 锁等等本文中的锁只是为了保证 I2C 的一个操作步骤是原子的, 所有锁的使用可以忽略, 如果想要了解更过关于锁的使用方法, 请关注另外一篇博客(还没来得及写, 以后会补充)
- typedef struct {
- i2c_gpio_t gpio;
- spinlock_t lock;
- struct mutex i2c_mutex;
- } i2c_info_t;
3. 软件模拟实现
3.1 I2C 总线的初始化
1)先初始化 I2C 总线, 具体要做的内容是, 先把外部调用 I2C 模块时要使用的 GPIO 引脚, 作为参数传递到 I2C 模块, 用来进行一系列的操作在这里将 GPIO 作为参数传递到 I2C 模块后, 保存在全局变量的结构体中
2)再初始化 I2C 总线的 GPIO 引脚, 即将用来代替模拟 I2C 总线中 SCLSDA 引脚的 GPIO 设置为输出, 并输出高电平, 因为两条线上都接有上拉电阻, I2C 总线空闲时默认 SCLSDA 都处于高电平, 也就是空闲状态
3)如果要使用锁机制, 需要在这一步中将锁初始化
- // I2C 模块初始化
- int i2c_init(i2c_gpio_t *gpio)
- {
- i2c_debug("i2c_init");
- // 初始化锁
- spin_lock_init(&i2c_info.lock);
- mutex_init(&i2c_info.i2c_mutex);
- // 初始化全局变量中 I2C 的 GPIO
- i2c_info.gpio.scl = gpio->scl;
- i2c_info.gpio.sda = gpio->sda;
- i2c_gpio_init();
- return 0;
- }
- // I2C 的 GPIO 初始化
- static void i2c_gpio_init(void)
- {
- i2c_debug("i2c_gpio_init");
- i2c_sda_init();
- i2c_scl_init();
- }
- // I2C 的 SCL 初始化
- static void i2c_scl_init(void)
- {
- i2c_debug("scl init");
- SET_SCL_OUT;
- SET_SCL_HIGH;
- }
- // I2C 的 SDA 初始化
- static void i2c_sda_init(void)
- {
- i2c_debug("sda init");
- SET_SDA_OUT;
- SET_SDA_HIGH;
- }
3.2 I2C 总线的起始位
I2C 总线在开始通信时要先发送一个起始位标志, 起始位是在 SCL 为高电平时, SDA 由高电平变为低电平
- // I2C 总线的起始位
- int i2c_start(void)
- {
- mutex_lock(&i2c_info.i2c_mutex);
- SET_SDA_OUT;
- udelay(I2C_DELAY);
- SET_SDA_HIGH;
- udelay(I2C_DELAY);
- SET_SCL_HIGH;
- udelay(I2C_DELAY);
- SET_SDA_LOW;
- udelay(I2C_DELAY);
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- mutex_unlock(&i2c_info.i2c_mutex);
- return 0;
- }
3.3 I2C 总线的结束位
I2C 总线在数据传输完成后, 需要发送一个结束位, 来结束 I2C 通讯, 并释放 I2C 总线, 结束位是在 SCL 为高电平时, SDA 由低电平变为高电平
- // I2C 总线的结束位
- int i2c_stop(void)
- {
- mutex_lock(&i2c_info.i2c_mutex);
- SET_SDA_OUT;
- udelay(I2C_DELAY);
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- SET_SDA_LOW;
- udelay(I2C_DELAY);
- SET_SCL_HIGH;
- udelay(I2C_DELAY);
- SET_SDA_HIGH;
- udelay(I2C_DELAY);
- mutex_unlock(&i2c_info.i2c_mutex);
- return 0;
- }
3.4 I2C 总线的应答
为了统一管理和使用方便, 将 I2C 总线的等待应答发送应答信号发送非应答信号封装在一起进行管理
1)I2C 总线的等待应答
在 I2C 总线通讯时, 主设备给从设备发送一个字节的数据后, 要等待从设备的一个应答信号, 这时候主设备处于等待应答状态, 需要检测从设备的应答信号是否到来, 如果从设备的应答信号到来, 主设备就继续给从设备发送下一个字节的数据, 或者发送停止位结束 I2C 通讯; 如果在主设备等待超时后, 从设备的应答信号时钟不到来, 就说明 I2C 总线通讯中出现问题, 主设备跳出等待, 直接发送结束位, 以结束 I2C 总线通讯
- // I2C 总线的等待应答
- static int i2c_wait_ack(void)
- {
- int ack_times = 0;
- int ret = 0;
- mutex_lock(&i2c_info.i2c_mutex);
- SET_SDA_OUT;
- udelay(I2C_DELAY);
- SET_SDA_HIGH;
- udelay(I2C_DELAY);
- SET_SDA_IN;
- udelay(I2C_DELAY);
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- SET_SCL_HIGH;
- udelay(I2C_DELAY);
- ack_times = 0;
- // 检测从设备应答信号
- while (GET_SDA_VAL) {
- ack_times++;
- // 判断等待是否超时
- if (ack_times == 10) {
- ret = 1;
- i2c_error("i2c ack error, no ack");
- break;
- }
- }
- SET_SCL_LOW;
- mutex_unlock(&i2c_info.i2c_mutex);
- return ret;
- }
2)I2C 总线的发送应答
在 I2C 总线通信的时候, 主设备每次接收到从设备发送的一个字节数据后, 要给从设备发送应答信号 (ACK) 以继续接收从设备的数据, 或者给从设备发送非应答信号 (NOACK) 以结束接收从设备的数据
应答信号 (ACK) 就是先拉低 SDA 线, 并在 SCL 为高电平期间保持 SDA 线为低电平
- // I2C 总线发送应答信号
- static int i2c_send_ack(void)
- {
- i2c_debug("i2c_send_ack");
- mutex_lock(&i2c_info.i2c_mutex);
- SET_SDA_OUT;
- udelay(I2C_DELAY);
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- SET_SDA_LOW;
- udelay(I2C_DELAY);
- SET_SCL_HIGH;
- udelay(I2C_DELAY);
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- mutex_unlock(&i2c_info.i2c_mutex);
- return 0;
- }
非应答信号 (NOACK) 就是不要拉低 SDA 线(此时 SDA 线为高电平), 并在 SCL 为高电平期间保持 SDA 线为高电平
- // I2C 总线发送非应答信号
- static int i2c_send_noack(void)
- {
- i2c_debug("i2c_send_noack");
- mutex_lock(&i2c_info.i2c_mutex);
- SET_SDA_OUT;
- udelay(I2C_DELAY);
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- SET_SDA_HIGH;
- udelay(I2C_DELAY);
- SET_SCL_HIGH;
- udelay(I2C_DELAY);
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- mutex_unlock(&i2c_info.i2c_mutex);
- return 0;
- }
3.5 I2C 总线的写操作
- // I2C 总线的写操作
- int i2c_write_byte(u8 data)
- {
- unsigned long flag = 0;
- u8 i = 0;
- local_irq_save(flag);
- preempt_disable();
- mutex_lock(&i2c_info.i2c_mutex);
- SET_SDA_OUT;
- udelay(I2C_DELAY);
- for (i = 0; i < 8; i++) {
- if (data & 0x80) {
- SET_SDA_HIGH;
- } else {
- SET_SDA_LOW;
- }
- udelay(I2C_DELAY);
- SET_SCL_HIGH;
- udelay(I2C_DELAY);
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- data <<= 0x1;
- }
- mutex_unlock(&i2c_info.i2c_mutex);
- preempt_enable();
- local_irq_restore(flag);
- return 0;
- }
- int i2c_write_byte_with_ack(u8 data)
- {
- i2c_write_byte(data);
- if (i2c_ack(I2C_WAIT_ACK)) {
- i2c_error("wait ack failed, no ack");
- i2c_stop();
- return -1;
- }
- return 0;
- }
3.6 I2C 总线的读操作
- // I2C 总线的读操作
- int i2c_read_byte(u8 *data)
- {
- unsigned long flag = 0;
- u8 ret = 0;
- u8 i = 0;
- local_irq_save(flag);
- preempt_disable();
- mutex_lock(&i2c_info.i2c_mutex);
- SET_SDA_IN;
- udelay(I2C_DELAY);
- for (i = 0; i < 8; i++) {
- SET_SCL_HIGH;
- udelay(I2C_DELAY);
- ret <<= 1;
- if (GET_SDA_VAL) {
- ret |= 0x01;
- }
- SET_SCL_LOW;
- udelay(I2C_DELAY);
- }
- mutex_unlock(&i2c_info.i2c_mutex);
- preempt_enable();
- local_irq_restore(flag);
- *data = ret;
- return 0;
- }
来源: https://www.cnblogs.com/microxiami/p/8528459.html