好习惯网站,对外贸营销型网站建设的几点建议,供电公司企业文化建设,石家庄网站建设加q.479185700梁山派GD32F470实战#xff1a;DS3231高精度RTC模块I2C驱动移植与时间管理 最近在做一个需要长时间记录数据的小项目#xff0c;发现单片机自带的RTC#xff08;实时时钟#xff09;精度不够#xff0c;断电后还得重新设置时间#xff0c;挺麻烦的。后来找到了DS3231这个…梁山派GD32F470实战DS3231高精度RTC模块I2C驱动移植与时间管理最近在做一个需要长时间记录数据的小项目发现单片机自带的RTC实时时钟精度不够断电后还得重新设置时间挺麻烦的。后来找到了DS3231这个模块它自带电池精度高用起来很方便。今天我就把在梁山派GD32F470开发板上驱动DS3231的整个过程分享给大家从硬件连接到软件编写一步步带你搞定。1. DS3231模块是个啥为啥要用它1.1 模块简介DS3231是一个通过I2C总线通信的高精度实时时钟模块。我用的这个模块是从淘宝买的资料链接在文章最后会提供。这模块有几个特别实用的特点高精度每天误差只有±0.432秒比普通RTC准多了自带电池模块上有纽扣电池接口主电源断开后还能继续走时自动日历能自动处理大小月、闰年不用我们软件干预温度补偿内部有温度传感器能根据温度变化调整时钟精度1.2 关键参数参数数值说明工作电压2.3-5.5V兼容3.3V和5V系统工作电流200-300uA功耗很低适合电池供电通信接口I2C只需要两根线引脚数量4个VCC、GND、SCL、SDA计时精度±0.432秒/天年误差约2.5分钟注意模块工作电压范围是2.3-5.5V梁山派开发板是3.3V系统可以直接连接。2. 硬件连接怎么把模块接到开发板上连接特别简单就4根线。我选择用PB8和PB9这两个引脚因为它们是GD32的I2C0外设引脚不过咱们这里用GPIO模拟I2C用哪个引脚都行。DS3231模块引脚梁山派引脚连接说明VCC5V或3.3V接开发板的5V或3.3V电源GNDGND接地SCLPB8I2C时钟线SDAPB9I2C数据线提示虽然模块支持5V但为了安全起见我建议接3.3V。GD32是3.3V器件直接接5V可能会有风险。接线实物图大概是这样DS3231模块 梁山派开发板 VCC ---- 3.3V GND ---- GND SCL ---- PB8 SDA ---- PB9接好线后给模块装上纽扣电池CR2032这样即使开发板断电时间也不会丢失。3. I2C通信原理模块怎么和单片机说话3.1 I2C基础I2C是一种两线制的串行通信协议一根是时钟线SCL一根是数据线SDA。多个设备可以挂接在同一条总线上每个设备有唯一的地址。DS3231的器件地址是固定的写地址0xD0读地址0xD13.2 通信时序I2C通信有严格的时序要求咱们用GPIO模拟时需要特别注意。写数据流程发送起始信号SCL高电平时SDA从高变低发送器件写地址0xD0等待模块应答发送要操作的寄存器地址等待模块应答发送要写入的数据等待模块应答发送停止信号SCL高电平时SDA从低变高读数据流程发送起始信号发送器件写地址0xD0等待模块应答发送要读取的寄存器地址等待模块应答重新发送起始信号发送器件读地址0xD1等待模块应答读取数据发送非应答信号告诉模块不读了发送停止信号4. 软件驱动编写手把手写代码4.1 创建工程文件首先在工程里创建两个文件bsp_ds3231.c- 驱动源文件bsp_ds3231.h- 驱动头文件4.2 头文件定义先来看头文件这里定义了引脚、宏和数据结构#ifndef _BSP_DS3231_H_ #define _BSP_DS3231_H_ #include gd32f4xx.h // 引脚定义 - 使用PB8和PB9 #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 // SDA方向切换宏 #define SDA_IN() {gpio_mode_set(PORT_SDA, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_SDA);} #define SDA_OUT() {gpio_mode_set(PORT_SDA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_SDA);} // 引脚操作宏 #define SCL(BIT) gpio_bit_write(PORT_SCL, GPIO_SCL, BIT?SET:RESET) #define SDA(BIT) gpio_bit_write(PORT_SDA, GPIO_SDA, BIT?SET:RESET) #define GETSDA() gpio_input_bit_get(PORT_SDA, GPIO_SDA) // 时间数据结构体 typedef struct _RTC_TIME_STRUCT_ { unsigned char sec; // 秒 unsigned char min; // 分 unsigned char hour; // 时 unsigned char week; // 星期 unsigned char date; // 日 unsigned char month; // 月 unsigned char year; // 年后两位 }_time_struct_; extern _time_struct_ RTC_Time; // 函数声明 void DS3231_GPIO_Init(void); 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); #endif注意这里用开漏输出模式GPIO_OTYPE_OD因为DS3231模块是5V器件而GD32是3.3V。开漏模式可以避免电平冲突。4.3 GPIO初始化接下来是GPIO初始化函数配置PB8和PB9为开漏输出模式void DS3231_GPIO_Init(void) { // 打开SDA与SCL的引脚时钟 rcu_periph_clock_enable(RCU_SCL); rcu_periph_clock_enable(RCU_SDA); // 设置SCL引脚 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引脚 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); // 初始化为高电平 }4.4 I2C底层驱动函数这些是模拟I2C通信的基础函数每个函数都要严格按照时序编写// I2C起始信号 void IIC_Start(void) { SDA_OUT(); SDA(1); delay_1us(5); SCL(1); delay_1us(5); SDA(0); // SCL为高时SDA从高变低 - 起始信号 delay_1us(5); SCL(0); delay_1us(5); } // I2C停止信号 void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); SCL(1); delay_1us(5); SDA(1); // SCL为高时SDA从低变高 - 停止信号 delay_1us(5); } // 等待从机应答 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; } // 写一个字节 void IIC_Write(unsigned char dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟开始数据传输 for(i 0; i 8; i) { // 先发送最高位 SDA((dat 0x80) 7); __nop(); __nop(); __nop(); // 小延时 dat 1; delay_1us(6); SCL(1); // 时钟上升沿数据被采样 delay_1us(4); SCL(0); // 时钟拉低准备下一位 delay_1us(4); } } // 读一个字节 unsigned char IIC_Read(void) { unsigned char i, receive 0; SDA_IN(); // SDA设置为输入 for(i 0; i 8; i) { SCL(0); delay_1us(5); SCL(1); // 时钟上升沿 delay_1us(5); receive 1; // 左移一位准备接收新位 if(GETSDA()) // 读取SDA电平 { receive | 1; // 收到1 } delay_1us(5); } return receive; }4.5 BCD码转换DS3231内部使用BCD码存储时间而咱们平时用的是十进制所以需要转换/* 十进制转BCD码 */ // 比如十进制数23转BCD码23/102十位23%103个位 // 2左移4位变成0x20加上3变成0x23 /* BCD码转十进制 */ // 比如BCD码0x23转十进制0x23/162十位0x23%163个位 // 2*103234.6 DS3231读写函数这是最核心的部分实现了对DS3231寄存器的读写// 向DS3231写入数据 unsigned char Write_DS3231(unsigned char addr, unsigned char dat) { unsigned char temp; /* 十进制转BCD码 */ temp dat / 10; // 获取十位 temp 4; // 左移4位到高4位 temp dat % 10 temp; // 加上个位 IIC_Start(); IIC_Write(0xD0); // 发送器件写地址 if(IIC_Wait_Ack() 1) // 等待应答 return 1; // 失败 IIC_Write(addr); // 发送寄存器地址 if(IIC_Wait_Ack() 1) return 2; // 失败 IIC_Write(temp); // 发送数据BCD格式 IIC_Wait_Ack(); // 等待应答 IIC_Stop(); // 发送停止信号 return 0; // 成功 } // 从DS3231读取数据 unsigned char Read_DS3231(unsigned char addr) { int i 0; unsigned char temp; unsigned char dat; IIC_Start(); IIC_Write(0xD0); // 发送器件写地址 if(IIC_Wait_Ack() 1) return 255; // 失败 IIC_Write(addr); // 发送要读取的寄存器地址 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(); // 读取数据 IIC_Send_Ack(1); // 发送非应答信号 IIC_Stop(); // 发送停止信号 /* BCD码转十进制 */ temp dat / 16; // 获取高4位十位 dat dat % 16; // 获取低4位个位 dat dat temp * 10; // 组合成十进制 return dat; }4.7 时间设置和读取函数为了方便使用我封装了两个高级函数// 全局时间结构体变量 _time_struct_ RTC_Time; // 获取DS3231的完整时间 void Get_RTC_Time(void) { RTC_Time.sec Read_DS3231(0x00); // 秒寄存器 RTC_Time.min Read_DS3231(0x01); // 分寄存器 RTC_Time.hour Read_DS3231(0x02); // 时寄存器 RTC_Time.week Read_DS3231(0x03); // 周寄存器 RTC_Time.date Read_DS3231(0x04); // 日寄存器 RTC_Time.month Read_DS3231(0x05); // 月寄存器 RTC_Time.year Read_DS3231(0x06); // 年寄存器 } // 设置DS3231时间 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) { Write_DS3231(0x00, sec); // 设置秒 Write_DS3231(0x01, min); // 设置分 Write_DS3231(0x02, hour); // 设置时 Write_DS3231(0x03, week); // 设置周 Write_DS3231(0x04, date); // 设置日 Write_DS3231(0x05, month); // 设置月 Write_DS3231(0x06, year); // 设置年 }5. 在主函数中使用最后在main函数中初始化并使用DS3231#include gd32f4xx.h #include systick.h #include bsp_usart.h #include stdio.h #include bsp_ds3231.h int main(void) { nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组 systick_config(); // 滴答定时器初始化 1ms usart_gpio_config(115200U); // 串口初始化 DS3231_GPIO_Init(); // DS3231初始化 printf(DS3231 RTC Demo Start \r\n); // 第一次上电时需要设置时间设置后可以注释掉 // Set_RTC_Time(23, 4, 7, 5, 13, 57, 0); // 2023年4月7日 星期五 13:57:00 while(1) { Get_RTC_Time(); // 读取当前时间 // 通过串口打印时间 printf(%02d-%02d-%02d 星期%d\r\n, RTC_Time.year, RTC_Time.month, RTC_Time.date, RTC_Time.week); printf(%02d:%02d:%02d\r\n\r\n, RTC_Time.hour, RTC_Time.min, RTC_Time.sec); delay_1ms(1000); // 每秒读取一次 } }6. 实际使用中的几个坑点我在实际使用中遇到过几个问题这里分享给大家第一次使用必须设置时间新模块或者电池耗尽后需要调用Set_RTC_Time函数设置时间设置一次后模块会自己保持。BCD码转换容易出错DS3231用BCD码存储时间比如23点存储为0x23二进制0010 0011而不是0x17。转换时一定要小心。I2C时序要精确GPIO模拟I2C时延时很重要。太快了模块反应不过来太慢了影响效率。我用的是1us延时实际测试很稳定。开漏输出模式这个特别重要因为DS3231是5V器件GD32是3.3V必须用开漏模式加上拉电阻。梁山派开发板内部有上拉所以代码里配置了上拉。星期设置DS3231的星期值是用户自定义的但要连续。比如你定义1星期日那么2星期一3星期二以此类推。电池备份记得给模块装上CR2032电池这样开发板断电后时间还能继续走。7. 移植验证编译下载程序后打开串口助手波特率115200应该能看到类似这样的输出DS3231 RTC Demo Start 23-04-07 星期5 13:57:00 23-04-07 星期5 13:57:01 23-04-07 星期5 13:57:02时间每秒更新一次说明DS3231驱动移植成功了如果需要完整的工程代码可以从百度网盘下载链接https://pan.baidu.com/s/1pp44yjD1Dhh7U9iZ2a11IA提取码LCKF这个DS3231模块我用在好几个项目里了特别是需要长时间记录数据日志的设备上。它的精度确实不错一个月下来误差也就十几秒比单片机自带的RTC强多了。关键是断电后还能继续走时重新上电不用再设置时间特别方便。