layout: post
title: MCU专题精讲-I2C
description: MCU专题精讲-I2C
categories:

  • MCU
    tags:
  • I2C

I2C通信基础知识

  1. 两线制:I2C使用两条线进行通信,一条是数据线(SDA),另一条是时钟线(SCL)。SDA线用于数据传输,SCL线用于同步。

  2. 设备角色:在I2C通信中,设备可以是主设备(Master)或从设备(Slave)。主设备是启动和结束数据传输的设备,从设备是响应主设备请求并发送或接收数据的设备。一条I2C总线上可以有多个主设备和多个从设备。

  3. 地址和数据传输:主设备通过在总线上发送从设备的地址来与之进行通信。在地址后面的数据可以由主设备发送到从设备,也可以由从设备发送到主设备。

  4. 多主设备:I2C支持多主设备模式。这意味着如果有多个主设备试图同时控制总线,就需要使用一种叫做仲裁的机制来决定哪个主设备可以控制总线。

  5. I2C硬件特性:I2C 协议使用的是开漏输出,这意味着设备只能下拉总线电压(对应逻辑“0”),而不能上拉总线电压(对应逻辑“1”)。总线电压的上拉是通过一个外部的上拉电阻完成的。当所有设备都不再下拉总线电压时,总线电压将通过上拉电阻恢复到高电平。这就是为什么 I2C 总线需要上拉电阻的原因

    逻辑分析仪I2C数据实例

I2C的工作流程

数据有效性

数据线SDA上的数据在时钟线SCL为高电平时必须保持稳定。只有当SCL为低电平时,SDA上的数据才能改变。

起始条件

在I2C通信中,起始信号是一种特殊的信号用来标识一个新的I2C通信周期的开始。起始信号是在时钟线(SCL)为高电平时,将数据线(SDA)从高电平转换到低电平。这个高到低的过渡标志着I2C通信的开始

// 首先,确保SCL和SDA都为高电平
set_gpio_high(SCL);
set_gpio_high(SDA);

// 稍微等待一会儿让电平稳定
delay();

// 然后在SCL为高电平时,将SDA拉低
set_gpio_low(SDA);

// 稍微等待一会儿让起始信号完成
delay();

起始信号

发送地址

主设备在总线上发送7位的设备地址(对于一些新设备,可能是10位)。这个地址确定了主设备要与之通信的从设备。地址之后的第8位是数据方向位(R/W),如果为0表示写,为1表示读。

void i2c_send_address(uint8_t address, bool read) {
    // 首先,我们将7位的地址左移一位,留出最后一位给读/写位
    uint8_t shifted_address = address << 1;

    // 如果我们打算读取数据,我们将最后一位设置为1
    if (read) {
        shifted_address |= 0x01;
    }

    // 然后,我们将每一位写到I2C总线上,从最高位开始
    for (int i = 7; i >= 0; --i) {
        bool bit = (shifted_address >> i) & 0x01;
        i2c_write_bit(bit);
    }

    // 最后,我们读取并检查从设备的应答位
    bool ack = i2c_read_bit();
    if (!ack) {
        // 如果我们没有收到应答位,那么可能发生了一些错误
        handle_error();
    }
}

ACK确认

每个被主设备寻址的从设备都需要在收到地址后,拉低SDA线一个时钟周期,发送一个ACK确认位给主设备。如果主设备没有收到确认位,它通常会发出停止信号结束通信,或者尝试重发地址。

bool i2c_read_ack() {
    // 读取一位数据
    bool bit = i2c_read_bit();

    // 如果数据为0,那么我们收到了一个ACK
    if (bit == 0) {
        return true;
    }
    // 否则,我们收到了一个NACK
    else {
        return false;
    }
}

然后,你可以在需要的地方使用这个函数来读取ACK,例如在发送地址或数据后:

// 发送地址
i2c_send_address(address, read);

// 读取ACK
bool ack = i2c_read_ack();
if (!ack) {
    // 如果没有收到ACK,那么可能发生了一些错误
    handle_error();
}

数据传输

之后的每一个时钟周期,主设备(如果是写操作)或从设备(如果是读操作)都会将一个字节的数据放到SDA线上。发送完一个字节后,发送设备会释放SDA线,接收设备会发送一个ACK位。

停止信号

通信在主设备发送停止条件后结束。在硬件层面,这表现为在SCL线为高电平时,SDA线从低电平变为高电平。

// 首先,确保SCL和SDA都为低电平
set_gpio_low(SCL);
set_gpio_low(SDA);

// 稍微等待一会儿让电平稳定
delay();

// 然后,将SCL拉高
set_gpio_high(SCL);

// 稍微等待一会儿让电平稳定
delay();

// 最后,在SCL为高电平时,将SDA拉高
set_gpio_high(SDA);

// 稍微等待一会儿让停止信号完成
delay();

结束信号

GD32中的硬件I2C

模块框图

image-20230803114909762

寄存器详解

控制寄存器 0 (I2C_CTL0)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —- | ———– | ———————————————————— |
| 0 | I2CEN | I2C 外设使能 (0:禁用 I2C, 1:使能 I2C) |
| 1 | SMBEN | SMBus/I2C 模式开关 (0:I2C 模式, 1:SMBus 模式) |
| 2 | 保留 | 必须保持复位值 |
| 3 | SMBSEL | SMBus 类型选择 (0:从机, 1:主机) |
| 4 | ARPEN | SMBus 下 ARP 协议开关 (0:关闭 ARP, 1:开启 ARP) |
| 5 | PECEN | PEC 使能 (0:PEC 计算禁用, 1:PEC 计算使能) |
| 6 | GCEN | 广播呼叫使能 (0:从机不响应广播呼叫, 1:从机将响应广播呼叫) |
| 7 | SS | 在从机模式下数据未就绪是否将 SCL 拉低 (0:拉低 SCL, 1:不拉低 SCL) |
| 8 | START | I2C 总线上产生一个 START 起始位 (0:不发送 START, 1:发送 START) |
| 9 | STOP | I2C 总线上产生一个 STOP 结束位 (0:不发送 STOP, 1:发送 STOP) |
| 10 | ACKEN | ACK 使能 (0:不发送 ACK, 1:发送 ACK) |
| 11 | POAP | ACK/PEC 的位置含义 |
| 12 | PECTRANS | PEC 传输 (0:不传输 PEC 值, 1:传输的 PEC 值) |
| 13 | SALT | 通过 SMBA 发布警告 (0:不通过 SMBA 发布警告, 1:通过 SMBA 引脚发送警告) |
| 14 | 保留 | 必须保持复位值 |
| 15 | SRESET | 软件复位 I2C (0:I2C 未复位, 1:I2C 复位) |

