我接手一个传感器模组的项目时第一次认真研究 DS2431。以前我写过不少 EEPROMAT24C02、AT24C256、W25Q64 都用过潜意识里觉得 EEPROM 无非就是地址加数据换个型号改改地址位宽就完事了。可 DS2431 这个容量只有 1Kb 的“小东西”硬是让我推翻了这个想法。它不走 I2C 也不走 SPI只用一根数据线通信靠的是 OneWire 协议存储区也不是一个从 0x00 到 0x3F 的连续数组那么简单写数据之前还要先经过一个叫暂存器Scratchpad的缓冲区甚至状态寄存器里还有一组只能从 1 改成 0、改完就回不来的写保护位。这枚芯片在STM32项目里其实很常见尤其是需要给设备写身份 ID、校准参数、序列号、电池信息这类“一锤子买卖”场景。如果你正在用 STM32 做类似需求或者只是想把 OneWire 这个协议彻底吃透这篇文章应该能帮你把从时序到驱动编写的路一次走通。我会从芯片硬件模型讲起拆完协议时序后直接给你一套可以拿去改的 STM32 分层驱动代码最后再聊聊实测中容易踩的坑。1. DS2431 这颗 EEPROM 为什么值得单独写一篇1.1 单总线与 I2C/SPI 的根本差异大多数工程师接触 EEPROM 的第一站是 I2C 接口的 AT24C02。I2C 有 SCL 和 SDA 两根线通信靠时钟沿控制数据线有明确的主从关系写一个字节要发设备地址、寄存器地址、数据一整套流程虽然繁琐但很规整。SPI 更直接MOSI、MISO、SCK、CS 四根线各司其职速度也快。DS2431 不一样它只有一根数据线 DQ外加 VCC 和 GND总共三根引脚。数据线上既传时钟信息又传数据靠的是严格的时序时隙Time Slot来区分 0 和 1。这种设计最大的优势是省 IO、省线缆传感器探头、电池组、仪表模块里经常是只有一根多余信号线的情况DS2431 正好填上这个坑。代价是协议必须由主机用软件模拟而且对时序的准确性要求比 I2C 更苛刻。还有一个容易忽略的点每一颗 DS2431 出厂时都带一个唯一的 64 位 ROM ID。这意味着同一条总线上可以挂多颗 DS2431主机通过 ROM 命令选择操作哪一颗。I2C 也能挂多颗但需要不同的地址引脚组合而 OneWire 是纯靠 ID 区分布线更简单代价是搜索算法比 I2C 寻址麻烦不少。1.2 引脚、上拉与容量分布数据手册里容易被绕晕的两个点DS2431 的引脚很少TO-92、SOT-23、倒装片封装都有。最常见的是 TO-92三根脚分别是 GND、DQ、VCC。注意 DQ 不是推挽输出它是开漏结构所以总线上必须接一个上拉电阻典型值 4.7kΩ。如果你的线缆比较长或者总线上挂的设备多就要降到 2.2kΩ 左右。上拉电阻选得太大信号上升沿会变缓时序窗口本来就短很容易读错。第二个容易绕晕的是容量。DS2431 标称 1Kb很多人以为能存 128 字节结果看数据手册发现用户可用空间只有 64 字节。原因在于这 1Kb 被拆成了好几块主 EEPROM64 字节地址 0x00 到 0x3F分成 4 页每页 16 字节。EPROM 仿真保护页32 字节地址 0x80 到 0x9F每页对应一个 8 字节的影子区。状态寄存器8 字节地址 0xA0 到 0xA7里面放着写保护位、EPROM 仿真模式位和版本标识。其中 0xA0 是保留字节0xA1 和 0xA2 是写保护寄存器0xA3 到 0xA6 是 EPROM 仿真寄存器0xA7 是版本寄存器。DS2431 的原厂版本寄存器读出值是 0xA5用 0xAA 表示兼容替代芯片。这个细节在选型时有用因为市场上某些兼容片在高温或长时间写入性能上可能与原厂有细微差别。地址范围区域大小用途0x00 - 0x3F用户 EEPROM64 字节正常读写数据0x80 - 0x9FEPROM 仿真保护页32 字节与 EPROM 仿真模式配合的影子区0xA0保留1 字节读出 0xFF0xA1 - 0xA2写保护寄存器2 字节控制四个页面的写保护状态0xA3 - 0xA6EPROM 仿真寄存器4 字节设置单个字节为 EPROM 模式0xA7版本寄存器1 字节芯片版本标识很多应用只使用前 64 字节根本不碰状态寄存器这没问题。但如果你的需求里有“出厂后锁定校准参数”这种要求那状态寄存器才是真正的主角。2. OneWire 协议时序的底层拆解把波形翻译成代码OneWire 的时序全部围绕一根线展开理解它的关键是四个动作复位、写 0、写 1、读。每个动作都在一个固定长度的时间窗口内完成DS2431 的数据手册里把窗口定成了 60us 的时隙。2.1 复位与存在脉冲一次请求一次应答主机想和 DS2431 通信第一件事永远是把总线拉低 480us 以上然后释放。释放后总线依靠上拉电阻恢复高电平DS2431 检测到这个上升沿后会在 15us 到 60us 之间主动把总线拉低 60us 到 240us作为存在脉冲Presence Pulse。主机什么时候检测这个低电平很关键。我的做法是拉低 480us 后释放等 60us然后读一次总线电平。这时候如果读到 0说明从机在线且完成了应答如果读到 1说明总线上没有设备或者设备没来得及响应。主机 : 拉低 480us - 释放 DS2431 : 等待 15~60us - 拉低 60~240us 主机采样点 : 释放后约 60us 处这个采样点不能太晚。存在脉冲最短只有 60us如果你用 HAL_Delay 这种毫秒级延时等再想去读的时候低电平早就结束了复位判断会莫名其妙地失败。2.2 写 0、写 1、读时隙15 微秒的采样窗口写时序的规则比复位更精细写 1主机拉低总线 1us 到 15us然后释放剩余时间保持高电平。从设备在时隙开始后的 15us 到 60us 区间读总线读到高电平记为 1。写 0主机拉低总线并保持至少 60us整个时隙内总线都是低电平从设备读到低电平记为 0。读时序稍微麻烦一点。主机拉低总线 1us 到 15us然后释放紧接着从设备决定是否把总线继续拉低。如果 DS2431 要返回 0它会在主机释放后主动拉低总线如果要返回 1它就不动作总线被上拉电阻拉高。所以主机必须在时隙开始后的 15us 之内完成采样太晚的话读 0 的时隙可能已经结束读 1 的时隙也可能受线路电容影响还没稳定。我把时序参数整理成了下面这张表方便你在调代码时对照动作低电平时间释放后行为主机采样点复位480us从机 15~60us 后拉低 60~240us释放后约 60us写 11~15us保持高电平从机在 15~60us 内采样写 060~120us保持低电平从机在 15~60us 内采样读1~15us从机控制总线电平主机在 15us 内采样2.3 ROM 层命令一条总线上挂多个 DS2431 的识别机制OneWire 总线的每个设备出厂时都有一个 64 位 ROM ID前 8 位是家族码DS2431 的家族码是 0x2D。主机可以通过几个 ROM 级命令来操作设备READ ROM0x33读取总线上唯一设备的 64 位 ID。如果总线上挂了多颗数据会冲突所以这个命令只适合单设备场景。SKIP ROM0xCC跳过 ROM 寻址直接对当前设备发存储命令。单设备场景最常用可以少传 8 字节 ID。MATCH ROM0x55后面跟 64 位 ROM ID只操作指定 ID 的设备。多设备场景下用这个。SEARCH ROM0xF0搜索总线上所有设备的 ROM ID。需要逐位读两个电平来判断分支算法稍复杂。如果你的系统里只有一颗 DS2431直接用 SKIP ROM 就够了。如果是多颗我的建议是产品初始化和维护阶段用 SEARCH ROM 把所有 ID 读出来存到 Flash 里运行阶段用 MATCH ROM 精确访问。3. DS2431 的命令体系暂存器、复制与状态寄存器3.1 为什么 DS2431 写 EEPROM 要分成两步DS2431 没有直接写 EEPROM 的指令写入必须走“写暂存器”和“复制暂存器”两步。这跟 AT24C02 很不一样刚切换过来的人容易懵。第一步是 WRITE SCRATCHPAD0x0F。主机发送命令后跟目标地址 TA1、TA2然后发送 1 到 8 字节数据数据先存到芯片内部的暂存器里。这个暂存器就是一个 8 字节的缓冲区还带 CRC 校验。第二步是 COPY SCRATCHPAD0x55。主机重新发送地址 TA1、TA2再跟一个 0xFF 字节这个字节叫 ES用来指示复制范围常规整 8 字节复制时发 0xFF芯片会把暂存器里的数据真正写进 EEPROM然后进入编程时间 tprog典型值 10ms最大 20ms。为什么这么设计因为 EEPROM 写入需要内部电荷泵时间较长如果直接把数据写进去万一总线上出现干扰数据写到一半就废了。先写暂存器、读回校验、再复制能最大程度避免坏数据落进 EEPROM。3.2 READ MEMORY 与页边界读取时的地址滚动读数据用 READ MEMORY0xF0命令后跟 16 位目标地址读的时候地址会自动递增。读主存储区时从 0x00 开始连续读 64 字节就能拿到全部用户数据。有一个细节READ MEMORY 读到地址 0xFFFF 后会继续输出 0xFF 通知主机结束。所以如果你循环读取时没有限制读取次数会一直读到一堆 0xFF。程序里一般按需求长度固定读或者读到连续数据末尾再判断结束。另外写数据时要注意页边界。主存储区每页 16 字节暂存器只有 8 字节意味着每次 COPY SCRATCHPAD 最多写入 8 字节。如果你想更新地址 0x0A 处的数据直接写一个字节其实是可以的但剩下的暂存器空间会残留旧值复制时可能把垃圾数据一起写进去。稳妥的做法是先把整 8 字节块读回 RAM在 RAM 里修改目标字节再整块写回。3.3 状态寄存器与写保护一次性烧写的物理限制状态寄存器是最容易被忽略、也最危险的部分。写保护寄存器 0xA1、0xA2 里每个存储页对应三个保护位默认全为 1。保护位是物理 EPROM 性质的位只能从 1 变成 0一旦写成 0就永久生效。它们控制的分别是对应页面的暂存器写入、EEPROM 复制等操作权限三个位全部清零后该页就彻底锁死。EPROM 仿真寄存器 0xA3 到 0xA6 也是一样的道理每个字节用两个位控制用户区的一个字节默认 00 表示普通 EEPROM 模式。如果你把某个字节对应的两位改成 10那么这个字节就进入了“EPROM 仿真模式”只能把 1 写成 0不能再从 0 恢复成 1适合做一次性熔丝位、版本号降级记录这类需求。这里我强烈建议除非你的产品确实需要产线锁定否则不要随便写状态寄存器。我见过有人想测试写保护功能把 0xA1 写了几个 0结果整页数据彻底锁死只能换芯片。写保护是物理性的不是软件标志位没有后悔药。4. STM32 端免驱模拟 OneWire驱动分层与代码落地4.1 为什么不直接用 USART 外设模拟单总线网上有教程用 STM32 的 USART 外设模拟 OneWire 时序思路是在 USART 发送 8 位数据时把波特率调成对应时隙利用起始位和停止位组合出 0 和 1 的波形。这么做确实能省 CPU但我在实际项目里不推荐原因有三个一是 USART 模拟对波特率精度要求高一旦系统时钟倍频配置改过时序就漂了。二是接收方向不好处理读时序需要精确采样USART 做不到。三是最关键的问题OneWire 读写过程中要反复切换方向和拉高拉低USART 的灵活性不够。所以老老实实 GPIO 模拟才是最适合大多数 STM32 项目的方案。4.2 微秒级延时的实现DWT 比空循环靠谱HAL_Delay 是毫秒级OneWire 的时隙是微秒级根本不能用。常见替代方案是空循环但空循环受编译器优化影响大换个优化等级时序就变了非常不稳。我推荐用 DWT 的 CYCCNT 计数器做微秒延时。它是内核自带的调试计数器不占用定时器资源精度高代码也简单static void delay_us_init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); }注意SystemCoreClock 必须在系统时钟初始化完成后才准确。如果换了时钟树配置这个值会变延时也会跟着变。4.3 onewire_bit 底层复位、读写 bit、读写 byte我习惯把驱动分成三层底层是一组 onewire_bit 操作中间是 DS2431 命令层顶层是应用层。底层代码这一部分核心就是四件事。GPIO 初始化用开漏模式加外部上拉。开漏模式下写 0 就是拉低写 1 就是释放总线读输入电平直接读 IDR不需要切换模式非常方便。如果你选的引脚是 PA15、PB3、PB4 这些复用脚记得先禁用 JTAG否则电平会被调试器干扰。void ow_gpio_init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio.Pin GPIO_PIN_1; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); }然后是复位函数。注意读存在脉冲的采样点要卡在释放后 60us 附近uint8_t ow_reset(void) { uint8_t presence; HAL_GPIO_WritePin(ONEWIRE_PORT, ONEWIRE_PIN, GPIO_PIN_RESET); delay_us(480); HAL_GPIO_WritePin(ONEWIRE_PORT, ONEWIRE_PIN, GPIO_PIN_SET); delay_us(60); presence HAL_GPIO_ReadPin(ONEWIRE_PORT, ONEWIRE_PIN); delay_us(420); return presence GPIO_PIN_RESET; }写 bit 和读 bit 是实现时序的核心void ow_write_bit(uint8_t bit) { if (bit) { HAL_GPIO_WritePin(ONEWIRE_PORT, ONEWIRE_PIN, GPIO_PIN_RESET); delay_us(6); HAL_GPIO_WritePin(ONEWIRE_PORT, ONEWIRE_PIN, GPIO_PIN_SET); delay_us(64); } else { HAL_GPIO_WritePin(ONEWIRE_PORT, ONEWIRE_PIN, GPIO_PIN_RESET); delay_us(60); HAL_GPIO_WritePin(ONEWIRE_PORT, ONEWIRE_PIN, GPIO_PIN_SET); delay_us(10); } } uint8_t ow_read_bit(void) { uint8_t bit; HAL_GPIO_WritePin(ONEWIRE_PORT, ONEWIRE_PIN, GPIO_PIN_RESET); delay_us(6); HAL_GPIO_WritePin(ONEWIRE_PORT, ONEWIRE_PIN, GPIO_PIN_SET); delay_us(5); bit HAL_GPIO_ReadPin(ONEWIRE_PORT, ONEWIRE_PIN); delay_us(50); return bit; }读写字节都是低位在前这个不要搞反。OneWire 的 LSB first 特性跟 I2C 的 MSB first 正好相反很多人在这里栽过跟头。void ow_write_byte(uint8_t data) { for (int i 0; i 8; i) { ow_write_bit(data 0x01); data 1; } } uint8_t ow_read_byte(void) { uint8_t data 0; for (int i 0; i 8; i) { data 1; if (ow_read_bit()) { data | 0x80; } } return data; }4.4 ds2431 命令层与应用层读 64 字节与写一页的完整流程有了底层DS2431 命令层就很好写了。先把命令和地址定义好#define DS2431_SKIP_ROM 0xCC #define DS2431_WRITE_SCRATCHPAD 0x0F #define DS2431_READ_SCRATCHPAD 0xAA #define DS2431_COPY_SCRATCHPAD 0x55 #define DS2431_READ_MEMORY 0xF0读 64 字节用户数据int ds2431_read_all(uint8_t *buf, uint8_t len) { if (!ow_reset()) return -1; ow_write_byte(DS2431_SKIP_ROM); ow_write_byte(DS2431_READ_MEMORY); ow_write_byte(0x00); ow_write_byte(0x00); for (int i 0; i len; i) { buf[i] ow_read_byte(); } return 0; }写入一个 8 字节块到指定页。这里的关键是“读回校验后再复制”int ds2431_write_block(uint8_t addr, const uint8_t *data) { uint8_t check; ow_reset(); ow_write_byte(DS2431_SKIP_ROM); ow_write_byte(DS2431_WRITE_SCRATCHPAD); ow_write_byte(addr); ow_write_byte(0x00); for (int i 0; i 8; i) { ow_write_byte(data[i]); } // 读回暂存器校验地址和数据 ow_reset(); ow_write_byte(DS2431_SKIP_ROM); ow_write_byte(DS2431_READ_SCRATCHPAD); (void)ow_read_byte(); // TA1 (void)ow_read_byte(); // TA2 for (int i 0; i 8; i) { check ow_read_byte(); if (check ! data[i]) return -2; } // 执行复制 ow_reset(); ow_write_byte(DS2431_SKIP_ROM); ow_write_byte(DS2431_COPY_SCRATCHPAD); ow_write_byte(addr); ow_write_byte(0x00); ow_write_byte(0xFF); delay_us(20000); // 等待 tprog return 0; }有一个细节值得说明READ SCRATCHPAD 读回数据后在暂存器末尾还会收到 CRC 字节。上面的代码没做 CRC 校验只做了数据比对。对大多数产品来说够用了但如果你做的是高可靠性设备建议把 CRC 也算上。应用层就简单多了。比如在设备启动时读取序列号uint8_t user_data[64]; if (ds2431_read_all(user_data, 64) 0) { // 数据有效检查版本、解析校准参数 }5. 实测中的坑时序、中断与恢复策略5.1 读回全是 0xFF先怀疑复位没成功最常见的故障现象是读回的数据全是 0xFF看起来像总线上不存在设备。我排查时的第一反应是看复位函数有没有返回 1。原因通常是上拉电阻问题。如果你的开发板上没有外部上拉只靠 STM32 的内部上拉那总线上升沿会非常慢DS2431 可能来不及检测到有效的上升沿自然也就不回存在脉冲。解决方法是加一颗 4.7kΩ 外部上拉或者把 GPIO 配置为开漏输出并开启内部上拉临时验证。还有一种情况是你在初始化 GPIO 时把引脚配置成了推挽输出。推挽模式下写 1 会强制拉高这时 DS2431 拉低总线会产生冲突时序完全乱掉。检查一下 GPIO 配置是不是 GPIO_MODE_OUTPUT_OD。5.2 时灵时不灵线长、上拉和中断的叠加效应如果某块板子测试正常换了一个线缆更长的工装就时灵时不灵大概率是上升沿变缓加上采样点太靠近窗口边缘导致的。OneWire 本身是为短距离设计的虽然也能走几米但线缆电容会把波形边沿磨圆。我的处理方式是上拉电阻从 4.7kΩ 换成 2.2kΩ同时把读时序里的采样点从 5us 微调到 10us 左右。这样能显著提高长线场景下的稳定性。中断是另一个大坑。如果读写过程中来了一个高优先级中断打断了 OneWire 时序哪怕只有 10us也可能导致整个时隙错位。最有效的解决办法是在单字节读写时进入临界区禁止中断响应__disable_irq(); ow_write_byte(cmd); __enable_irq();如果你用的是 FreeRTOS更好的方案是把这个操作放在低优先级任务里同时用临界区保护每一笔完整的事务而不是每次只保护一个 bit。因为一个 bit 才 60us频繁开关中断反而增加抖动。5.3 写保护位一旦改错不可恢复的教训前面说过写保护位是 EPROM 性质的位只能由 1 改 0不能恢复。我在测试阶段踩过这个坑当时想验证写保护寄存器能不能正常写入直接对 0xA1 写了 0x00想当然地以为重启后能重新写回 0xFF。结果就是四个用户页全部锁定芯片报废。正确做法是先读当前状态寄存器的值做好记录如果有可写保护的小批量芯片先用它们做验证正式产品里写保护位的置位操作必须放在产线最后一步而且要有确认弹窗之类的二次保护机制。如果你确实需要保护部分数据我建议优先用 EPROM 仿真模式而不是整页写保护。比如只把 0x00 到 0x07 这 8 个字节作为熔丝位通过 0xA3 寄存器把对应字节设成 EPROM 模式这样只影响一小块区域不至于整页锁死。5.4 与 I2C EEPROM 移植习惯的差异从 AT24C02 移植过来的人最容易犯的错是把 I2C 的“写完后读回 ACK”思维带进来。OneWire 的 ACK 机制不是在同一事务里完成的它靠的是复位和存在脉冲每次命令结束都要重新复位。另一个是地址方向I2C 地址字节是 MSB firstOneWire 的字节传输是 LSB first。代码里如果沿用 I2C 的移位逻辑数据会全部颠倒。还有一点DS2431 的 COPY SCRATCHPAD 命令后面必须跟 ES 字节如果直接移植 I2C 写内存的三字节地址模式漏掉 ES芯片不会进入编程状态看起来就是“写不进去”。6. 一些后续扩展和我的使用建议如果你以后要在 FPGA 上用 Verilog 实现 OneWire 主机我的建议是不要照搬本文的延时函数思路。FPGA 更合适的方式是用计数器状态机来精确划分时隙把 60us、15us、6us 这类参数换算成对应时钟周期的计数值。相比 STM32 的软件延时FPGA 的时序更稳定但灵活性不如单片机改时序参数要重新综合。如果要做多设备管理建议把 SEARCH ROM 算法的搜索表放在上位机或产品初始化阶段做不要在每次读写时都跑一遍。搜索过程需要逐位读两次电平来判断分支效率低而且对时序更敏感。还有一点DS2431 的工作电压是 2.8V 到 5.25V但 OneWire 总线的高速模式对电压有额外要求。如果系统是 3.3V 供电上拉电阻接到 3.3V 即可如果是 5V 系统要注意 STM32 引脚是否容忍 5V避免烧引脚。文章开头说过我为传感器模组选 DS2431用它存身份 ID 和校准参数。产品跑了几个月后我最满意的不是它的 EEPROM 容量而是 ONE Wire 总线把主板和探头之间的连线简化到了极致。校准数据写保护后产线再也没出现过参数被误改的返修。如果你手头的项目正好受限于 IO 数量或者需要一个带唯一 ID 的存储芯片DS2431 是个值得花半天时间把协议吃透的选择。