MCU专题精讲-I2C
layout: post
title: MCU专题精讲-I2C
description: MCU专题精讲-I2C
categories:
- MCU
tags: I2C
I2C通信基础知识
两线制:I2C使用两条线进行通信,一条是数据线(SDA),另一条是时钟线(SCL)。SDA线用于数据传输,SCL线用于同步。
设备角色:在I2C通信中,设备可以是主设备(Master)或从设备(Slave)。主设备是启动和结束数据传输的设备,从设备是响应主设备请求并发送或接收数据的设备。一条I2C总线上可以有多个主设备和多个从设备。
地址和数据传输:主设备通过在总线上发送从设备的地址来与之进行通信。在地址后面的数据可以由主设备发送到从设备,也可以由从设备发送到主设备。
多主设备:I2C支持多主设备模式。这意味着如果有多个主设备试图同时控制总线,就需要使用一种叫做仲裁的机制来决定哪个主设备可以控制总线。
I2C硬件特性:I2C 协议使用的是开漏输出,这意味着设备只能下拉总线电压(对应逻辑“0”),而不能上拉总线电压(对应逻辑“1”)。总线电压的上拉是通过一个外部的上拉电阻完成的。当所有设备都不再下拉总线电压时,总线电压将通过上拉电阻恢复到高电平。这就是为什么 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
模块框图
寄存器详解
控制寄存器 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做主机
主机发送数据
- 发送起始信号:主设备首先会发送一个起始信号。在I2C中,起始信号是在时钟信号(SCL)为高电平时,数据信号(SDA)从高电平转为低电平。
- 发送地址:接着,主设备会发送目标设备的地址。在I2C中,设备地址通常是7位或10位。地址后面的一位是读/写位,用来指示这次传输是读操作还是写操作。如果是发送数据,那么读/写位应该设为0(写)。
- 等待应答信号:每发送一字节(8位)数据,主设备会释放数据线并生成一个时钟脉冲,等待从设备的应答信号。在I2C中,应答信号是在时钟信号(SCL)为高电平时,数据信号(SDA)为低电平。
- 发送数据:如果从设备响应了应答信号,那么主设备会继续发送数据。每发送一字节数据后,主设备会再次等待从设备的应答信号。
- 发送停止信号:当所有数据都发送完毕后,主设备会发送一个停止信号。在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);
主机接收从机返回的数据
发送起始信号:主设备首先会发送一个起始信号。在I2C中,起始信号是在时钟信号(SCL)为高电平时,数据信号(SDA)从高电平转为低电平。
发送地址:接着,主设备会发送目标设备的地址。在I2C中,设备地址通常是7位或10位。地址后面的一位是读/写位,用来指示这次传输是读操作还是写操作。如果是接收数据,那么读/写位应该设为1(读)。
等待应答信号:每发送一字节(8位)数据,主设备会释放数据线并生成一个时钟脉冲,等待从设备的应答信号。在I2C中,应答信号是在时钟信号(SCL)为高电平时,数据信号(SDA)为低电平。
接收数据:如果从设备响应了应答信号,那么主设备会继续接收数据。每接收一字节数据后,主设备需要回应一个应答信号,告诉从设备它已经成功接收了这个字节。
发送非应答信号:当主设备接收完最后一个字节时,它会回应一个非应答信号,告诉从设备它不再接收更多的数据。
发送停止信号:最后,主设备会发送一个停止信号。在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做从机
- 等待接收起始信号:从设备首先会等待主设备发送一个起始信号,同时,主设备也会发送出从设备的地址和读/写位。在I2C中,如果读/写位是1,那么主设备就是请求读取数据。
- 检查设备地址:从设备接收到起始信号后,会接收接下来的8位数据,并检查这8位数据是否包含了自己的设备地址和读位。
- 发送应答信号:如果地址匹配,从设备就会在下一个时钟周期发送一个应答信号,表示它已经准备好发送数据了。
- 发送数据:从设备接着会开始发送数据。在I2C中,数据是8位一组,按位发送。每发送完一组数据,从设备就会释放数据线,等待主设备的应答信号。
- 等待主设备的应答信号:如果主设备响应了应答信号,那么从设备会继续发送下一组数据,直到所有数据都发送完毕。
- 停止发送数据:当主设备发送一个非应答信号或者一个停止信号时,从设备会知道主设备已经接收完数据,不再需要更多的数据了,于是停止发送数据。
I2C通信的问题与解决方案
I2C开发过程中可能出现的问题
硬件方面
- 硬件连接问题:首先要保证电路板的I2C SDA和SCL线路连接正确,电源和地线无误,以及合适的上拉电阻。如果电路连接错误或者上拉电阻不适合,都可能导致I2C通信出现问题。
- 线路噪声或干扰:如果电路布局设计不合理,或者I2C总线线路过长,都可能产生信号干扰,导致I2C通信错误。这种情况下可以通过改进PCB设计,例如使用更短的线路、更大的地面铜皮、改善供电质量等来解决。
- 硬件故障:如果硬件出现故障,例如I2C设备的故障,也会导致通信出现问题。此时可能需要更换硬件或者寻求硬件供应商的帮助。
软件方面
- 配置错误:I2C通信中,频率、时钟延迟、I2C地址等参数都需要正确配置。如果配置不正确,可能导致通信失败。这种情况下需要检查相关的配置代码。
- 驱动错误:驱动程序错误也是常见的问题。可能是驱动的实现存在问题,例如开始和结束条件未正确处理,数据格式错误等。这时需要仔细检查驱动程序代码,如果使用的是开源或第三方驱动,也可能需要查看相关的文档和问题反馈。
- 软件流程错误:在进行I2C通信时,软件流程的控制非常重要。例如,数据发送和接收的顺序,是否在正确的时机发送开始和结束信号,数据接收后是否正确处理等。如果流程控制有误,可能会导致通信错误。
I2C问题分析方法
- 检查硬件连接:首先,检查硬件是否正确连接。使用示波器或逻辑分析仪检查SCL和SDA线是否有正确的电平变化。同时,也需要检查电源和地线是否正确连接,以及是否有合适的上拉电阻。
- 使用逻辑分析仪:使用逻辑分析仪或示波器来分析I2C总线上的信号。这种工具能够显示SCL和SDA线上的电平变化,并能够解码成I2C协议的数据。通过比较解码的数据和预期的数据,可以很容易地发现问题所在。
- 使用软件调试工具:许多微控制器提供了软件调试工具,可以用来查看和控制I2C接口的状态。或是使用MCU单步运行来定位问题。
- 检查软件:检查使用的驱动程序或库是否正确实现了I2C协议。检查驱动程序是否正确处理了开始和停止条件,以及数据和应答位的传输。检查驱动程序是否在传输数据前正确配置了I2C接口。
- 简化系统:如果可能,尝试简化系统。例如,如果总线上有多个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作为主机的发送端,因此需要
- 在检测到起始位中断后发送从地址
- 检测到从机回复了地址检测后清除相关标志位
- 检测到发送数据寄存器为空后发送数据
/***************************************************
* 名称: 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发送的数据,因此需要
- 检测到主机发送了地址后进行回复
- 检测到数据寄存器为非空时记录数据
- 检测到主机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_flag
为true
时,开始处理接收到的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通道可通过下表查询
软件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总线和这个传感器。
设备树描述:
在设备树源文件 (
.dts
文件) 中添加以下内容来描述I²C控制器和温度传感器。&i2c1 { // 这个节点名应与您的具体平台匹配 status = "okay"; clock-frequency = <100000>; // 100kHz temperature_sensor: tmp102@48 { compatible = "ti,tmp102"; reg = <0x48>; }; };
在这个例子中:
&i2c1
引用了主设备树文件中定义的I²C控制器节点。ti,tmp102
是TMP102温度传感器的兼容性字符串。0x48
是TMP102在I²C总线上的地址。内核配置:
使用
menuconfig
来确保内核配置了I²C和TMP102驱动。make menuconfig
在
Device Drivers > I²C support
和Device Drivers > Hardware Monitoring support
下,选择相应的选项以启用I²C和TMP102支持。驱动程序:
假设Linux内核已经包含了TMP102的驱动,这样当内核检测到上述的设备树节点时,它会自动加载并初始化TMP102驱动。
用户空间测试:
一旦系统启动,您可以使用
i2c-tools
工具进行测试。查看已经检测到的I²C设备:
i2cdetect -y 1
在这里,
1
是I²C总线的编号。如果您看到地址0x48
处有一个设备,那么这就是TMP102传感器。读取TMP102的温度值 (如果有相应的用户空间支持):
cat /sys/class/hwmon/hwmon0/temp1_input
STM32使用HAL库初始化I2C
**配置STM32CubeMX :
打开STM32CubeMX。
选择您的STM32芯片型号或开发板。
在"Pinout & Configuration"选项卡下,点击你需要的I²C (如:I2C1)。
在弹出的下拉菜单中选择
I²C
。在左侧的“Configuration”面板中,可以进一步配置I²C参数。
最后,生成代码。
初始化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(); } }
初始化GPIO:
使用STM32CubeMX时,生成的代码中通常会自动包括GPIO的初始化。否则,需要手动配置用于I²C的SCL和SDA引脚为开漏输出,并连接上拉电阻。
开始使用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()
等来发送和接收数据