控制寄存器 1 (I2C_CTL1)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —– | ———– | ———————————————————— |
| 0-6 | I2CCLK[6:0] | I2C 外设时钟频率 (I2CCLK[6:0]应该是输入 APB1 时钟频率,最低 2MHz。 0d – 1d:无时钟, 2d – 60d:2 MHz~60MHz, 61d – 127d:由于 APB1 时钟限制,无时钟。 注意:在标准模式下,APB1 时钟频率需大于或者等于 2MHz。在快速模式下,APB1 时钟频率需大于或者等于 8MHz。在快速+模式下,APB1 时钟频率需大于或者等于 24MHz.) |
| 7 | 保留 | 必须保持复位值 |
| 8 | ERRIE | 错误中断使能 (0:禁用错误中断, 1:使能错误中断,意味着当 BERR、LOSTARB、AERR、OUERR、PECERR、SMBTO 或 SMBALT 标志位生效时产生中断.) |
| 9 | EVIE | 事件中断使能 (0:禁用事件中断, 1:使能事件中断,意味着当 SBSEND、ADDSEND、ADD10SEND、STPDET 或 BTC 标志位有效或当 BUFIE=1 时 TBE=1 或 RBNE=1 时产生中断.) |
| 10 | BUFIE | 缓冲区中断使能 (0:禁用缓存区中断, 1:使能缓存区中断,如果 EVIE = 1,当 TBE = 1 或 RBNE = 1 时产生中断.) |
| 11 | DMAON | DMA 模式开关 (0:DMA 模式关, 1:DMA 模式开) |
| 12 | DMALST | DMA 最后传输标志位 (0:下一个 DMA EOT 不是最后传输, 1:下一个 DMA EOT 是最后传输) |
| 13-15 | 保留 | 必须保持复位值 |

从机地址寄存器 0 (I2C_SADDR0)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —– | ———— | ——————————————– |
| 0 | ADDRESS0 | 10 位地址的第 0 位 |
| 1-7 | ADDRESS[7:1] | 7 位地址或者 10 位地址的第 7-1 位 |
| 8-9 | ADDRESS[9:8] | 10 位地址的最高两位 |
| 10-14 | 保留 | 必须保持复位值 |
| 15 | ADDFORMAT | I2C 从机地址格式 (0:7 位地址, 1:10 位地址) |

从机地址寄存器 1 (I2C_SADDR1)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —- | ————- | ———————————————————– |
| 0 | DUADEN | 双重地址模式使能 (0:禁用双重地址模式, 1:使能双重地址模式) |
| 1-7 | ADDRESS2[7:1] | 从机在双重地址模式下第二个 I2C 地址 |
| 8-15 | 保留 | 必须保持复位值 |

传输缓冲区寄存器 (I2C_DATA)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —- | ———– | —————— |
| 0-7 | TRB[7:0] | 数据发送接收缓冲区 |
| 8-15 | 保留 | 必须保持复位值 |

传输状态寄存器 0 (I2C_STAT0)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —- | ———– | ———————————————————— |
| 0 | SBSEND | 主机模式下发送 START 起始位. 0:未发送 START 条件; 1:START 条件被发送 |
| 1 | ADDSEND | 主机模式下:成功发送了地址. 从机模式下:接收到了地址并且和自身的地址匹配. 0:无地址被发送或接收; 1:地址在主机模式下被发送或从机模式下接收到匹配地址 |
| 2 | BTC | 字节发送结束. 0:未发生 BTC; 1:发生了 BTC |
| 3 | ADD10SEND | 主机模式下 10 位地址地址头被发送. 0:主机模式下未发送 10 位地址的地址头; 1:主机模式下发送 10 位地址的地址头 |
| 4 | STPDET | 从机模式下监测到 STOP 结束位. 0:从机模式下未监测到 STOP 结束位; 1:从机模式下监测到 STOP 结束位 |
| 5 | 保留 | 必须保持复位值 |
| 6 | RBNE | 接收期间 I2C_DATA 非空. 0:I2C_DATA 为空; 1:I2C_DATA 非空,软件可以读 |
| 7 | TBE | 发送期间 I2C_DATA 为空. 0:I2C_DATA 非空; 1:I2C_DATA 空,软件可以写 |
| 8 | BERR | 总线错误,表示 I2C 总线上发生了预料之外的 START 起始位 t 或 STOP 结束位. 0:无总线错误; 1:发生了总线错误 |
| 9 | LOSTARB | 主机模式下仲裁丢失. 0:无仲裁丢失; 1:发生仲裁丢失,I2C 模块返回从机模式 |
| 10 | AERR | 应答错误. 0:未发生应答错误; 1:发生了应答错误 |
| 11 | OUERR | 当禁用 SCL 拉低功能后,在从机模式下发生了过载或欠载事件. 0:无溢出和欠载错误发生; 1:发生溢出或欠载错误 |
| 12 | 保留 | 必须保持复位值 |
| 13 | PECERR | 接收数据时 PEC 错误. 0:接收到 PEC 且校验正确; 1:接收到 PEC 但检验错误,此时 I2C 将无视 ACKEN 位直接发送 NACK |
| 14 | SMBTO | SMBus 模式下超时信号. 0:无超时错误; 1:超时事件发生(SCL 被拉低达 25ms) |
| 15 | SMBALT | SMBus 警报状态. 0:SMBA 引脚未被拉低(从机模式)或未监测到警报(主机模式); 1:SMBA 引脚被拉低(从机模式)或监测到警报(主机模式) |

传输状态寄存器 1 (I2C_STAT1)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —- | ———– | ———————————————————— |
| 0 | MASTER | 主机模式标志. 0:从机模式; 1:主机模式 |
| 1 | I2CBSY | 忙标志. 0:无 I2C 通讯; 1:I2C 正在通讯 |
| 2 | TR | 发送端或接收端标志. 0:接收端; 1:发送端 |
| 3 | 保留 | 必须保持复位值 |
| 4 | RXGC | 是否接收到广播地址(0x00). 0:未接收到广播呼叫地址(0x00); 1:接收到广播呼叫地址(0x00) |
| 5 | DEFSMB | SMBus 设备缺省地址. 0:SMBus 设备没有缺省地址; 1:SMBus 设备接收到一个缺省地址 |
| 6 | HSTSMB | 从机模式下监测到 SMBus 主机地址头. 0:未监测到 SMBus 主机地址头; 1:监测到 SMBus 主机地址头 |
| 7 | DUMODF | 从机模式下双标志位表明哪个地址和双地址模式匹配. 0:地址和 I2C_SADDR0 匹配; 1:地址和 I2C_SADDR1 匹配 |
| 8-15 | PECV[7:0] | 当 PEC 使能后硬件计算出的 PEC 值 |

时钟配置寄存器 (I2C_CKCFG)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —– | ———– | ——————————————————— |
| 0-11 | CLKC[11:0] | 主机模式下 I2C 时钟控制 |
| 12-13 | 保留 | 必须保持复位值 |
| 14 | DTCY | 快速模式下占空比. 0:Tlow/Thigh = 2; 1:Tlow/Thigh = 16/9 |
| 15 | FAST | 主机模式下 I2C 速度选择. 0:标准速度; 1:快速 |

