您的当前位置: 网站首页 > 新闻动态 > 公司新闻

MCU微课堂 | CKS32F4xx系列产品IIC通信

更新时间:2023-03-22 10:12:21

MCU微课堂CKS32F4xx列产品IIC通信

第七期 2023.3.22

中科芯CKS32F4xx系列产品内部提供两个看门狗定时器单元,独立型看I2C通讯协议(Inter-Integrated Circuit)是由Phiilps公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要USART、CAN等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路间的通讯。

CKS32F4xx系列产品IIC介绍

CKS32F4xx系列的I2C外设可用作通讯的主机及从机,支持100Kbit/s和 400Kbit/s的速率,支持7位、10位设备地址,支持DMA数据传输,并具有数据校验功能。它的I2C外设还支持SMBus2.0协议,SMBus协议与I2C类似,主要应用于笔记本电脑的电池管理中。IIC接口的结构框图如下图所示:

I2C的所有硬件架构都是根据上图中左侧SCL线和SDA线展开的(其中SMBA线用于SMBUS的警告信号,I2C通讯没有使用)。CKS32F4xx系列芯片有多个I2C 外设,它们的I2C通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚,具体的引脚映射关系,请参考芯片数据手册。

SCL线的时钟信号,由I2C接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时钟频率。配置I2C的CCR寄存器可修改通讯速率相关的参数,I2C通讯的“标准/快速”模式分别对应100/400Kbit/s的通讯速率。

I2C的SDA信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存器 (DR)、地址寄存器(OAR)、PEC寄存器以及SDA数据线。当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过SDA信号线发送出去;当从外部接收数据的时候,数据移位寄存器把 SDA信号线采样到的数据一位一位地存储到“数据寄存器”中。若使能了数据校验,接收到的数据会经过PCE计算器运算,运算结果存储在“PEC 寄存器”中。当CKS32的I2C工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器会把接收到的地址CKS32的自身的“I2C 地址寄存器”的值作比较,以便响应主机的寻址。CKS32的自身I2C地址可通过修改“自身地址寄存器”修改,支持同时使用两个I2C设备地址,两个地址分别存储在OAR1和OAR2中。

使用I2C外设通讯时,在通讯的不同阶段它会对“状态寄存器(SR1及SR2)”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。当CKS32的IIC作为通讯的主机端向外发送数据的过程为:

S: 起始位

P:停止位

A:应答

EV5:SB=1

EV6:ADDR=1

EV8:TxE=1

EV8_2: TxE=1,BTF=1

主发送器发送流程及事件说明如下:

(1) 控制产生起始信号(S),当发生起始信号后,它产生事件“EV5”,并会对SR1寄存器的“SB”位置1,表示起始信号已经发送;

(2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”及“EV8”,这时SR1寄存器的“ADDR”位及“TXE”位被置1,ADDR为1表示地址已经发送,TXE为1表示数据寄存器为空;

(3) 以上步骤正常执行并对ADDR位清零后,我们往I2C的“数据寄存器DR”写入要发送的数据,这时TXE位会被重置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后,又会产生“EV8”事件,即TXE位被置 1,重复这个过程,就可以发送多个字节数据了;

(4) 当我们发送数据完成后,控制I2C设备产生一个停止信号(P),这个时候会产生EV2事件,SR1的TXE位及BTF位都被置1,表示通讯结束。

假如我们使能了I2C 中断,以上所有事件产生时,都会产生I2C中断信号,进入同一个中断服务函数,到I2C中断服务程序后,再通过检查寄存器位来了解是哪一个事件。

CKS32的IIC作为通讯的主机端从外部接收数据的过程为:

S:起始位 

P:停止位 

A:应答 

NA:非应答

EV5:SB=1

EV6:ADDR=1

EV7:RxNE=1

EV7_1: RxNE=1

主接收器接收流程及事件说明如下:

(1) 同主发送流程,起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对SR1寄存器的“SB”位置1,表示起始信号已经发送;

(2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1寄存器的“ADDR”位被置1,表示地址已经发送。

(3) 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件,SR1寄存器的RXNE被置1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制I2C发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;

(4) 发送非应答信号后,产生停止信号(P),结束传输。

CKS32F4xx系列产品IIC的配置

接下来我们讲解如何利用CKS32F4xx系列固件库来完成对IIC的配置使用。跟其它外设一样,CKS32标准库提供了I2C初始化结构体及初始化函数来配置 I2C外设。了解初始化结构体后我们就能对I2C外设运用自如了,代码如下:

typedef struct{ 
uint32_t I2C_ClockSpeed;
uint16_t I2C_Mode;
uint16_t I2C_DutyCycle;
uint16_t I2C_OwnAddress1;
uint16_t I2C_Ack;
uint16_t I2C_AcknowledgedAddress;
}I2C_InitTypeDef;

结构体中各个成员变量的介绍及初始化时可被赋的值如下:

1) I2C_ClockSpeed:本成员设置的是I2C的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到I2C的时钟控制寄存器CCR。而我们写入的这个参数值不得高于400Kbit/s。

2) I2C_Mode:本成员是选择I2C的使用方式,可选的参数值如下:

