尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于STM32 HAL库的DS1302实时时钟驱动从零实现与调试

基于STM32 HAL库的DS1302实时时钟驱动从零实现与调试 1. 时钟这件事为什么我最后还是选了 DS1302手头这块 STM32 板子要带时间显示功能最早我想的是直接用 MCU 内部的 RTC 加一颗 32.768kHz 晶振省一颗芯片、省几毛钱成本、PCB 也干净。真上手才发现事情没那么简单STM32 的 RTC 要占用一个独立的低速晶振电路走线稍微长一点或者晶振负载电容选得不对起振就飘日误差能到好几分钟换电池的时候还得考虑 VBAT 域的切换逻辑稍微马虎一点掉电就归零。折腾了两轮之后我把方案换成外挂一颗 DS1302问题一下就收敛了。DS1302 这颗片子年纪不小Dallas 出品后来归到 Maxim 门下定位就是涓流充电时钟芯片三根线CE、I/O、SCLK就能跟 MCU 对话自带 31 字节的静态 RAM主电断了靠一颗纽扣电池接着走时。它最大的特点是协议极简单、时序极宽松不需要硬件 SPI、不需要 I2C 控制器随便三个 GPIO 加上一把软件延时就能跑起来。对于学习笔记类的项目来说这一点太重要了——你不用先啃完 STM32 的 RTC 时钟树、不用搞明白备份域寄存器的解锁顺序只要会点 GPIO 翻转就能完整体验一次写驱动的全过程。这篇笔记记录的是我基于 STM32F103 系列把 DS1302 驱动从零写通的全过程代码走的是 HAL 库 纯 GPIO 模拟时序的路子开源出来给刚接触嵌入式驱动、想找一个麻雀虽小五脏俱全的练手项目的人参考。内容涵盖选型理由、硬件连接、三线协议时序、STM32 侧驱动分层、逐段代码实现、上板调试与踩坑排查最后还有一份工程组织建议。如果你用的是标准库或者别的 Cortex-M 芯片把 GPIO 操作那一层换掉就行核心逻辑一字不用改。DS1302 解决的其实是一个很朴素的需求在系统主电源不可靠、又想低成本保住时间的情况下把走时这件事从主控身上剥离出去。它不参与任何复杂运算只负责一秒一秒往前走主控想读就读、想写就写。这种把事情交给专用小芯片的思路在很多低成本项目里比用 MCU 内部资源更稳。2. 从硬件开始DS1302 到底怎么接、为什么这么接2.1 引脚定义与最小系统连接DS1302 一共 8 个脚典型封装是 DIP-8 或 SOP-8。它的引脚分布很规律引脚名称作用连接要点1VCC2主电源接 3.3V 或 5V与 VCC1 比较取高者供电2X1晶振输入接 32.768kHz 晶振一端3X2晶振输出接晶振另一端4GND地共地5CE片选/使能接 MCU GPIO低电平复位通信6I/O数据线接 MCU GPIO双向需上拉7SCLK串行时钟接 MCU GPIO由 MCU 驱动8VCC1备用电源接纽扣电池正极跟 MCU 相连的其实只有三根线CE、I/O、SCLK。VCC2 和 VCC1 的电源切换是芯片内部自动完成的谁的电压高就用谁不需要外部做 ORing 电路这一点比很多同类方案省事。典型接法是 VCC2 接 3.3V 系统电源VCC1 接一颗 3V 纽扣电池。晶振那块要单独说一句DS1302 内部集成了振荡器需要的负载电容但实际布板时X1、X2 的走线要尽量短、尽量对称走线长了容易引入寄生电容导致起振困难。我一般会在晶振两端各留一个 6pF 左右的预留焊盘起振不正常的时候补上去。32.768kHz 这个频率也不是随便选的——它是 2 的 15 次方芯片内部做 15 级二分频刚好得到 1Hz硬件实现成本最低。2.2 三线协议的本质它不是 SPI但很像很多人第一眼看到 DS1302 就以为它是 SPI因为它有片选、有时钟、有数据线。但严格说它是一种自定义的三线同步串行协议和 SPI 的关键差别在于DS1302 没有独立的 MOSI/MISOI/O 是一根双向线读写方向由命令字节决定而且它的时钟边沿定义跟 SPI 的 CPOL/CPHA 模式对不上——数据在 SCLK 上升沿被 DS1302 采样写入在下降沿被 DS1302 输出读出。正因为不是标准 SPI所以你不能直接拿 STM32 的 SPI 外设去驱它除非接受一堆别扭的时序拼凑老老实实软件翻转 GPIO 反而最省心。协议流程固定这么几步CE 从低拉高启动一次通信先发一个命令字节最低位是读写标志1 读 0 写其余位是寄存器地址紧接着发一个数据字节写操作或收一个数据字节读操作CE 拉低通信结束。命令字节和地址的对应关系是理解这颗芯片的钥匙后面讲寄存器映射会展开。2.3 上拉电阻一个不能省的小细节I/O 引脚在 DS1302 内部是开漏结构它自己只能把线拉低要拉高必须靠外部上拉。这是新手最容易栽的地方——不上拉读出来的数据全是 0你排查半天以为时序写错了其实是硬件缺了个电阻。典型上拉阻值取 4.7kΩ 到 10kΩ我一般用 4.7kΩ兼顾上升沿速度和静态功耗。如果 I/O 线走线比较长可以适当减小到 2.2kΩ 加快上升沿但别太小否则 DS1302 拉低时的灌电流会变大。至于 CE 和 SCLK它们是 MCU 单向驱动的不需要上拉除非你担心上电瞬间电平不定可以加弱下拉。2.4 电池与涓流充电CR2032 和 LIR2032 的天壤之别备用电源这块有个隐藏的坑必须讲清楚。DS1302 内部有一个涓流充电寄存器配置好了之后主电源在的时候它会给备用电池轻微充电。这个功能听起来贴心但它只对可充电电池有意义。如果你装的是普通不可充电的 CR2032 锂锰电池又错误地打开了涓流充电等于一直在给一颗不可充电电池灌电流长期下来会漏液、鼓包严重时腐蚀 PCB。所以我的做法是默认不打开涓流充电用普通 CR2032 就够了一颗能用好几年只有当项目明确使用 LIR2032可充电型号时才去配置涓流充电寄存器。这个寄存器后面代码实现那一节会给出具体配置值。这个细节在很多现成例程里都被忽略了直接甩一个 0xA5 进去用 CR2032 的用户就中招了。3. STM32 侧驱动怎么分层别把时序和应用糊在一起3.1 为什么不用硬件 SPI有人会问既然 STM32 有 SPI为什么不用原因前面提过DS1302 的时钟边沿和 SPI 模式对不上硬要凑的话得选一个看起来接近的模式再靠插入额外延时去补时序结果就是代码难懂、移植性差、还容易和别的 SPI 从机冲突。三根 GPIO 软件模拟代码几十行时序清清楚楚还方便你在示波器上直接抓波形对照学习价值高得多。3.2 三层结构GPIO 层、时序层、应用层我在写这类小驱动的时候习惯分三层不是为了装架构而是为了好改、好移植GPIO 抽象层把引脚的高/低/读/方向切换全部封装成宏换芯片只改这一层时序层实现写一个字节读一个字节这类跟协议强相关的函数和引脚无关应用层实现设置时间读取时间读写 RAM这些业务接口调用者只关心时分秒。这样分的好处是你从 F103 换到 F407只需要改 GPIO 层的几个宏你要把驱动从软件模拟换成别的实现只需要重写时序层。分层不是给自己找麻烦是为了以后少改代码。3.3 关键数据结构设计时间数据怎么传是个小问题。我见过有人用七个全局变量分别存年月日时分秒周调用处一堆g_year、g_month代码乱。更清爽的做法是定义一个结构体typedef struct { uint8_t year; // 00-99表示 2000-2099 uint8_t month; // 1-12 uint8_t day; // 1-31 uint8_t week; // 1-7用户自定义含义 uint8_t hour; // 0-23 uint8_t minute; // 0-59 uint8_t second; // 0-59 } DS1302_Time_t;这里有两个设计决定值得说。第一年份只存两位因为 DS1302 的年份寄存器就是 8 位 BCD能表示 00-99你在应用层把它解释成 2000-2099 就行别在驱动层硬编码世纪。第二所有字段都用十进制BCD 与十进制的转换藏在驱动内部。这个约定很重要——一旦你在外面传 BCD 进去用的人就懵了容易传错。驱动对外只暴露人类能读懂的时间。4. 逐段实现从 GPIO 翻转讲到完整读写4.1 GPIO 抽象与微秒延时先定义引脚和操作宏。以 STM32F103 HAL 库为例假设 CE 接 PB5、I/O 接 PB6、SCLK 接 PB7#define DS1302_CE_PORT GPIOB #define DS1302_CE_PIN GPIO_PIN_5 #define DS1302_IO_PORT GPIOB #define DS1302_IO_PIN GPIO_PIN_6 #define DS1302_SCLK_PORT GPIOB #define DS1302_SCLK_PIN GPIO_PIN_7 #define DS1302_CE_HIGH() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET) #define DS1302_CE_LOW() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET) #define DS1302_SCLK_HIGH() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET) #define DS1302_SCLK_LOW() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET) #define DS1302_IO_HIGH() HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_SET) #define DS1302_IO_LOW() HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET) #define DS1302_IO_READ() HAL_GPIO_ReadPin(DS1302_IO_PORT, DS1302_IO_PIN)I/O 的方向切换有两种写法我强烈推荐开漏输出方案这是这份笔记里最实用的一个小技巧。常规写法是读的时候把引脚切成交输入、写完再切回输出每次切换都要重配寄存器既慢又啰嗦。而开漏输出GPIO_MODE_OUTPUT_OD写 1 时引脚是释放状态靠外部上拉电阻拉高这时候直接读 IDR 就能读到线上的真实电平写 0 时真正拉低。所以读写都不用切换模式一次初始化成开漏输出就够了。I2C 软件模拟也是同样的技巧学一次能用一辈子。static void DS1302_GPIO_Init(void) { GPIO_InitTypeDef gpio {0}; // CE 和 SCLK 是单向输出推挽即可 gpio.Pin DS1302_CE_PIN | DS1302_SCLK_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio); // I/O 配成开漏输出写1释放、写0拉低读时直接读IDR gpio.Pin DS1302_IO_PIN; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; // 外部没上拉时用内部弱上拉兜底 HAL_GPIO_Init(GPIOB, gpio); DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_IO_HIGH(); }延时的选择很宽松。DS1302 手册里给的时序要求是 SCLK 高低电平最小 250ns 级别CE 建立时间 4us 级别而实际上你给到几微秒甚至几十微秒都完全没问题。我一般用一个简单的软件延时循环做基准static void DS1302_DelayUs(uint32_t us) { // 粗略延时值根据主频调整72MHz 下 8 次循环约 1us while (us--) { for (volatile uint32_t i 0; i 8; i) {} } }有人纠结要不要用 DWT 的 CYCCNT 做精确延时我的观点是驱动 DS1302 根本不需要精确延时。它不像某些高速器件要求纳秒级窗口多给点余量反而更稳。用 DWT 属于杀鸡用牛刀而且不同芯片、不同主频移植时还要重算得不偿失。当然如果你项目里已经有现成的精确延时函数直接拿来用也行。4.2 写一个字节与读一个字节这两个函数是整个驱动的核心写通了后面都是套模板。先看写字节注意数据必须在 SCLK 上升沿之前建立所以我习惯在 SCLK 低电平期间把数据放上去然后再拉高时钟产生上升沿static void DS1302_WriteByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { DS1302_IO_LOW(); // 先拉低准备改数据 if (dat 0x01) DS1302_IO_HIGH(); // 放最低位 else DS1302_IO_LOW(); DS1302_DelayUs(2); DS1302_SCLK_HIGH(); // 上升沿DS1302 采样 DS1302_DelayUs(2); DS1302_SCLK_LOW(); // 拉低准备下一位 DS1302_DelayUs(2); dat 1; // 低位先发 } }DS1302 是低位先传LSB first这一点和很多人直觉里的高位先传相反写代码时容易搞反现象是写进去的时间完全不对。记住这句先发 bit0最后发 bit7。读字节也是低位先出但要在下降沿之后采样static uint8_t DS1302_ReadByte(void) { uint8_t dat 0; for (uint8_t i 0; i 8; i) { DS1302_SCLK_LOW(); // 下降沿DS1302 输出数据 DS1302_DelayUs(2); dat 1; if (DS1302_IO_READ()) dat | 0x80; // 第一个bit放到bit7 DS1302_SCLK_HIGH(); // 拉高准备下一个下降沿 DS1302_DelayUs(2); } return dat; }这里有个容易绕晕的点明明是低位先出为什么读回来dat | 0x80因为每读一位就把dat右移一次第一个读到的位经过 8 次移位最终会落在 bit7。这么写实际上是把 LSB-first 的数据流还原成了我们习惯的字节表示。你可以对照着写一遍移位过程验证或者干脆抓一次波形实测看第一位到底是哪一位。4.3 寄存器映射与 BCD 编码DS1302 的寄存器地址是偶数用于写、奇数用于读两者只差最低位。比如秒寄存器写地址 0x80、读地址 0x81分是 0x82/0x83以此类推。完整映射如下寄存器写地址读地址范围与说明秒0x800x81bit7 是时钟停止位 CH1 表示停振分0x820x83BCD00-59时0x840x85bit71 为 12 小时制0 为 24 小时制日0x860x87BCD01-31月0x880x89BCD01-12周0x8A0x8B1-7含义用户自定年0x8C0x8DBCD00-99写保护0x8E0x8Fbit7WP1 禁止写涓流充电0x900x91配置充电二极管和电阻突发模式0xBE0xBF一次读写全部 7 个时间寄存器RAM0xC0-0xFC0xC1-0xFD31 字节偶数地址写、奇数读地址这块有个绝对要记住的坑写地址是偶数读地址是它加一。很多人写读函数时图省事直接用写地址结果读出乱码因为地址最低位就是读写标志位。我的做法是让底层函数接收写地址内部自动判断如果是要读就把地址| 0x01。再说 BCD。DS1302 里所有时间寄存器都存 BCD 码也就是用 4 个二进制位表示一位十进制数。比如 25 分存的是 0x25而不是十进制的 250x19。驱动程序里必须做 BCD 和十进制之间的转换// 十进制 - BCD static uint8_t DecToBcd(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); } // BCD - 十进制 static uint8_t BcdToDec(uint8_t bcd) { return ((bcd 4) * 10) (bcd 0x0F); }这两行一定要背下来任何跟 DS1302 打交道的代码都躲不开。用生活化的话说十进制是人话BCD 是芯片话进出驱动都要翻译一遍。4.4 单寄存器读写与突发模式有了字节读写和转换函数单寄存器读写就很简单了static void DS1302_WriteReg(uint8_t addr, uint8_t val) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); // 启动通信 DS1302_WriteByte(addr 0xFE); // 保证最低位为0即写 DS1302_WriteByte(val); DS1302_CE_LOW(); // 结束通信 } static uint8_t DS1302_ReadReg(uint8_t addr) { uint8_t val; DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(addr | 0x01); // 置读标志 val DS1302_ReadByte(); DS1302_CE_LOW(); DS1302_SCLK_LOW(); return val; }注意CE在每次通信前要先拉低再拉高这个先低再高的动作是必须的。因为 CE 从高到低的跳变会复位芯片内部的状态机如果它本来就停在高电平你直接拉高不起作用。我第一次调的时候没注意这个通信时好时坏后来看了手册才明白。如果要把时间一次性读完用突发模式更高效。发 0xBF 之后DS1302 会连续把秒、分、时、日、月、周、年七个寄存器依次吐出来省掉了七次独立的 CE 启停static void DS1302_BurstRead(uint8_t *buf7) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(0xBF); // 突发读命令 for (uint8_t i 0; i 7; i) { buf7[i] DS1302_ReadByte(); } DS1302_CE_LOW(); DS1302_SCLK_LOW(); }突发模式还能保证读到的七个字是同一时刻的避免你在依次读的过程中正好跨过秒进位导致读到59 分 60 秒这种尴尬组合。对时间精度要求高的场合突发模式是首选。4.5 初始化与时间设置DS1302 上电后秒寄存器的 CH 位可能是 1停振所以初始化第一步就是把 CH 清掉让晶振开始走。另外写时间前必须先解除写保护写完再加回来防止误写void DS1302_Init(void) { DS1302_GPIO_Init(); DS1302_WriteReg(0x8E, 0x00); // 解除写保护 DS1302_WriteReg(0x80, 0x00); // 秒清零同时清 CH 位启动振荡 // 默认不开涓流充电保护不可充电纽扣电池 DS1302_WriteReg(0x90, 0x00); DS1302_WriteReg(0x8E, 0x80); // 加回写保护 } void DS1302_SetTime(const DS1302_Time_t *t) { DS1302_WriteReg(0x8E, 0x00); // 解除写保护 DS1302_WriteReg(0x80, DecToBcd(t-second) 0x7F); // 清CH DS1302_WriteReg(0x82, DecToBcd(t-minute)); DS1302_WriteReg(0x84, DecToBcd(t-hour)); // bit70 即24小时制 DS1302_WriteReg(0x86, DecToBcd(t-day)); DS1302_WriteReg(0x88, DecToBcd(t-month)); DS1302_WriteReg(0x8A, DecToBcd(t-week)); DS1302_WriteReg(0x8C, DecToBcd(t-year)); DS1302_WriteReg(0x8E, 0x80); // 加回写保护 } void DS1302_GetTime(DS1302_Time_t *t) { uint8_t buf[7]; DS1302_BurstRead(buf); // 顺序秒 分 时 日 月 周 年 t-second BcdToDec(buf[0] 0x7F); // 屏蔽 CH 位 t-minute BcdToDec(buf[1]); t-hour BcdToDec(buf[2] 0x3F); // 屏蔽12/24制标志位 t-day BcdToDec(buf[3]); t-month BcdToDec(buf[4]); t-week BcdToDec(buf[5]); t-year BcdToDec(buf[6]); }写时寄存器时用DecToBcd(t-hour)直接写bit7 是 0说明是 24 小时制这正是我们想要的。如果你想用 12 小时制项目里一般没必要才需要把 bit7 置 1 并把小时数改成 12 小时表示实际项目里我几乎从不碰它。至于涓流充电寄存器的配置如果确实用了 LIR2032可以选常用值 0xA51 个二极管 2kΩ 电阻或 0xAB2 个二极管 8kΩ 电阻。为什么高 4 位必须是 1010因为芯片用这个模式识别你是不是真的想配置充电防止乱码误触发充电。用普通 CR2032 的话老老实实写 0x00别动它。5. 上板之后那些让人抓狂的问题和排查思路5.1 常见问题速查表驱动写完不代表就完事上板跑起来才是真正的考验。我把踩过的坑整理成一张表遇到问题先对照现象可能原因排查方向读出来全是 0xFF 或 0x00I/O 没有上拉电阻补 4.7kΩ 上拉或确认开漏配置正确时间完全不更新CH 位没清晶振没起振读秒寄存器看 bit7检查晶振和负载电容写进去读回来不一样写保护没解除写前先写 0x8E0x00时间数值离谱BCD 和十进制搞混检查转换函数看原始寄存器值时好时坏偶发乱码CE 没做先低再高复位每次通信前 CE 拉低一次读到的位顺序反了LSB/MSB 弄反确认低位先发读时移位方向换电池后时间归零电池没接或电量耗尽量 VCC1 电压检查电池座接触秒位一直停在某个值时钟停止位被置位重新初始化清 0x80 的 bit75.2 时序排查示波器是最好的老师如果手边有示波器或者逻辑分析仪DS1302 这类低速协议排查起来简直是降维打击。我一般会把 CE、SCLK、I/O 三根线同时接到分析仪上然后跑一次读时间对着波形看几个关键点一是命令字节。一次读操作发出的应该是比如 0x81读秒在波形上能看到 8 个时钟脉冲下I/O 线上依次出现的位。手工把波形上的位拼起来看看是不是 0x81如果拼出来是 0x80 或 0x82那说明你的地址处理或者移位方向有问题。二是边沿和数据的对应关系。重点确认 DS1302 是在上升沿采样。如果波形显示数据是在下降沿才建立那大概率你的写字节函数里数据建立和时钟翻转的顺序写反了得调整。三是CE 的时序。确认 CE 从低到高之后SCLK 才开始翻动通信结束时 CE 先拉低。如果 CE 在 SCLK 还在动的时候就被拉低会截断数据传输。没有分析仪也不是没法玩。可以用一个笨办法在代码里把每次读到的原始寄存器值通过串口打出来跟期望值对比。比如你设的是 12 分 30 秒读出来原始值是 0x12、0x30那就说明 BCD 存储没问题问题只在转换层如果读出来是 0x0C、0x1E那就是你把十进制当 BCD 写进去了芯片存的是它理解的 BCD两边对不上。5.3 电源与电池相关的坑电池这块的坑往往最隐蔽。有一次我做了一版测的时候一切正常放了两天再上电时间就归零了。查了半天发现是纽扣电池座的焊盘有一侧虚焊主电断的时候电池其实没供上电。所以上电后一定要实测 VCC1 的电压别只看电路图。还有个小细节VCC1 和 VCC2 谁供电是芯片自动判断的但如果主电源在关机时缓慢下降比如有大电容慢慢放电可能在某个中间电平上出现短暂的判断模糊。解决办法是在主电源和 VCC2 之间保证足够的电压差或者干脆在主电源回路里让关机时掉得快一点。这类问题一般只在低功耗场景才需要操心普通项目不用太纠结。另外上电瞬间的电平状态也值得注意。CE、SCLK、I/O 在 MCU 复位期间是浮空的DS1302 可能把它们当成无效的噪声。稳妥的做法是给 CE 加一个下拉电阻比如 10kΩ保证 MCU 没上电时 CE 是低电平通信不会误启动。这个电阻在大多数例程里都省掉了但量产项目里加上更放心。5.4 读数跳变和数据合理性校验即便驱动写得没问题读时间时偶尔也会遇到这一秒读到 59下一秒又回到 58的跳变。这通常是因为读的时候恰好跨过秒进位如果不是用突发模式一次性读就可能在读分和读秒之间被时钟打断。解决办法是用突发模式或者读两遍对比一致才采用。还有一个实用的做法在应用层加一层合理性校验。把读到的年月日时分秒都做范围检查月不能超过 12、日不能超过 31、秒不能超过 59任何一项越界就判定为读取异常重新读或者用上一帧数据兜底。这个校验不占多少代码但在电磁环境复杂或者电源不稳的场合能救你一命避免把乱码直接显示到界面上。我最初觉得这有点小题大做后来在一次电机干扰比较严重的项目里正是这层校验帮我挡住了界面上一闪而过的乱码。6. 工程怎么写才让人看得懂6.1 目录结构建议开源一份学习笔记光有能跑的代码还不够得让人一眼看明白结构。我一般这么组织ds1302_driver/ ├── Inc/ │ ├── ds1302.h // 对外接口和数据结构 │ └── main.h // 引脚宏定义集中放这里 ├── Src/ │ ├── ds1302.c // 驱动实现 │ └── main.c // 示例初始化 循环读时间打印 ├── README.md // 接线说明、使用步骤、注意事项 └── docs/ └── timing_note.md // 时序要点和典型波形说明把引脚定义单独抽到main.h或者专门的配置头文件里是因为不同项目接的 GPIO 不一样改配置文件比改驱动主体安全得多。别人拿到你的代码只需要改几行宏定义就能接到自己的板子上这比让他翻遍.c找引脚要友好得多。6.2 一份好的驱动笔记该写什么在给这份驱动配 README 的时候我总结了几个必写项。第一是接线表明确哪个引脚接哪里尤其是上拉电阻和电池这两个是新手最容易漏的。第二是最小使用示例最好是个十来行、复制粘贴就能跑的main函数让人三分钟看到效果。第三是注意事项把前面提到的 CH 位、写保护、BCD、上拉这些坑列清楚这是你踩过别人没踩过的地方含金量最高。我特别不建议在 README 里大段讲原理。原理放到代码注释里贴着代码走README 只讲怎么用和注意什么。因为看 README 的人往往只想赶紧跑起来等真遇到问题了才会去翻代码注释里的原理说明。急的人看怎么用慢的人看为什么分开写两拨人都舒服。另外开源代码里避免硬编码绝对路径和芯片型号是个好习惯。把平台相关的部分集中在一个文件里用条件编译或者简单的宏切换。这样你的 F103 版本能很方便地变成 F407 版本别人也更容易把你的代码用在自己的项目上。一份能被复用、能被读懂、能少踩坑的代码才算真正开源得有价值。说完这些我在实际写这份驱动时最深的一个体会是DS1302 这种老芯片之所以到现在还有人用不是因为它多先进而是因为它的边界足够清晰——它只干一件事把这件事干得透透的还给了你一颗宽裕的时序余量。写好它的驱动你也就摸到了嵌入式驱动开发最核心的那套思路把硬件行为抽象成函数把硬件约束写进注释把硬件坑点记进文档。这套思路比这颗芯片本身值钱得多。
返回列表