上升时间寄存器 (I2C_RT)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —- | ————- | ———————————————————— |
| 0-6 | RISETIME[6:0] | 主机模式下最大上升时间,RISETIME 值应该为 SCL 最大上升时间加 1 |
| 7-15 | 保留 | 必须保持复位值 |

快速+ 模式配置寄存器 (I2C_FMPCFG)

复位值:0x0000

| 位号 | 位/位域名称 | 描述 |
| —- | ———– | —————————————————— |
| 0 | FMPEN | 快速+ 模式使能。当该位被置 1 时,I2C 设备支持高达 1MHz |
| 1-15 | 保留 | 必须保持复位值 |

MCU的I2C编程

MCU做主机

主机发送数据

  1. 发送起始信号:主设备首先会发送一个起始信号。在I2C中,起始信号是在时钟信号(SCL)为高电平时,数据信号(SDA)从高电平转为低电平。
  2. 发送地址:接着,主设备会发送目标设备的地址。在I2C中,设备地址通常是7位或10位。地址后面的一位是读/写位,用来指示这次传输是读操作还是写操作。如果是发送数据,那么读/写位应该设为0(写)。
  3. 等待应答信号:每发送一字节(8位)数据,主设备会释放数据线并生成一个时钟脉冲,等待从设备的应答信号。在I2C中,应答信号是在时钟信号(SCL)为高电平时,数据信号(SDA)为低电平。
  4. 发送数据:如果从设备响应了应答信号,那么主设备会继续发送数据。每发送一字节数据后,主设备会再次等待从设备的应答信号。
  5. 发送停止信号:当所有数据都发送完毕后,主设备会发送一个停止信号。在I2C中,停止信号是在时钟信号(SCL)为高电平时,数据信号(SDA)从低电平转为高电平。
// 1. 发送起始信号
I2C_GenerateSTART(I2C1, ENABLE);

// 2. 检查是否发送了起始信号
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

// 3. 发送设备地址和读写位
I2C_Send7bitAddress(I2C1, DeviceAddress, I2C_Direction_Transmitter);

// 4. 等待设备应答
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

// 5. 发送数据
I2C_SendData(I2C1, data);

// 6. 等待数据发送完毕
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

// 7. 发送停止信号
I2C_GenerateSTOP(I2C1, ENABLE);

主机接收从机返回的数据

  1. 发送起始信号:主设备首先会发送一个起始信号。在I2C中,起始信号是在时钟信号(SCL)为高电平时,数据信号(SDA)从高电平转为低电平。

  2. 发送地址:接着,主设备会发送目标设备的地址。在I2C中,设备地址通常是7位或10位。地址后面的一位是读/写位,用来指示这次传输是读操作还是写操作。如果是接收数据,那么读/写位应该设为1(读)。

  3. 等待应答信号:每发送一字节(8位)数据,主设备会释放数据线并生成一个时钟脉冲,等待从设备的应答信号。在I2C中,应答信号是在时钟信号(SCL)为高电平时,数据信号(SDA)为低电平。

  4. 接收数据:如果从设备响应了应答信号,那么主设备会继续接收数据。每接收一字节数据后,主设备需要回应一个应答信号,告诉从设备它已经成功接收了这个字节。

  5. 发送非应答信号:当主设备接收完最后一个字节时,它会回应一个非应答信号,告诉从设备它不再接收更多的数据。

  6. 发送停止信号:最后,主设备会发送一个停止信号。在I2C中,停止信号是在时钟信号(SCL)为高电平时,数据信号(SDA)从低电平转为高电平。

    // 1. 发送起始信号
    I2C_GenerateSTART(I2C1, ENABLE);
    
    // 2. 检查是否发送了起始信号
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
    
    // 3. 发送设备地址和读写位
    I2C_Send7bitAddress(I2C1, DeviceAddress, I2C_Direction_Receiver);
    
    // 4. 等待设备应答
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
    
    // 5. 接收数据
    while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    uint8_t data = I2C_ReceiveData(I2C1);
    
    // 6. 发送非应答信号
    I2C_AcknowledgeConfig(I2C1, DISABLE);
    
    // 7. 发送停止信号
    I2C_GenerateSTOP(I2C1, ENABLE);
    

MCU做从机

  1. 等待接收起始信号:从设备首先会等待主设备发送一个起始信号,同时,主设备也会发送出从设备的地址和读/写位。在I2C中,如果读/写位是1,那么主设备就是请求读取数据。
  2. 检查设备地址:从设备接收到起始信号后,会接收接下来的8位数据,并检查这8位数据是否包含了自己的设备地址和读位。
  3. 发送应答信号:如果地址匹配,从设备就会在下一个时钟周期发送一个应答信号,表示它已经准备好发送数据了。
  4. 发送数据:从设备接着会开始发送数据。在I2C中,数据是8位一组,按位发送。每发送完一组数据,从设备就会释放数据线,等待主设备的应答信号。
  5. 等待主设备的应答信号:如果主设备响应了应答信号,那么从设备会继续发送下一组数据,直到所有数据都发送完毕。
  6. 停止发送数据:当主设备发送一个非应答信号或者一个停止信号时,从设备会知道主设备已经接收完数据,不再需要更多的数据了,于是停止发送数据。

I2C通信的问题与解决方案

I2C开发过程中可能出现的问题

硬件方面
  1. 硬件连接问题:首先要保证电路板的I2C SDA和SCL线路连接正确,电源和地线无误,以及合适的上拉电阻。如果电路连接错误或者上拉电阻不适合,都可能导致I2C通信出现问题。
  2. 线路噪声或干扰:如果电路布局设计不合理,或者I2C总线线路过长,都可能产生信号干扰,导致I2C通信错误。这种情况下可以通过改进PCB设计,例如使用更短的线路、更大的地面铜皮、改善供电质量等来解决。
  3. 硬件故障:如果硬件出现故障,例如I2C设备的故障,也会导致通信出现问题。此时可能需要更换硬件或者寻求硬件供应商的帮助。
软件方面
  1. 配置错误:I2C通信中,频率、时钟延迟、I2C地址等参数都需要正确配置。如果配置不正确,可能导致通信失败。这种情况下需要检查相关的配置代码。
  2. 驱动错误:驱动程序错误也是常见的问题。可能是驱动的实现存在问题,例如开始和结束条件未正确处理,数据格式错误等。这时需要仔细检查驱动程序代码,如果使用的是开源或第三方驱动,也可能需要查看相关的文档和问题反馈。
  3. 软件流程错误:在进行I2C通信时,软件流程的控制非常重要。例如,数据发送和接收的顺序,是否在正确的时机发送开始和结束信号,数据接收后是否正确处理等。如果流程控制有误,可能会导致通信错误。

