配置 tomcat 做网站,建网站的好处,WordPress标签加HTML,客厅装修效果图片大全基于梁山派GD32F470的DS1307 RTC模块I2C驱动移植与时间管理实战 最近在做一个需要记录数据生成时间的项目#xff0c;用到了DS1307实时时钟模块。我发现很多刚开始接触嵌入式开发的朋友#xff0c;对于如何给单片机“加上一块表”感到头疼。今天#xff0c;我就以手头的梁山…基于梁山派GD32F470的DS1307 RTC模块I2C驱动移植与时间管理实战最近在做一个需要记录数据生成时间的项目用到了DS1307实时时钟模块。我发现很多刚开始接触嵌入式开发的朋友对于如何给单片机“加上一块表”感到头疼。今天我就以手头的梁山派GD32F470开发板为例带大家从硬件连接到软件驱动完整地走一遍DS1307 RTC模块的移植过程。整个过程就像搭积木我会把每一步都拆解清楚保证你看完就能在自己的项目里用起来。1. 认识你的“电子手表”DS1307模块在动手接线写代码之前咱们先得搞清楚DS1307是个什么东西它能干什么。简单来说DS1307就是一块专门用来“看时间”的芯片。它内部有一个非常精准的时钟电路即使你的主控单片机比如GD32断电了只要模块上自带的纽扣电池还有电它就能一直走时分秒不差。这对于需要记录事件发生时间、定时唤醒或者做数据日志的系统来说是必不可少的。我用的这个模块网上通常叫“Tiny RTC I2C模块”它集成了三样东西DS1307时钟芯片核心负责计时。AT24C32存储芯片一个额外的EEPROM存储器可以用来存点别的数据比如配置参数。DS18B20接口预留了一个温度传感器的接口需要你自己焊传感器上去。它们都通过同一个I2C总线与单片机通信但各自的“门牌号”器件地址不同所以互不干扰。这个模块用起来很简单就4根线VCC接5V电源。GND接地。SDAI2C数据线。SCLI2C时钟线。注意模块工作电压是4.5-5.5V而咱们的梁山派GPIO是3.3V电平。别担心I2C总线是开漏输出加上上拉电阻后可以兼容后面配置GPIO时会特别强调这一点。2. 硬件连接把模块插到开发板上接线是最简单的一步但一定要接对。根据原始资料梁山派和DS1307模块的连接关系如下DS1307模块引脚梁山派GD32F470引脚VCC5V电源引脚GNDGNDSCLPB8SDAPB9你只需要用杜邦线按照上表把这四根线一一对应连接起来就好了。接好后给开发板上电模块上的红色电源指示灯应该会亮起。3. 软件驱动移植手把手编写代码硬件准备好了接下来就是重头戏——写软件驱动。我会把核心代码都列出来并加上详细注释你跟着做就行。3.1 建立工程文件与头文件首先在你的工程里新建两个文件bsp_ds1307.c和bsp_ds1307.h。“bsp”意思是板级支持包专门放这种外设的驱动代码。先来看头文件bsp_ds1307.h它主要做了三件事定义引脚、定义数据结构、声明函数。#ifndef _BSP_DS1307_H_ #define _BSP_DS1307_H_ #include gd32f4xx.h // 1. 定义I2C引脚使用软件模拟I2C更灵活 #define RCU_SCL RCU_GPIOB #define PORT_SCL GPIOB #define GPIO_SCL GPIO_PIN_8 #define RCU_SDA RCU_GPIOB #define PORT_SDA GPIOB #define GPIO_SDA GPIO_PIN_9 // 2. 定义引脚操作宏方便切换SDA引脚方向 #define SDA_IN() {gpio_mode_set(PORT_SDA, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_SDA);} //SDA设为输入 #define SDA_OUT() {gpio_mode_set(PORT_SDA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_SDA);} //SDA设为输出 #define SCL(BIT) gpio_bit_write( PORT_SCL, GPIO_SCL, BIT?SET:RESET) //设置SCL电平 #define SDA(BIT) gpio_bit_write( PORT_SDA, GPIO_SDA, BIT?SET:RESET) //设置SDA电平 #define GETSDA() gpio_input_bit_get( PORT_SDA, GPIO_SDA) //读取SDA电平 // 3. 定义时间结构体用来存放读出的年月日时分秒 typedef struct _RTC_TIME_STRUCT_ { unsigned char sec; // 秒 (0-59) unsigned char min; // 分 (0-59) unsigned char hour; // 时 (1-12或0-23) unsigned char week; // 星期 (1-7) unsigned char date; // 日 (1-31) unsigned char month; // 月 (1-12) unsigned char year; // 年 (00-99) } _time_struct_; extern _time_struct_ RTC_Time; // 声明一个全局的时间变量 // 4. 函数声明 void DS1307_GPIO_Init(void); // 初始化GPIO引脚 unsigned char Write1307(unsigned char add, unsigned char dat); // 向DS1307写数据 unsigned char Read1307(unsigned char add); // 从DS1307读数据 void Set_RTC_Time(uint8_t year, uint8_t month, uint8_t date, uint8_t week, uint8_t hour, uint8_t min, uint8_t sec); // 设置时间 void Get_RTC_Time(void); // 获取时间 #endif3.2 编写核心驱动代码 (bsp_ds1307.c)这个文件内容稍长但逻辑很清晰。我们采用软件模拟I2C的方式这样不依赖硬件I2C外设移植性更好。3.2.1 GPIO初始化首先初始化连接SDA和SCL的两个GPIO引脚。这里有个关键点必须配置为开漏输出Open-Drain模式并启用内部上拉。因为DS1307模块是5V器件而GD32是3.3V开漏模式配合上拉电阻可以实现电平兼容避免损坏单片机。void DS1307_GPIO_Init(void) { // 打开PB8和PB9的时钟 rcu_periph_clock_enable(RCU_SCL); rcu_periph_clock_enable(RCU_SDA); // 配置SCL (PB8) 为上拉开漏输出 gpio_mode_set(PORT_SCL, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_SCL); gpio_output_options_set(PORT_SCL, GPIO_OTYPE_OD, GPIO_OSPEED_2MHZ, GPIO_SCL); gpio_bit_write(PORT_SCL, GPIO_SCL, SET); // 初始化为高电平 // 配置SDA (PB9) 为上拉开漏输出 gpio_mode_set(PORT_SDA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_SDA); gpio_output_options_set(PORT_SDA, GPIO_OTYPE_OD, GPIO_OSPEED_2MHZ, GPIO_SDA); gpio_bit_write(PORT_SDA, GPIO_SDA, SET); // 初始化为高电平 }3.2.2 软件模拟I2C底层时序I2C通信就像两个人打电话有一套固定的“开场白”、“说话”和“结束语”的规则。下面这几个函数就是实现这套规则的。// I2C起始信号SCL高电平时SDA从高变低 void IIC_Start(void) { SDA_OUT(); SDA(1); delay_1us(5); SCL(1); delay_1us(5); SDA(0); delay_1us(5); // SDA产生下降沿 SCL(0); delay_1us(5); } // I2C停止信号SCL高电平时SDA从低变高 void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); SCL(1); delay_1us(5); SDA(1); delay_1us(5); // SDA产生上升沿 } // 主机发送应答(ack0)或非应答(ack1) void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); SDA(0); delay_1us(5); if(!ack) SDA(0); // 发送应答信号低电平 else SDA(1); // 发送非应答信号高电平 SCL(1); delay_1us(5); SCL(0); SDA(1); } // 主机等待从机DS1307的应答 // 返回0表示有应答返回1表示无应答可能设备不存在或通信失败 unsigned char IIC_Wait_Ack(void) { char ack 0; unsigned char ack_flag 10; // 超时计数 SDA_IN(); // SDA切换为输入模式准备读取 SDA(1); delay_1us(5); SCL(1); delay_1us(5); // 等待SDA被从机拉低应答 while( (GETSDA()1) ( ack_flag ) ) { ack_flag--; delay_1us(5); } if( ack_flag 0 ) // 超时无应答 { IIC_Stop(); return 1; } else // 收到应答 { SCL(0); SDA_OUT(); } return ack; } // I2C写一个字节8位数据 void IIC_Write(unsigned char dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟准备数据 for( i 0; i 8; i ) { // 从最高位(bit7)开始发送 SDA( (dat 0x80) 7 ); __nop(); __nop(); __nop(); // 短暂延时稳定数据 dat 1; delay_1us(6); SCL(1); // 时钟上升沿数据被采样 delay_1us(4); SCL(0); // 时钟拉低准备下一位 delay_1us(4); } } // I2C读一个字节 unsigned char IIC_Read(void) { unsigned char i, receive 0; SDA_IN(); // SDA设为输入准备读取从机数据 for(i0; i8; i ) { SCL(0); delay_1us(5); SCL(1); delay_1us(5); // 时钟上升沿读取数据 receive 1; // 左移为下一位腾出空间 if( GETSDA() ) // 如果SDA为高电平 { receive | 1; // 将该位置1 } delay_1us(5); } return receive; }3.2.3 DS1307的读写函数有了底层的I2C“电话线”我们就可以给DS1307“打电话”了。这里涉及到两个关键点器件地址DS1307的写地址是0xD0读地址是0xD1。数据格式DS1307内部寄存器存的是BCD码不是我们直接用的十进制数。所以写入前要转BCD读出后要转回十进制。BCD码用4位二进制数来表示1位十进制数0-9。例如十进制数23转换成BCD码就是0010 0011即0x23。// 向DS1307指定寄存器地址(add)写入数据(dat) // 返回0成功1或2表示通信失败 unsigned char Write1307(unsigned char add, unsigned char dat) { unsigned char temp; /* 10进制转BCD码 */ temp dat / 10; // 取十位 temp 4; // 左移4位放到高4位 temp temp | (dat % 10); // 加上个位组成BCD码 IIC_Start(); IIC_Write(0xD0); // 发送器件写地址 if( IIC_Wait_Ack() 1 ) return 1; // 无应答失败 IIC_Write(add); // 发送要写的寄存器地址 if( IIC_Wait_Ack() 1 ) return 2; // 无应答失败 IIC_Write(temp); // 发送BCD码数据 IIC_Wait_Ack(); // 等待应答 IIC_Stop(); return 0; } // 从DS1307指定寄存器地址(add)读取数据 // 返回255表示失败否则返回转换后的十进制数 unsigned char Read1307(unsigned char add) { int i 0; unsigned char temp; unsigned char dat; IIC_Start(); IIC_Write(0xD0); // 发送器件写地址 if( IIC_Wait_Ack() 1 ) return 255; IIC_Write(add); // 发送要读的寄存器地址 if( IIC_Wait_Ack() 1 ) return 255; // 重新发起起始条件切换为读操作 do { i; if( i 20 ) return 255; // 超时判断 delay_1ms(1); IIC_Start(); IIC_Write(0xD1); // 发送器件读地址 } while(IIC_Wait_Ack() 1); // 等待应答 dat IIC_Read(); // 读取一个字节数据BCD码 IIC_Send_Ack(1); // 发送非应答信号表示读取结束 IIC_Stop(); /* BCD码转10进制 */ temp dat 4; // 取高4位十位 dat dat 0x0F; // 取低4位个位 dat dat temp * 10; // 组合成十进制数 return dat; }3.2.4 时间设置与读取函数这是最终给我们用的两个高级函数一个用于设置初始时间一个用于读取当前时间。// 全局时间结构体变量 _time_struct_ RTC_Time; // 获取当前时间并存入全局变量 RTC_Time void Get_RTC_Time(void) { RTC_Time.sec Read1307(0x00); // 秒寄存器 RTC_Time.min Read1307(0x01); // 分寄存器 RTC_Time.hour Read1307(0x02); // 时寄存器 RTC_Time.week Read1307(0x03); // 星期寄存器 RTC_Time.date Read1307(0x04); // 日寄存器 RTC_Time.month Read1307(0x05); // 月寄存器 RTC_Time.year Read1307(0x06); // 年寄存器 } // 设置DS1307的初始时间 // 参数范围年(00-99), 月(01-12), 日(01-31), 星期(01-07), 时(00-23或01-12), 分(00-59), 秒(00-59) // 示例设置 2023年4月7日星期五13点57分00秒 - Set_RTC_Time(23, 4, 7, 5, 13, 57, 0) void Set_RTC_Time(uint8_t year, uint8_t month, uint8_t date, uint8_t week, uint8_t hour, uint8_t min, uint8_t sec) { // 注意秒寄存器最高位(CH位)是时钟停止位写0才能使能振荡器开始计时。 // 所以设置秒时要确保最高位是0。我们的Write1307函数传入的sec是0-59转换后最高位就是0。 Write1307(0x00, sec); // 设置秒 Write1307(0x01, min); // 设置分 Write1307(0x02, hour); // 设置时 Write1307(0x03, week); // 设置周 Write1307(0x04, date); // 设置日 Write1307(0x05, month); // 设置月 Write1307(0x06, year); // 设置年 }重要提示DS1307的秒寄存器地址0x00的最高位第7位是时钟停止位CH。当这个位是1时芯片内部振荡器停止时钟不走。当它是0时时钟才开始运行。所以第一次给模块上电或者更换电池后你必须调用Set_RTC_Time函数设置一次时间这个操作会自动将CH位清零时钟才会开始走。如果你发现时间不更新首先检查这里。4. 在main函数中测试驱动写好了最后一步就是在主函数里调用它看看效果。#include gd32f4xx.h #include systick.h #include bsp_usart.h #include stdio.h #include bsp_ds1307.h // 包含我们的驱动头文件 int main(void) { // 系统初始化 nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); systick_config(); // 初始化滴答定时器用于delay usart_gpio_config(115200U); // 初始化串口用于打印 DS1307_GPIO_Init(); // 初始化DS1307的I2C引脚 // !!! 关键步骤 !!! // 第一次上电或者模块电池没电后需要设置一次时间。 // 设置好后把这行代码注释掉否则每次重启都会重置时间。 // Set_RTC_Time(23, 4, 7, 5, 13, 57, 0); // 设置时间为 2023-4-7 星期五 13:57:00 printf(DS1307 RTC Demo Start\r\n); while(1) { Get_RTC_Time(); // 读取当前时间 // 通过串口打印出来 printf(Date: 20%02d-%02d-%02d Week:%d\r\n, RTC_Time.year, RTC_Time.month, RTC_Time.date, RTC_Time.week); printf(Time: %02d:%02d:%02d\r\n\r\n, RTC_Time.hour, RTC_Time.min, RTC_Time.sec); delay_1ms(1000); // 每秒打印一次 } }将代码编译下载到梁山派开发板打开串口助手波特率115200你应该能看到每秒更新一次的时间信息输出。第一次运行时记得先取消Set_RTC_Time那行的注释设置好正确时间然后再注释掉重新编译下载。这样一个独立于单片机系统的精准时钟就成功运行起来了。这个驱动我已经在几个需要记录数据时间戳的项目里稳定使用过。如果你遇到时间读出来全是255或者不对首先检查硬件连接特别是SCL和SDA有没有接反然后检查GPIO是否配置为开漏输出并上拉最后确认是否在第一次上电时成功设置了时间即CH位被清零。