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

前言

前面的文章介绍了在单片机中常用的两种通信协议(USARTSPI),并给出了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_PORT					GPIOB
#define IIC1_SCL_PIN				GPIO_PIN_10
#define IIC1_SDA_PIN				GPIO_PIN_11

void 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--;
		else				break;
	}
	
	/* 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--;
		else				break;
	}
	
	/* 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--;
		else				break;
	}
	/* 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--;
		else				break;
	}
	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--;
			else				break;
		}
		
		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--;
		else				break;
	}
}

五、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--;
		else				break;
	}
	/* 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--;
		else				break;
	}
	
	/* send slave address to I2C bus*/
	i2c_master_addressing(i2c_periph, DevAddress, I2C_TRANSMITTER);//I2C_RECEIVER		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--;
		else				break;
	}
	/* 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--;
		else				break;
	}	

	/* 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--;
		else				break;
	}	
	
	/* 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--;
		else				break;
	}
	/* 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--;
		else				break;
	}

	/* 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--;
		else				break;
	}
	/* 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--;
				else				break;			
			}
			/* 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--;
			else				break;			
		}
    	/* 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--;
		else				break;
	}	
	
	/* 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 类型的结构体变量,为修改实时时钟的时基做好准备,比如这里设置为:2021年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原创发布,复制或转载请注明出处:)!!!

猜你喜欢

转载自blog.csdn.net/qq_37554315/article/details/120875305