I2C问题分析方法

  1. 检查硬件连接:首先,检查硬件是否正确连接。使用示波器或逻辑分析仪检查SCL和SDA线是否有正确的电平变化。同时,也需要检查电源和地线是否正确连接,以及是否有合适的上拉电阻。
  2. 使用逻辑分析仪:使用逻辑分析仪或示波器来分析I2C总线上的信号。这种工具能够显示SCL和SDA线上的电平变化,并能够解码成I2C协议的数据。通过比较解码的数据和预期的数据,可以很容易地发现问题所在。
  3. 使用软件调试工具:许多微控制器提供了软件调试工具,可以用来查看和控制I2C接口的状态。或是使用MCU单步运行来定位问题。
  4. 检查软件:检查使用的驱动程序或库是否正确实现了I2C协议。检查驱动程序是否正确处理了开始和停止条件,以及数据和应答位的传输。检查驱动程序是否在传输数据前正确配置了I2C接口。
  5. 简化系统:如果可能,尝试简化系统。例如,如果总线上有多个I2C设备,尝试只使用一个设备。这样可以排除其他设备引起的干扰,使问题更容易识别。

I2C通信的应用案例

GD32 I2C主机发送和从机中断接收

一、I2C初始化

此程序采用I2C0作为主机,I2C1作为从机,因此需要初始化I2C0和I2C1

/***************************************************
 * 名称: i2c_init
 * 描述: i2c初始化函数
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_init(void)
{
    i2c_master_init();    // 主机初始化
    i2c_slave_init();     // 从机初始化
    i2c_nvic_init();      // i2c中断初始化
}
1.主机初始化
/***************************************************
 * 名称: i2c_master_init
 * 描述: 主机初始化
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_master_init(void)
{
    // 主机初始化
    rcu_periph_clock_enable(RCU_GPIOB);    // 使能GPIOB时钟
    rcu_periph_clock_enable(RCU_I2C0);     // 使能I2C0时钟

    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_6);    // 配置I2C0_SCL到PB6
    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_7);    // 配置I2C0_SDA到PB7

    // 配置I2C所用的IO口
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_6);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_7);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_7);

    i2c_clock_config(I2C0, 100000, I2C_DTCY_2);                                               // 配置I2C时钟
    i2c_mode_addr_config(I2C0, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, I2C0_OWN_ADDRESS);    // 配置I2C地址
    i2c_enable(I2C0);                                                                         // 使能I2C0
    i2c_ack_config(I2C0, I2C_ACK_ENABLE);
}
2.从机初始化
/***************************************************
 * 名称: i2c_slave_init
 * 描述: 从机初始化
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_slave_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOB);    // 使能GPIOB时钟
    rcu_periph_clock_enable(RCU_I2C1);     // 使能I2C0时钟

    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_10);    // 配置I2C1_SCL到PB10
    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_11);    // 配置I2C1_SDA到PB11

    // 配置I2C所用的IO口
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_10);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_11);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_11);

    i2c_clock_config(I2C1, 100000, I2C_DTCY_2);                                               // 配置I2C时钟
    i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, I2C0_OWN_ADDRESS);    // 配置I2C地址
    i2c_enable(I2C1);                                                                         // 使能I2C1
    i2c_ack_config(I2C1, I2C_ACK_ENABLE);
}
3.中断配置
/***************************************************
 * 名称: i2c_nvic_init
 * 描述: i2c中断初始化
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_nvic_init(void)
{
    // 中断优先级分组
    nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);
    // 配置中断优先级
    nvic_irq_enable(I2C0_EV_IRQn, 0, 3);
    nvic_irq_enable(I2C1_EV_IRQn, 0, 4);
    nvic_irq_enable(I2C0_ER_IRQn, 0, 2);
    nvic_irq_enable(I2C1_ER_IRQn, 0, 1);

    // 使能I2C0中断
    i2c_interrupt_enable(I2C0, I2C_INT_ERR);
    i2c_interrupt_enable(I2C0, I2C_INT_EV);
    i2c_interrupt_enable(I2C0, I2C_INT_BUF);

    // 使能I2C1中断
    i2c_interrupt_enable(I2C1, I2C_INT_ERR);
    i2c_interrupt_enable(I2C1, I2C_INT_EV);
    i2c_interrupt_enable(I2C1, I2C_INT_BUF);
}

二、中断服务函数

1.I2C0事件中断

由于I2C0作为主机的发送端,因此需要

  1. 在检测到起始位中断后发送从地址
  2. 检测到从机回复了地址检测后清除相关标志位
  3. 检测到发送数据寄存器为空后发送数据
/***************************************************
 * 名称: I2C0_EV_IRQHandler
 * 描述: I2C0 事件中断
 * 参数: void
 * 返回: void
 ***************************************************/
void I2C0_EV_IRQHandler(void)
{
    if (i2c_interrupt_flag_get(I2C0, I2C_INT_FLAG_SBSEND)) {
        i2c_master_addressing(I2C0, I2C1_SLAVE_ADDRESS, I2C_TRANSMITTER);    // 发送从机地址
    }
    else if (i2c_interrupt_flag_get(I2C0, I2C_INT_FLAG_ADDSEND)) {
        i2c_interrupt_flag_clear(I2C0, I2C_INT_FLAG_ADDSEND);    // 清ADDSEND标志位
    }
    else if (i2c_interrupt_flag_get(I2C0, I2C_INT_FLAG_TBE)) {
        if (I2C_nBytes > 0) {
            i2c_data_transmit(I2C0, *i2c_txbuffer++);    // 发送数据
            I2C_nBytes--;
        }
        else {
            i2c_stop_on_bus(I2C0);    // 发送停止位
            i2c_interrupt_disable(I2C0, I2C_INT_ERR);
            i2c_interrupt_disable(I2C0, I2C_INT_BUF);
            i2c_interrupt_disable(I2C0, I2C_INT_EV);
        }
    }
}
2.I2C1事件中断

I2C1作为从机,负责接收主机I2C0发送的数据,因此需要

  1. 检测到主机发送了地址后进行回复
  2. 检测到数据寄存器为非空时记录数据
  3. 检测到主机STOP信号后给出接收完毕的信号
/***************************************************
 * 名称: I2C1_EV_IRQHandler
 * 描述: I2C1 事件中断
 * 参数: void
 * 返回: void
 ***************************************************/
void I2C1_EV_IRQHandler(void)
{
    if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_ADDSEND)) {
        i2c_interrupt_flag_clear(I2C1, I2C_INT_FLAG_ADDSEND);    // 清除ADDSEND位
    }
    else if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_RBNE)) {
        *i2c_rxbuffer++ = i2c_data_receive(I2C1);
        // recv_buf[recv_buf_len++] = *(i2c_rxbuffer - 1);    // 记录接收数据
    }
    else if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_STPDET)) {
        i2c_recv_finish_flag = true;
        i2c_enable(I2C1);
        i2c_interrupt_disable(I2C1, I2C_INT_ERR);
        i2c_interrupt_disable(I2C1, I2C_INT_BUF);
        i2c_interrupt_disable(I2C1, I2C_INT_EV);
    }
}

三、测试函数

