我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

STM32 GPIO模拟时序驱动DS1302时钟芯片实战

STM32 GPIO模拟时序驱动DS1302时钟芯片实战 1. 为什么还要折腾一颗古董级时钟芯片DS1302 这颗芯片稍微有点年头的电子爱好者都不陌生。它诞生于上世纪末走的是三线串行接口靠一颗纽扣电池就能把年月日时分秒外加星期全部记住掉电不丢。放到今天看它既没有 I2C 那么省引脚精度也拼不过那些自带温补的时钟模块但它在学习类项目里依然活跃原因很实在便宜、好买、时序透明、资料铺天盖地拿来练手再合适不过。我这次把它接到 STM32 上目标很明确——不依赖现成的库自己从时序层面把驱动写出来顺便把单片机怎么跟一颗慢速外设对话这件事彻底搞明白。这篇笔记适合两类人一类是刚上手 STM32、想找个外设练 GPIO 时序的另一类是以前用过 DS1302、但一直靠复制粘贴代码、没真正搞懂读写逻辑的。整篇内容围绕STM32 驱动 DS1302展开从硬件连线、时序原理、寄存器操作到完整代码和踩坑记录全部摊开讲。需要先说明一点DS1302 的通信协议是它自己定义的跟标准 SPI、I2C 都不兼容所以你不能直接拿 STM32 的硬件 SPI 外设去套只能老老实实用普通 GPIO 模拟时序。这一点很多人第一次接触时会想当然结果调半天没反应。下面我会把每个环节的为什么都讲清楚让你不只是抄代码而是能自己判断问题出在哪。2. 硬件层连线、供电与那颗不能省的电池2.1 引脚功能与最小系统连线DS1302 一共 8 个引脚实际用到的核心信号线就三根CE有的资料叫 RST复位/片选、I/O数据线、SCLK串行时钟。另外还有 VCC2主电源、VCC1备用电池、X1/X232.768kHz 晶振和 GND。接线思路很直接我把它接到 STM32 的一组普通 GPIO 上DS1302 引脚功能接到 STM32说明VCC2主电源3.3V主供电VCC1备用电源纽扣电池正极掉电走时靠它GND地GND共地X1/X2晶振32.768kHz 晶振必须接否则不走时CE片选/复位任意 GPIO如 PA0拉高才开始通信I/O数据任意 GPIO如 PA1双向需切换输入输出SCLK时钟任意 GPIO如 PA2由主机产生这里有个关键点I/O 是双向的。写数据时 STM32 输出读数据时 STM32 要把它切成输入。所以这个引脚在代码里必须能动态切换方向不能一上来就固定成推挽输出。我见过不少人读出来永远是 0xFF 或者 0x00八成就是忘了切输入模式。2.2 供电与电池的取舍VCC2 接 3.3V 没问题DS1302 的工作电压范围是 2.0V 到 5.5V3.3V 完全在范围内。VCC1 接一颗 CR2032 纽扣电池标称 3V。芯片内部有个电源切换逻辑主电源正常时用 VCC2主电源掉到电池电压以下时自动切到 VCC1。这个切换是硬件自动完成的软件不用管。注意电池正极一定要接对而且建议串一个二极管或者直接用带焊脚的电池座。我早期图省事直接把电池焊在板子上结果想换电池时非常麻烦。另外如果 VCC1 不接电池掉电后时间就归零每次上电都得重新设置调试阶段会很烦。晶振这块DS1302 要求外接32.768kHz晶振这个频率是 2 的 15 次方芯片内部做 15 级二分频正好得到 1Hz 的秒信号。晶振旁边一般还要配两个负载电容具体容值看晶振规格书常见是 6pF 到 12pF。如果走时偏快或偏慢很多时候就是负载电容没匹配好而不是芯片的问题。2.3 上拉电阻要不要加I/O 和 CE、SCLK 这几根线理论上 DS1302 内部对 I/O 有弱上拉但实际用下来尤其是杜邦线拉得比较长的时候加一个 4.7k 到 10k 的外部上拉会更稳。我自己的板子上 I/O 加了一个 10k 上拉到 3.3V读数据从来没出现过毛刺。如果你发现读出来的数据偶尔跳变先别怀疑代码量一下波形多半是信号完整性的事。3. 时序原理把三线串行这件事讲透3.1 通信的基本节奏DS1302 的通信有个固定套路先把 CE 拉高然后每个 SCLK 上升沿写一位、下降沿读一位。注意这个细节——写的时候数据在上升沿被芯片采样读的时候芯片在下降沿把数据放到 I/O 线上。所以写和读的时序是错开的这也是为什么它不能简单套 SPI 的原因。一次完整的操作分两步先发一个命令字节再发或收数据字节。命令字节决定了你要操作哪个寄存器、是读还是写。命令字节的格式是这样的第 0 位最低位1 表示读0 表示写第 1 到 6 位寄存器地址第 7 位最高位固定为 1这是 DS1302 的启动位必须为 1举个例子要写秒寄存器地址 0x80命令字节就是 0x80要读秒寄存器命令字节就是 0x81。这个规律很整齐写命令 地址读命令 地址 | 0x01。3.2 数据是低位先出这是另一个容易踩的坑DS1302 的数据传输是LSB first最低位先发。也就是说你发一个字节 0x80二进制 1000 0000实际先发出去的是最低位的 0最后才发最高位的 1。很多标准 SPI 默认是 MSB first如果你按那个习惯写循环数据就全反了。我在代码里处理的方式是循环 8 次每次取data 0x01发出去然后data 1。读的时候反过来每次把读到的位放到当前最高位然后右移。这个逻辑一定要记牢写反了现象就是能通信但数据全错。3.3 写一个字节的完整时序把上面两点合起来写一个字节的流程是CE 拉高循环 8 次I/O 设为输出把当前最低位放到 I/O 上SCLK 拉高芯片在上升沿采样短暂延时SCLK 拉低数据右移一位8 位发完后如果是写操作CE 拉低结束读一个字节则是CE 拉高循环 8 次I/O 设为输入SCLK 拉高短暂延时SCLK 拉低芯片在下降沿输出数据读 I/O 上的位放到结果的对应位置CE 拉低提示读的时候SCLK 拉低之后要稍微等一下再读 I/O给芯片一点时间把数据稳定放出来。这个延时不用长几个微秒就够但完全不加的话高速 MCU 上很容易读到还没稳定的电平。3.4 为什么延时不能省STM32 主频动辄 72MHz 甚至更高一个 GPIO 翻转就是纳秒级。DS1302 是颗老芯片它的时序要求虽然不算苛刻但也有最小脉宽和建立保持时间的要求。如果你不加任何延时SCLK 可能快到芯片根本反应不过来。我的做法是在每次电平变化后加一个几微秒的短延时用简单的空循环实现就行不用上定时器。这个延时是保险丝宁可慢一点也别为了追求速度把通信搞挂。4. 寄存器地图时间到底存在哪4.1 时间寄存器的地址分布DS1302 内部有一片 RAM 和一组时间寄存器。时间寄存器从 0x80 开始每个占一个地址存的是BCD 码不是普通二进制。这一点极其重要后面转换的时候会专门讲。寄存器写地址读地址范围说明秒0x800x8100-59最高位是 CH时钟暂停位分0x820x8300-59时0x840x8500-2312/24 小时制可选日0x860x8701-31月0x880x8901-12星期0x8A0x8B01-07用户自定义含义年0x8C0x8D00-99只存后两位秒寄存器的最高位bit7是CH 位Clock Halt。这一位为 1 时芯片停止走时为 0 时才正常计时。所以第一次上电初始化的时候必须先把这一位清零否则你会发现时间永远不动。这个坑我踩过当时以为是晶振没起振查了半天才发现是 CH 位没清。4.2 写保护寄存器地址 0x8E 是写保护寄存器。它的 bit7WP 位为 1 时所有时间寄存器都写不进去为 0 时才允许写。所以每次要设置时间之前必须先往 0x8E 写 0x00 解除保护设置完再写回 0x80 重新上锁。这个设计是为了防止误写但如果你忘了解锁现象就是写命令发出去了读回来还是旧值非常迷惑。4.3 涓流充电寄存器地址 0x90 是涓流充电寄存器用来配置是否给备用电池充电。如果你用的是不可充电的纽扣电池这一位千万别乱开否则可能把电池充坏。我一般直接不碰它保持默认关闭状态。只有用可充电的电池或者超级电容时才需要按规格书去配置充电电阻和二极管组合。4.4 BCD 码绕不开的转换DS1302 里存的时间全是 BCD 码。所谓 BCD就是用四位二进制表示一位十进制数。比如十进制 25BCD 码是 0010 0101也就是 0x25而不是二进制的 0x19。这个区别如果不注意读出来的时间会完全对不上。转换逻辑很简单BCD 转十进制dec (bcd 4) * 10 (bcd 0x0F)十进制转 BCDbcd ((dec / 10) 4) | (dec % 10)我习惯把这两个操作封装成宏或者内联函数读写的时候统一转换避免在业务代码里到处手算。这样即使以后换别的时钟芯片只要改转换函数就行上层逻辑不用动。5. 代码实现从 GPIO 到完整驱动5.1 GPIO 初始化与方向切换先定义好三根线的宏方便换引脚#define DS1302_CE_PORT GPIOA #define DS1302_CE_PIN GPIO_PIN_0 #define DS1302_IO_PORT GPIOA #define DS1302_IO_PIN GPIO_PIN_1 #define DS1302_SCLK_PORT GPIOA #define DS1302_SCLK_PIN GPIO_PIN_2初始化时把 CE 和 SCLK 配成推挽输出I/O 先配成推挽输出写模式。关键是 I/O 的方向切换函数static void DS1302_IO_Output(void) { GPIO_InitTypeDef gpio {0}; gpio.Pin DS1302_IO_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DS1302_IO_PORT, gpio); } static void DS1302_IO_Input(void) { GPIO_InitTypeDef gpio {0}; gpio.Pin DS1302_IO_PIN; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(DS1302_IO_PORT, gpio); }这里用 HAL 库演示如果你用寄存器或者 LL 库思路一样就是改 CRL/CRH 或者 MODER 寄存器。切换方向这个动作在读操作里每次都要做别嫌麻烦。5.2 底层读写字节函数这是整个驱动的核心写对了这两个函数后面就顺了static void DS1302_WriteByte(uint8_t data) { DS1302_IO_Output(); for (uint8_t i 0; i 8; i) { if (data 0x01) HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_SET); else HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET); DS1302_Delay(); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); DS1302_Delay(); data 1; } } static uint8_t DS1302_ReadByte(void) { uint8_t data 0; DS1302_IO_Input(); for (uint8_t i 0; i 8; i) { data 1; HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET); DS1302_Delay(); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); DS1302_Delay(); if (HAL_GPIO_ReadPin(DS1302_IO_PORT, DS1302_IO_PIN) GPIO_PIN_SET) data | 0x80; } return data; }注意读函数里data 1放在最前面这样第一次读到的位最终会落在 bit7符合 LSB first 的规则。这个顺序我调过好几次才对写的时候一定要在纸上画一遍位的位置。5.3 写寄存器与读寄存器有了字节读写寄存器操作就是发命令 发/收数据static void DS1302_WriteReg(uint8_t cmd, uint8_t data) { HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET); DS1302_WriteByte(cmd); DS1302_WriteByte(data); HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); } static uint8_t DS1302_ReadReg(uint8_t cmd) { uint8_t data; HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET); DS1302_WriteByte(cmd); data DS1302_ReadByte(); HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); return data; }每次操作前先把 CE 和 SCLK 拉低是为了保证一个干净的起始状态。CE 拉高之后整个通信过程保持高电平直到操作结束才拉低。这个CE 包住整次通信的节奏是 DS1302 协议的基本要求。5.4 时间设置与读取设置时间的时候先解保护再逐个写最后上锁void DS1302_SetTime(uint8_t year, uint8_t month, uint8_t day, uint8_t week, uint8_t hour, uint8_t min, uint8_t sec) { DS1302_WriteReg(0x8E, 0x00); // 解除写保护 DS1302_WriteReg(0x8C, DEC2BCD(year)); DS1302_WriteReg(0x88, DEC2BCD(month)); DS1302_WriteReg(0x86, DEC2BCD(day)); DS1302_WriteReg(0x8A, DEC2BCD(week)); DS1302_WriteReg(0x84, DEC2BCD(hour)); DS1302_WriteReg(0x82, DEC2BCD(min)); DS1302_WriteReg(0x80, DEC2BCD(sec) 0x7F); // 清 CH 位启动走时 DS1302_WriteReg(0x8E, 0x80); // 重新上锁 }注意秒寄存器那里 0x7F就是把最高位 CH 清零让芯片开始走时。这一步如果漏了时间就永远停在设置的那一刻。读取时间就是反过来把每个寄存器读出来再转成十进制void DS1302_GetTime(Time_t *t) { t-sec BCD2DEC(DS1302_ReadReg(0x81) 0x7F); t-min BCD2DEC(DS1302_ReadReg(0x83)); t-hour BCD2DEC(DS1302_ReadReg(0x85) 0x3F); t-day BCD2DEC(DS1302_ReadReg(0x87)); t-month BCD2DEC(DS1302_ReadReg(0x89)); t-week BCD2DEC(DS1302_ReadReg(0x8B)); t-year BCD2DEC(DS1302_ReadReg(0x8D)); }读秒的时候 0x7F是屏蔽掉 CH 位读小时的时候 0x3F是屏蔽掉 12/24 小时制标志位。这些掩码操作看着不起眼但不加的话读出来的数值会莫名其妙偏大。6. 调试实录那些让人抓狂的坑6.1 时间不走先查 CH 位再查晶振现象设置完时间读回来数值对但过一会儿再读还是原来的值秒不动。排查顺序我总结成一条链先确认秒寄存器的 CH 位是不是 0再确认晶振有没有起振最后才怀疑芯片坏没坏。CH 位的问题占了一大半因为很多人写秒寄存器的时候直接写了 BCD 值没做 0x7F结果 CH 位被置 1芯片就停摆了。晶振的问题一般是没接、接错频率、或者负载电容不对。用示波器探头搭在 X1 上能看到 32.768kHz 的正弦波就说明起振了。6.2 读出来全是 0xFF 或 0x00这个现象基本锁定在 I/O 方向切换上。读操作之前必须把 I/O 切成输入如果一直是输出模式你读到的就是自己输出的电平要么全高要么全低。还有一种可能是上拉没加I/O 悬空读到的值随机跳。我的建议是读函数第一件事就切输入别偷懒。6.3 数据整体错位或全反如果读出来的时间数值看着像那么回事但完全不对比如设置 12 点读出来是 21 点那多半是 LSB/MSB 顺序搞反了。DS1302 是低位先出检查你的循环里是不是先处理了最高位。另一个可能是 BCD 转换写错了把 BCD 当二进制直接用了。这两种错误的现象很像区分方法是如果数值是位序颠倒那是 LSB 问题如果数值是十进制和十六进制混淆那是 BCD 问题。6.4 通信偶尔失败偶发性失败尤其是杜邦线连接的时候优先怀疑信号完整性。加短延时、加上拉电阻、缩短连线这三招基本能解决。如果是在面包板上搭的接触不良也是常见原因换一组孔位试试。软件层面可以在每次操作前把 CE 和 SCLK 都拉低复位一下保证状态干净。6.5 常见问题速查表现象可能原因排查动作时间不走CH 位为 1 / 晶振没起振检查秒寄存器写入值、示波器看晶振读全 0xFF/0x00I/O 没切输入 / 无上拉检查方向切换、加 10k 上拉数值全错LSB/MSB 反 / BCD 转换错核对位序、核对转换函数写不进去写保护没解除先写 0x8E 为 0x00偶发失败信号完整性 / 接触不良加延时、加上拉、换连线走时偏快偏慢负载电容不匹配按晶振规格调整电容提示调试这类慢速串行外设最有效的工具其实是逻辑分析仪或者带协议解码的示波器。把 CE、SCLK、I/O 三根线一起抓下来对着时序图一位一位看问题一目了然。纯靠打印调试效率会低很多。7. 一些让代码更好用的封装思路7.1 把时间结构体独立出来我习惯定义一个Time_t结构体把年月日时分秒星期都放进去读写函数直接操作结构体。这样上层业务代码不用关心寄存器地址换芯片的时候只改底层。结构体里年份我一般存完整的 2000 年份显示的时候直接用不用每次加 2000。7.2 加一层时间合法性校验设置时间之前最好校验一下参数范围比如月份 1 到 12、日期不超过当月天数、小时不超过 23。DS1302 本身不会帮你校验你写进去什么它就存什么写个 13 月它也不报错但读出来就是错的。加一层校验能省掉很多莫名其妙的 bug。7.3 星期几的处理DS1302 的星期寄存器就是个 1 到 7 的计数器具体哪天对应星期几完全由你自己定义。我一般约定 1 是周一7 是周日。如果你要做日历显示记得在设置时间的时候把星期也算对或者干脆用年月日自己算星期不依赖芯片的星期寄存器。7.4 读取时的原子性读时间的时候如果正好跨过秒的进位可能出现秒读了 59分还是旧值的情况。严谨的做法是连读两次如果两次结果一致才采用或者先读秒、再读其他、最后再读一次秒如果两次秒相同说明中间没进位。对于一般应用这个概率很低但做计时类项目时值得注意。8. 这套驱动还能怎么扩展DS1302 除了时间寄存器还有 31 字节的通用 RAM地址从 0xC0 开始。这些 RAM 掉电也能靠电池保住可以用来存一些配置参数比如闹钟时间、设备编号之类的。用法和时间寄存器一样只是地址不同写命令就是地址本身读命令是地址加 1。如果你后面想升级到精度更高的方案可以看看那些自带温度补偿的时钟芯片走时误差能小一个数量级。但 DS1302 作为学习载体它的价值在于把时序这件事赤裸裸地摆在你面前没有硬件外设帮你兜底每一根线的电平都得你自己控制。把它的驱动写通一遍再去看 I2C、SPI 这些标准协议会有种豁然开朗的感觉。我个人在实际操作中的体会是这类慢速串行外设的调试八成的问题都出在三个地方方向没切对、位序搞反了、写保护忘了解。把这三件事在代码里做成固定套路剩下的就是耐心对着时序图核对。等你哪天不看资料就能默写出读写字节的循环这颗芯片也就算真正吃透了。
返回列表