
简介面向STM32开发者的AT93C46串行EEPROM驱动资源包围绕Atmel 93C46芯片的SPI通信与读写控制提供可直接参考的C语言工程与编译产物。压缩包内含17个文件包含prj工程文件、c源码文件、hex固件、lst列表及调试辅助文件等完整覆盖从代码编写、编译到下载验证的主要环节整体仅26KB轻量易用。已有389人学习下载适合正在学习SPI外设或需要快速集成93C46的嵌入式初学者与项目开发者。资源中的源码展示了SPI总线初始化、按地址写字节与读字节等典型驱动函数同时包含工程配置和生成文件便于对照理解工具链组织方式。结合93C46的64字节存储特性可应用于Bootloader参数、设备ID等配置存储场景帮助读者打通芯片手册到实际代码的实现路径。1. 为什么一块 3 线 EEPROM 比 I2C 更容易让 STM32 项目卡壳AT93C46 是一颗 1Kbit 的 Microwire 总线 EEPROM很多工程师把它放进 STM32 项目里当参数存储用结果发现它既不是 I2C 也不是 SPICS、SK、DI、DO 四根线读写只能一位一位来而且没有现成的硬件外设帮你收尾。更反直觉的是这颗芯片的地址位数和字长由 ORG 引脚决定ORG 接高是 64x16接低是 128x8同样一段驱动在两种配置下读出的数据完全不同。如果你在调试时遇到“读回来全是 0xFF”或者“写不进去”大概率不是芯片坏了而是指令帧长度和 ORG 状态不匹配。从路由器 MAC 备份到变频器校准值再到 STM32 的 Bootloader 参数区都有它出现的位置。下面按“先看懂指令帧、再用 GPIO 模拟读写、最后做可校验备份”的顺序展开代码按 AT93C46 的 16 位模式给出8 位模式只需改一个宏。2. AT93C46 的指令集与读写时序先分清 ORG 再写驱动2.1 存储结构ORG 引脚决定 128 x 8 还是 64 x 16AT93C46 的物理容量固定为 1024 位但对外地址空间有两种组织方式。ORG 接高电平时按 16 位字访问共 64 个地址0x00~0x3FORG 接低电平时按 8 位字节访问共 128 个地址0x00~0x7F。对驱动的影响直接落在指令帧地址位数上16 位模式只要 6 位地址8 位模式要 7 位。如果硬件 ORG0而驱动按 6 位发地址实际单元会被错位访问读出来的每个字都像被“推”了一位。配置ORG 电平地址数地址位宽典型用途16 位字高646 位参数与校验字连续存放8 位字节低1287 位字节流日志或兼容旧设计选型时为什么不用 STM32 内部 Flash 而是外挂这颗小芯片内部 Flash 的擦写次数和掉电一致性在某些场合不够可靠外部 EEPROM 能让参数区独立于固件区而且它没有页缓存和扇区概念读写单位就是字驱动代码量很小。我的经验是原理图阶段直接把 ORG 上拉或下拉到固定电平不要在运行时用 GPIO 切换两个模式一颗 1Kbit 的器件不值得让固件长期维护两套地址映射。2.2 指令码与位序起始位之后才轮到操作码Microwire 每个操作都以 CS 拉高开始随后先在 DI 上发一个 1 作为起始位再是操作码和地址。这个“1”非常容易和操作码混在一起很多移植失败的驱动把 READ 指令直接拼成二进制 110 后加地址去移位丢掉起始位芯片自然不响应。部分数据手册里把起始位一并纳入指令字所以你会看到 READ 被写成 110实际发送顺序仍然是 1 10 6 位地址。16 位模式下的常用指令帧如下表。指令帧结构起始位 操作码 其余总位宽说明READ1 10 6 位地址9读一个 16 位字WRITE1 01 6 位地址 16 位数据25写一个 16 位字ERASE1 11 6 位地址9指定字复位为 0xFFFFEWEN1 00 11 4 位任意9写使能上电默认关闭EWDS1 00 00 4 位任意9禁止写入ERAL1 00 10 4 位任意9整片擦除WRAL1 00 01 4 位任意 16 位数据25整片写同一数据所有指令都按 MSB first 移位也就是先发高位。驱动里可以做一层通用封装把帧位宽和帧内容交给同一个发送函数#define AT93C46_ORG_16BIT 1 #if AT93C46_ORG_16BIT #define AT93C46_ADDR_BITS 6 #else #define AT93C46_ADDR_BITS 7 #endif static void AT93C46_SendBits(uint16_t frame, uint8_t len) { for (uint8_t i len; i 0; i--) { AT93C46_SendBit((frame (i - 1)) 1U); } }这里 frame 是拼好的完整指令字len 是总位宽。以 READ 为例16 位模式下地址上限是 64帧可以写成AT93C46_SendBits(0x180 | addr, 9);0x180 的 bit8 是起始位 1bit7 和 bit6 是操作码 10低 6 位留给地址。把地址位宽做成宏ORG 换挡时只改一个常量比满屏改addr 5安全得多。2.3 写周期与 DO 引脚复用轮询还是延时写操作发完并不代表立即生效芯片内部会进入自定时写周期典型时间在 2ms 左右。两种处理方式固定延时 3~5ms或者发完帧后把 CS 拉高通过读 DO 引脚判断状态——DO 为高表示写完成低表示忙。第二种省时间在启动阶段连续写十几组校准时累计时间差异非常明显。不过 DO 在总线空闲时是高阻态外部最好加一个 10kΩ 上拉电阻否则轮询时可能误判。这跟 I2C 的地址应答逻辑不同I2C 从机拉低 SDA 表示收到应答Microwire 没有应答机制写失败只能读回比较。所以我的驱动里从不用“发完就延时”的裸奔写法而是读回校验不一致就重试。2.4 为什么不用硬件 SPI 而是 GPIO 模拟Microwire 与 SPI 最直观的区别是 CS 极性SPI 的 CS 是低电平选中Microwire 要求 CS 拉高表示操作开始。用 STM32 的硬件 SPI 可以配出相近的时钟相位但 CS 必须用普通 GPIO 手动拉高起始位也得软件先发外设的 8 位字节缓冲还要处理 9 位帧的组包问题。算下来寄存器初始化、中断标志判断的代码量往往比纯 GPIO 模拟还多。GPIO 模拟另一大优势是跨平台同一套 C 代码从 Keil 工程换到 VSCode 配置的 GCC 工程或者从 STM32 换到 APM32、GD32改动量几乎为零。3. 在 STM32 上用 GPIO 模拟 AT93C46 读写可直接抄的 C 驱动3.1 引脚映射与 GPIO 初始化用三个输出和一个输入接 AT93C46下面以 STM32F103 的 GPIOA 为例。引脚分配只影响宏定义不涉及驱动逻辑。STM32 引脚AT93C46 引脚方向PA4CS输出PA5SK输出PA6DI输出PA7DO输入#define AT93C46_CS_PIN GPIO_PIN_4 #define AT93C46_SK_PIN GPIO_PIN_5 #define AT93C46_DI_PIN GPIO_PIN_6 #define AT93C46_DO_PIN GPIO_PIN_7 #define AT93C46_PORT GPIOA static void AT93C46_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin AT93C46_CS_PIN | AT93C46_SK_PIN | AT93C46_DI_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(AT93C46_PORT, GPIO_InitStruct); GPIO_InitStruct.Pin AT93C46_DO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(AT93C46_PORT, GPIO_InitStruct); HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_SK_PIN, GPIO_PIN_RESET); }三个输出都用推挽高速度DO 配上拉。SK 初始化为低避免上电瞬间芯片看到异常时钟沿。SK 速度档不建议选最高速杜邦线连接时过快的边沿会产生过冲和串扰反而容易误采样。3.2 逐位移位一位一位来别想着并行Microwire 没有时钟极性寄存器可配置发送时先放 DI、再产生 SK 上升沿接收时在 SK 上升沿之后读 DO。按这个顺序做时序就是对的。static void AT93C46_DelayUs(uint32_t us); static void AT93C46_SendBit(uint8_t bit) { HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_DI_PIN, bit ? GPIO_PIN_SET : GPIO_PIN_RESET); AT93C46_DelayUs(1); HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_SK_PIN, GPIO_PIN_SET); AT93C46_DelayUs(1); HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_SK_PIN, GPIO_PIN_RESET); AT93C46_DelayUs(1); } static uint8_t AT93C46_RecvBit(void) { uint8_t bit; HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_SK_PIN, GPIO_PIN_SET); AT93C46_DelayUs(1); bit (HAL_GPIO_ReadPin(AT93C46_PORT, AT93C46_DO_PIN) GPIO_PIN_SET); HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_SK_PIN, GPIO_PIN_RESET); AT93C46_DelayUs(1); return bit; }AT93C46_DelayUs 的实现有讲究HAL_Delay 只能到毫秒级微秒延时可以用 DWT 的 CYCCNT也可以用空循环。工程里如果跑了 FreeRTOS注意延时期间不要让长临界区打断否则时序毛刺很难查。1us 延时对 100kHz 级别的 SK 时钟已经足够不必追求极限速率。3.3 读一个字的完整帧读单个字CS 拉高发起始位 1发操作码 10发 6 位地址然后连续接收 16 个数据位。uint16_t AT93C46_Read(uint8_t addr) { uint16_t value 0; int i; if (addr (1U AT93C46_ADDR_BITS)) return 0xFFFF; HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_SET); AT93C46_DelayUs(1); AT93C46_SendBit(1); // 起始位 AT93C46_SendBit(1); // 操作码 10 AT93C46_SendBit(0); for (i AT93C46_ADDR_BITS - 1; i 0; i--) { AT93C46_SendBit((addr i) 0x01); } for (i 15; i 0; i--) { // 读 16 位数据 if (AT93C46_RecvBit()) { value | (1U i); } } HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_RESET); return value; }地址入口用 uint8_t但只取低 6 位越界直接返回 0xFFFF 作保护。读数据的循环从 bit15 开始拼装确保 value 的位序和帧里的 D15 对齐。如果读回来固定是 0xFFFF优先检查发地址时是 6 位还是 7 位其次查 DO 引脚是否被配置成了复用功能。3.4 写使能与写操作AT93C46 上电后写入功能是关闭的必须先发 EWEN再发 WRITE 帧。void AT93C46_WriteEnable(void) { int i; HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_SET); AT93C46_SendBit(1); AT93C46_SendBit(0); AT93C46_SendBit(0); AT93C46_SendBit(1); AT93C46_SendBit(1); for (i 0; i 4; i) AT93C46_SendBit(0); HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_RESET); } uint8_t AT93C46_Write(uint8_t addr, uint16_t value) { int i; if (addr (1U AT93C46_ADDR_BITS)) return 0; AT93C46_WriteEnable(); HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_SET); AT93C46_DelayUs(1); AT93C46_SendBit(1); // 起始位 AT93C46_SendBit(0); // 操作码 01 AT93C46_SendBit(1); for (i AT93C46_ADDR_BITS - 1; i 0; i--) { AT93C46_SendBit((addr i) 0x01); } for (i 15; i 0; i--) { AT93C46_SendBit((value i) 0x01); } HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_RESET); AT93C46_DelayUs(100); HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_SET); AT93C46_DelayUs(1); while (HAL_GPIO_ReadPin(AT93C46_PORT, AT93C46_DO_PIN) GPIO_PIN_RESET) { ; // 等待写周期结束DO 拉高表示完成 } HAL_GPIO_WritePin(AT93C46_PORT, AT93C46_CS_PIN, GPIO_PIN_RESET); return (AT93C46_Read(addr) value) ? 1 : 0; }每次写前都发 EWEN避免不同执行路径上遗漏使能。CS 拉低后等 100us 再进入轮询这是给内部写周期启动留余量。轮询循环在演示代码里没加超时量产时必须加一个 10ms 上限否则 DO 引脚焊接异常会把整个任务卡死。提示AT93C46 的 DO 在写周期内被拉低CS 拉高后若一直读不到高电平先查 DO 上拉电阻再查电源纹波。3.5 移植到寄存器版 STM32 驱动不用 HAL 的工程可以直接操作 BSRR 和 IDR代码更短#define CS_LO (GPIOA-BSRR GPIO_PIN_4) #define CS_HI (GPIOA-BRR GPIO_PIN_4) #define DI_HI (GPIOA-BRR GPIO_PIN_6) #define DO_READ ((GPIOA-IDR GPIO_PIN_7) ! 0)按 STM32 的惯例BSRR 写 1 置位BRR 写 1 复位别和标准库的PAout(4)1混用。寄存器版主要规避 HAL_GPIO_WritePin 的函数调用开销在低主频 MCU 上能跑出更高的 SK 频率但实际收益有限更大的价值是换 APM32 或 GD32 时用寄存器版重写一遍可以快速验证引脚定义兼容性。4. AT93C46 驱动的 5 个关键参数与 3 类常见故障4.1 必调的 5 个参数及调整方法参数建议值对项目的影响tCSS至少 250nsCS 拉高到第一个起始位的时间太短首帧不识别tSKH / tSKL至少 250nsSK 高/低电平宽度决定总线速率地址位宽6ORG1/ 7ORG0错一位整片数据错位tWP典型 2ms上限约 10ms写周期等待建议轮询或取 5ms 延时写使能每次写前发 EWEN漏发则写操作静默失败拿到新批次的芯片我会先用逻辑分析仪测 tWP 的实际分布开启写使能后写 0x5A5A轮询 DO 上升沿并记录时间。实测值在 1.2ms 到 1.8ms 之间时固定延时取 3ms 是稳妥的。注意 STM32 主频改动会影响延时函数比如从 72MHz 降到 48MHz 而延时循环常量不变真实等待时间会明显缩短这也是“换个工程就写不进”的常见原因。4.2 故障 1读回来全部是 0xFF先查 ORG 引脚。很多开发板把 ORG 悬空内部下拉到 8 位模式而你在 16 位模式下读地址和字宽全对不上。其次是 CS 时序用示波器看 CS 上升沿到第一个起始位之间是否满足 tCSS。最后确认 DI 和 DO 没有接反杜邦线连接时这两根线最容易互换。4.3 故障 2写不进或读回旧值最常见的两个原因没发 EWEN或发完 EWEN 后 CS 拉低时 SK 上还有毛刺。EWEN 帧末尾 4 位任意值建议都发 0防止个别批次芯片解析出歧义。另一个隐藏点是电源电压偏低Microwire EEPROM 在 2.5V 以下时写周期不稳定单独量一下芯片 VCC 引脚别只盯着 STM32 的 3.3V 网络看。4.4 故障 3边界地址数据错位0x00 正常而 0x01 读出的是 0x00 的内容这是地址移位问题驱动按 6 位发地址芯片实际按 7 位解析地址被左移了一位。用万用表量 ORG 电平再对着宏改AT93C46_ADDR_BITS。提示故障排查思路不要照搬 I2C EEPROM 的习惯。I2C 的设备地址是固定值应答失败说明器件没响应Microwire 没有应答机制只能读回校验。4.5 和 I2C EEPROM 调试习惯的差异I2C 发完地址后从机拉低 SDA 就是应答马上能判断链路通没通。Microwire 全程没有握手发完帧就是发完了有没有写进去必须读回才知道。我一般调试阶段先写一个扫描函数把全部地址读出来从串口打印两遍结果一致再继续往下做void AT93C46_Dump(void) { for (uint16_t addr 0; addr (1U AT93C46_ADDR_BITS); addr) { printf(%02X:%04X , addr, AT93C46_Read(addr)); if ((addr 0x07) 0x07) printf(\r\n); } printf(\r\n); }打印格式固定成地址:数据一组 8 个地址换行方便和 93C46 工具导出的 bin 文件做 diff。如果扫描结果出现“每隔两个地址跳一组数据”基本就是地址位宽和帧长不匹配而不是芯片损坏。5. 用 93C46 工具把备份做成可校验的 bin 文件调通驱动后下一步是把整片 AT93C46 内容备份到电脑。常见做法是让 STM32 通过串口把整片数据按 16 位小端序导出成 bin再在上位机做校验。上位机协议可以定得很简单发单字节 0xA5设备收到后从地址 0x00 开始连续发 128 字节对应 64 个字乘以 2 字节发完再追加 2 字节 CRC16。串口工具把原始数据存成at93c46_backup.bin然后用 Python 脚本解析和校验。脚本先按 16 位小端把每两个字节合成一个字再以 8 个一行的格式输出文本最后用 CRC16-XMODEM 做完整性校验import sys def crc16_xmodem(data: bytes) - int: crc 0 for byte in data: crc ^ byte 8 for _ in range(8): if crc 0x8000: crc ((crc 1) ^ 0x1021) 0xFFFF else: crc (crc 1) 0xFFFF return crc raw open(at93c46_backup.bin, rb).read() words [] for i in range(0, len(raw) ~1, 2): words.append(f{raw[i] | (raw[i1] 8):04X}) with open(at93c46_dump.txt, w) as fp: for i in range(0, len(words), 8): fp.write(f{i // 8:02X}: .join(words[i:i8]) \n) stored_crc int(sys.argv[1], 16) computed_crc crc16_xmodem(raw) print(fcomputed: {computed_crc:04X}) print(OK if stored_crc computed_crc else MISMATCH)运行方式是python check_at93c46.py 0x3C8A第二个参数传串口收到的 CRC 原始值。文本输出文件里地址列对应 AT93C46 的字地址每行 8 个字和 STM32 串口打印的 Dump 格式一致肉眼比对也很方便。一个容易被忽略的细节是写回备份的擦除策略WRITE 指令只能把 1 写成 0不能把 0 写成 1。如果目标单元原本是 0x0000而备份数据里有位从 0 恢复到 1必须先对整片发一次 ERAL 擦除再做逐字写回。也就是说写回过程不是简单的“读旧值、覆盖写”而是先整片擦除再逐字写入最后全片读回算一遍 CRC确认和源 bin 一致。本文还有配套的精品资源点击获取