/***************************************************
 * 名称: i2c_test
 * 描述: i2c接收测试
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_test(void)
{
    uint8_t i = 0;
    for (i = 0; i < 16; i++) {
        i2c_transmitter[i] = test_num++; // 测试数据赋值
    }
    // 由于i2c_txbuffer和i2c_rxbuffer申请时为空指针,因此需要在这里指向地址
    i2c_txbuffer = i2c_transmitter;
    i2c_rxbuffer = i2c_buffer_receiver;
    I2C_nBytes = 16;
    // 中断使能
    i2c_interrupt_enable(I2C0, I2C_INT_ERR);
    i2c_interrupt_enable(I2C0, I2C_INT_EV);
    i2c_interrupt_enable(I2C0, I2C_INT_BUF);
    i2c_interrupt_enable(I2C1, I2C_INT_ERR);
    i2c_interrupt_enable(I2C1, I2C_INT_EV);
    i2c_interrupt_enable(I2C1, I2C_INT_BUF);
    // 主机等待空闲
    while (i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)) {};
    // 主机发送起始位
    i2c_start_on_bus(I2C0);

    while (I2C_nBytes > 0) {};
}

IIC DMA发送和接收并采用中断获取信息

一、使用背景

目前项目中采用的I2C发送和接收均采用软件发送和接收,为了优化效率,更新I2C驱动为DMA发送和接收,并提供相应的接口,采用I2C接收停止中断来通知数据处理程序进行处理,可采用标志位、信号量、任务通知的方式进行。

二、初始化

1.I2C初始化

此部分初始化与普通I2C初始化相同,初始化完成后I2C默认处于从机模式,产生I2C起始信号后变为主机模式。

#define I2C0_GPIO_PROT    RCU_GPIOB     // I2C0 映射的IO口号
#define I2C0_SCL_GPIO_PIN GPIO_PIN_6    // I2C0 SCL 映射的IO口
#define I2C0_SDA_GPIO_PIN GPIO_PIN_7    // I2C0 SDA 映射的IO口

#define I2C1_GPIO_PROT    RCU_GPIOB      // I2C1 映射的IO口号
#define I2C1_SCL_GPIO_PIN GPIO_PIN_10    // I2C1 SCL 映射的IO口
#define I2C1_SDA_GPIO_PIN GPIO_PIN_11    // I2C1 SDA 映射的IO口

/***************************************************
 * 名称: i2c_master_init
 * 描述: 主机初始化
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_master_init(void)
{
    // 主机初始化
    rcu_periph_clock_enable(RCU_GPIOB);    // 使能GPIOB时钟
    rcu_periph_clock_enable(RCU_I2C0);     // 使能I2C0时钟

    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_6);    // 配置I2C0_SCL到PB6
    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_7);    // 配置I2C0_SDA到PB7

    // 配置I2C所用的IO口
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_6);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_7);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_7);

    i2c_clock_config(I2C0, 100000, I2C_DTCY_2);    // 配置I2C时钟
    i2c_mode_addr_config(I2C0, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS,
                         I2C0_OWN_ADDRESS);    // 配置I2C地址
    i2c_enable(I2C0);                          // 使能I2C0
    i2c_ack_config(I2C0, I2C_ACK_ENABLE);
}

/***************************************************
 * 名称: i2c_slave_init
 * 描述: 从机初始化
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_slave_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOB);    // 使能GPIOB时钟
    rcu_periph_clock_enable(RCU_I2C1);     // 使能I2C0时钟

    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_10);    // 配置I2C1_SCL到PB10
    gpio_af_set(GPIOB, GPIO_AF_4, GPIO_PIN_11);    // 配置I2C1_SDA到PB11

    // 配置I2C所用的IO口
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_10);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_11);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_11);

    i2c_clock_config(I2C1, 100000, I2C_DTCY_2);                                               // 配置I2C时钟
    i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, I2C0_OWN_ADDRESS);    // 配置I2C地址
    i2c_enable(I2C1);                                                                         // 使能I2C1
    i2c_ack_config(I2C1, I2C_ACK_ENABLE);
}

其实此处的主机和从机并没有实际意义,优化后重新封装I2C初始化函数

/***************************************************
 * 名称: i2c_init
 * 描述: i2c初始化函数封装
 * 参数: port:I2C端口号
 *      speed:I2C速度
 *      addr:主从机地址
 * 返回: void
 ***************************************************/
void i2c_init(uint32_t port, uint32_t speed, uint8_t addr)
{
    switch (port) {
        case I2C0: {
            rcu_periph_clock_enable(I2C0_GPIO_PROT);    // 使能GPIO时钟
            rcu_periph_clock_enable(RCU_I2C0);          // 使能I2C0时钟

            gpio_af_set(I2C0_GPIO_PROT, GPIO_AF_4, I2C0_SCL_GPIO_PIN);    // 配置I2C0_SCL
            gpio_af_set(I2C0_GPIO_PROT, GPIO_AF_4, I2C0_SDA_GPIO_PIN);    // 配置I2C0_SDA

            // 配置I2C所用的IO口
            gpio_mode_set(I2C0_GPIO_PROT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, I2C0_SCL_GPIO_PIN);
            gpio_output_options_set(I2C0_GPIO_PROT, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, I2C0_SCL_GPIO_PIN);
            gpio_mode_set(I2C0_GPIO_PROT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, I2C0_SDA_GPIO_PIN);
            gpio_output_options_set(I2C0_GPIO_PROT, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, I2C0_SDA_GPIO_PIN);

            i2c_clock_config(I2C0, speed, I2C_DTCY_2);                                    // 配置I2C时钟
            i2c_mode_addr_config(I2C0, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, addr);    // 配置I2C地址
            i2c_enable(I2C0);                                                             // 使能I2C0
            i2c_ack_config(I2C0, I2C_ACK_ENABLE);

            break;
        }
        case I2C1: {
            rcu_periph_clock_enable(I2C1_GPIO_PROT);    // 使能GPIO时钟
            rcu_periph_clock_enable(RCU_I2C1);          // 使能I2C1时钟

            gpio_af_set(I2C1_GPIO_PROT, GPIO_AF_4, I2C1_SCL_GPIO_PIN);    // 配置I2C1_SCL
            gpio_af_set(I2C1_GPIO_PROT, GPIO_AF_4, I2C1_SDA_GPIO_PIN);    // 配置I2C1_SDA

            // 配置I2C所用的IO口
            gpio_mode_set(I2C1_GPIO_PROT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, I2C1_SCL_GPIO_PIN);
            gpio_output_options_set(I2C1_GPIO_PROT, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, I2C1_SCL_GPIO_PIN);
            gpio_mode_set(I2C1_GPIO_PROT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, I2C1_SDA_GPIO_PIN);
            gpio_output_options_set(I2C1_GPIO_PROT, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, I2C1_SDA_GPIO_PIN);

            i2c_clock_config(I2C1, speed, I2C_DTCY_2);                                    // 配置I2C时钟
            i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, addr);    // 配置I2C地址
            i2c_enable(I2C1);                                                             // 使能I2C0
            i2c_ack_config(I2C1, I2C_ACK_ENABLE);

            break;
        }
        default: break;
    }
}
2.中断初始化

