STM32外设地图-I2C
一、I2C(Inter-Integrated Circuit)
Master 轮询/中断发送
软件置位START标志,然后等待 SB标志置位,等到后,软件把从机地址写入 I2C_DR,然后等待 ADDR标志置位,等到后,软件清零ADDR标志并把Data1写入 I2C_DR,后面每次等TXE标志置位时 软件写入1字节数据到 I2C_DR,当写完最后一字节后,软件等待 BTF标志置位,等到后,软件置位STOP标志,然后等待 STOP标志清零,等STOP标志清零后,发送结束。
Master DMA发送
软件配置 I2C TX DMA channel 并使能 I2C的 Tx DMA request。
开始发送:软件置位START标志,然后等待 SB标志置位,等到后,软件把从机地址写入 I2C_DR,然后等待 ADDR标志置位,等到后,软件清零ADDR标志(I2C开始发送 Tx DMA request)。
结束发送:I2C TX DMA channel把最后1字节数据写入 I2C_DR后,触发 I2C TX DMA channel中断,在中断函数中,软件禁能I2C的 Tx DMA request,然后使能 I2C的 BTF中断,在BTF置位触发的I2C中断函数中,软件禁能BTF中断,并置位STOP标志,然后等待 STOP标志清零,等STOP标志清零后,发送结束。
Master 中断接收(结束接收 Method1,I2C中断优先级配置为 最高)
软件置位START标志,然后等待 SB标志置位,等到后,软件把从机地址写入 I2C_DR,然后等待 ADDR标志置位,等到后,软件清零ADDR标志(若只接收1Byte数据,则此时软件 清零ACK标志 并置位STOP标志),后面每次等RXNE标志置位时 软件从I2C_DR 读1字节数据,当读完倒数第二字节时,软件清零ACK标志 并置位STOP标志,当读完最后一字节时,软件等待 STOP标志清零,等STOP标志清零后,接收结束。
Master 轮询/中断接收(结束接收 Method2,I2C中断优先级 非最高)
接收字节长度 > 2时:软件置位START标志,然后等待 SB标志置位,等到后,软件把从机地址写入 I2C_DR,然后等待 ADDR标志置位,等到后,软件清零ADDR标志,后面每次等BTF标志置位时 软件从I2C_DR 读1字节数据,当软件正要读 DataN-2时,需严格按以下步骤来结束接收:清零ACK标志,关中断,读 I2C_DR(DataN-2),置位STOP标志,读I2C_DR(DataN-1),开中断;软件读完DataN-1后,等RXNE标志置位时读最后一字节DataN,然后等待STOP标志清零,等STOP标志清零后,接收结束。
接收字节长度 == 2时:软件置位START标志,然后等待 SB标志置位,等到后,软件把从机地址写入 I2C_DR,然后等待 ADDR标志置位,等到后,软件置位POS标志,接着关中断,清零ADDR标志,清零ACK标志,然后开中断。后面等BTF标志置位时,软件关中断,置位STOP标志,读 I2C_DR(Data1),开中断,然后再读 I2C_DR(Data2),最后等待 STOP标志清零,等STOP标志清零后,接收结束。
接收字节长度 == 1时:软件置位START标志,然后等待 SB标志置位,等到后,软件把从机地址写入 I2C_DR,然后等待 ADDR标志置位,等到后,软件清零ACK标志,接着关中断,清零ADDR标志,置位STOP标志,然后开中断。后面等RXNE标志置位时,软件读 I2C_DR(Data1),然后等待STOP标志清零,等STOP标志清零后,接收结束。
Master DMA接收
软件配置 I2C RX DMA channel,使能 I2C的 Rx DMA request 并 置位LAST标志(I2C正在接收DataN时,若收到了I2C RX DMA channel的 EOT_1 信号 且此时LAST = 1,则I2C在接收完DataN时 会发送NACK)。
开始接收:软件置位START标志,然后等待 SB标志置位,等到后,软件把从机地址写入 I2C_DR,然后等待 ADDR标志置位,等到后,软件清零ADDR标志(I2C开始接收数据,每收到1Byte即发送 Rx DMA request)。
结束接收:
①、I2C RX DMA channel 从I2C_DR 读完倒数第二字节时(即读完DataN-1时),发送EOT_1信号给I2C;I2C此时正在接收DataN,当收到EOT_1信号 且 LAST == 1,I2C会在接收完DataN时发送NACK。
②、I2C RX DMA channel 从I2C_DR 读完最后一字节时(即读完DataN时),置位TC标志并触发中断,在中断函数中,软件置位STOP标志,然后等待STOP标志清零,等STOP标志清零后,接收结束。
二、程序开发流程
以I2C1为例:Master模式,DMA发送 & DMA接收(从机地址为 7bit)
#define TRANSFER_OK 0
#define TRANSFER_UNKNOWN 1
#define TRANSFER_ERR 2
static int g_transmit_result;
static int g_receive_result;
/* 函数功能:用i2c1发送一帧数据. 此接口供外部模块调用
返回值:TRUE, 发送成功; FALSE, 发送失败
*/
bool i2c1_transmit(uint8_t addr, uint8_t *send_buf, uint16_t length, uint16_t timeout)
{
ASSERT(send_buf != NULL && length > 0);
uint32_t tick_start = rt_tick_get();
while((BUSY标志 == 1) && (!IS_TIME_OUT(tick_start, timeout)));
if(BUSY标志 == 1)
return FALSE; //超时时间内未等到总线空闲, 发送失败
配置I2C TX DMA channel:
单次模式,传输方向为 memory --> peripheral,搬运次数为 length;
peripheral:起始地址为寄存器I2C_DR地址,地址不增加,单数据长度为 1字节;
memory:起始地址为 &send_buf[0],地址增加,单数据长度为 1字节;
使能I2C TX DMA channel的TC中断;
使能I2C TX DMA channel;
最后使能I2C的Tx DMA Request(TXE = 1时 I2C发送Tx DMA Request);
使能I2C的error中断;
置位START标志; //软件触发发送START信号
while((SB标志 == 0) && (!IS_TIME_OUT(tick_start, timeout)));
if(SB标志 == 0)
return FALSE; //超时时间内未发完START信号, 发送失败
i2c1->DR = addr << 1; //软件把地址写入 I2C_DR, I2C开始发送地址
while((ADDR标志 == 0) && (!IS_TIME_OUT(tick_start, timeout)));
if(ADDR标志 == 0)
return FALSE; //超时时间内地址没发送成功, 发送失败
g_transmit_result = TRANSFER_UNKNOWN;
清零ADDR标志; //I2C开始发送Tx DMA request
while((g_transmit_result == TRANSFER_UNKNOWN) && (!IS_TIME_OUT(tick_start, timeout))); //超时等待发送结果
return (g_transmit_result == TRANSFER_OK);
}
/* 函数功能:用i2c1接收一帧数据. 此接口供外部模块调用
返回值:TRUE, 接收成功; FALSE, 接收失败
*/
bool i2c1_receive(uint8_t addr, uint8_t *rcv_buf, uint16_t length, uint16_t timeout)
{
ASSERT(rcv_buf != NULL && length > 0);
uint32_t tick_start = rt_tick_get();
while((BUSY标志 == 1) && (!IS_TIME_OUT(tick_start, timeout)));
if(BUSY标志 == 1)
return FALSE; //超时时间内未等到总线空闲, 接收失败
/* 接收字节长度为1Byte时, 使用轮询方式接收 */
if(length == 1)
{
置位START标志; //软件触发发送START信号
while((SB标志 == 0) && (!IS_TIME_OUT(tick_start, timeout)));
if(SB标志 == 0)
return FALSE; //超时时间内未发完START信号, 发送失败
i2c1->DR = (addr << 1) | 1; //软件把地址写入 I2C_DR, I2C开始发送地址
while((ADDR标志 == 0) && (!IS_TIME_OUT(tick_start, timeout)));
if(ADDR标志 == 0)
return FALSE; //超时时间内地址没发送成功, 发送失败
清零ACK标志; //收到Data1时 发送NACK
interrupt_disable();
清零ADDR标志; //I2C开始接收Data1
置位STOP标志; //I2C收到Data1后发送STOP信号
interrupt_enable();
while((RXNE标志 == 0) && (!IS_TIME_OUT(tick_start, timeout)));
if(RXNE标志 == 0)
return FALSE; //超时时间内没收到Data1, 接收失败
*rcv_buf = i2c1->DR;
return TRUE; //接收Data1 成功
}
/* 接收字节长度 >= 2时, 使用DMA方式接收 */
配置I2C RX DMA channel:
单次模式,传输方向为 peripheral --> memory,搬运次数为 length;
peripheral:起始地址为寄存器I2C_DR地址,地址不增加,单数据长度为 1字节;
memory:起始地址为 &rcv_buf[0],地址增加,单数据长度为 1字节;
使能I2C RX DMA channel的TC中断;
使能I2C RX DMA channel;
最后使能I2C的Rx DMA Request(RXNE = 1时 I2C发送Rx DMA Request);
置位LAST标志;
使能I2C的error中断;
置位START标志; //软件触发发送START信号
while((SB标志 == 0) && (!IS_TIME_OUT(tick_start, timeout)));
if(SB标志 == 0)
return FALSE; //超时时间内未发完START信号, 发送失败
i2c1->DR = (addr << 1) | 1; //软件把地址写入 I2C_DR, I2C开始发送地址
while((ADDR标志 == 0) && (!IS_TIME_OUT(tick_start, timeout)));
if(ADDR标志 == 0)
return FALSE; //超时时间内地址没发送成功, 发送失败
g_receive_result = TRANSFER_UNKNOWN;
清零ADDR标志; //I2C开始接收数据, 当收到数据时发送Rx DMA request
while((g_receive_result == TRANSFER_UNKNOWN) && (!IS_TIME_OUT(tick_start, timeout))); //超时等待接收结果
return (g_receive_result == TRANSFER_OK);
}
void i2c1_event_irq_handler(void) //i2c1 event interrupt
{
if(BTF == 1 且BTF中断已使能) //最后一字节数据已发成功发完
{
清零BTF标志, 禁能I2C的BTF中断;
禁能I2C的error中断;
置位STOP标志;
g_transmit_result = TRANSFER_OK;
}
}
void i2c1_error_irq_handler(void) //i2c1 error interrupt
{
if(BERR == 1 || ARLO == 1 || AF == 1)
{
清零BERR、ARLO、AF标志;
禁能I2C的error中断;
清零BTF标志, 禁能I2C的BTF中断;
if(I2C的Rx DMA Request已使能)
{
禁能I2C的Rx DMA Request;
禁能I2C RX DMA channel的TC中断;
禁能I2C RX DMA channel;
g_receive_result = TRANSFER_ERR;
}
if(I2C的Tx DMA Request已使能)
{
禁能I2C的Tx DMA Request;
禁能I2C TX DMA channel的TC中断;
禁能I2C TX DMA channel;
g_transmit_result = TRANSFER_ERR;
}
if(AF == 1)
置位STOP标志;
}
}
void i2c1_tx_dma_channel_irq_handler(void) //i2c1 tx dma channel interrupt
{
if(TC标志 == 1 且TC中断已使能) //最后一字节数据已写入I2C_DR, 等待数据发送完成
{
清零TC标志, 禁能TC中断;
禁能I2C的Tx DMA Request;
禁能I2C TX DMA channel;
使能I2C的BTF中断;
}
}
void i2c1_rx_dma_channel_irq_handler(void) //i2c1 rx dma channel interrupt
{
if(TC标志 == 1 且TC中断已使能) //最后一字节已读到内存buff, 数据接收完成
{
清零TC标志, 禁能TC中断;
禁能I2C的Rx DMA Request;
禁能I2C RX DMA channel;
禁能I2C的error中断;
置位STOP标志;
g_receive_result = TRANSFER_OK;
}
}
void i2c1_init(void)
{
使能DMA1时钟;
使能i2c1时钟;
配置i2c1引脚; //SCL, SDA都配置为复用开漏输出
FREQ[5:0] = 36; //Fp1clk = 36Mhz
F/S bit = 0; //Standard mode
DUTY = 0; //Fast mode时, Tlow / Thigh = 2
CCR[11:0] = 0xB4; // I2C速率 100Khz
TRISE[5:0] = 37; // 1000ns / ((1 / 36) * 1000ns) = 36 + 1
软件置位SWRST标志;
软件清零SWRST标志; //软件复位I2C
PE = 1; //使能I2C
在NVIC中, 设置I2C1 event中断优先级并使能, 设置I2C1 error中断优先级并使能;
在NVIC中, 设置I2C TX DMA channel中断优先级并使能, 设置I2C RX DMA channel中断优先级并使能;
}
三、注意事项
1、在硬件方面排查bug时,一是确认 SCL、SDA能否正常输出高低电平,二是换个小一点的上拉电阻,因为I2C上拉电阻越大,SCL/SDA上升沿周期就越久,出错概率越大。
2、从机持续拉低SCL的异常情况分析
I2C协议允许从机Clock stretching(即拉低SCL)来通知主机降低通信速率,但如果从机的软件出了BUG(比如当从机接收到数据并保存至接收缓存后,软件一直不去读接收缓存;或当从机需要发送数据时,软件一直不往发送缓存中写入数据),导致从机一直拉低SCL,进而导致主机一直无法启动传输。
应对方法:在设计电路时,让主机能够通过硬线信号直接复位从机,或者让主机能够控制从机的电源。当主机发现SCL一直被从机拉低时,主机通过硬线复位从机,或通过控制从机的电源来复位从机。
3、从机持续拉低SDA的异常情况分析
Master 在通信过程中,突然复位(此时Slave发送了ACK信号或数据0 到SDA),导致 Slave持续拉低SDA,Master 复位后无法再次启动通信。
PS:Slave 在发送数据1 到SDA时,Master突然复位,当Master复位后能正常启动通信。原因是:Master复位后检测到总线空闲,发送START信号,Slave收到START信号后,认为这是一个Restart信号,主动释放SDA。
Ⅰ、Master 读 Slave
①、Slave收到地址后,发送了ACK到SDA时,Master复位
A、Slave 收到地址后,(在SCL低电平时)Slave 发送了ACK信号(低电平)到SDA(此时 Master 复位,SCL被上拉电阻持续拉高,Slave 认为此时 Master 正在读 ACK信号),然后等待SCL再次变低(等SCL变低时 Slave 发送下一数据的bit7 到SDA)(I2C协议规定:在SCL低电平期间,可以改变SDA的值;在SCL高电平期间,禁止改变SDA的值)
—-由于Master 复位(SCL被上拉电阻持续拉高),SDA被 Slave 持续拉低(Slave 持续等待 Master拉低SCL)
B、Master 复位后,发现SDA为低(在Master 复位前,Slave发送了ACK信号到SDA,导致SDA为低),认为此时总线被其他主机占用,因此 Master 等待SDA变高(I2C协议规定 Master 要在总线空闲时才能启动传输)
—-Master复位后,持续等待 Slave释放SDA(等Slave 释放SDA后,Master才会控制SCL引脚输出时钟信号)
②、Slave在发送了数据的bit7(bit7的值恰好是 0)到SDA时,Master复位
A、(在SCL低电平时)Slave 发送了数据的bit7(bit7的值恰好是 0)到SDA(此时 Master 复位,SCL被上拉电阻持续拉高,Slave 认为此时 Master 正在读 bit7),然后等待SCL再次变低(等SCL变低时 Slave 发送数据的bit6 到SDA)(I2C协议规定:在SCL低电平期间,可以改变SDA的值;在SCL高电平期间,禁止改变SDA的值)
—-由于Master 复位(SCL被上拉电阻持续拉高),SDA被 Slave 持续拉低(Slave 持续等待 Master拉低SCL)
B、Master 复位后,发现SDA为低(在Master 复位前,Slave发送了数据的bit7到SDA,且bit7的值为0),认为此时总线被其他主机占用,因此 Master 等待SDA变高(I2C协议规定 Master 要在总线空闲时才能启动传输)
—-Master复位后,持续等待 Slave释放SDA(等Slave 释放SDA后,Master才会控制SCL引脚输出时钟信号)
Ⅱ、Master 写 Slave
①、Slave 收到 地址/数据 后,发送了ACK到SDA时,Master复位
A、Slave 收到 地址/数据 后,(在SCL低电平时)Slave 发送了ACK信号(低电平)到SDA(此时 Master 复位,SCL被上拉电阻持续拉高,Slave 认为此时 Master 正在读 ACK信号),然后等待SCL再次变低(等SCL变低时 Slave 释放SDA)(I2C协议规定:在SCL低电平期间,可以改变SDA的值;在SCL高电平期间,禁止改变SDA的值)
—-由于Master 复位(SCL被上拉电阻持续拉高),SDA被 Slave 持续拉低(Slave 持续等待 Master拉低SCL)
B、Master 复位后,发现SDA为低(在Master 复位前,Slave发送了ACK信号到SDA,导致SDA为低),认为此时总线被其他主机占用,因此 Master 等待SDA变高(I2C协议规定 Master 要在总线空闲时才能启动传输)
—-Master复位后,持续等待 Slave释放SDA(等Slave 释放SDA后,Master才会控制SCL引脚输出时钟信号)
4、当EV7, EV7_1, EV6_1, EV6_3, EV2, EV8, and EV3 events 没有在当前字节传输结束前处理完时,I2C传输可能出现数据错误:比如收到一个多余的数据,同一数据读了两次,数据丢失。
为了避免数据错误,有四种解决思路:
①、使用DMA传输(Master只接收一个字节的情况除外);
②、使用 I2C中断 传输,并在NVIC中把 I2C中断的优先级设置为最高;
③、软件先关全局中断,然后处理事件,处理完后再开全局中断;
④、软件先接管SCL控制权并Clock stretching(配置SCL为开漏输出,然后配置GPIO输出寄存器控制SCL输出低电平,I2C外设检测到SCL低电平时不会传输数据,从机检测到SCL低电平时也不会传输数据),然后处理事件,处理完后再把SCL控制权还给I2C(配置SCL为复用推挽输出)。
5、软件置位START标志后,I2C不发送START信号的情况:
Ⅰ、I2C检测到 misplaced STOP
A、I2C在总线上检测到START信号后,紧接着START马上又检测到STOP信号(BERR标志不置位)(此后I2C将不会再发送START信号)
B、I2C在总线上检测到 misplaced STOP(在(8bit + ACK)期间检测到STOP信号,BERR标志自动置位)时,若START标志置位,则I2C不发送START信号。
软件应对办法:软件置位START标志后,超时检测SB标志,若超时时间内SB标志没有置位,则软件通过置位SWRST标志复位I2C。当BERR置位时,若START标志也置位,则软件可置位SWRST标志复位I2C。
Ⅱ、I2C为Slave mode,且BUSY标志置位(I2C内部的 SCL或SDA 信号为低电平)
原因分析及应对方法:
①、总线被其他主机占用,此时需等待其他主机释放总线
②、总线异常,比如接触不良、总线短路、上拉电阻异常等等,此时需人为介入恢复总线
③、上一次通信时I2C异常复位,导致从机持续拉低SDA,此时软件可控制SCL输出10个SCL以便让从机释放SDA,或通过控制复位引脚或电源 直接复位从机。
④、I2C外设自己的问题(MCU上电复位后 或 有静电时,I2C analog filter 可能锁定内部SCL、SDA为低电平,导致BUSY标志一直置位):外部的 SCL、SDA引脚保持为高电平,但内部的 SCL and SDA analog filter output 被硬件强制锁定为 低电平,导致 BUSY标志 一直置位,此时就算软件复位I2C、甚至MCU复位都没用,因为复位后内部的 SCL and SDA analog filter output仍然被硬件锁定为低电平,BUSY标志依然会置位。
此时,软件可控制SCL、SDA引脚电平翻转(SCL、SDA引脚电平翻转后,I2C内部对应的SCL、SDA信号会自动更新),来恢复I2C内部的SCL、SDA锁定状态。恢复过程如下:
Step1、软件清零PE标志(禁能I2C)
Step2、软件配置SCL、SDA引脚为开漏输出,并输出高电平,然后软件读GPIO输入寄存器确认 SCL、SDA引脚已输出高电平
Step3、软件控制 SDA引脚 输出低电平,并读GPIO输入寄存器确认 SDA引脚已输出低电平
Step4、软件控制 SCL引脚 输出低电平,并读GPIO输入寄存器确认 SCL引脚已输出低电平
Step5、软件控制 SCL引脚 输出高电平,并读GPIO输入寄存器确认 SCL引脚已输出高电平
Step6、软件控制 SDA引脚 输出高电平,并读GPIO输入寄存器确认 SDA引脚已输出高电平
Step7、软件配置SCL、SDA引脚为复用开漏输出
Step8、软件置位SWRST标志
Step9、软件清零SWRST标志
Step10、软件置位PE标志(使能I2C)
6、接收数据时,若软件无法保证能在1Byte传输时间内读完 I2C_DR(线程轮询读取时 被中断打断 或被更高优先级的线程打断,或在中断函数中读取时 被更高优先级的中断打断),则建议不使用RXNE标志(否则I2C容易出错), 改为用 BTF标志,当BTF置位时,软件读一次 I2C_DR。
发送数据时,若软件无法保证能在1Byte传输时间内写完 I2C_DR,则可不用TXE标志,改为用 BTF标志,当BTF置位时,软件写一次I2C_DR。
PS:使用BTF标志 发送或接收,会降低通信速率,但提高了传输的可靠性。
7、I2C为 Master、Standard mode时,I2C通信速率为 88Khz ~ 100Khz时 Restarat信号的建立时间可能会有点超标,导致Restart信号无法正常发送,此时建议软件把速率配置为 88K以下 或 使用 Fast mode(从设备支持Fast mode时)
参考资料
[1] STM32F103xx datasheet.
[2] STM32F10xxx reference manual.
[3] STM32F101xC/D/E STM32F103xC/D/E Errata sheet
[4] AN2824 Application note:STM32F10xxx I2C optimized examples
作者:节墨之大刀