ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32驱动DS1302实时时钟完整教程:协议、代码与踩坑记录

STM32驱动DS1302实时时钟完整教程:协议、代码与踩坑记录 手头正好有个项目要用到实时时钟翻了一圈方案最后还是选了DS1302配STM32。这颗芯片虽然老但胜在接口简单、资料多、成本低非常适合做学习入门和一般带时间戳功能的嵌入式项目。这篇笔记就把我完整的驱动过程和踩坑记录整理出来代码已经开源放在文末说明的位置希望能帮你少走点弯路。1. 项目整体设计与方案选型先说结论DS1302是一颗Dallas现在叫Maxim被ADI收了出的串行实时时钟芯片支持BCD码格式的秒、分、时、日、月、星期、年自带31字节静态RAM还能做涓流充电给后备电池补电。它和MCU之间只需要三根线CE片选使能、SCLK串行时钟、I/O数据标准的SPI扩展方案但注意它和标准SPI有区别后面我会专门讲。当初纠结过要不要直接用STM32内置RTC后来还是选了外部DS1302原因有三个第一STM32内置RTC在断电后需要VBAT引脚单独供电而且很多型号的RTC校准和时钟源配置比较绕后备电池切换电路还得自己搭。DS1302的方案直接把电池座接在VCC2引脚上芯片内部自带电源切换电路主电源掉电后自动切到备用电池省事很多。第二DS1302走时精度取决于外接32.768kHz晶振这个频率的晶振非常成熟温漂特性比STM32内部RC振荡器稳。实测常温下一个月误差基本在10秒以内对大多数应用完全够用。第三DS1302对新手极其友好协议简单到可以逐位操作不像I2C那样有地址应答时序也不像标准SPI那样有极性和相位配置。你只要会操作GPIO的高低电平和延时就能把它跑起来。当然现在也有不少人直接上DS3231那芯片带温度补偿精度高一个数量级但价格也贵好几倍而且走I2C接口。如果项目对时间精度要求极高比如每天误差不能超过1秒建议直接DS3231如果只是记录个日志时间戳、做个定时闹钟DS1302便宜又皮实完全够用。我这个项目属于学习笔记性质重点是把底层协议吃透所以选了DS1302。选型时还考虑过用硬件SPI去接DS1302后来实测发现没必要。因为DS1302的三线协议虽然长得像SPI但它的数据是在SCLK上升沿写入、下降沿读出的而且I/O线是双向的需要在读写之间切换方向。硬件SPI处理这种模式比较别扭反而用GPIO模拟更灵活。GPIO模拟还有个额外好处任意三个引脚都能接画板子的时候布线自由度极大。2. 硬件连接与原理细节2.1 引脚功能与连接方案DS1302的封装最常见的是DIP-8引脚不多逐个说引脚编号名称功能说明1VCC2主电源接3.3V或5V2X132.768kHz晶振输入3X232.768kHz晶振输出4GND接地5CE片选使能高电平有效6I/O串行数据输入/输出7SCLK串行时钟输入8VCC1备用电池正极接纽扣电池我用的STM32F103C8T6最小系统板DS1302模块是网上几块钱那种现成的上面已经焊好了晶振、退耦电容和电池座引脚排针直接引出。连接方式非常简单CE - PA1SCLK - PA0I/O - PA2VCC - 3.3VGND - GND有一点需要提醒网上卖的DS1302模块很多是给5V单片机设计的板载了上拉电阻到VCC。如果你用3.3V的STM32模块的VCC就接3.3V不要接5V否则I/O引脚电平可能超过STM32的承受范围。我的模块实测3.3V供电完全正常工作。2.2 电源切换与后备电池设计DS1302的电源管理是它的一大亮点。芯片内部有个电源切换电路比较VCC1和VCC2的电压谁高就用谁。正常工作时VCC2接主电源3.3VVCC1接纽扣电池3V芯片自动选择VCC2主电源断电后VCC2掉到0芯片自动切到VCC1的纽扣电池维持走时。这个切换过程是硬件自动完成的软件完全不用管。设计注意一个细节VCC1接纽扣电池时电池电压一般3.0V~3.3V而VCC2接3.3V时两者非常接近。为了确保正常工作时优先用主电源VCC2不要和VCC1串二极管之类的东西DS1302内部已经处理好了电源路径。另外纽扣电池座一定要买带弹片的接触不良会导致时间频繁丢失。还有一个容易忽略的点DS1302的涓流充电功能。如果你用的是可充电的LIR2032或者法拉电容可以通过设置寄存器0x90开启涓流充电给备用电源补电。如果用的是不可充电的CR2032千万不要开这个功能否则电池会鼓包甚至漏液。我的项目用的就是CR2032所以代码里特意把涓流充电寄存器清零了。2.3 晶振的选择与连接DS1302必须外接32.768kHz的晶振才能振荡走时。这个晶振不是普通的6PF负载的贴片晶振DS1302数据手册推荐的是6pF负载电容的石英晶振常见的有圆柱晶振49S封装和贴片晶振MC-306之类。网上卖的模块一般已经焊好了但如果你自己画板子注意晶振的两个引脚要尽量靠近X1、X2走线短且等长不要穿过大电流区域。晶振不起振或者走时不准90%的问题出在晶振上。我踩过的坑是某次手头只有12.5pF负载的晶振焊上去之后走时明显偏慢一天能慢三四秒。这是因为负载电容不匹配导致振荡频率偏移。解决办法要么换6pF晶振要么在X1、X2对地各加一个15~20pF的微调电容并联调整但这属于补救措施最好还是直接买对规格的晶振。3. 软件驱动设计与协议分析3.1 DS1302的命令字格式DS1302的所有操作都是通过写命令字节开始的。命令字节是8位格式如下位7位6位5~位1位01RAM/CKA4~A0RD/WR最高位固定为1。第6位用来区分操作时钟寄存器还是RAM为0操作时钟寄存器为1操作RAM。第5到第1位是寄存器地址。最低位是读写方向为1表示读为0表示写。举个例子要写秒寄存器地址0x00命令字节是0x80写或0x81读。要写控制寄存器地址0x07写保护命令字节是0x8E。这个地址计算方式非常规律写代码时可以抽象成一个函数传入寄存器地址和读写标志自动拼出命令字节。3.2 寄存器映射与BCD码DS1302的时钟寄存器从0x00到0x07含义如下地址寄存器名数据范围说明0x00秒0~59bit7为时钟暂停位1时停振0x01分0~59无特殊位0x02时1~12或0~23bit7为12/24小时制选择0x03日1~31无特殊位0x04月1~12无特殊位0x05星期1~71周一0x06年0~99存年的后两位0x07写保护-最高位WP1时禁止写入特别注意DS1302内部所有时间数据都用BCD码存储。什么是BCD码就是一个字节的高四位存十位低四位存个位。比如十进制数23BCD码就是0x23二进制是0010 0011。而不是普通二进制的0x17。这一点特别容易搞错如果你直接按十六进制数值去解析读出来的时间会完全不对。命令字0x8E写控制寄存器的WP位时在写时间数据之前必须把WP清0否则写入会被忽略。这个写保护机制是为了防止系统跑飞时意外修改时间所以写完时间后建议再把WP置1提高安全性。3.3 三线时序的逐位实现DS1302的时序核心可以概括成一句话CE拉高开始通信每个SCLK上升沿写入或移出1位数据命令字节先发之后根据命令字的读写方向决定是继续发数据字节还是读数据字节。整个传输过程以CE拉低结束。写时序MCU向DS1302发数据的步骤CE先拉低然后准备数据。将I/O引脚配置为输出模式。CE拉高开始通信。发送命令字节每次在SCLK上升沿之前把I/O置为对应数据位然后产生一个上升沿。通常的做法是先拉高SCLK设置I/O再拉低SCLK。因为DS1302是在SCLK的上升沿采样I/O数据所以数据必须在上升沿之前准备好。命令字节发送完后紧接着逐位发送数据字节时序和命令字节一样。全部发完后CE拉低结束通信。读时序MCU从DS1302读数据的步骤相比写时序有一个关键区别同样CE拉低I/O配置为输出CE拉高。发送命令字节方式完全一样。命令字节的最后一位发送完成后芯片会在SCLK的下降沿把数据放到I/O线上。所以接下来要把I/O引脚切换为输入模式然后产生SCLK脉冲在每个SCLK上升沿读取I/O引脚的电平。读完8位后CE拉低结束通信。注意读时序里那个切换输入模式的时机。如果切换晚了或者忘了切换I/O线一直被MCU的输出驱动着DS1302根本拉不动这根线读回来的数据全是引脚当前输出电平的值要么全0要么全1典型症状就是读出来0xFF或者0x00。这是新手最容易犯的错误。3.4 为什么不用硬件SPI可能有人会问DS1302的命令字序列和SPI很像能不能直接用SPI外设驱动我试过结论是能通但不划算。原因主要有两点第一DS1302的读时序要求在命令字节最后一位之后马上把I/O切换为输入并且数据是在SCLK下降沿输出的这个切换时机用硬件SPI很难精确控制尤其在CubeMX生成的HAL库代码里你得在SPI传输完成的回调里立刻改GPIO模式时序上容易出问题。第二STM32的SPI外设是全双工的发送和接收同时进行而DS1302半双工通信虽然也能用发送时忽略接收数据接收时发0xFF给时钟但多出来的无用收发反而增加理解成本。GPIO模拟的话代码逻辑一目了然出问题也好查。我的建议是学习阶段老老实实用GPIO模拟把DS1302的时序吃透如果以后做高并发、频繁读取时间的项目再来考虑硬件SPI优化也不迟。4. 核心功能代码实现完整代码已开源这里挑几个关键函数详细展开把所有细节说清楚。4.1 底层引脚操作首先定义引脚宏方便移植到别的板子#define DS1302_GPIO_PORT GPIOA #define DS1302_CE_PIN GPIO_PIN_1 #define DS1302_SCLK_PIN GPIO_PIN_0 #define DS1302_IO_PIN GPIO_PIN_2 #define DS1302_CE_HIGH() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_CE_PIN, GPIO_PIN_SET) #define DS1302_CE_LOW() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_CE_PIN, GPIO_PIN_RESET) #define DS1302_SCLK_HIGH() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET) #define DS1302_SCLK_LOW() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET) void DS1302_IO_Output(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS1302_IO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(DS1302_GPIO_PORT, GPIO_InitStruct); } void DS1302_IO_Input(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS1302_IO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DS1302_GPIO_PORT, GPIO_InitStruct); }这里有个小细节I/O引脚在输出模式下我选了推挽输出速度选LOW就够。因为DS1302的通信速率本身不高GPIO翻转速度太快反而可能在长线上产生振铃。另外初始状态要让SCLK保持低电平CE保持低电平这是DS1302的静态要求。4.2 字节读写函数写一个字节的核心就是前面说的时序代码不复杂但每个延时都要到位void DS1302_WriteByte(uint8_t data) { DS1302_IO_Output(); for (uint8_t i 0; i 8; i) { if (data 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); DS1302_SCLK_HIGH(); delay_us(2); DS1302_SCLK_LOW(); delay_us(2); data 1; } }注意这里先发送低位LSB firstDS1302的协议是低位先行的。每次SCLK拉高前必须先把I/O的电平设置好然后等2微秒左右再拉高SCLK确保数据稳定后芯片才采样。读一个字节uint8_t DS1302_ReadByte(void) { uint8_t data 0; DS1302_IO_Input(); // 关键切换为输入 for (uint8_t i 0; i 8; i) { data 1; DS1302_SCLK_HIGH(); delay_us(2); if (HAL_GPIO_ReadPin(DS1302_GPIO_PORT, DS1302_IO_PIN)) data | 0x80; DS1302_SCLK_LOW(); delay_us(2); } return data; }读字节时我先拉高SCLK再读数据这样对应的是读取I/O线上当前的数据位。实际根据数据手册DS1302是在SCLK下降沿之后把下一位数据放到I/O线上所以有些实现是在SCLK拉低后延时再读。两种写法都能跑通关键在于你的延时是不是足够让数据稳定。我的经验是在SCLK拉高后延时2微秒读取然后SCLK拉低这样时序对齐最稳。4.3 命令发送与时间读写有了字节读写函数命令发送就很简单了void DS1302_WriteReg(uint8_t reg, uint8_t data) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(reg 1); // reg是寄存器地址左移一位拼上写标志0 DS1302_WriteByte(data); DS1302_CE_LOW(); } uint8_t DS1302_ReadReg(uint8_t reg) { uint8_t data; DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte((reg 1) | 0x01); // 最低位置1表示读 data DS1302_ReadByte(); DS1302_CE_LOW(); return data; }这里用reg 1来拼命令字很巧妙因为命令字的bit0是读写标志bit1~bit5是寄存器地址直接把寄存器地址左移一位再按位或上读写标志就行。比如寄存器0x00秒写命令是0x00 1 0x00加上最低位写标志0就是0x00等等这里有个坑实际命令字是0x80或0x81因为最高位必须为1。我说得再清楚一点DS1302命令字的最高位固定是1寄存器地址只占bit1~bit5。所以正确的命令字应该是0x80 | (reg 1)写或0x81 | (reg 1)读。上述代码里简写成了reg 1前提是你调用时传进来的reg已经带了0x80的头。我习惯把寄存器地址定义为完整命令字的基础值#define DS1302_ADDR_SEC 0x80 // 秒寄存器写命令基地址 #define DS1302_ADDR_MIN 0x82 #define DS1302_ADDR_HOUR 0x84 #define DS1302_ADDR_DAY 0x86 #define DS1302_ADDR_MONTH 0x88 #define DS1302_ADDR_WEEK 0x8A #define DS1302_ADDR_YEAR 0x8C #define DS1302_ADDR_CTRL 0x8E这样写起来就很直观读秒寄存器用DS1302_ReadReg(DS1302_ADDR_SEC | 0x01)写秒寄存器用DS1302_WriteReg(DS1302_ADDR_SEC, bcd_data)。4.4 时间写入与读取写时间函数注意顺序是先关写保护、再写时间、最后开写保护void DS1302_SetTime(DS1302_Time_t *time) { uint8_t bcd; // 关闭写保护 DS1302_WriteReg(DS1302_ADDR_CTRL, 0x00); // 写入秒、分、时、日、月、星期、年 bcd DecToBcd(time-sec); DS1302_WriteReg(DS1302_ADDR_SEC, bcd); bcd DecToBcd(time-min); DS1302_WriteReg(DS1302_ADDR_MIN, bcd); // 小时寄存器注意位7是12/24小时制标志 bcd DecToBcd(time-hour); DS1302_WriteReg(DS1302_ADDR_HOUR, bcd); bcd DecToBcd(time-day); DS1302_WriteReg(DS1302_ADDR_DAY, bcd); bcd DecToBcd(time-month); DS1302_WriteReg(DS1302_ADDR_MONTH, bcd); bcd DecToBcd(time-week); DS1302_WriteReg(DS1302_ADDR_WEEK, bcd); bcd DecToBcd(time-year); // 存年的后两位 DS1302_WriteReg(DS1302_ADDR_YEAR, bcd); // 打开写保护 DS1302_WriteReg(DS1302_ADDR_CTRL, 0x80); // 涓流充电关掉保护不可充电电池 DS1302_WriteReg(0x90, 0x00); }批次写入的好处是减少CE拉高拉低的次数时序更紧凑。每个寄存器写完后芯片内部会自动把数据更新到对应计数器不需要额外操作。读时间函数要特别注意连续读多个寄存器时如果中间有进位比如秒从59变成00分进位可能会出现不一致。我处理的办法是连续读两次比较秒寄存器是否一致不一致就再读一次。最简单的是直接读回来先用着如果项目对一致性要求高可以在读之前先暂停时钟把秒寄存器最高位置1读完再恢复但这样会引入秒级跳变一般不建议。void DS1302_GetTime(DS1302_Time_t *time) { uint8_t sec1, sec2; do { sec1 DS1302_ReadReg(DS1302_ADDR_SEC | 0x01); time-sec BcdToDec(sec1 0x7F); // 去掉最高位时钟暂停标志 time-min BcdToDec(DS1302_ReadReg(DS1302_ADDR_MIN | 0x01) 0x7F); time-hour BcdToDec(DS1302_ReadReg(DS1302_ADDR_HOUR | 0x01) 0x3F); // 去掉12/24小时标志和保留位 time-day BcdToDec(DS1302_ReadReg(DS1302_ADDR_DAY | 0x01) 0x3F); time-month BcdToDec(DS1302_ReadReg(DS1302_ADDR_MONTH | 0x01) 0x1F); time-week BcdToDec(DS1302_ReadReg(DS1302_ADDR_WEEK | 0x01) 0x07); time-year BcdToDec(DS1302_ReadReg(DS1302_ADDR_YEAR | 0x01)); sec2 DS1302_ReadReg(DS1302_ADDR_SEC | 0x01); } while (sec1 ! sec2); // 检测跨秒边界保证时间一致性 }BCD转换函数很简单uint8_t DecToBcd(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); } uint8_t BcdToDec(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); }4.5 初始化与主程序调用DS1302的初始化其实非常简单因为芯片不需要像LCD那样复杂的上电时序。只需要确认一下时钟是否在走如果秒寄存器最高位是1时钟暂停就重新设置一次时间。void DS1302_Init(void) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); uint8_t sec DS1302_ReadReg(DS1302_ADDR_SEC | 0x01); if (sec 0x80) // 时钟暂停标志位为1说明没在走 { DS1302_Time_t default_time { .year 24, .month 1, .day 1, .hour 0, .min 0, .sec 0, .week 1 }; DS1302_SetTime(default_time); } }主程序里读取时间的调用DS1302_Time_t current_time; DS1302_Init(); while (1) { DS1302_GetTime(current_time); printf(20%02d-%02d-%02d %02d:%02d:%02d 星期%d\r\n, current_time.year, current_time.month, current_time.day, current_time.hour, current_time.min, current_time.sec, current_time.week); HAL_Delay(1000); }串口输出我建议加上格式化的星期映射把数字1~7转成周一到周日这里就不展开了。5. 常见问题与调试经验这部分梳理我在调试DS1302过程中遇到的全部典型问题按症状分类整理成速查表现象可能原因排查方式与解决办法读回来的数据全是0xFFI/O引脚没有切换为输入模式检查命令字节发送后是否调用了DS1302_IO_Input()读回来的数据全是0x00命令字节错误或CE时序不对用示波器或逻辑分析仪抓CE、SCLK、I/O时序确认命令字最高位是1时间不走一直是同一个值秒寄存器最高位被置1时钟暂停读秒寄存器看bit7置0后重新写秒值写入时间后立刻被清掉写保护位WP为1写时间前先写0x00到控制寄存器写完再写0x80走时误差特别大晶振负载电容不匹配换6pF负载的32.768kHz晶振检查PCB走线掉电后时间丢失纽扣电池没接好或者电池没电测量VCC1引脚电压检查电池座接触通信不稳定偶尔读出错误GPIO速度配置过高或线太长降低GPIO翻转速度延时加大到5us杜邦线尽量短涓流充电寄存器写入无效写保护未关闭0x90寄存器受WP位控制先关写保护再设置5.1 读回数据全0xFF的经典坑这个坑我几乎每次移植都会踩一次。症状就是不管读哪个寄存器读回来的都是0xFF。查到最后一定是忘了切换GPIO模式。很多人的代码逻辑是发送命令字节时I/O是输出模式发完最后一位后紧接着就循环读数据。但DS1302在命令字节的最后一位的下降沿之后才会把第一位数据放到I/O线上这个过程需要一点时间。如果代码没有先切换I/O为输入模式就开始读那么I/O引脚仍然是MCU的推挽输出状态DS1302根本拉不动它读到的就是当前输出电平的值通常是高电平所以是0xFF。解决的办法就是严格按照读时序来发送完命令字节后立刻切换I/O方向为输入然后延时至少1微秒再产生SCLK脉冲读取数据位。我把这个切换放在了DS1302_ReadByte()里面这样每次读字节前强制成输入模式不容易漏。5.2 上电后时间不走的坑另一个高频问题程序烧进去串口能读到时间但时间就是不走永远停在写入的初始值。这个问题十有八九是秒寄存器最高位被置1了。DS1302的秒寄存器0x00最高位叫做CHClock Halt默认状态不确定如果为1芯片就停止振荡时间不走。写入秒数时必须确保最高位是0。我在DS1302_SetTime里先把十进制数转换BCD再直接写入如果初始时间字符串里没有特别处理这个位恰好是0那就没问题。但如果在调试过程中不小心写入了一个超过59的值比如秒写入了0x80那时间就永远停了。我的代码在读取秒寄存器时特意做了sec 0x7F的掩码操作就是去掉CH位。初始化时也检测了CH位的状态发现为1就重置时间这样能保证上电后芯片一定会走时。5.3 关于涓流充电的补充警告前面提过一次这里再强调DS1302的0x90寄存器可以配置涓流充电给VCC1上的可充电电池或超级电容充电。如果用的是CR2032这种一次性锂电池千万不要开启这个功能。这个寄存器比较特殊只有写成特定模式才会启用充电比如0xA5表示1个二极管加2kΩ电阻。我见过有人网上抄代码把0x90写成0xA5结果好好的CR2032用了一个月就鼓包了。最稳妥的做法是初始化时直接写0x00彻底关闭充电功能。如果你确定要用可充电电池再仔细看数据手册选合适的分压电阻和二极管配置。5.4 调试工具推荐调DS1302这种半双工三线协议强烈建议备一个逻辑分析仪几十块钱的就能用。抓时序的时候把CE、SCLK、I/O三根线同时接上采样率设1MHz以上然后对比数据手册上的时序图逐位检查。我第一次调通DS1302就是靠逻辑分析仪发现命令字发完之后I/O方向切换晚了2个字节导致前两个时间寄存器读出来全是0x00。这种问题用万用表根本看不出来用示波器也能查但逻辑分析仪可以长时间抓波形方便回放对比效率高得多。5.5 上位机时间校准思路做项目时经常需要给DS1302写一个初始时间调试时总不能每次都改代码里的结构体然后重新烧录。我的做法是做一个简单的串口命令协议在串口中断里识别类似SET:2024-01-15 10:30:00的命令字符串解析后调用DS1302_SetTime。这样通过串口助手就能随时校准时间不用频繁烧录。解析代码也很简单用sscanf就能搞定int year, month, day, hour, min, sec; if (sscanf((char*)rx_buffer, SET:%d-%d-%d %d:%d:%d, year, month, day, hour, min, sec) 6) { DS1302_Time_t t; t.year year % 100; t.month month; t.day day; t.hour hour; t.min min; t.sec sec; // 星期不填也可以DS1302会自动算不行星期必须手动算 // 用蔡勒公式或查表法计算星期 t.week CalcWeek(year, month, day); DS1302_SetTime(t); }这里有个细节DS1302的星期寄存器不会根据日期自动计算你必须自己算好再写入。很多新手把日期写对了星期不对还以为芯片坏了。一个星期几计算函数用蔡勒公式实现网上随便一搜就有这里不贴了。6. 移植到其他STM32型号的要点这个项目目前用的是STM32F103C8T6但代码几乎没有芯片特异性移植到其他STM32型号F4、F0、L4系列都行只需要改三个地方第一是引脚宏定义。根据你的板子实际连接的引脚改DS1302_CE_PIN、DS1302_SCLK_PIN、DS1302_IO_PIN对应的GPIO端口和引脚号。注意三个引脚最好分布在同一个GPIO端口代码不需要改宏定义改一下就行。第二是延时函数。我用了delay_us(2)在HAL库里对应DWT-CYCCNT或者HAL_Delay都不是特别准最简单的实现是空循环static void delay_us(uint32_t us) { uint32_t count us * (SystemCoreClock / 1000000) / 4; // 粗略估算 while (count--) { __NOP(); } }这个估算方法在72MHz主频下够用DS1302时序本来就允许微秒级误差。如果你用不同主频的芯片微调/4那个系数就行。要求严格的话建议用定时器微秒延时但这里完全没必要。第三是底层HAL函数名。如果你用标准外设库SPL而不是HAL库把HAL_GPIO_WritePin和HAL_GPIO_ReadPin替换成GPIO_WriteBit和GPIO_ReadInputDataBitGPIO_InitTypeDef的写法也要对应改逻辑完全一样。还有一点如果你的STM32工作电压是5V比如某些工业板DS1302的I/O电平参考VCC2可以直接兼容3.3V逻辑。但如果I/O上拉了5V而MCU是3.3V那要加电平转换或分压。STM32的GPIO大部分是FT5V容忍引脚但最好确认一下数据手册别硬接。7. 开源地址与后续扩展思路这个项目的完整代码包括main.c里的测试循环、串口校时协议代码、星期计算函数已经开源代码量不大总共不到三百行适合直接抄作业或者二次修改。开源文件里包含了Keil工程和CubeMX的.ioc文件导入后改一下引脚配置就能用。如果你在移植过程中遇到问题也可以对照这篇笔记排查基本覆盖了我踩过的大部分坑。后续扩展的话我个人觉得有几个方向值得试试。一是把DS1302和ESP8266/ESP32组合做一个自动对时的桌面时钟通过WiFi获取网络时间然后校准DS1302这样既保留了DS1302的独立走时能力又解决了手动校时的麻烦。二是把读取到的数据通过OLED显示出来做一个桌面小时钟顺便学习一下I2C OLED驱动。三是把DS1302接到FreeRTOS里作为一个独立任务定时刷新时间到共享变量其他任务读取时可以避免阻塞延时。这些扩展项目都不复杂但能把STM32的外设、通信协议、RTOS调度串起来对学习帮助很大。说回这颗芯片本身虽然DS1302已经诞生几十年了但它用最简单的三线协议实现了完整的RTC功能是理解嵌入式时序逻辑、学习通信协议非常好的教材级芯片。把它彻底搞懂之后再去碰SPI、I2C、甚至更复杂的SDIO、USB底层思路都是共通的看得懂数据手册的时序图写得出对应的GPIO操作代码然后通过仪器验证你的时序对不对。这个能力比单纯会用某个芯片的库函数值钱得多。
返回列表