此处我们使用中断主要用于获取是否I2C接收完毕,因此只需要初始化I2C接收事件中断

/***************************************************
 * 名称: i2c_nvic_init
 * 描述: i2c中断初始化
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_nvic_init(void)
{
    // 中断优先级分组
    nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);
    // 配置中断优先级
    nvic_irq_enable(I2C1_EV_IRQn, 0, 2);
    // 使能I2C1接收中断
    i2c_interrupt_enable(I2C1, I2C_INT_EV);
}
3.DMA初始化

DMA用于I2C的发送与接收,因此需要初始化DMA发送和接收的通道

#define I2C0_TX_PERIPH  DMA0            // I2C0 TX DMA0
#define I2C0_TX_CHANNEL DMA_CH6         // I2C0 TX CH6
#define I2C0_RX_PERIPH  DMA0            // I2C0 RX DMA0
#define I2C0_RX_CHANNEL DMA_CH5         // I2C0 RX CH5
#define I2C0_TX_SUBPERI DMA_SUBPERI1    // I2C0 TX 通道
#define I2C0_RX_SUBPERI DMA_SUBPERI1    // I2C0 RX 通道

#define I2C1_TX_PERIPH  DMA0            // I2C1 TX DMA0
#define I2C1_TX_CHANNEL DMA_CH7         // I2C1 TX CH7
#define I2C1_RX_PERIPH  DMA0            // I2C1 RX DMA0
#define I2C1_RX_CHANNEL DMA_CH2         // I2C1 RX CH2
#define I2C1_TX_SUBPERI DMA_SUBPERI7    // I2C1 TX 通道
#define I2C1_RX_SUBPERI DMA_SUBPERI7    // I2C1 RX 通道

#define I2C0_DATA_ADDRESS 0x40005410    // I2C0寄存器地址 0x40005410 I2C_DATA(I2C0)
#define I2C1_DATA_ADDRESS 0x40005810    // I2C1寄存器地址 0x40005810 I2C_DATA(I2C1)


/***************************************************
 * 名称: i2c_dma_init
 * 描述: i2c dma初始化
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_dma_init(uint32_t port)
{
    dma_single_data_parameter_struct dma_init_struct;    // 初始化DMA配置结构体
    rcu_periph_clock_enable(RCU_DMA0);                   // 使能DMA0时钟
    switch (port) {
        case I2C0: {
            // DMA发送配置
            dma_deinit(I2C0_TX_PERIPH, I2C0_TX_CHANNEL);                                     // 复位DMA寄存器
            dma_init_struct.direction = DMA_MEMORY_TO_PERIPH;                                // DMA方向配置
            dma_init_struct.memory0_addr = (uint32_t)i2c0_tx_buff;                           // 内存地址
            dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;                         // 内存自增配置
            dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;                     // 数据宽度
            dma_init_struct.number = I2C0_TX_BUFF_LEN;                                       // DMA数据长度
            dma_init_struct.periph_addr = I2C0_DATA_ADDRESS;                                 // 外设地址配置
            dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;                        // 外设地址自增配置
            dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;                              // DMA优先级
            dma_single_data_mode_init(I2C0_TX_PERIPH, I2C0_TX_CHANNEL, &dma_init_struct);    // 初始化

            dma_circulation_disable(I2C0_TX_PERIPH, I2C0_TX_CHANNEL);                              // 循环模式关闭
            dma_channel_subperipheral_select(I2C0_TX_PERIPH, I2C0_TX_CHANNEL, I2C0_TX_SUBPERI);    // 指定DMA外围设备
            // DMA接收配置
            dma_deinit(I2C0_RX_PERIPH, I2C0_RX_CHANNEL);                                     // 复位DMA寄存器
            dma_init_struct.direction = DMA_PERIPH_TO_MEMORY;                                // DMA方向配置
            dma_init_struct.memory0_addr = (uint32_t)i2c0_rx_buff;                           // 内存地址
            dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;                         // 内存自增配置
            dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;                     // 数据宽度
            dma_init_struct.number = I2C0_RX_BUFF_LEN;                                       // DMA数据长度
            dma_init_struct.periph_addr = I2C0_DATA_ADDRESS;                                 // 外设地址配置
            dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;                        // 外设地址自增配置
            dma_init_struct.priority = DMA_PRIORITY_HIGH;                                    // DMA优先级
            dma_single_data_mode_init(I2C0_RX_PERIPH, I2C0_RX_CHANNEL, &dma_init_struct);    // 初始化

            dma_circulation_disable(I2C0_RX_PERIPH, I2C0_RX_CHANNEL);                              // 循环模式关闭
            dma_channel_subperipheral_select(I2C0_RX_PERIPH, I2C0_RX_CHANNEL, I2C0_RX_SUBPERI);    // 指定DMA外围设备
            break;
        }
        case I2C1: {
            // DMA发送配置
            dma_deinit(I2C1_TX_PERIPH, I2C1_TX_CHANNEL);                    // 复位DMA寄存器
            dma_init_struct.direction = DMA_MEMORY_TO_PERIPH;               // DMA方向配置
            dma_init_struct.memory0_addr = (uint32_t)i2c1_tx_buff;          // 内存地址
            dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;        // 内存自增配置
            dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;    // 数据宽度
            dma_init_struct.number = I2C1_TX_BUFF_LEN;                      // DMA数据长度
            dma_init_struct.periph_addr = I2C1_DATA_ADDRESS;                // 外设地址配置
            dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;       // 外设地址自增配置
            dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;             // DMA优先级
            dma_single_data_mode_init(I2C1_TX_PERIPH, I2C1_TX_CHANNEL,
                                      &dma_init_struct);    // 初始化

            dma_circulation_disable(I2C1_TX_PERIPH,
                                    I2C1_TX_CHANNEL);    // 循环模式关闭
            dma_channel_subperipheral_select(I2C1_TX_PERIPH, I2C1_TX_CHANNEL,
                                             I2C1_TX_SUBPERI);    // 指定DMA外围设备
            // DMA接收配置
            dma_deinit(I2C1_RX_PERIPH, I2C1_RX_CHANNEL);                    // 复位DMA寄存器
            dma_init_struct.direction = DMA_PERIPH_TO_MEMORY;               // DMA方向配置
            dma_init_struct.memory0_addr = (uint32_t)i2c1_rx_buff;          // 内存地址
            dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;        // 内存自增配置
            dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;    // 数据宽度
            dma_init_struct.number = I2C1_RX_BUFF_LEN;                      // DMA数据长度
            dma_init_struct.periph_addr = I2C1_DATA_ADDRESS;                // 外设地址配置
            dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;       // 外设地址自增配置
            dma_init_struct.priority = DMA_PRIORITY_HIGH;                   // DMA优先级
            dma_single_data_mode_init(I2C1_RX_PERIPH, I2C1_RX_CHANNEL,
                                      &dma_init_struct);    // 初始化

            dma_circulation_disable(I2C1_RX_PERIPH,
                                    I2C1_RX_CHANNEL);    // 循环模式关闭
            dma_channel_subperipheral_select(I2C1_RX_PERIPH, I2C1_RX_CHANNEL,
                                             I2C1_RX_SUBPERI);    // 指定DMA外围设备
            break;
        }
        default: break;
    }
}

二、中断函数

1.采用标志位做任务通知
/***************************************************
 * 名称: I2C1_EV_IRQHandler
 * 描述: I2C1 事件中断
 * 参数: void
 * 返回: void
 ***************************************************/
