MCU微课堂 | CKS32F4xx系列产品IIC通信
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_9I2C_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);//设置系统中断优先级分组2USART_Configuration();I2C_EE_Init();printf("start\r\n");I2C_Test();while (1){}}
主函数代码比较简单,主要是完成GPIO初始化、串口初始化和I2C的初始化,初始化完成之后会执行一次I2C_Test函数,在串口调试助手上会打印输出一些信息。
- 深圳市汇创科电子科技有限公司
- 电话:0755-27809147
- 传真:0755-27809147
- 手机:13823247950
- 网址:www.hck-tech.com
- 销售中心:深圳市宝安区西乡街道固戍二路下围园七星创意园B座501
