前面几期麻烦点开头像找到分享查看,而且本期测评的视频已经发布,可以前往Blibli查看,链接如下:
https://www.bilibili.com/video/BV1NeEizrEQB
欢迎各位同志前往观看!
今天我来开源一个DS1302的模块代码,我们采用Nucleo-C092RC来驱动,那么让我们开始吧。
在这里我们先来看一下程序的现象,将代码烧到板卡中:

这时你可以看到现在电脑开始接收到年、月、日、时、分、秒这些必要的信息,代码特意写了跨年,可以看到各个数据在跨年中的转换也是正常的。这时LD1和LD2交替闪烁,这就是程序现象了。



这段程序最开始我参考了Arduino中的库来移植出来的。源代码的缺点是它的输出是一整个字符串输出,如果你需要把它显示在屏幕或者数码管上,你需要用命令将字符串切割,并且将切割后的字符串转换成数字;而我这个程序输出直接就是数字,方便显示和使用数据,且方便你将它移植到其它需要用到的不同I/O口及不同的MCU中。
这个自定义库.H文件里面一共有十个与外部代码交互的函数,分别是DS1302的初始化,写入初始时间1(时分秒设置),写入初始时间2(年月日周设置),从DS1302中读出时分秒,年月日周的各种数据命令。
库文件如下:
/*使用STM32C092RCT6驱动DS1302的库文件,支持从DS1302中写入、读取时间,24小时制
C70E敞车 2025年5月11日移植成功。
*/
#ifndef __STM32C092_DS1302_H
#define __STM32C092_DS1302_H
#include "main.h"
/*下方宏定义为DS1302引脚信息,改动引脚在此处修改.*/
#define DS1302_DAT_Pin GPIO_PIN_11
#define DS1302_DAT_GPIO_Port GPIOC
#define DS1302_CE_Pin GPIO_PIN_12
#define DS1302_CE_GPIO_Port GPIOC
#define DS1302_CLK_Pin GPIO_PIN_10
#define DS1302_CLK_GPIO_Port GPIOC
/******************************************************************************/
/* Include files */
/******************************************************************************/
void DS1302_Init(uint8_t Freq);//DS1302初始化(当前配置单片机频率)
void DS1302_SetTime1(uint8_t Hou,uint8_t Min,uint8_t Sec);//DS1302_SetTime1(时,分,秒),写入初始时间
void DS1302_SetTime2(int Yea,uint8_t Mou,uint8_t Date,uint8_t Week);//DS1302_SetTime1(年,月,日,周),写入初始时间
uint8_t DS1302_ReadSec(void);//DS1302读出秒数据
uint8_t DS1302_ReadMin(void);//DS1302读出分数据
uint8_t DS1302_ReadHour(void);//DS1302读出时数据
uint8_t DS1302_ReadDay(void);//DS1302读出日数据
uint8_t DS1302_ReadWeek(void);//DS1302读出周数据
uint8_t DS1302_ReadMouth(void);//DS1302读出月数据
int DS1302_ReadYear(void);//DS1302读出年数据(2000年开始)
#endif
这个自定义库.C文件里定义了各个GPIO的高低电平以及初始化,以及DAT线处于输入或输出模式的初始化,以方便完成数据的收发,然后就是延时、转换及其他的相关命令代码。
库文件如下:
/*使用STM32C092RCT6驱动DS1302的库文件,支持从DS1302中写入、读取时间,24小时制
C70E敞车 2025年5月11日移植成功。
*/
#include "STM32C092_DS1302.h"
uint8_t freq;
/*下方宏定义为DS1302所有寄存器地址,增加程序可读性,下方所有寄存器地址均为写入地址,读出地址在写入地址基础上+1.
例如DS1302_SecADD写地址为0x80,则读地址为0x81.*/
#define DS1302_SecADD 0x80 //秒寄存器地址
#define DS1302_MinADD 0x82 //分寄存器地址
#define DS1302_HourADD 0x84 //时寄存器地址
#define DS1302_DayADD 0x86 //日寄存器地址
#define DS1302_MouADD 0x88 //月寄存器地址
#define DS1302_WeekADD 0x8A //周寄存器地址
#define DS1302_YearADD 0x8C //年寄存器地址
#define DS1302_WriteProtect 0x8E//写保护寄存器地址
#define DS1302_Bat 0x90 //涓流充电寄存器地址
/*CLK,CE脚高电平/低电平*/
#define DS1302_CLK_1 HAL_GPIO_WritePin(DS1302_CLK_GPIO_Port, DS1302_CLK_Pin, GPIO_PIN_SET); //定义CLK线高电平
#define DS1302_CLK_0 HAL_GPIO_WritePin(DS1302_CLK_GPIO_Port, DS1302_CLK_Pin, GPIO_PIN_RESET); //定义CLK线低电平
#define DS1302_CE_1 HAL_GPIO_WritePin(DS1302_CE_GPIO_Port, DS1302_CE_Pin, GPIO_PIN_SET); //定义使能线高电平
#define DS1302_CE_0 HAL_GPIO_WritePin(DS1302_CE_GPIO_Port, DS1302_CE_Pin, GPIO_PIN_RESET); //定义使能线低电平
/*【无返回值无参数】DS1302配置GPIO初始化.*/
void DS1302_GPIO_Init(void){
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE();
HAL_GPIO_WritePin(GPIOC, DS1302_DAT_Pin|DS1302_CE_Pin|DS1302_CLK_Pin, GPIO_PIN_SET);
GPIO_InitStruct.Pin = DS1302_DAT_Pin|DS1302_CE_Pin|DS1302_CLK_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
}
/*【无返回值无参数】DAT线配置成输出模式.*/
void DS1302_DAT_OUT(void){
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE(); // 使能GPIO时钟
GPIO_InitStruct.Pin = DS1302_DAT_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(DS1302_DAT_GPIO_Port, &GPIO_InitStruct);
}
/*【无返回值无参数】DAT线配置成输入模式.*/
void DS1302_DAT_IN(void){
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE(); // 使能GPIO时钟
GPIO_InitStruct.Pin = DS1302_DAT_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(DS1302_DAT_GPIO_Port, &GPIO_InitStruct);
}
/*【无返回值无参数】配置微秒延时.*/
void Delay_us(uint32_t __us) {
__us*=freq/4;
while(__us){
__NOP();
__us--;
}
}
/*【有返回值有参数】十进制转换BCD.*/
int dec_bcd(int dec)
{
return ((dec / 10) << 4 | (dec % 10));
}
/*【有返回值有参数】BCD转换十进制.*/
int bcd_dec(int bcd)
{
return ((bcd & 0xF0) >> 4) * 10 + (bcd & 0x0F);
}
/*【有参数无返回值】DS1302写入一个字节*/
void DS1302_WriteByte(uint8_t dat){
DS1302_CLK_0;//初始时钟线置位0
for (uint8_t i = 0; i < 8; i++){ //开始传输8个字节的数据
DS1302_DAT_OUT();//把DAT脚配置成输出模式
if(dat & 0x01){
HAL_GPIO_WritePin(DS1302_DAT_GPIO_Port, DS1302_DAT_Pin, GPIO_PIN_SET);
}
else{
HAL_GPIO_WritePin(DS1302_DAT_GPIO_Port, DS1302_DAT_Pin, GPIO_PIN_RESET);
}
//digitalWrite(DS1302_DAT, dat & 0x01);
dat=dat>>1; //数据右移一位,准备传输下一位数据
Delay_us(2);
DS1302_CLK_1; //时钟线拉高,制造上升沿,SDA的数据被传输
Delay_us(2);
DS1302_CLK_0; //时钟线拉低,为下一个上升沿做准备
}
}
/*【无参数有返回值】DS1302读取一个字节*/
int DS1302_ReadByte(){
int dat=0;
for(uint8_t i=0;i<8;i++){ //开始传输8个字节的数据
DS1302_DAT_IN(); //把DAT引脚配置成输入模式
dat=dat>>1;//要返回的数据右移一位
if(HAL_GPIO_ReadPin(DS1302_DAT_GPIO_Port,DS1302_DAT_Pin)==1){//当数据线为高时,证明该位数据为 1
dat=dat|0x80;//要传输数据的当前值置为1
}
DS1302_CLK_1;//拉高时钟线
Delay_us(2);
DS1302_CLK_0;//制造下降沿
Delay_us(2);
}
return dat;//返回读取出的数据
}
/*【有参数无返回值】DS1302写入字节*/
void DS1302_Write(uint8_t add,uint8_t dat_Write){
DS1302_CE_0; //初始使能线置0
DS1302_CLK_0; //初始时钟线置为0
DS1302_CE_1; //初始使能线置1,传输开始
Delay_us(2);
DS1302_WriteByte(add); //写入需要写入的地址信息
DS1302_WriteByte(dat_Write);//写入需要写入的数据(BCD码格式写入)
DS1302_CLK_1; //传输结束,CLK线置1
DS1302_CE_0; //传输结束,使能线置0
}
/*【有参数有返回值】DS1302读取字节*/
int DS1302_Read(uint8_t add){
int dat_Read=0;
DS1302_CE_0; //初始使能线置0
DS1302_CLK_0; //初始时钟线置为0
DS1302_CE_1; //初始使能线置1,传输开始
Delay_us(2);
DS1302_WriteByte(add+1); //写入需要读取的地址
dat_Read=DS1302_ReadByte(); //把DS1302_ReadByte()命令中返回的值赋值给dat_Read
DS1302_CLK_1; //传输结束,CLK线置1
DS1302_CE_0; //传输结束,使能线置0
return dat_Read; //返回读取出的dat_Read数值
}
/*【无参数有返回值】DS1302去除写保护*/
void DS1302_ResetProtect(){
DS1302_Write(DS1302_WriteProtect,0x00); //写入地址为1302写保护位,写入数据为0000 0000,去除写保护
}
/*【无参数有返回值】DS1302加上写保护*/
void DS1302_SetProtect(){
DS1302_Write(DS1302_WriteProtect,0x80); //写入地址为1302写保护位,写入数据为1000 0000,加上写保护
}
/*【无参数无返回值】DS1302写入禁用涓流充电,模组上CR2032为不可充电电池,需要禁止充电功能*/
void DS1302_NotBat(){
//pinMode(DS1302_CLK,OUTPUT);
//pinMode(DS1302_CE,OUTPUT);
DS1302_ResetProtect(); //复位写保护
DS1302_Write(DS1302_Bat,0x00);//写入地址为DS1302涓流充电控制,写入数据为0000 0000,禁止涓流充电功能
DS1302_SetProtect(); //置位写保护
}
/*【无参数无返回值】DS1302初始化*/
void DS1302_Init(uint8_t Freq){
DS1302_GPIO_Init();//初始化DS1302引脚
freq=Freq;//给微秒延时一个主频频率数
DS1302_NotBat();//关掉DS1302涓流充电
}
/*【有参数无返回值】DS1302写入时 分 秒数据*/
void DS1302_SetTime1(uint8_t Hou,uint8_t Min,uint8_t Sec){ //DS1302_SetTime1(时,分,秒),写入初始时间
Hou = dec_bcd(Hou);
Min=dec_bcd(Min);//把分钟从十进制转换成BCD格式并赋值给分钟
Sec=dec_bcd(Sec);//把秒从十进制转换成BCD格式并赋值给秒
DS1302_ResetProtect(); //复位写保护
DS1302_Write(DS1302_HourADD,Hou);//写入地址为1302小时位,写入数据为转换成BCD码格式的小时
DS1302_Write(DS1302_MinADD,Min); //写入地址为1302分钟位,写入数据为转换成BCD码格式的分钟
DS1302_Write(DS1302_SecADD,Sec); //写入地址为1302秒位,写入数据为转换成BCD码格式的秒
DS1302_SetProtect(); //置位写保护
}
/*【有参数无返回值】DS1302写入年 月 日 周数据*/
void DS1302_SetTime2(int Yea,uint8_t Mou,uint8_t Date,uint8_t Week){//DS1302_SetTime1(年,月,日,周),写入初始时间
Yea = dec_bcd(Yea - 2000); //把年从十进制转换成BCD码并赋值给年
Mou = dec_bcd(Mou); //把月从十进制转换成BCD码并赋值给月
Date = dec_bcd(Date); //把日从十进制转换成BCD码并赋值给日
Week = dec_bcd(Week); //把周从十进制转换成BCD码并赋值给周
DS1302_ResetProtect();
DS1302_Write(DS1302_YearADD,Yea);
DS1302_Write(DS1302_MouADD,Mou);
DS1302_Write(DS1302_DayADD,Date);
DS1302_Write(DS1302_WeekADD,Week);
DS1302_SetProtect();
}
/*【无参数有返回值】DS1302读出秒数据*/
uint8_t DS1302_ReadSec(void){
int dat=bcd_dec(DS1302_Read(DS1302_SecADD));
return dat;
}
/*【无参数有返回值】DS1302读出分数据*/
uint8_t DS1302_ReadMin(void){
int dat=bcd_dec(DS1302_Read(DS1302_MinADD));
return dat;
}
/*【无参数有返回值】DS1302读出时数据*/
uint8_t DS1302_ReadHour(void){
uint8_t dat = DS1302_Read(DS1302_HourADD);
if (dat >= 0x80) {
dat = dat - 0x80;
if (dat >= 0x20) {
dat = dat - 0x20;
}
}
dat = bcd_dec(dat);
return dat;
}
/*【无参数有返回值】DS1302读出日数据*/
uint8_t DS1302_ReadDay(void) {
uint8_t dat = bcd_dec(DS1302_Read(DS1302_DayADD));
return dat;
}
/*【无参数有返回值】DS1302读出周数据*/
uint8_t DS1302_ReadWeek(void) {
uint8_t dat = bcd_dec(DS1302_Read(DS1302_WeekADD));
return dat;
}
/*【无参数有返回值】DS1302读出月数据*/
uint8_t DS1302_ReadMouth(void) {
uint8_t dat = bcd_dec(DS1302_Read(DS1302_MouADD));
return dat;
}
/*【无参数有返回值】DS1302读出年数据*/
int DS1302_ReadYear(void) {
int dat = bcd_dec(DS1302_Read(DS1302_YearADD)) + 2000;
return dat;
}
建议在DS1302的硬件部分加上上拉电阻,以保证DS1302通信稳定性——我手上的这一片DS1302是兼容片,它可以在5V时正常工作,但只要用3.3V供电,它就会通信错误。但是我有一次无意之中实验中发现,在CLK对地接一个差不多20到22PF的电容,在3.3V的情况下又可以工作了。