void I2C1_EV_IRQHandler(void)
{
    if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_ADDSEND)) {
        i2c_interrupt_flag_clear(I2C1, I2C_INT_FLAG_ADDSEND);    // 清除ADDSEND位
    }
    else if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_RBNE)) {
    }
    else if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_STPDET)) {
        i2c_interrupt_flag_clear(I2C1, I2C_INT_FLAG_STPDET);    // 清STPDET位
        i2c_enable(I2C1);
        i2c_recv_finish_flag = true;    // 发送接收完成任务通知
    }
}

当其他任务中的i2c_recv_finish_flagtrue时,开始处理接收到的I2C数据。

2. 采用信号量做任务通知
i2c_recv_finish_binary_semaphore = xSemaphoreCreateBinary(); // 创建二值信号量

/***************************************************
 * 名称: I2C1_EV_IRQHandler
 * 描述: I2C1 事件中断
 * 参数: void
 * 返回: void
 ***************************************************/
void I2C1_EV_IRQHandler(void)
{
    if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_ADDSEND)) {
        i2c_interrupt_flag_clear(I2C1, I2C_INT_FLAG_ADDSEND);    // 清除ADDSEND位
    }
    else if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_RBNE)) {
    }
    else if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_STPDET)) {
        i2c_interrupt_flag_clear(I2C1, I2C_INT_FLAG_STPDET);    // 清STPDET位
        i2c_enable(I2C1);
        xSemaphoreGive(i2c_recv_finish_binary_semaphore);    // 发送接收完成任务通知
    }
}
3.采用任务通知
/***************************************************
 * 名称: I2C1_EV_IRQHandler
 * 描述: I2C1 事件中断
 * 参数: void
 * 返回: void
 ***************************************************/
void I2C1_EV_IRQHandler(void)
{
    if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_ADDSEND)) {
        i2c_interrupt_flag_clear(I2C1, I2C_INT_FLAG_ADDSEND);    // 清除ADDSEND位
    }
    else if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_RBNE)) {
    }
    else if (i2c_interrupt_flag_get(I2C1, I2C_INT_FLAG_STPDET)) {
        i2c_interrupt_flag_clear(I2C1, I2C_INT_FLAG_STPDET);    // 清STPDET位
        i2c_enable(I2C1);
        xTaskNotifyGive((TaskHandle_t)i2c_data_manage_task_handler);    // 发送接收完成任务通知
    }
}
4.几种通知方式的区别

标志位做判断需要一个任务一直轮询标志位状态,比较浪费时间,并且如果有多个任务访问或更改这个标志位,可能会产生异常。

任务通知相对于信号量存在速度更快,占用内存更小的优点,但是任务通知只能通知到单个任务,信号量则能够完成更多更复杂的通知。

三、DMA发送测试函数

/***************************************************
 * 名称: i2c_dma_send
 * 描述: I2C DMA发送数据
 * 参数: void
 * 返回: void
 ***************************************************/
void i2c_dma_send(void)
{
    while (i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)) {};                    // 等待I2C总线空闲
    i2c_start_on_bus(I2C0);                                            // 发送起始标志位
    while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)) {};                   // 等待SBSEND
    i2c_master_addressing(I2C0, I2C0_OWN_ADDRESS, I2C_TRANSMITTER);    // 发送从机地址
    while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) {};                  // 等待ADDSEND
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);                            // 清ADDSEND

    i2c_dma_enable(I2C1, I2C_DMA_ON);    // DMA使能
    i2c_dma_enable(I2C0, I2C_DMA_ON);    // DMA使能

    dma_channel_enable(I2C0_TX_PERIPH, I2C0_TX_CHANNEL);    // DMA通道使能
    dma_channel_enable(I2C1_RX_PERIPH, I2C1_RX_CHANNEL);    // DMA通道使能

    while (!dma_flag_get(I2C0_TX_PERIPH, I2C0_TX_CHANNEL, DMA_FLAG_FTF)) {};    // 等待DMA全满
    while (!dma_flag_get(I2C1_RX_PERIPH, I2C1_RX_CHANNEL, DMA_FLAG_FTF)) {};    // 等待DMA全满

    i2c_stop_on_bus(I2C0);    // 发送停止信号

    while (I2C_CTL0(I2C0) & 0x0200) {};    // 等待停止信号接收
}

DMA接收通道应保持一直使能,DMA发送通道在需要发送时使能

四、DMA通道查询

目前I2C0和I2C1均已通过用户手册查得各个参数的初始化值,即宏定义中表示的参数:

#define I2C0_TX_PERIPH  DMA0            // I2C0 TX DMA0
#define I2C0_TX_CHANNEL DMA_CH6         // I2C0 TX CH6
#define I2C0_RX_PERIPH  DMA0            // I2C0 RX DMA0
#define I2C0_RX_CHANNEL DMA_CH5         // I2C0 RX CH5
#define I2C0_TX_SUBPERI DMA_SUBPERI1    // I2C0 TX 通道
#define I2C0_RX_SUBPERI DMA_SUBPERI1    // I2C0 RX 通道

#define I2C1_TX_PERIPH  DMA0            // I2C1 TX DMA0
#define I2C1_TX_CHANNEL DMA_CH7         // I2C1 TX CH7
#define I2C1_RX_PERIPH  DMA0            // I2C1 RX DMA0
#define I2C1_RX_CHANNEL DMA_CH2         // I2C1 RX CH2
#define I2C1_TX_SUBPERI DMA_SUBPERI7    // I2C1 TX 通道
#define I2C1_RX_SUBPERI DMA_SUBPERI7    // I2C1 RX 通道

#define I2C0_DATA_ADDRESS 0x40005410    // I2C0寄存器地址 0x40005410 I2C_DATA(I2C0)
#define I2C1_DATA_ADDRESS 0x40005810    // I2C1寄存器地址 0x40005810 I2C_DATA(I2C1)

例如GD32FXX系列的DMA通道可通过下表查询

DMA0通道查询

软件I2C

#include <stdio.h>

#define SDA_PIN     // 定义你的SDA引脚
#define SCL_PIN     // 定义你的SCL引脚

// 用于设置SDA和SCL的函数
void set_sda_high(void);
void set_sda_low(void);
void set_scl_high(void);
void set_scl_low(void);

// 用于读取SDA状态的函数
int read_sda(void);

void i2c_init(void)
{
    // 初始化I2C的SDA和SCL为高电平
    set_sda_high();
    set_scl_high();
}