I2C_Mode_I2C                //I2C模式
I2C_Mode_SMBusDevice // SMBus从模式
I2C_Mode_SMBusHost // SMBus主模式

3) I2C_DutyCycle: 本成员设置的是I2C的SCL线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为2:1(I2C_DutyCycle_2)和16:9 (I2C_DutyCycle_16_9)。其实这两个模式的比例差别并不大,一般任意选一个就可以了

I2C_DutyCycle_16_9            
I2C_DutyCycle_2

6) I2C_AcknowledgedAddress: 本成员选择I2C的寻址模式是7位还是10 位地址。这需要根据实际连接到I2C总线上设备的地址进行选择,这个成员的配置也影响到I2C_OwnAddress1成员,只有这里设置成10位模式时,I2C_OwnAddress1才支持10位地址。

配置完这些结构体成员值,调用库函数I2C_Init即可把结构体的配置写入到寄存器中。

I2C_AcknowledgedAddress_7bit   //7位寻址
I2C_AcknowledgedAddress_10bit //10位寻址

配置完这些结构体成员值,调用库函数I2C_Init即可把结构体的配置写入到寄存器中。

CKS32F4xx读写EEPROM实验

本小节以EEPROM的读写实验为大家讲解CKS32F4xx的I2C使用方法。实验中CKS32F4xx的I2C外设采用主模式,分别用作主发送器和主接收器,通过查询事件的方式来确保正常通讯。实验中用的EEPROM芯片(型号:AT24C02) 的SCL及SDA引脚连接到了CKS32F4xx对应的 I2C引脚中(PF1和PF0),结合上拉电阻,构成了I2C通讯总线,它们通过I2C总线交互。EEPROM 芯片的设备地址一共有7位,其中高4位固定为:1010b,低3位则由A0/A1/A2 信号线的电平决定,在本实验中I2C设备的写地址为0xA0,I2C设备的读地址为0xA1。

1.编程要点

(1) 配置通讯使用的目标引脚为开漏模式; 

(2) 使能I2C外设的时钟;

(3) 配置I2C外设的模式、地址、速率等参数并使能I2C外设;

(4) 编写基本I2C按字节收发的函数; 

(5) 编写读写 EEPROM 存储内容的函数; 

(6) 编写测试程序,对读写数据进行校验。

2.代码分析

代码清单1:I2C GPIO口初始化配置:

主要是完成对I2C引脚的初始化,把引脚初始化成复用开漏模式。

static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EEPROM_I2C_CLK_INIT(EEPROM_I2C_CLK, ENABLE);
RCC_AHB1PeriphClockCmd(EEPROM_I2C_SCL_GPIO_CLK | EEPROM_I2C_SDA_GPIO_CLK, ENABLE);
GPIO_PinAFConfig(EEPROM_I2C_SCL_GPIO_PORT, EEPROM_I2C_SCL_SOURCE, EEPROM_I2C_SCL_AF);
GPIO_PinAFConfig(EEPROM_I2C_SDA_GPIO_PORT, EEPROM_I2C_SDA_SOURCE, EEPROM_I2C_SDA_AF);
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(EEPROM_I2C_SCL_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;
GPIO_Init(EEPROM_I2C_SDA_GPIO_PORT, &GPIO_InitStructure);}

代码清单2:I2C模式配置函数

