前言
前面的文章介绍了在单片机中常用的两种通信协议(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_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原创发布,复制或转载请注明出处:)!!!