void i2c_start(void)
{
    set_sda_high();
    set_scl_high();
    __nop();
    set_sda_low();
    __nop();
    set_scl_low();
    __nop();
}

void i2c_stop(void)
{
    set_sda_low();
    __nop();
    set_scl_high();
    __nop();
    set_sda_high();
    __nop();
}

void i2c_send_bit(char bit)
{
    set_scl_low();
    __nop();
    if (bit) {
        set_sda_high();
    } else {
        set_sda_low();
    }
    __nop();
    set_scl_high();
    __nop();
}

char i2c_read_bit(void)
{
    char bit;
    set_scl_high();
    __nop();
    bit = read_sda();
    __nop();
    set_scl_low();
    __nop();
    return bit;
}

void i2c_send_byte(char byte)
{
    for(int i = 0; i < 8; i++) {
        i2c_send_bit(byte & 0x80);
        byte <<= 1;
    }
}

char i2c_read_byte(void)
{
    char byte = 0;
    for(int i = 0; i < 8; i++) {
        byte <<= 1;
        byte |= i2c_read_bit();
    }
    return byte;
}

char i2c_wait_ack(void)
{
    // 等待ACK
    set_scl_low();
    __nop();
    set_sda_high(); // 释放SDA线
    __nop();
    set_scl_high();
    __nop();
    char ack = read_sda(); // 读取ACK
    __nop();
    set_scl_low();
    __nop();
    return ack;
}

void i2c_write(char device_addr, char reg_addr, char data)
{
    i2c_start();
    i2c_send_byte(device_addr);
    if (i2c_wait_ack()) {
        printf("No ACK for device address\n");
        return;
    }
    i2c_send_byte(reg_addr);
    if (i2c_wait_ack()) {
        printf("No ACK for register address\n");
        return;
    }
    i2c_send_byte(data);
    if (i2c_wait_ack()) {
        printf("No ACK for data\n");
        return;
    }
    i2c_stop();
}

char i2c_read(char device_addr, char reg_addr)
{
    char data;
    i2c_start();
    i2c_send_byte(device_addr);
    if (i2c_wait_ack()) {
        printf("No ACK for device address\n");
        return 0;
    }
    i2c_send_byte(reg_addr);
    if (i2c_wait_ack()) {
        printf("No ACK for register address\n");
        return 0;
    }
    i2c_start();
    i2c_send_byte(device_addr | 0x01);
    if (i2c_wait_ack()) {
        printf("No ACK for device address with read bit\n");
        return 0;
    }
    data = i2c_read_byte();
    i2c_stop();
    return data;
}

Linux中的i2c驱动

假设我们使用一个ARM开发板,这个板上有一个I²C温度传感器。我们需要初始化I²C总线和这个传感器。

  1. 设备树描述:

    在设备树源文件 (.dts 文件) 中添加以下内容来描述I²C控制器和温度传感器。

    &i2c1 {  // 这个节点名应与您的具体平台匹配
       status = "okay";
       clock-frequency = <100000>; // 100kHz
    
       temperature_sensor: tmp102@48 {
           compatible = "ti,tmp102";
           reg = <0x48>;
       };
    };
    

    在这个例子中:

  2. &i2c1 引用了主设备树文件中定义的I²C控制器节点。

  3. ti,tmp102 是TMP102温度传感器的兼容性字符串。

  4. 0x48 是TMP102在I²C总线上的地址。

  5. 内核配置:

    使用menuconfig来确保内核配置了I²C和TMP102驱动。

    make menuconfig
    

    Device Drivers > I²C supportDevice Drivers > Hardware Monitoring support下,选择相应的选项以启用I²C和TMP102支持。

  6. 驱动程序:

    假设Linux内核已经包含了TMP102的驱动,这样当内核检测到上述的设备树节点时,它会自动加载并初始化TMP102驱动。

  7. 用户空间测试:

    一旦系统启动,您可以使用i2c-tools工具进行测试。

  8. 查看已经检测到的I²C设备:

     i2cdetect -y 1
    

    在这里,1 是I²C总线的编号。如果您看到地址0x48处有一个设备,那么这就是TMP102传感器。

  9. 读取TMP102的温度值 (如果有相应的用户空间支持):

     cat /sys/class/hwmon/hwmon0/temp1_input
    

STM32使用HAL库初始化I2C

  1. **配置STM32CubeMX :

  2. 打开STM32CubeMX。

  3. 选择您的STM32芯片型号或开发板。

  4. 在"Pinout & Configuration"选项卡下,点击你需要的I²C (如:I2C1)。

  5. 在弹出的下拉菜单中选择 I²C

  6. 在左侧的“Configuration”面板中,可以进一步配置I²C参数。

  7. 最后,生成代码。

  8. 初始化I²C:

    I2C_HandleTypeDef hi2c1;
    
    void MX_I2C1_Init(void)
    {
       hi2c1.Instance = I2C1;
       hi2c1.Init.ClockSpeed = 100000;  // 100 kHz
       hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
       hi2c1.Init.OwnAddress1 = 0;  // 如果为从设备,可以设置地址
       hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
       hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
       hi2c1.Init.OwnAddress2 = 0;
       hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
       hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    
       if (HAL_I2C_Init(&hi2c1) != HAL_OK)
       {
           // 初始化错误处理
           Error_Handler();
       }
    }
    
  9. 初始化GPIO:

    使用STM32CubeMX时,生成的代码中通常会自动包括GPIO的初始化。否则,需要手动配置用于I²C的SCL和SDA引脚为开漏输出,并连接上拉电阻。

  10. 开始使用I²C:

    一旦I²C初始化完成,您可以使用HAL_I2C_Master_Transmit(), HAL_I2C_Master_Receive(), HAL_I2C_Slave_Transmit(), HAL_I2C_Slave_Receive()等HAL库函数进行数据传输。

STM32使用标准库初始化I2C

#include "stm32fxxx.h"
#include "stm32fxxx_i2c.h"
#include "stm32fxxx_rcc.h"
#include "stm32fxxx_gpio.h"

GPIO_InitTypeDef  GPIO_InitStructure;

// 开启GPIO和I2C时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

// 初始化I2C的SCL和SDA引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;  // I2C1的SCL和SDA
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

I2C_InitTypeDef  I2C_InitStructure;

I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0x00;  // 只有在从模式下才会用到
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 100000;  // 100 kHz

I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);

有了上述初始化后,可以使用标准外设库提供的函数,如I2C_SendData(), I2C_ReceiveData(), I2C_StartCondition()等,进行I²C通信

瑞萨 Renesas

#include "platform.h"
#include "r_riic_rx_if.h"

riic_config_t i2c_config;

i2c_config.bps = RIIC_BPS_100K;

if (R_RIIC_Init(RIIC_CHANNEL_0, &i2c_config) != RIIC_SUCCESS)
{
    // 初始化失败处理
}

可以调用函数如 R_RIIC_MasterSend(), R_RIIC_MasterReceive(), R_RIIC_SlaveSend(), R_RIIC_SlaveReceive() 等来发送和接收数据