根据CKS32F4xx系列产品IIC的配置所讲的对I2C进行配置,它把I2C外设通讯时钟SCL的低/高电平比设置为2,使能响应功能,使用7位地址 I2C_OWN_ADDRESS7以及速率配置为400Kbit/s。最后调用库函数I2C_Init把这些配置写入寄存器,并调用I2C_Cmd函数使能外设。

static void I2C_Mode_Configu(void)
{
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 =I2C_OWN_ADDRESS7;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
I2C_Init(EEPROM_I2C, &I2C_InitStructure);
I2C_Cmd(EEPROM_I2C, ENABLE);
I2C_AcknowledgeConfig(EEPROM_I2C, ENABLE); }

代码清单3:EEPROM单字节写入函数

uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr)
{ I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{ if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
}
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
}
I2C_SendData(EEPROM_I2C, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
}
I2C_SendData(EEPROM_I2C, *pBuffer);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
return 1;
}

首先是I2C_TIMEOUT_UserCallback函数,这个函数的功能就是向串口打印调试信息。在I2C通讯的很多过程,都需要检测事件,当检测到某事件后才能继续下一步的操作,但有时通讯错误或者I2C总线被占用,我们不能无休止地等待下去,所以我们设定每个事件检测都有等待的时间上限,若超过这个时间,我们就调用I2C_TIMEOUT_UserCallback函数输出调试信息,并终止I2C通讯。

I2C_EE_ByteWrite函数,这个函数实现了前面讲的I2C 主发送器通讯流程。

(1) 使用库函数I2C_GenerateSTART产生I2C起始信号,其中的EEPROM_I2C宏是I2C2;

(2) 对I2CTimeout变量赋值为宏I2CT_FLAG_TIMEOUT,这个I2CTimeout变量在下面的while循环中每次循环减 1,该循环通过调用库函数I2C_CheckEvent检测事件EV5,若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测,当检I2CT_FLAG_TIMEOUT 次都还没等待到事件则认为通讯失败,调用前面的I2C_TIMEOUT_UserCallbac输出调试信息,并退出通讯;

(3) 调用库函数I2C_Send7bitAddress发送EEPROM的设备地址,并把数据传输方向设置为I2C_Direction_Transmitter(即发送方向),这个数据传输方向就是通过设置I2C通讯中紧跟地址后面的R/W位实现的。发送地址后以同样的方式检测EV6标志;

(4) 调用库函I2C_SendDat向EEPROM发送要写入的内部地址,该地址是 I2C_EE_ByteWrite 函数的输入参数,发送完毕后等待EV8事件。要注意这个内部地址跟上面EEPROM 地址不一 样,上面的是指I2C总线设备的独立地址,而此处的内部地址是EEPROM内数据组织的地址,也可理解为EEPROM内存的地址或 I2C设备的寄存器地址;

(5) 调用库函数I2C_SendData向EEPROM发送要写入的数据,该数据是 I2C_EE_ByteWrite函数的输入参数,发送完毕后等待EV8事件;

(6) 一个I2C通讯过程完毕,调用I2C_GenerateSTOP发送停止信号。

代码清单4:EEPROM多字节快速写入函数

