
简介一份针对AT25F1024串行闪存芯片的SPI驱动程序源码面向嵌入式开发者与单片机学习人群主要解决微控制器通过SPI接口读写该1Mb串行EEPROM的软件实现问题。包内共1个文件为C语言源代码整个压缩包仅1KB代码结构紧凑无多余依赖便于阅读和移植。驱动实现了初始化、数据读取、数据写入和扇区擦除等核心功能详细演示了SPI接口的时钟相位/极性配置、主从模式设定、字节顺序以及写命令后的状态轮询等关键步骤并针对该闪存必须按扇区擦除、不能单字节擦除等特性做了正确处理可帮助使用者避开常见坑点。该资源已有153人学习下载适合需要快速集成或参考学习SPI Flash驱动写法的工程师。通过这份代码既能理清AT25F1024的指令集和操作流程也能总结出一套通用的SPI闪存驱动框架为后续开发类似存储芯片驱动提供基础。1. AT25F1024 是谁一颗老牌 SPI NOR Flash 的驱动要点很多嵌入式工程师手里会有一个叫SPI.rar之类的压缩包解压以后是一堆at25f1024.c、at25f1024.h单看文件名知道是驱动但放进工程里就是跑不起来。AT25F1024 是一颗 1Mbit128KB的 SPI NOR Flash容量不大却把 SPI 设备驱动的典型问题占全了页编程边界、状态寄存器轮询、写使能锁存器、片选时序。这篇文章从指令集开始讲清楚这颗芯片的驱动该怎么写再分别给出 Linux 用户态和 STM32 裸机两套可抄的方案最后用四个断言验证驱动到底通没通。适合刚接触 SPI Flash 的开发者也适合被老芯片坑过的熟手对照检查自己的驱动实现。2. 写驱动前先吃透 AT25F1024 的指令集、状态与页编程边界2.1 AT25F1024 的引脚、SPI 模式与最小接线AT25F1024 是标准 4 线 SPI 器件引脚只有CS、SCK、SI、SO四根信号线外加WP、HOLD两个控制引脚。WP是写保护输入拉低时禁止写状态寄存器HOLD是暂停输入拉低时让芯片暂停接收时钟。驱动代码里这两个引脚必须固定接高否则在调试时偶发的HOLD抖动会让通信无故中断而且这种问题极难复现。SPI 模式上AT25F1024 支持模式 0CPOL0, CPHA0和模式 3CPOL1, CPHA1。模式 0 是默认选择绝大多数 MCU 的硬件 SPI 和逻辑分析仪都按这个模式工作。时钟频率方面数据手册标称的极限值在实际设计中很少能跑满从 1MHz 起步调试确认波形和读写正确后再往上提是比较稳妥的做法。引脚方向连接说明CS输入MCU GPIO / SPI CS低电平有效整个事务期间保持低SCK输入SPI SCK时钟SI输入SPI MOSI指令和数据写入SO输出SPI MISO数据读出WP输入VCC拉高禁用写保护HOLD输入VCC拉高禁用暂停功能2.2 指令集读写擦的字节序列长什么样驱动操作 AT25F1024 靠的是指令码每条指令都是CS拉低后先发 1 个字节的指令码再跟着地址或数据。最常用的指令就五个读0x03、页编程0x02、扇区擦除0x52、读状态寄存器0x05、写使能0x06。其中地址固定是 3 字节高字节在前。读数据0x03的时序是发0x03再发 3 字节地址然后持续给时钟芯片就会从 SO 上一位一位送出数据。这里有一个很多新手忽略的点SPI 是全双工协议读数据时主机必须持续发0xFF来产生时钟MOSI 上的内容对芯片没有意义但不发时钟就什么都读不到。页编程0x02则是发完地址后紧跟着最多 256 字节的写入数据写完这一串字节之后才允许拉高 CS。指令字节码后续参数完成时间量级读数据0x033字节地址 N字节读出与长度相关页编程0x023字节地址 1~256字节数据几毫秒扇区擦除0x523字节地址4KB对齐几十到几百毫秒整片擦除0x62无秒级读状态寄存器0x05读出1字节立即写使能0x06无立即2.3 状态寄存器与写使能锁存器两个必须遵守的状态约束AT25F1024 的状态寄存器只有一个字节驱动里真正需要关心的只有两个位bit0 是就绪标志BUSYbit1 是写使能锁存器WEL。BUSY 为 1 表示芯片正在执行页编程或擦除此时除了读状态寄存器其他指令都不接受。WEL 为 1 表示写使能已置位页编程和擦除指令只有在 WEL1 时才会被执行。每次写操作或擦除操作之前必须先发0x06写使能指令。写完或擦完WEL 自动清零。漏发0x06是所有 AT25F1024 驱动中最高频的 bug指令发得完全正确但数据就是写不进去回读全是 0xFF。诊断方法很简单操作完成后读一下状态寄存器如果 WEL 一直是 0说明写使能环节没做好。等待总线忙的轮询代码通常是这样的uint8_t at25_wait_busy(uint32_t timeout_ms) { uint32_t elapsed 0; while (elapsed timeout_ms) { uint8_t status; spi_select(1); spi_transfer_byte(0x05); /* 读状态寄存器指令 */ status spi_transfer_byte(0xFF); /* 读回状态字节 */ spi_select(0); if ((status 0x01) 0) /* bit0 为 0 表示不忙 */ return 0; delay_ms(1); elapsed; } return 1; /* 超时返回失败 */ }spi_transfer_byte是底层 SPI 字节收发函数发送一个字节同时接收一个字节。注意这里每次查询都是完整的 CS 拉低、传指令、取数据、拉高的过程不能把 CS 一直低着做多次查询。2.4 页编程跨边界AT25F1024 最常见的隐含 bugAT25F1024 的页大小是 256 字节但页编程的地址边界不是任意 256 字节对齐而是低 8 位地址为 0 的位置。换句话说页的起始地址是0xXX00。如果一次页编程发送的数据跨越了0xXX00这个边界多余的数据会回卷写到该页的开头覆盖掉这一页前面的数据这也是 NOR Flash 驱动里危害最大的错误。正确的做法是在驱动层做拆分先计算当前地址距离下一个页边界还剩多少字节每次只写不超过这个余量的数据。写成代码就是chunk min(len, 256 - (addr 0xFF))然后循环处理。下面的逻辑只要移植到任何平台都可以直接套用static int at25_write_loop(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len 0) { uint32_t chunk 256 - (addr 0xFF); /* 页内剩余字节数 */ if (chunk len) chunk len; at25_page_program(addr, buf, chunk); /* 单页编程 */ if (at25_wait_busy(100)) return -1; addr chunk; buf chunk; len - chunk; } return 0; }addr 0xFF取的是地址低位也就是当前地址在页内的偏移。256 - 偏移就是从当前位置到页末尾的距离。这个写法比判断(addr % 256 0)更直观也更容易看出边界条件。凡是遇到数据写到一半乱了回读时前 256 字节和预期不符这类问题优先怀疑这个拆分逻辑。3. Linux 下驱动 AT25F1024从 spidev 用户态程序到内核 jedec 匹配3.1 先确认硬件枚举spi-nor 框架认不认这颗老片子Linux 内核的spi-nor框架是通用的 NOR Flash 驱动它通过读芯片的 Jedec ID 来匹配驱动表。但 AT25F1024 这颗芯片发布得早ID 定义和现代 SPI NOR 的 JEDEC 标准并不完全一致在较新的内核里很可能出现spi-nor spi0.0: unrecognized JEDEC id bytes: ...的报错。出现这种提示不代表芯片坏了只是内核不认识它。这种情况下有两条路。第一条是给内核的jedec_ids表补一条记录把 AT25F1024 的名称、扇区大小、总容量按数据手册填进去重新编译内核或驱动模块。第二条更简单用spidev把 Flash 暴露给用户态驱动逻辑全部放在应用程序里。对 AT25F1024 这种老芯片来说用户态方案的可维护性反而更好换内核版本不用跟着重编驱动。设备树里把节点compatible设为spidevspi-max-frequency同时设上加载spidev模块后就能在/dev下看到设备节点。如果你的板子片选挂在 GPIO 上还需要在设备树里把cs-gpios配好否则 spidev 拉不了片选。3.2 用 spidev 在用户态读写 AT25F1024 的最小程序用户态操作 spidev 的流程是固定的open设备节点用ioctl设置模式和速率再用SPI_IOC_MESSAGE发送传输描述符。下面这段代码可以直接编译运行完成读 ID 和读 512 字节数据的操作#include stdio.h #include stdint.h #include fcntl.h #include sys/ioctl.h #include linux/spi/spidev.h static int spi_fd; static void spi_init(void) { spi_fd open(/dev/spidev0.0, O_RDWR); uint8_t mode SPI_MODE_0; /* CPOL0, CPHA0 */ uint32_t speed 1000000; /* 先从 1MHz 开始 */ ioctl(spi_fd, SPI_IOC_WR_MODE, mode); ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, speed); } static void spi_xfer(uint8_t *tx, uint8_t *rx, uint32_t len) { struct spi_ioc_transfer tr { .tx_buf (unsigned long)tx, .rx_buf (unsigned long)rx, .len len, .speed_hz 1000000, .bits_per_word 8, .delay_usecs 0, .cs_change 0, }; ioctl(spi_fd, SPI_IOC_MESSAGE(1), tr); } static void read_id(void) { uint8_t tx[4] {0x9F, 0xFF, 0xFF, 0xFF}; uint8_t rx[4] {0}; spi_xfer(tx, rx, 4); printf(ID: %02X %02X %02X\n, rx[1], rx[2], rx[3]); } static void read_flash(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t tx[4 512]; uint8_t rx[4 512]; tx[0] 0x03; /* 读指令 */ tx[1] (addr 16) 0xFF; tx[2] (addr 8) 0xFF; tx[3] addr 0xFF; memset(tx[4], 0xFF, len); /* 提供时钟的填充字节 */ spi_xfer(tx, rx, 4 len); memcpy(buf, rx[4], len); }这个程序里有两个关键点。第一spi_xfer的len必须覆盖完整事务的字节数读 ID 是 4 字节读数据是 4 字节指令加数据长度因为 SPI 是全双工收发是同时进行的。第二读数据时tx[4]到tx[4len-1]填0xFF这些字节的作用纯粹是产生时钟让芯片把数据推出来填充值本身无关紧要。SPI_IOC_MESSAGE(1)里的1表示一次事务中传输描述符的数量。读完 512 字节后如果要读更多注意 spidev 的单次传输长度受内核配置限制常见的内核默认上限在 4096 字节左右更大的读取要自己做分块循环。3.3 Linux SPI 片选硬件片选与软件拉片选的取舍Linux 的 SPI 子系统里片选有两种实现。硬件片选由 SPI 控制器的CS引脚自动管理ioctl发起传输时由控制器硬件拉低传输结束自动拉高用户态感知不到这个过程。软件片选则把 GPIO 配置为 CS由内核 SPI 框架在传输前后操作 GPIO。从驱动代码的角度看这两种方式对用户态是透明的不需要写额外的 GPIO 操作代码。但有一个隐蔽的问题软件片选时序受 GPIO 翻转速度影响如果 GPIO 挂在慢速总线上片选拉低和第一个时钟沿之间的间隔可能不满足芯片的建立时间要求。遇到第一笔传输正常连续传输时偶发错误的情况可以检查一下设备树用的是硬件 CS 还是 GPIO CS。另外要注意cs_change这个字段。置 0 表示一整次SPI_IOC_MESSAGE里所有传输共用一次片选拉低置 1 表示每笔传输之间片选会短暂释放再拉低。AT25F1024 的页编程和擦除指令要求 CS 在整个指令期间保持低电平所以这类操作的描述符一定要把cs_change设成 0。4. 裸机移植 AT25F1024 驱动STM32 上的文件划分与 API 设计4.1 驱动文件怎么组织裸机环境下AT25F1024 的驱动建议按三个文件划分at25f1024.h定义对外 API 和调试开关at25f1024.c放读、写、擦、状态查询等核心逻辑platform_spi.c封装底层的 SPI 收发和片选操作。这样划分的原因是芯片逻辑和硬件平台解耦换一块 MCU 只需要重写platform_spi.c里的三四个函数。用 STM32CubeMX 配置 SPI 时把 SPI 设为全双工主机、8 位数据宽度、模式 0预分频系数先取能让 SCLK 落在 1MHz 左右的值。NSS 引脚配置为软件管理因为 AT25F1024 的 CS 需要在多字节事务内持续拉低硬件 NSS 的自动翻转行为不可控。在platform_spi.c里只需要对外提供三个函数片选拉低、片选拉高、字节收发。用 HAL 库的写法如下void platform_spi_cs_low(void) { HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_RESET); } void platform_spi_cs_high(void) { HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_SET); } uint8_t platform_spi_byte(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, 100); return rx; }这里片选用的 GPIO 输出速度要配置成 High否则 CS 边沿太缓在高速 SPI 下会触发芯片内部的毛刺滤波导致第一笔数据丢失。4.2 底层 SPI 字节收发与片选时序在驱动层每个完整的操作都是一个事务CS 拉低、发指令、发地址、读或写数据、CS 拉高。CS 拉低和拉高之间不能插入其他设备的 SPI 操作这也意味着同一个 SPI 总线上如果挂了多个器件CS 的管理必须小心不能让两个器件的片选在时间上重叠。读 ID 和读状态寄存器这类短事务可以直接用platform_spi_byte完成。页编程稍微复杂因为数据长度在接受0x02指令后是不定长的CS 一直低到最后一个数据字节发完才能释放。如果使用中断或 DMA 发送要确保最后一字节发送完成的标志被正确处理用发送寄存器空作为完成标志是不够的必须等发送完成标志否则 CS 会提前拉高。4.3 读、写、擦除的 API 实现对外 API 设计成下面这组函数调用方不用关心页边界和状态寄存器int at25_read_id(uint8_t id[3]); int at25_read(uint32_t addr, uint8_t *buf, uint32_t len); int at25_write(uint32_t addr, const uint8_t *buf, uint32_t len); int at25_erase_sector(uint32_t addr); int at25_chip_erase(void);读操作的实现是把0x03指令、3 字节地址依次发出然后连续调用platform_spi_byte(0xFF)来取数据。擦除操作需要先发0x06写使能再发0x52和地址最后轮询 BUSY。注意扇区擦除的地址必须 4KB 对齐AT25F1024 内部是按 4KB 扇区组织的传入未对齐地址并不会报错但擦除的实际扇区从对齐边界开始。写操作用到第 2 章里的页边界拆分逻辑完整的实现如下int at25_write(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len 0) { uint32_t chunk 256 - (addr 0xFF); if (chunk len) chunk len; platform_spi_cs_low(); platform_spi_byte(0x06); /* 写使能 */ platform_spi_cs_high(); platform_spi_cs_low(); platform_spi_byte(0x02); /* 页编程 */ platform_spi_byte((addr 16) 0xFF); platform_spi_byte((addr 8) 0xFF); platform_spi_byte(addr 0xFF); for (uint32_t i 0; i chunk; i) platform_spi_byte(buf[i]); platform_spi_cs_high(); if (at25_wait_busy(100)) return -1; addr chunk; buf chunk; len - chunk; } return 0; }写使能和页编程分成了两个独立的 CS 事务这是数据手册要求的0x06指令结束后 CS 必须拉高芯片才会把 WEL 位置 1。如果 CS 一直低着写使能指令不会生效。4.4 用 unsigned long 地址还是三字节地址驱动接口设计AT25F1024 容量 128KB地址范围是0x00000到0x1FFFF19 位地址在 32 位系统中可以用uint32_t表示。对外 API 的地址参数类型直接用无符号 32 位整数即可内部发指令时再拆成三个字节。有一个设计分歧点是拆地址的代码应该放在 API 层还是底层。常见做法是在 API 层拆因为不同的指令需要的地址字节顺序不同统一在 API 层拆能保持底层platform_spi_byte足够简单。如果要兼容其他型号的 Flash可以把拆地址也抽成一个函数spi_send_addr(uint32_t addr)后续换芯片时只改这个函数。顺便提一句如果你是 ESP8266 或类似平台的用户硬件 SPI 被 SDK 占用时完全可以用 GPIO 模拟时序来驱动 AT25F1024。它的时序是标准 4 线 SPI频率不高GPIO 翻转速度完全够用驱动逻辑和上面这套是一模一样的只需把platform_spi_byte换成位操作实现。5. 验证 AT25F1024 驱动是否可靠的四个断言5.1 断言一上电读 ID 能连续 3 次返回同一组字节驱动写完先不急着读写数据第一步是读 ID。连续调用 3 次读 ID每次返回的字节必须完全一致。如果第一次读到全0xFF大概率是 CS 时序问题检查片选是否在整个事务期间保持低电平如果读到的数据每次都不一样优先怀疑时钟极性配错了把模式 0 改成模式 3 对比测试。一个有用的小技巧是读 ID 时故意把速率降到 100kHz如果低速下 ID 稳定、高速下不稳定说明是信号完整性问题而不是驱动逻辑问题。此时检查 SPI 信号线长度和上下拉配置比继续调代码更有意义。5.2 断言二全片写入 0x5A 后逐字节回读一致AT25F1024 必须先擦后写直接写入会得到全0xFF和写入数据的混合体。所以第二个断言要完整走一遍擦除、写入、回读的流程uint8_t pattern[256]; uint8_t readback[256]; for (int i 0; i 256; i) pattern[i] 0x5A; at25_chip_erase(); at25_wait_busy(2000); /* 整片擦除耗时较长 */ at25_write(0x00000, pattern, 256); at25_read(0x00000, readback, 256);回读的每个字节都要等于0x5A。这一步能同时验证写使能、页编程、状态寄存器轮询是否正确。如果回读全是0xFF回到第 2 章去看写使能时序如果前半段正确后半段错乱页边界拆分有 bug。5.3 断言三page 边界写入不丢字节在地址0x00FF处写入连续递增的 16 字节数据然后从0x00F8开始回读 32 字节检查地址0x00F8~0x00FF是旧数据0x0100~0x010F是刚写入的递增序列。这个测试直接命中跨页边界能验证驱动的拆分逻辑是否真的生效。5.4 断言四擦除后读到 0xFF 且状态寄存器能感知忙信号最后一个断言验证擦除和忙检测对某个扇区执行扇区擦除擦除过程中反复调用at25_wait_busy确认它在擦除期间返回忙擦除结束后返回空闲。然后读该扇区前 16 字节全部必须是0xFF。这一步同时验证了擦除指令和 BUSY 轮询两条路径任何一条有问题都会在这里暴露。Linux 用户态可以用spidev配合dd和cmp做同样的测试先dd if/dev/spidev0.0读全片保存擦除后再读两次文件比较即可。注意 spidev 的读写都在用户态做整片测试时速度不会很快128KB 全片回读大概需要几秒这是正常的。本文还有配套的精品资源点击获取