所以我在这个板子上预留了电容位置。如果说你的1302的模组不存在这个问题,就可以不用加电容,如果存在的话,你可以尝试一下我的方法,或许你通信错误的问题就能得到解决;还有就是我对这个晶振也进行了一下处理,晶振电路我没有无脑铺铜,我之前做过DS1302的模组,铺铜产生的寄生电容会把晶振的振荡频率拖慢,宏观上看就是RTC在一天之后就慢个两三秒钟,DS1302如果说你要是校准的好的话,是可以校的比较准的。
以下是main函数中的代码:
/*
*作者:下行路轨上的C70E 论坛ID钟详
*Nucleo-C092 demo程序任务11:使用DS1302 RTC模组设置时间后读取当前片内时间,将收到的数据发出给串口助手,同时控制LED交替闪烁。
*采用24小时制来进行计时。
*使用意法半导体Nucleo板卡,配置外置晶体48M,波特率9600
*前往龙安的列车即将到达,请站在黄线外排队
*/
/*包含所需头文件*/
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "STM32C092_DS1302.h"
#include "stdio.h"
/*函数声明*/
void SystemClock_Config(void);
/*主函数*/
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
DS1302_Init(48);//DS1302初始化
/*打印作者信息*/
printf("\r\nST Chinese Forum Evaluation Plan\r\n");
printf("\r\nBoard Mode:Nucleo-C092\r\n");
printf("\r\nDemo11:DS1302 RTC Test \r\n");
printf("\r\nReviewer:Xiang Zhong Bli:C70E\r\n");
DS1302_SetTime1(23,59,57);//设置时间23时59分57秒
DS1302_SetTime2(2025,5,11,7);//设置2025年5月11日周天
/*进入循环体*/
while (1){
HAL_GPIO_TogglePin(User_LED1_GPIO_Port,User_LED1_Pin);
printf("Year: %d\r\n", DS1302_ReadYear());
printf("Mouth: %d\r\n", DS1302_ReadMouth());
printf("Day: %d\r\n", DS1302_ReadDay());
printf("Week: %d\r\n", DS1302_ReadWeek());
printf("Time: %d:%d:%d\r\n", DS1302_ReadHour(),DS1302_ReadMin(),DS1302_ReadSec());
HAL_GPIO_TogglePin(User_LED2_GPIO_Port,User_LED2_Pin);
HAL_Delay(980);
}
}
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_FLASH_SET_LATENCY(FLASH_LATENCY_1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSE;
RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1) != HAL_OK)
{
Error_Handler();
}
}
/*printf函数支持(阻塞法打印数据)*/
int fputc(int ch,FILE *f)
{
HAL_UART_Transmit(&huart2,(uint8_t *)&ch,1,0xFFFF);//阻塞方式打印
return ch;
}
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
这里简单说明DS1302的校准原理,就是当你这个模组做出来之后,你通电让它运行一天,你看一下一天之内它是快了还是慢了,如果说你的时钟快了,那说明你晶振频率较高,你就需要加负载电容;但是如果如果说你的时钟慢了,就证明你的负载电容+杂散电容的总和稍微大了。当然也不可能完全校准,这是一种大致判断负载电容是否合适的方法。DS1302适配的是精度为±5PPM,负载电容为6PF的32768晶振。如果说你用的是精度为±5PPM,负载电容为12.5PF的32768晶振,那你就需要加在外部加两个负载电容,让外部补偿掉是6.5PF左右,以获得准确的时间。不过值得注意的是,使用负载电容后DS1302启动一定会被拖慢一点点。
这就是我对D1302的理解,希望对使用这个模块的同志们能有所帮助。不过像这种外置晶振RTC时钟来说的话它的精度确实不是那么好。但是它很便宜!不管是在某宝还是某创商城都差不多1.5-1.7就能买到一片。在精度要求不高或者可以联网获取时间的场合,你可以用它来计时,但是需要经常性的对时,用以消除它的误差。
以下是Nucleo板卡的CubeMX中的配置部分基本参考前面分享的几期,如下:



其实,这些代码和视频你可以看出,5月就搞好了,但是那时候很忙,就草草发布了视频到B站,没空更新。最近我们的上岗证考试刚刚复审结束,我前段时间都在忙考试的事情,咕咕咕了,现在闲下来,就给它更新掉。