200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > GD32F303调试小记(三)之IIC(硬件IIC+PCF8563实时时钟)

GD32F303调试小记(三)之IIC(硬件IIC+PCF8563实时时钟)

时间:2023-03-08 23:52:22

相关推荐

GD32F303调试小记(三)之IIC(硬件IIC+PCF8563实时时钟)

前言

前面的文章介绍了在单片机中常用的两种通信协议(USART和SPI),并给出了GD32F303对应的配置流程。这次介绍第三种常见的通信协议IIC。这此使用GD32的硬件IIC通信PCF8563实时时钟。

IIC

IIC,又名I2C,也是一种串口通信协议。其中包含一根时钟线一根数据线。

首先,标准的IIC总线是需要外部上拉的,即时钟线和数据线外部同时接一个上拉电阻(常见10k)到供电(MCU供电电压3.3V或5V或其它供电电压))。对应的通信IO配置为开漏输出。这样做的目的很简单,当这组IIC总线上挂载多个设备时,可以接收从设备的应答信号数据接收。所以在空闲状态下,这两根线被外部上拉为高电平。根据上面的基础,IIC定义了起始信号内容传输应答信号结束信号起始信号在时钟线在高电平的情况下,数据线由高电平被下拉成低电平。出现在每次通信的开始。结束信号是时钟线在高电平的情况下,数据线由低电平被上拉成高电平。出现在每次通信的结尾。内容传输是跟在起始信号之后的,这里分为7位、8位和10位传输。7位与8位可以理解成一个概念,8位无非是芯片厂家规格书里把最低位表示的读/写位也加了进去。发从设备地址时都是由固定的高7位数据加最后一位(写0,读1)组成。10位则是先发送高两位的地址和读写位,再发送低8位的地址。如0x06F0(0b-xxxx-x110-1111-0000),其实是发送了11位有效数据。本文介绍7(8)位地址传输应答信号就更好理解了,在一帧内容传输完后(8bit)的一个时钟通信周期内,从设备控制数据线是高电平还是低电平。低电平意味着从机收到刚刚传输的8bit内容,高电平代表着未收到(记得标准的IIC是开漏输出,外部上拉为高,通讯失败时默认就是高)。通讯速度。不同于SPI,SPI是只要主机能发送的通信频率够快,从机也能接受足够高的通信频率,理论上是可以无限大的。像USART有常见的通信频率有4800、9600、38400、115200等等。IIC标准速率为100kbps,也有快速模式400kbps和高速模式3.4Mbps。在GD32F303中支持标准速率为100kbps,快速模式400kbps和快速+模式1Mbps

由于IIC通信速率的限制和较为完整的通信协议,通常用于数据量不是非常大的场合。如从AT24CXX系列EEPROM芯片读取或写入几个或几十个数据、从PCF8563时钟芯片读取或写入实时时钟数据、利用IIC通讯一些特定的功能IC(如南芯的SC8812去实现PD协议充电,又如通信MPU6050获取各种姿态角数据等等)、又或者驱动0.96寸的OLED显示等等。本文则是使用GD32F303的硬件IIC去实现对PCF8563时钟的设置与读取时间

各模块程序编写

在配置前,请确保你已经有一个GD32F303包含其对应标准库的keil工程,工程可使用官方的例程或可按照GD32F303调试小记(零)之工程创建与编译创建。此外,强烈建议身边有个示波器逻辑分析仪,用于查看我们端口输出的通信波形。

一、时钟配置

开启GPIO端口时钟、GPIO引脚复用时钟、AF时钟、IIC模块的时钟(注意我用的是IIC1模块)。