void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr, u16 NumByteToWrite)
{
u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
Addr = WriteAddr % I2C_PageSize; count = I2C_PageSize - Addr;
NumOfPage = NumByteToWrite / I2C_PageSize; NumOfSingle = NumByteToWrite % I2C_PageSize;
if(Addr == 0) { if(NumOfPage == 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
else
{
while(NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
if(NumOfSingle!=0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
else
{
if(NumOfPage== 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
else
{
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;

if(count != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
I2C_EE_WaitEepromStandbyState();
WriteAddr += count;
pBuffer += count;
}
while(NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
if(NumOfSingle != 0)
{ I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
}

这段代码是快速的向EEPROM写入多个字节。pBuffer是缓冲区的指针,WriteAddr是写入的地址,NumByteToWrite是要写入的字节数。代码的主旨就是对输入的数据进行分页(AT24C02每页8个字节)。通过“整除”计算出要写入的数据NumByteToWrite能写满多少“完整的页”,计算得到的值存储在 NumOfPage中,但有时数据不是刚好能写满完整页的,会多一点出来,通过“求余”计算得出“不满一页的数据个数”就存储在NumOfSingle中。

除了基本的分页传输,还要考虑首地址的问题。若首地址不是刚好对齐到页的首地址,会需要一个count值,用于存储从该首地址开始写满该地址所在的页,还能写多少个数据。实际传输时,先把这部分count个数据先写入,填满该页,然后把剩余的数据(NumByteToWrite-count),再重复上述求出NumOPage及 NumOfSingle的过程,按页传输到EEPROM。

代码清单5:EEPROM读取数据函数

uint32_t I2C_EE_BufferRead(u8* pBuffer, u8 ReadAddr, u16 NumByteToRead)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
while(I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
}
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
}
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(11);
} I2C_Cmd(EEPROM_I2C, ENABLE);
I2C_SendData(EEPROM_I2C, ReadAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(12);
}
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(13);
}
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Receiver);
I2CTimeout = I2CT_FLAG_TIMEOUT;
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
if((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(14);
}
while(NumByteToRead)
{
if(NumByteToRead == 1)
{
I2C_AcknowledgeConfig(EEPROM_I2C, DISABLE);
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
}
I2CTimeout = I2CT_LONG_TIMEOUT;
while(I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
{
*pBuffer = I2C_ReceiveData(EEPROM_I2C);
pBuffer++;
NumByteToRead--;
}
}
I2C_AcknowledgeConfig(EEPROM_I2C, ENABLE);
return 1;
}

从EEPROM读取数据是一个复合的I2C时序,它实际上包含一个写过程和一个读过程。第一个通讯过程中,使用I2C发送设备地址寻址 (写方向),接着发送要读取的“内存地址”, 代码的流程和前面讲的CKS32的IIC作为通讯的主机端向外发送数据的过程是一致的;第二个通讯过程中,再次使用I2C发送设备地址寻址,但这个时候的数据方向是读方向;在这个过程之后,EEPROM 会向主机返回从读“内存地址”开始的数据,一个字节一个字节地传输,只要主机的响应为“应答信号”,它就会一直传输下去,主机想结束传输时,就发送“非应 答信号”,并以“停止信号”结束通讯,作为从机的 EEPROM 也会停止传输。代码的流程和前面讲的CKS32的IIC作为通讯的主机端从外面接收数据的过程是一致的。

代码清单6:EEPROM读写测试函数

uint8_t I2C_Test(void)
{
u16 i;
EEPROM_INFO("写入的数据");
for ( i=0; i<=10; i++ )
{
I2c_Buf_Write[i] = i;
printf("0x%02X ", I2c_Buf_Write[i]);
}
I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 11);
EEPROM_INFO("写成功");
EEPROM_INFO("读出的数据");
I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 11);
for (i=0; i<11; i++)
{
if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
{
printf("0x%02X ", I2c_Buf_Read[i]);
EEPROM_ERROR("错误:I2C EEPROM写入与读出的数据不一致");
return 0;
}
printf("0x%02X ", I2c_Buf_Read[i]);
}
EEPROM_INFO("I2C(AT24C02)读写测试成功");
return 1;
}

代码中先填充一个数组,数组的内容为 0,1,2 至 10,接着利用前面讲到的函数I2C_EE_BufferWrite把这个数组的内容写入到EEPROM中。写入完毕后再利用前面讲到的函数I2C_EE_BufferRead从EEPROM的地址中读取数据, 把读取到的数据与写入的数据进行校验,若一致说明读写正常,并打印输出数据。否则读写过程有问题或者EEPROM芯片不正常,打印相应的错误信息。

代码清单7:主函数

int main(void)
{
GPIO_Configuration();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
USART_Configuration();
I2C_EE_Init();
printf("start\r\n");
I2C_Test();
while (1)
{
}
}

主函数代码比较简单,主要是完成GPIO初始化、串口初始化和I2C的初始化,初始化完成之后会执行一次I2C_Test函数,在串口调试助手上会打印输出一些信息。

新闻动态News
联系我们CONTACT US
  • 深圳市汇创科电子科技有限公司
  • 电话:0755-27809147
  • 传真:0755-27809147
  • 手机:13823247950
  • 网址:www.hck-tech.com
  • 销售中心:深圳市宝安区西乡街道固戍二路下围园七星创意园B座501
服务热线
0755-27809147