DS1302实时时钟芯片驱动详解:STM32裸机三线协议与BCD时间管理 📅 发布时间:2026/9/16 8:03:45 👁 浏览次数: 1. 为什么是 DS1302——从一块“电子钟芯”讲起你拆过老式电子台历吗或者翻过家里那台用了十年的万年历收音机里面往往藏着一块不起眼的黑色小芯片八脚封装印着“DS1302”四个字母。它不靠主控供电自带纽扣电池断电后时间照样走——这就是我们今天要聊的实时时钟RTC芯片 DS1302。在 STM32 项目里它不是最先进、也不是集成度最高的选择但却是我带新人入门时第一个亲手焊、第一段裸机驱动、第一块能独立跑通的外设芯片。为什么选它不是因为它多强大恰恰是因为它“够简单、够真实、够典型”。DS1302 是 Dallas现属 Maxim推出的串行 RTC 芯片采用三线同步通信CLK、CE、IO没有 I²C 的地址冲突问题也没有 SPI 的模式配置陷阱连时序都“慢得友好”最高通信速率仅 100kHz读写一次寄存器耗时约 10–20μs对新手极其宽容。它内置 31 字节 RAM可作用户数据区、支持秒/分/时/日/月/星期/年七位 BCD 编码时间、自动闰年补偿还带涓流充电电路——这些功能在车载仪表、智能鱼缸、温湿度记录仪、工业 PLC 的本地时钟模块中至今仍是可靠的选择。尤其当你用 STM32F103 这类资源有限的主控又不想为高精度 RTC 额外加晶振、校准电路、温度补偿算法时DS1302 就成了那个“不折腾、不掉链子、不烧脑”的务实答案。关键词里反复出现的“开源”和“学习笔记”恰恰点中了它的核心价值它不是黑盒数据手册公开Maxim 官网可下载 Rev.07 版 PDF协议清晰无专利壁垒它的驱动逻辑完全可手工推演不需要 HAL 库“一键生成”也不依赖 CubeMX 自动生成代码——这意味着你能真正看清每一根 GPIO 是怎么被拉高拉低的每一条指令背后对应的是芯片内部哪个寄存器、哪一位被置位。我见过太多初学者卡在 HAL_RTC_Init() 返回 ERROR 上却从没见人卡在 DS1302 的Write_Byte(0x8E, 0x00)这条指令上。因为后者你只要拿示波器抓三根线就能亲眼看到 CLK 的上升沿采样 IOCE 的高电平使能通信整个过程像看慢动作回放一样透明。这种“看得见、摸得着、改得动”的体验正是嵌入式学习最珍贵的起点。所以这不只是一篇驱动教程它是你和硬件建立信任关系的第一课当你的代码让一个物理芯片开始计时那种确定性带来的踏实感远胜于跑通一百个例程。2. 硬件连接与底层时序——GPIO 模拟三线协议的硬核细节2.1 引脚定义与物理连接别小看这三根线DS1302 的三线接口看似简单但引脚功能和电气特性必须抠到毫米级。它的三个信号线分别是SCLKSerial Clock由 STM32 主动输出的时钟信号上升沿采样下降沿准备数据。注意DS1302 不支持双向时钟SCLK 必须是纯输出且需严格满足最小高/低电平时间tWH≥ 1μstWL≥ 1μs否则通信会丢帧。RST/CEChip Enable高电平有效用于启动/终止一次通信事务。关键点在于CE 必须在 SCLK 为低电平时拉高且在通信结束前保持高电平。若 CE 在 SCLK 高电平时跳变DS1302 会误判为新命令起始导致后续字节错位。I/OData Input/Output双向数据线但在同一字节传输中方向固定写操作时 STM32 输出读操作时 STM32 输入。难点在于方向切换时机——必须在 CE 拉高后、第一个 SCLK 上升沿前完成 GPIO 方向配置且切换后需插入至少 1μs 延迟软件 NOP 或 __NOP()否则 DS1302 可能未完成内部状态机切换。实际接线时我推荐使用 STM32 的任意三个 GPIO如 PA0/PA1/PA2但务必避开复位、BOOT 引脚及存在默认外设功能的管脚。例如不要用 PA11/PA12USB DP/DN也不要选 PB10/PB11I²C2。我的标准接法是PA0 → SCLKPA1 → RST/CEPA2 → I/O所有引脚均配置为推挽输出SCLK、CE或开漏输出I/O配合上拉电阻。这里有个易错点DS1302 的 I/O 是漏极开路结构必须外接 4.7kΩ 上拉电阻至 3.3V非 5V否则读操作时 STM32 输入无法识别高电平。我曾因省掉这个电阻调试两小时才发现示波器上 I/O 线始终是浮空的“毛刺”而非稳定的高低电平。2.2 时序图解构手把手画出关键窗口DS1302 的通信时序核心就一张图见数据手册 Figure 2但新手常忽略三个致命窗口CE 建立时间tCSCE 从低变高后必须等待 ≥ 2μs 才能发送第一个 SCLK 上升沿。这是芯片内部使能电路的响应延迟软件上用__NOP(); __NOP();即可满足每个 NOP 约 0.5μs。数据建立时间tDSSCLK 上升沿采样 I/O 数据因此数据必须在上升沿到来前 ≥ 1μs 稳定。写操作时STM32 需在 SCLK 下降沿后立即更新 I/O 值并保持 ≥ 1μs读操作时STM32 需在 SCLK 下降沿后立即切换 I/O 为输入并在下一个上升沿前读取。字节间间隔tCH一个字节8 位传输完毕后CE 必须保持高电平且 SCLK 需至少维持一个完整周期高低 ≥ 2μs才能开始下一字节。若连续发送必须确保 SCLK 在第 8 个下降沿后再等一个完整周期才发第 9 个上升沿。我用示波器实测过当 SCLK 频率设为 50kHz周期 20μs上述所有窗口都能轻松覆盖但若盲目提高到 100kHz周期 10μstCH就可能被压缩到临界值导致偶发通信失败。因此初学者请坚持用 50kHz别追求“快”先求“稳”。等你能用逻辑分析仪抓出完整的 8 个 SCLK 脉冲、CE 高电平包络、I/O 数据波形后再尝试优化。2.3 GPIO 模拟实现不依赖库的裸机代码逻辑DS1302 驱动的本质就是用软件精准控制三根 GPIO 的电平跳变与时序。以下是我经过 17 个版本迭代后确认最鲁棒的裸机实现基于 STM32F103 标准外设库不依赖 HAL// 宏定义引脚便于移植 #define DS1302_SCLK_PIN GPIO_Pin_0 #define DS1302_CE_PIN GPIO_Pin_1 #define DS1302_IO_PIN GPIO_Pin_2 #define DS1302_GPIO_PORT GPIOA // 初始化全部设为推挽输出CE 初始为低 void DS1302_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // SCLK 和 CE 设为推挽输出 GPIO_InitStructure.GPIO_Pin DS1302_SCLK_PIN | DS1302_CE_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DS1302_GPIO_PORT, GPIO_InitStructure); // I/O 设为开漏输出需外接上拉 GPIO_InitStructure.GPIO_Pin DS1302_IO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; // 关键不是推挽 GPIO_Init(DS1302_GPIO_PORT, GPIO_InitStructure); // 初始状态SCLK0, CE0, IO1上拉 GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_SCLK_PIN | DS1302_CE_PIN); GPIO_SetBits(DS1302_GPIO_PORT, DS1302_IO_PIN); } // SCLK 上升沿先拉低再拉高 void DS1302_SCLK_High(void) { GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_SCLK_PIN); __NOP(); __NOP(); // 确保低电平 ≥1μs GPIO_SetBits(DS1302_GPIO_PORT, DS1302_SCLK_PIN); } // SCLK 下降沿先拉高再拉低 void DS1302_SCLK_Low(void) { GPIO_SetBits(DS1302_GPIO_PORT, DS1302_SCLK_PIN); __NOP(); __NOP(); GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_SCLK_PIN); } // CE 拉高必须在 SCLK 为低时执行 void DS1302_CE_High(void) { // 确保 SCLK 为低 while (GPIO_ReadInputDataBit(DS1302_GPIO_PORT, DS1302_SCLK_PIN)); GPIO_SetBits(DS1302_GPIO_PORT, DS1302_CE_PIN); __NOP(); __NOP(); // t_CS ≥2μs } // CE 拉低任意时刻均可 void DS1302_CE_Low(void) { GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_CE_PIN); }这段代码的精妙之处在于它把时序约束转化为可验证的 GPIO 操作序列。比如DS1302_SCLK_High()中的两次__NOP()就是为 tWLSCLK 低电平时间兜底而DS1302_CE_High()中的while循环强制等待 SCLK 归零杜绝了 CE 在 SCLK 高电平时跳变的风险。这些细节在 HAL 库的抽象层下是完全不可见的但正是它们决定了你的驱动能否在 -40℃ 到 85℃ 全温域稳定工作。3. 寄存器操作与时间管理——BCD 编码、RAM 访问与校准实战3.1 地址映射与命令字读懂 DS1302 的“语言”DS1302 的所有操作都通过地址数据完成其地址空间分为两类时间寄存器0x80–0x8D共 8 个寄存器但实际使用 7 个0x80 秒、0x82 分、0x84 时、0x86 日、0x88 月、0x8A 星期、0x8C 年每个寄存器 8 位存储 BCD 编码Binary-Coded Decimal。例如23 分 → 0x23而非 0x1709 日 → 0x09而非 0x09巧合相同但逻辑不同。RAM 寄存器0xC0–0xFD31 字节通用 RAM地址为奇数0xC1, 0xC3…0xFD用于存储用户数据。注意RAM 地址必须是奇数写入偶数地址会失败。所有读写操作都需发送一个 8 位命令字Command Byte其格式为1 0 0 0 A2 A1 A0 R/W。其中最高位恒为 1标识 DS1302 命令A2–A0 为寄存器地址的最低 3 位如秒寄存器 0x80 → A2A1A0000R/W 位1读0写。因此写秒寄存器的命令字是0b100000000x80读秒寄存器是0b100000010x81。这个设计意味着你永远不能直接“读-改-写”一个寄存器必须先读出原始值BCD 解码修改十位/个位再 BCD 编码写回。例如要把当前时间加 1 分钟不能简单sec_reg而要读出分寄存器0x82→ 得到 0x23BCD 解码minute ((0x23 0xF0) 4) * 10 (0x23 0x0F) 2*103 23minute→ 24BCD 编码new_min ((24 / 10) 4) | (24 % 10) 0x24写回 0x82。这个过程看似繁琐却是理解嵌入式数据表示的必经之路。我建议初学者手写一个BCD_To_Dec()和Dec_To_BCD()函数而不是依赖库函数——因为一旦你习惯用sprintf()处理时间就永远失去了对底层编码的直觉。3.2 时间初始化与读写流程一个字节一个字节地“对话”DS1302 的单字节读写是原子操作必须严格遵循“命令→数据”的两阶段流程。以下是写入秒寄存器地址 0x80的完整步骤拉高 CE调用DS1302_CE_High()等待 ≥2μs发送命令字 0x80循环 8 次每次SCLK 拉低设置 I/O 为输出输出命令字当前位MSB 在前SCLK 拉高上升沿采样SCLK 拉低为下一位准备发送数据字节同上循环 8 次发送要写入的 BCD 秒值如 0x15拉低 CE结束本次事务。读操作类似但在发送命令字如 0x81后需在每个 SCLK 上升沿前将 I/O 切换为输入并在上升沿后读取 I/O 电平。关键代码片段如下// 写一个字节addr 是寄存器地址如 0x80data 是 BCD 值 void DS1302_Write_Byte(uint8_t addr, uint8_t data) { uint8_t i; DS1302_CE_High(); // 发送命令字addr | 0x00因写操作 R/W0 for (i 0; i 8; i) { DS1302_SCLK_Low(); if (addr 0x80) GPIO_SetBits(DS1302_GPIO_PORT, DS1302_IO_PIN); else GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_IO_PIN); addr 1; DS1302_SCLK_High(); } // 发送数据字节 for (i 0; i 8; i) { DS1302_SCLK_Low(); if (data 0x80) GPIO_SetBits(DS1302_GPIO_PORT, DS1302_IO_PIN); else GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_IO_PIN); data 1; DS1302_SCLK_High(); } DS1302_CE_Low(); } // 读一个字节addr 是寄存器地址如 0x81R/W1 uint8_t DS1302_Read_Byte(uint8_t addr) { uint8_t i, data 0; DS1302_CE_High(); // 发送命令字addr | 0x01 for (i 0; i 8; i) { DS1302_SCLK_Low(); if (addr 0x80) GPIO_SetBits(DS1302_GPIO_PORT, DS1302_IO_PIN); else GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_IO_PIN); addr 1; DS1302_SCLK_High(); } // 切换 I/O 为输入关键 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DS1302_IO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DS1302_GPIO_PORT, GPIO_InitStructure); __NOP(); __NOP(); // 等待方向切换 // 读取数据LSB 在前DS1302 是 LSB-first for (i 0; i 8; i) { DS1302_SCLK_Low(); DS1302_SCLK_High(); // 上升沿采样 data 1; // 注意右移因 LSB 先来 if (GPIO_ReadInputDataBit(DS1302_GPIO_PORT, DS1302_IO_PIN)) data | 0x80; } // 恢复 I/O 为输出避免影响后续写操作 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_Init(DS1302_GPIO_PORT, GPIO_InitStructure); DS1302_CE_Low(); return data; }注意两个魔鬼细节一是读操作时 DS1302 返回的是 LSB-first最低位先传所以data 1二是读完必须将 I/O 恢复为输出模式否则下次写操作会失败。这些细节只有亲手抓波形、逐行单步调试才能刻进肌肉记忆。3.3 实战校准解决“每天慢 2 秒”的玄学问题DS1302 的晶振32.768kHz精度标称为 ±20ppm理论日误差约 ±1.7 秒。但实测中我遇到过新芯片日误差达 8 秒的情况。原因有三晶振负载电容不匹配、PCB 走线分布电容、环境温度漂移。校准不是“调个参数”那么简单而是系统工程硬件校准首选DS1302 支持“涓流充电”和“外部晶振微调”。其 0x90 寄存器TRICKLE CHARGER的 bit7-bit4 可设置充电电流bit3-bit0 设置二极管数量用于给备用电池充电。但这不影响走时精度。真正影响精度的是晶振匹配电容——DS1302 数据手册推荐 12.5pF但实际需根据所用晶振规格调整。我用网络分析仪测过将两个 15pF 电容改为 12pF 后日误差从 5.2 秒降至 0.8 秒。软件校准常用在主程序中定期如每小时读取 DS1302 时间与 NTP 服务器或 GPS 时间比对计算偏差 Δt然后按比例修正后续读数。例如若 1 小时后发现慢了 3 秒则每分钟自动加 0.000833 秒3/3600。但此法需额外时间源不适合离线设备。实用折中法推荐给初学者利用 DS1302 的 RAM 存储“校准偏移量”。首次上电时手动设置准确时间运行 24 小时后记录 DS1302 显示时间与标准时间的差值如慢 4.3 秒将其存入 RAM如地址 0xC1。此后每次读取时间后自动减去该偏移量。代码只需在DS1302_Get_Time()函数末尾加一行time-second - g_cal_offset; // g_cal_offset 从 RAM 读出 if (time-second 0) { time-second 60; time-minute--; }这种方法无需外部时间源成本为零且效果显著。我用此法将某款鱼缸控制器的日误差稳定在 ±0.5 秒内已持续运行 18 个月无漂移。4. 开源实践与学习笔记沉淀——从代码到文档的完整闭环4.1 开源项目的结构设计为什么目录比代码更重要“开源”不是把代码扔到 GitHub 就完事。一个真正可复用的 DS1302 驱动项目其目录结构本身就是知识体系的骨架。我维护的 stm32-ds1302-driver 仓库结构如下├── docs/ # 文档是灵魂 │ ├── hardware/ # 硬件设计指南含 PCB 布局图、BOM 表、晶振选型表 │ ├── protocol/ # DS1302 时序详解含示波器截图、逻辑分析仪导出 CSV │ └── faq.md # 常见问题如“为什么读出来全是 0xFF” ├── examples/ # 场景化示例 │ ├── basic_clock/ # 最简裸机时钟LED 显示秒闪 │ ├── rtc_with_lcd/ # 带 1602 LCD 的万年历 │ └── alarm_system/ # 闹钟蜂鸣器驱动 ├── src/ # 核心驱动 │ ├── ds1302.c # 主驱动含 BCD 转换、时间读写、RAM 访问 │ ├── ds1302_hal.c # HAL 封装层适配不同 STM32 系列 │ └── ds1302_config.h # 用户可配置项引脚定义、校准偏移、备用电池检测 ├── tests/ # 可验证性保障 │ ├── timing_test/ # 用定时器捕获 SCLK 周期验证时序精度 │ └── stress_test/ # 连续读写 10000 次统计错误率 └── .github/ # 社区规范 ├── CONTRIBUTING.md # 如何提交 PR必须附波形截图 └── ISSUE_TEMPLATE.md # Bug 报告模板强制填写 MCU 型号、晶振型号、示波器截图这个结构的设计哲学是降低贡献门槛提高使用信心。新人 clone 后直接看docs/protocol/就能理解时序本质想快速验证跑examples/basic_clock/即可遇到问题先查docs/faq.md再提 issue。我刻意避免在src/目录下放任何“高级功能”如自动网络校时因为 DS1302 的核心价值在于“确定性”追加复杂功能反而模糊了学习焦点。真正的“高级”体现在tests/目录——那里有我用 STM32F407 的高级定时器TIM1精确测量 SCLK 周期的代码误差小于 1ns这才是工程师该有的较真劲儿。4.2 学习笔记的写作技巧把“踩坑”变成“避坑指南”“学习笔记”不是流水账而是认知升级的刻度尺。我写笔记坚持三个原则问题驱动、证据支撑、可复现。例如关于“DS1302 读出全 0xFF”的问题我的笔记这样写现象调用DS1302_Read_Byte(0x81)返回 0xFF无论读哪个寄存器。排查过程用万用表测 VCC3.3VGND 通畅备用电池电压 2.9V → 电源正常示波器抓 SCLK有规则方波频率 50kHz → 时钟发出抓 CE高电平宽度 10μs符合 tCS要求 → 使能正常抓 I/O在 SCLK 上升沿时I/O 始终为高电平无数据变化 → 关键线索根因I/O 引脚未正确配置为浮空输入。在DS1302_Read_Byte()中GPIO_Init()后缺少__NOP()延迟导致方向切换未生效I/O 仍为输出DS1302 无法驱动线路。修复在GPIO_Init()后添加for(volatile int i0;i10;i);延迟10 个周期 ≈ 1μs。验证修复后I/O 波形出现清晰的数据跳变读值恢复正常。教训GPIO 方向切换不是瞬时的必须留出硬件响应时间。所有输入模式切换必须加延迟或读回寄存器确认。你看这不是“我遇到了问题然后解决了”而是构建了一个完整的故障树从现象出发用仪器逐层排除最终定位到硬件响应延迟这个微观机制。这样的笔记别人看了能立刻复现、立刻验证而不是“照着做但不知道为什么”。4.3 从个人笔记到社区贡献如何让代码被真正用起来开源的价值不在“公开”而在“被用”。为了让 DS1302 驱动走出我的工位我做了三件事提供“即插即用”的配置宏在ds1302_config.h中用户只需修改两行#define DS1302_SCLK_GPIO_PORT GPIOA #define DS1302_SCLK_GPIO_PIN GPIO_Pin_0其余引脚、时序参数、校准值均有合理默认值。我测试过 12 种常见 STM32 型号F0/F1/F3/F4/F7/H7全部兼容。录制 10 分钟“零基础实操视频”不讲原理只做三件事焊芯片 → 下载代码 → 用串口打印当前时间。视频里我故意暴露一个错误忘记接上拉电阻然后演示如何用万用表查出问题。观看完小白能独立完成。建立“问题-方案”映射表在 README.md 中用表格列出高频问题问题现象可能原因快速验证方法解决方案时间不准晶振负载电容不匹配用频谱仪测晶振实际频率更换匹配电容12pF→15pF读写失败CE 电平跳变时机错误示波器抓 CE/SCLK 相位修改DS1302_CE_High()增加while(SCLK1)RAM 数据丢失备用电池电压低于 2.0V万用表测电池电压更换 CR2032 电池这个表格是我从 237 个 GitHub Issue 中提炼的。它让使用者不再需要“猜”而是“查”。上周一位大四学生用这个表格15 分钟内解决了毕业设计中的 RTC 问题他在 issue 里留言“原来不是我的代码烂是电池烂。”——这就是开源最朴素的力量。5. 常见问题与硬核排查技巧——那些手册不会写的真相5.1 “读出来全是 0x00”电源与晶振的隐秘战争这是新手最常遇到的“幽灵问题”硬件连好代码编译通过但DS1302_Read_Byte(0x81)永远返回 0x00。手册只会说“检查电源”但真相更微妙VCC 与 VBAT 的电压差DS1302 的 VCC主电源和 VBAT备用电池之间必须有 ≥0.2V 压差否则内部电源开关无法切换导致芯片处于“假死”状态。实测中若 VCC3.3VVBAT3.15V旧电池压差仅 0.15V读写即失效。解决方案更换新电池或在 VBAT 路径上串联一个肖特基二极管压降 0.15V人为制造压差。晶振启振失败DS1302 的晶振电路需满足三点式振荡条件。常见错误是PCB 上晶振到 GND 的铺铜面积过大形成寄生电容抑制振荡。我用热风枪吹下晶振用示波器探头直接接触晶振引脚若无 32.768kHz 正弦波则确认未启振。此时应删除晶振附近所有铺铜并将两个负载电容12pF就近焊在晶振引脚上而非放在芯片旁。ESD 损伤DS1302 的 I/O 引脚 ESD 防护较弱。焊接时若未接地腕带静电可能击穿内部保护二极管导致 I/O 永久短路到 VCC。现象是I/O 线始终为高电平且无法被 STM32 拉低。验证方法断开 DS1302测 I/O 引脚对地电阻若 1kΩ则芯片已损毁。提示遇到 0x00 问题优先用万用表测 VCC/VBAT 压差再用示波器查晶振最后检查焊接虚焊。别急着改代码——90% 的“软件问题”其实是硬件没做好。5.2 “时间跳变”BCD 编码溢出的连锁反应某次调试中我发现时间从 23:59:59 突然跳到 00:00:00但第二天又回到 23:59:59循环往复。根源在于 BCD 编码的“进位陷阱”DS1302 的秒寄存器0x80范围是 0x00–0x59即 0–59 秒。当写入 0x60 时芯片不会报错而是将 0x60 解释为 BCD 的 60但内部逻辑将其视为“60 秒”自动进位到分寄存器。问题在于如果分寄存器本身是 0x5959 分进位后变为 0x60同样触发进位到时寄存器……最终导致整点跳变异常。我的解决方案是在DS1302_Set_Time()函数中加入严格的 BCD 边界检查// 检查并修正 BCD 值 if (time-second 0x59) time-second 0x00; if (time-minute 0x59) time-minute 0x00; if (time-hour 0x23) time-hour 0x00; // ...其他寄存器同理但更根本的预防是在时间设置 UI 层就禁用非法输入如秒输入框最大值设为 59。这提醒我们外设芯片的“宽容”不是优点而是隐患软件必须比硬件更严格。5.3 “低温失效”-20℃ 下的时序收缩危机在车载项目中DS1302 需在 -40℃ 工作。测试发现-20℃ 时通信失败率骤升至 30%。示波器抓波形发现SCLK 周期未变但 CE 的高电平宽度被“压缩”了约 1.2μs低于 tCS的 2μs 要求。原因在于MCU 的 GPIO 输出驱动能力随温度降低而减弱导致