void SystemClock_Reconfig(void){/* Enable all peripherals clocks you need*/rcu_periph_clock_enable(RCU_GPIOA);rcu_periph_clock_enable(RCU_GPIOB);rcu_periph_clock_enable(RCU_GPIOC);rcu_periph_clock_enable(RCU_GPIOD);rcu_periph_clock_enable(RCU_DMA0);rcu_periph_clock_enable(RCU_DMA1);rcu_periph_clock_enable(RCU_I2C1);//rcu_periph_clock_enable(RCU_ADC0);//rcu_periph_clock_enable(RCU_ADC2);//rcu_periph_clock_enable(RCU_USART1);rcu_periph_clock_enable(RCU_USART2);rcu_periph_clock_enable(RCU_SPI2);/* Timer1,2,3,4,5,6,11,12,13 are hanged on APB1,* Timer0,7,8,9,10 are hanged on APB2*/rcu_periph_clock_enable(RCU_TIMER1);rcu_periph_clock_enable(RCU_AF);

二、GPIO配置

根据上图中手册中对IIC1引脚的描述,相关IO配置如下:

// IIC port and pins definition#define IIC1_PORTGPIOB#define IIC1_SCL_PINGPIO_PIN_10#define IIC1_SDA_PINGPIO_PIN_11void GPIO_Init(void){/* 使用SW下载,不使用JTAG下载,管脚用作其它功能 */gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE);/* demo board IIC1 I/O */gpio_init(IIC1_PORT, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, IIC1_SCL_PIN | IIC1_SDA_PIN);}

三、IIC配置

配置IIC1,速率60kHz、快速模式和快速+模式下高低电平比(我们用的标准,这个随意)、使用IIC模式和IIC从机7位地址模式、使能IIC1应答并使能IIC1模块。

/* IIC通信中PCF8563芯片的地址 */#define ADDRESS_PCF8563((uint8_t)0xA2)void IICx_Init(void){/* configure I2C1 clock */i2c_clock_config(I2C1,60000,I2C_DTCY_2);/* configure I2C1 address */i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, ADDRESS_PCF8563);/* enable I2C1 */i2c_enable(I2C1);/* enable acknowledge */i2c_ack_config(I2C1, I2C_ACK_ENABLE);// /* enable I2C1 DMA */// i2c_dma_enable(I2C1, I2C_DMA_ON);}

四、IIC写函数

使用IIC与从设备通信的代码不是唯一不变的,这由从设备自身的内部寄存器寻址位数决定(有的从设备除了寄存器本身还有页操作)。我们先来看看PCF8563这个芯片的数据手册中对其内部寄存器的描述:

重点关注上图中用红色框线圈出来的部分,他们是我们使用IIC写入和读取的寄存器,且每个寄存器并不都是用足了8位,我们这里作个宏定义:

/* PCF8563芯片状态控制寄存器的地址 * 00H~01H共2个8位寄存器:*/#define ADDRESS_CTL_STATUS1((uint8_t)0x00)//控制状态寄存器 1#define ADDRESS_CTL_STATUS2((uint8_t)0x01)//控制状态寄存器 2/* PCF8563芯片里时间和日期寄存器的地址 * 从02H~08H共七个8位寄存器依次包含:秒(0~59)、分(0~59)、时(0~23)、* 日(1~31)、周几(0~6)、月份(1~12)、年份(0~99)*/#define ADDRESS_SECOND_RES((uint8_t)0x02)//秒寄存器 #define ADDRESS_MINUTE_RES((uint8_t)0x03)//分寄存器 #define ADDRESS_HOUR_RES((uint8_t)0x04)//时寄存器 #define ADDRESS_DAY_RES((uint8_t)0x05)//日期寄存器 #define ADDRESS_WEEKDAY_RES((uint8_t)0x06)//周几寄存器 #define ADDRESS_MONTH_RES((uint8_t)0x07)//月份寄存器 #define ADDRESS_YEAR_RES((uint8_t)0x08)//年份秒寄存器 /* PCF8563芯片时间寄存器最大位数* 秒和分最多7Bits* 时和日期最多6Bits*/#define BCD_MinAndSec((uint8_t)0x7F)//取低7位#define BCD_HourAndDay((uint8_t)0x3F)//取低6位#define BCD_Weekday((uint8_t)0x07)//取低3位#define BCD_Months((uint8_t)0x1F)//取低5位#define BCD_Years((uint8_t)0xFF)//取低8位#define BCD_Century((uint8_t)0x80)//取第7位 month寄存器里的bit7

接着我们看看数据手册中推荐的多字节写流程。

主机发送起始信号、主机发送从设备地址并在最低位写0表明是写操作、主机发送从地址中要操作的寄存器首地址、等待从机应答、主机发送8bit数据、再等待从机应答、主机再发数据、再等待从机应答、多次应答与发送数据后主机再发送停止信号。注意加粗的字,我们下面写的逻辑也应如此。我们再看看GD32中对写操作的流程建议。

那我们根据上面两个手册中的时序要求我们的写法如下:DevAddress为从设备地址,MemAddress为从设备中要操作的寄存器,再然后才是我们真正想要写入的数据。为了保证IIC时序的正确,我们看到官方给的每一个信号操作后都有标志位,还是那句老话,在所有非必要的死循环里有超时跳出机制。

void IICx_Mem_Write(uint32_t i2c_periph,uint8_t DevAddress,uint8_t MemAddress,uint8_t* ndata,uint8_t size,uint32_t Timeout){uint32_t Timeout_t=0;uint8_t i=0;Timeout_t = Timeout;/* wait until I2C bus is idle */while(i2c_flag_get(i2c_periph, I2C_FLAG_I2CBSY)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* send a start condition to I2C bus */i2c_start_on_bus(i2c_periph);Timeout_t = Timeout;/* wait until SBSEND bit is set */while(!i2c_flag_get(i2c_periph, I2C_FLAG_SBSEND)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* send slave address to I2C bus*/i2c_master_addressing(i2c_periph, DevAddress, I2C_TRANSMITTER);Timeout_t = Timeout;/* wait until ADDSEND bit is set*/while(!i2c_flag_get(i2c_periph, I2C_FLAG_ADDSEND)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* clear ADDSEND bit */i2c_flag_clear(i2c_periph, I2C_FLAG_ADDSEND);/* send a data byte */i2c_data_transmit(i2c_periph,MemAddress);Timeout_t = Timeout;/* wait until the transmission data register is empty*/while(!i2c_flag_get(i2c_periph, I2C_FLAG_TBE)){if(Timeout_t > 0)Timeout_t--;elsebreak;}for(i=0;i<size;i++){/* send a data byte */i2c_data_transmit(i2c_periph, (*ndata));Timeout_t = Timeout;/* wait until the transmission data register is empty*/while(!i2c_flag_get(i2c_periph, I2C_FLAG_TBE)){if(Timeout_t > 0)Timeout_t--;elsebreak;}ndata++;}/* send a stop condition to I2C bus*/i2c_stop_on_bus(i2c_periph);Timeout_t = Timeout;/* wait until stop condition generate */ while(I2C_CTL0(i2c_periph)&0x0200){if(Timeout_t > 0)Timeout_t--;elsebreak;}}

五、IIC读函数

还是先看看PCF8563芯片手册中的读时序。

Fig 19的读时序为:主机发送起始信号、主机发送从设备地址并在最低位写0表明是写操作、主机发送从地址中要操作的寄存器首地址、等待从机应答、主机再次发送起始信号、主机发送从设备地址并在最低位写1表明是读操作、从机发送8bit数据、再等待主机应答、从机再发数据、再等待主机应答、多次从机发送数据与主机应答后主机不应答并发送停止信号。这里推荐Fig 19的读时序,道理也很简单。从寄存器读数据,首先你得知道你读的是什么寄存器。否则即使数据读出来了,你也不知道是谁的数据。每次读之前要先指向第一个要读的寄存器。接着我们看看GD32中对读时序的操作流程。

上述GD32给出了两个主机接收方案。方案A对应使用IIC接收中断,方案B对应不使用IIC接收中断。考虑到通信不是很频繁,这里咱们使用B方案,也就是堵塞查询接收。DevAddress为从设备地址,MemAddress为从设备中要操作的寄存器。先指向我们要读取的寄存器,再进行多个字节的读取。注意我这里给出的是多字节读取读取字节数必须不少于3(代码里有段 i==(size - 3) )。如是要单字节读取,读完一次后,直接发送停止信号即可,不用管是否应答。

void IICx_Mem_Read(uint32_t i2c_periph,uint8_t DevAddress,uint8_t MemAddress,uint8_t* ndata,uint8_t size,uint32_t Timeout){uint32_t Timeout_t=0;uint8_t i=0;/******************************************************//*Send Slave address and Specified Register Address *//******************************************************/Timeout_t = Timeout;/* wait until I2C bus is idle */while(i2c_flag_get(i2c_periph, I2C_FLAG_I2CBSY)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* send a start condition to I2C bus */i2c_start_on_bus(i2c_periph);Timeout_t = Timeout;/* wait until SBSEND bit is set */while(!i2c_flag_get(i2c_periph, I2C_FLAG_SBSEND)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* send slave address to I2C bus*/i2c_master_addressing(i2c_periph, DevAddress, I2C_TRANSMITTER);//I2C_RECEIVERI2C_TRANSMITTERTimeout_t = Timeout;/* wait until ADDSEND bit is set*/while(!i2c_flag_get(i2c_periph, I2C_FLAG_ADDSEND)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* clear ADDSEND bit */i2c_flag_clear(i2c_periph, I2C_FLAG_ADDSEND);/* send a data byte */i2c_data_transmit(i2c_periph,MemAddress);Timeout_t = Timeout;/* wait until the transmission data register is empty*/while(!i2c_flag_get(i2c_periph, I2C_FLAG_TBE)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* send a stop condition to I2C bus*/i2c_stop_on_bus(i2c_periph);Timeout_t = Timeout;/* wait until stop condition generate */ while(I2C_CTL0(i2c_periph)&0x0200){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* enable acknowledge */i2c_ack_config(i2c_periph, I2C_ACK_ENABLE);/******************************************************//* Send Slave address and Read Data *//******************************************************/Timeout_t = Timeout;/* wait until I2C bus is idle */while(i2c_flag_get(i2c_periph, I2C_FLAG_I2CBSY)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* send a start condition to I2C bus */i2c_start_on_bus(i2c_periph);Timeout_t = Timeout;/* wait until SBSEND bit is set */while(!i2c_flag_get(i2c_periph, I2C_FLAG_SBSEND)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* send slave address to I2C bus*/i2c_master_addressing(i2c_periph, DevAddress, I2C_RECEIVER);Timeout_t = Timeout;/* wait until ADDSEND bit is set*/while(!i2c_flag_get(i2c_periph, I2C_FLAG_ADDSEND)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* clear ADDSEND bit */i2c_flag_clear(i2c_periph, I2C_FLAG_ADDSEND);for(i=0;i<size;i++){if( i==(size - 3) ){Timeout_t = Timeout;/* wait until the second last data byte is received into the shift register */while(!i2c_flag_get(i2c_periph, I2C_FLAG_BTC)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* disable acknowledge */i2c_ack_config(i2c_periph, I2C_ACK_DISABLE);}Timeout_t = Timeout;/* wait until the RBNE bit is set */while(!i2c_flag_get(i2c_periph, I2C_FLAG_RBNE)){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* read data from I2C_DATA */(*ndata) = i2c_data_receive(i2c_periph);ndata++;}/* send a stop condition to I2C bus*/i2c_stop_on_bus(i2c_periph);Timeout_t = Timeout;/* wait until stop condition generate */ while(I2C_CTL0(i2c_periph)&0x0200){if(Timeout_t > 0)Timeout_t--;elsebreak;}/* enable acknowledge */i2c_ack_config(i2c_periph, I2C_ACK_ENABLE);}

六、实时时钟数据的处理

做完了上面的第五步,还差一个数据处理,我们看看时钟芯片手册中是怎么描述其数据存储的形式的:

看到上图手册中红框部分,我们知道时间和日期寄存器中都是以BCD码的形式记录的,且是BCD码中的8421码。这意味着我们把数据发送前得把我们正常的十进制数转换成BCD码,读取到数据后再把数据转化成十进制数。

1.先定义一个结构体,其中包含我们所需要时分秒年月日和周几:

typedef struct {struct{uint8_t Second;uint8_t Minute;uint8_t Hour;}time;struct{uint8_t weekday;uint8_t day;uint8_t month;uint8_t year;}date;} PCF8563_Info;extern PCF8563_Info RTC_Message;

2. 十进制数转化成BCD码函数:

static uint8_t RTC_BinToBcd(uint8_t BinValue){uint8_t cacheBuf = 0;while(BinValue >= 10){BinValue -= 10;cacheBuf += 1;}cacheBuf = (cacheBuf<<4) + BinValue;return (cacheBuf);}

3. BCD码转化成十进制数函数:

static uint8_t RTC_BcdToBin(uint8_t BCDValue,uint8_t xRegister){uint8_t cacheBuf = 0;cacheBuf = ( BCDValue & (xRegister&0xF0) ) >> 4;cacheBuf = cacheBuf*10 + (BCDValue & (xRegister&0x0F)); return (cacheBuf);}

4.写实时时钟函数:

void Write_To_PCF8563(PCF8563_Info* set_pcf8563_time){static uint8_t temp_send_BCD[7]={0};*temp_send_BCD = RTC_BinToBcd(set_pcf8563_time->time.Second);*(temp_send_BCD + 1)= RTC_BinToBcd(set_pcf8563_time->time.Minute);*(temp_send_BCD + 2)= RTC_BinToBcd(set_pcf8563_time->time.Hour);*(temp_send_BCD + 3)= RTC_BinToBcd(set_pcf8563_time->date.day);*(temp_send_BCD + 4)= RTC_BinToBcd(set_pcf8563_time->date.weekday);*(temp_send_BCD + 5)= RTC_BinToBcd(set_pcf8563_time->date.month);*(temp_send_BCD + 6)= RTC_BinToBcd(set_pcf8563_time->date.year);IICx_Mem_Write(I2C1,ADDRESS_PCF8563,ADDRESS_SECOND_RES,temp_send_BCD,7,0xFFFFU);}

5.读实时时钟函数:

PCF8563_Info Read_From_PCF8563(void){PCF8563_Info Readbuf={0};static uint8_t ReadFromPCF[7]={0};IICx_Mem_Read(I2C1,ADDRESS_PCF8563,ADDRESS_SECOND_RES,ReadFromPCF,7,0xFFFFU);Readbuf.time.Second = RTC_BcdToBin(ReadFromPCF[0],BCD_MinAndSec);Readbuf.time.Minute = RTC_BcdToBin(ReadFromPCF[1],BCD_MinAndSec);Readbuf.time.Hour= RTC_BcdToBin(ReadFromPCF[2],BCD_HourAndDay);Readbuf.date.day= RTC_BcdToBin(ReadFromPCF[3],BCD_HourAndDay);Readbuf.date.weekday= RTC_BcdToBin(ReadFromPCF[4],BCD_Weekday);Readbuf.date.month= RTC_BcdToBin(ReadFromPCF[5],BCD_Months);Readbuf.date.year= RTC_BcdToBin(ReadFromPCF[6],BCD_Years);return (Readbuf);}

七、主函数部分

1. 显示部分

这里我在之前的SPI章节配置好了屏显,并移植进去了lvgl图形界面库。这个是lvgl的任务处理函数,这里主要就是刷屏的。

void TASK_LCD_REFRESH(void){lv_task_handler();}

2. 任务函数

PCF8563_Info RTC_Message={0};void TASK_PCF8563(void){RTC_Message = Read_From_PCF8563();}

3. 主函数

这里定义一个PCF8563_Info 类型的结构体变量,为修改实时时钟的时基做好准备,比如这里设置为:11月10日23点59分55秒,周五。TMT是个时间片框架,源码见GITEE,这里我们设一个任务,每过1000ms,读取一次实时时钟里的数据。lvgl是一个轻量级的图形界面库,让一般的32位单片机都能有一个很好的UI界面显示,B站演示视频很多,这里不多介绍,我会在后期的文章中介绍如何把lvgl移植进GD32里。

int main(void){PCF8563_Info set_pcf8563={.time.Second= 55,.time.Minute= 59,.time.Hour= 23,.date.weekday= 4,.date.day= 10,.date.month= 11,.date.year= 21};SystemTick_Init();SystemClock_Reconfig();GPIO_Init();Timer1_Init();Timer3_Init();DMA_Init();USARTx_Init();SPIx_Init();IICx_Init();FWDGT_Init();NVIC_Init();/* 时间片框架(可忽略) */TMT_Init();/* lvgl库初始化(可忽略) */lv_init(); lv_port_disp_init();lv_port_indev_init(); /* TMT任务创建,这里知道LCD_REFRESH每10ms执行一次,其它1s执行一次即可 */TMT.Create(TASK_LCD_REFRESH,10);TMT.Create(TASK_PCF8563,1000);TMT.Create(TASK_FWDGT_RELOAD,1000);/* 此处为lvgl中的btn控件创建,能让屏上显示变量这里也忽略即可 */lv_obj_t * btn1;lv_obj_t * btn2;lv_obj_t * btn3;lv_obj_t * btn4;lv_obj_t * btn5;lv_obj_t * btn6;static lv_style_t style1;.../*======================*//* 这里把赋的初值传过去 */Write_To_PCF8563(&set_pcf8563);while(1){TMT.Run();lv_label_set_text_fmt(label_1,"hello C world");lv_label_set_text_fmt(label_4,"date:20%02d-%02d-%02d", RTC_Message.date.year,RTC_Message.date.month,RTC_Message.date.day);lv_label_set_text_fmt(label_5,"time: %02d:%02d:%02d", RTC_Message.time.Hour,RTC_Message.time.Minute,RTC_Message.time.Second);lv_label_set_text_fmt(label_6,"weekday:%d", RTC_Message.date.weekday);}}

八、结果演示

1. 实际效果

硬件IIC读取时间

也可点击此处查看效果视频连接

2. 驱动波形

- IIC完整波形

完整波形如上,黄线为时钟线,蓝线为数据线。频率应为60kHz,这里由于由于缩小了,导致示波器算频率不正确。

- IIC起始部分

- 这里就好多了,频率约60k的样子,从左边看是不是先有起始信号第一个数据是不是0xA2(0b1010 0010),然后有一个时钟周期的应答,看看是不是被从机拉低了。然后第二个数据是不是秒寄存器0x02(0b0000 0010)接着又是一个从机应答。以此类推。

- IIC停止部分

- 结束这边也一样,看看最后是不是一个停止信号

九、总结

至此,我们把单片机中最常见的三种通信(SPI和USART可参见我的其它文章)都用GD32自带的硬件模块实现了。其实还有单总线通讯(只用一根线),学习之初用的DS18B20以及红外遥控器里的信号接收都是这个,我们还可以自己定义一个单总线协议,规定起始信号是什么波形、停止信号是什么模型、高电平是什么波形、低电平又是什么波形。就像打暗号一样,只要双方约定好一定的规范,那么它就能形成协议,并且通讯成功。

!!!本文为欢喜6666在CSDN原创发布,复制或转载请注明出处:)!!!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。