SPI NOR Flash驱动实战:AT32F403A SPI2与GD25Q16详解
2026/9/7 16:07:06 网站建设 项目流程

简介:这份面向单片机与嵌入式硬件开发场景的驱动代码,可帮助使用雅特丽AT32F403A微控制器的开发者,解决通过SPI2串行外设接口读写GD25Q16 SPI Flash存储芯片的工程需求。资源基于PB12、PB13、PB14、PB15四个引脚完成连接,提供完整的底层驱动与调用示例:在主函数中依次完成系统时钟配置和GD25_spi2_init初始化,再调用test_GD25测试函数即可验证读写功能,适合需要快速集成Flash存储功能的入门及中级开发者参考。压缩包共4个文件,全部为源码形式,包含3个头文件和1个C文件,代码量精简,整体大小仅12KB,便于直接移植到现有工程。已有967人学习下载,说明该方案在实际项目中具备一定参考价值。通过阅读这份源码,可以获得SPI2引脚配置、GD25Q16命令时序、数据读写封装以及典型初始化流程等关键实现细节,有效节省自行查阅数据手册和编写调试驱动的时间。

GD25Q16 SPI NOR Flash驱动:AT32F403A SPI2实战记录

前段时间在项目里需要给AT32F403A外挂一颗SPI NOR Flash,用来存参数和升级镜像。网上能搜到的资料大多是STM32的Flash驱动,AT32F403A虽然内核和寄存器风格接近,但外设库函数、时钟树和GPIO复用还是有不少差异,直接套用会踩坑。调通之后我把整个过程整理一下,包括SPI2的初始化细节、GD25Q16的命令时序、页编程跨页处理,以及几个实打实的调试教训。

这篇内容适合手里正好有AT32F403A开发板、需要外接GD25Q16这类SPI Flash的工程师参考。如果你只是临时读写几个字节,可以用GPIO模拟SPI,但一旦涉及升级固件、批量日志这类大流量场景,还是老老实实用硬件SPI2。我这次就是先把电路和驱动做扎实,后面再做DMA和OTA扩展。

1. 项目背景与方案选型

1.1 为什么用AT32F403A的SPI2外设

AT32F403A是雅特力(Artery)的Cortex-M4F系列MCU,主频最高能到240MHz,外设资源非常丰富。项目里选择它主要是因为串口数量多、价格合适,整板就靠它一颗芯片承担通信和存储管理。

关于SPI接口,F403A有好几组SPI,我选择SPI2而不是SPI1,是因为SPI2对应的PB13/PB14/PB15引脚在PCB布局上离Flash更近,走线短,信号质量容易保证。另一个考虑是SPI1往往被调试器或外部高速设备占用,把Flash挂在SPI2上可以避免资源冲突。

用硬件SPI2而不是GPIO模拟,主要有三个原因:第一是速度快,GPIO模拟到1MHz就顶天了,硬件SPI2配到十几MHz毫无压力;第二是代码简单,硬件SPI的收发状态机由外设完成,主循环里不用死等每个bit;第三是后续升级DMA方便,硬件SPI2可以配合DMA做大块数据搬运,这对固件升级功能非常重要。

/* 外设时钟使能 */ crm_periph_clock_enable(CRM_GPIOB_PERIPH_CLOCK, TRUE); crm_periph_clock_enable(CRM_SPI2_PERIPH_CLOCK, TRUE);

1.2 型号澄清:GD32Q16还是GD25Q16

很多刚接触GigaDevice产品的朋友会把型号搞混。标题里写的GD32Q16,实际项目中我们用的是GD25Q16,这是GigaDevice生产的SPI NOR Flash,容量16Mbit,也就是2MByte。GD32系列是MCU,GD25系列是存储芯片,两者虽然都是兆易创新的产品线,但完全不是一回事。

GD25Q16的关键参数:页大小256字节,扇区大小4K字节,块大小64K字节,擦写次数典型10万次,数据保持能力20年。芯片通过标准的SPI接口访问,支持Mode 0和Mode 3两种SPI模式,我这次用的是Mode 0(CPOL=0, CPHA=0)。

选这颗Flash的原因很直接:2MB空间足够放固件副本和运行参数,4K字节扇区对参数存储正好,擦除粒度不会太大浪费空间。而且GD25Q16引脚兼容市面上主流的SPI Flash,后期如果要换华邦、兆易其他型号,硬件和驱动改动都很小。

2. 硬件连接与SPI2外设初始化

2.1 引脚规划与电路注意点

AT32F403A的SPI2有多种引脚映射,我选的是PB13(SCK)、PB14(MISO)、PB15(MOSI),片选CS用PB12控制。这里有个细节:很多时候工程师习惯把CS接到芯片的硬件NSS引脚上,但我建议用普通GPIO控制片选,也就是软件CS。原因后面细说。

信号AT32F403A引脚方向说明
SCKPB13输出SPI2时钟
MISOPB14输入Flash数据输出
MOSIPB15输出Flash数据输入
CSPB12输出片选,低有效

硬件上需要特别注意两个引脚:WP(写保护)和HOLD。WP拉低会禁止状态寄存器和块擦除、写操作,HOLD拉低会让Flash暂停通信。很多板子习惯把这两个脚悬空,这在干扰大的环境里非常危险。正确做法是WP接VCC或通过10K电阻上拉,HOLD也必须接VCC。

Flash的供电去耦同样重要。VCC引脚就近放一颗100nF陶瓷电容,在高温或者高频应用下再加一颗4.7uF钽电容。我遇到过整板SPI通信随机出错,排查到最后就是Flash供电不稳,波形上有毛刺。

2.2 SPI2外设初始化:主模式、模式0、软件NSS

AT32F403A的GPIO复用配置和STM32不太一样,需要先设置GPIO模式为复用模式,再调用gpio_pin_mux_config指定复用功能编号。以PB13/PB14/PB15为例,复用功能编号查数据手册可以定位到SPI2,这里我用的是手册推荐配置。

void flash_spi_init(void) { gpio_init_type gpio_init_struct; spi_init_type spi_init_struct; /* 使能时钟 */ crm_periph_clock_enable(CRM_GPIOB_PERIPH_CLOCK, TRUE); crm_periph_clock_enable(CRM_SPI2_PERIPH_CLOCK, TRUE); /* CS引脚配置:普通推挽输出 */ gpio_default_para_init(&gpio_init_struct); gpio_init_struct.gpio_pins = GPIO_PINS_12; gpio_init_struct.gpio_mode = GPIO_MODE_OUTPUT; gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL; gpio_init_struct.gpio_pull = GPIO_PULL_UP; gpio_init(GPIOB, &gpio_init_struct); FLASH_CS_HIGH(); /* SCK/MISO/MOSI配置为复用功能 */ gpio_init_struct.gpio_pins = GPIO_PINS_13 | GPIO_PINS_14 | GPIO_PINS_15; gpio_init_struct.gpio_mode = GPIO_MODE_MUX; gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL; gpio_init_struct.gpio_pull = GPIO_PULL_NONE; gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER; gpio_init(GPIOB, &gpio_init_struct); gpio_pin_mux_config(GPIOB, GPIO_PINS_SOURCE13 | GPIO_PINS_SOURCE14 | GPIO_PINS_SOURCE15, GPIO_MUX_5); /* SPI2主模式配置 */ spi_default_para_init(&spi_init_struct); spi_init_struct.transmission_mode = SPI_TRANSMIT_FULL_DUPLEX; spi_init_struct.master_slave_mode = SPI_MODE_MASTER; spi_init_struct.mclk_freq_division = SPI_MCLK_DIV_16; spi_init_struct.first_bit = SPI_FIRST_BIT_MSB; spi_init_struct.frame_bit_num = SPI_FRAME_8BIT; spi_init_struct.clock_polarity = SPI_CLOCK_POLARITY_LOW; spi_init_struct.clock_phase = SPI_CLOCK_PHASE_1EDGE; spi_init_struct.cs_mode_selection = SPI_CS_SOFTWARE_MODE; spi_init(SPI2, &spi_init_struct); spi_enable(SPI2, TRUE); }

时钟分频这里要注意,AT32F403A的SPI2挂在APB1总线上,如果系统主频配到240MHz,APB1分频后可能是60MHz或120MHz。我这边APB1跑60MHz,DIV16就是3.75MHz,这个频率对GD25Q16来说非常稳。如果你想压速度,DIV4可以到15MHz,但要看PCB走线长度和Flash手册支持的最高时钟,别一上来就用最大频率。

为什么CS模式必须用软件模式?原因很简单:硬件NSS模式下,片选由SPI外设自动控制,但很多Flash要求在发送命令字节之前CS就要拉低,命令结束后再拉高,中间读状态寄存器时CS要保持低。硬件NSS的自动控制策略不一定完全符合Flash的时序习惯,搞不好就出现"发完了命令而CS没拉对"的玄学问题。用GPIO手动拉CS,时序完全可控,写驱动也更直观。

还有个细节:SPI数据收发时,发送和接收是同时发生的,每个字节的读写本质是同一个操作。写一个字节的同时会收到一个字节,通常叫dummy字节或者无意义字节。底层的收发函数要基于这个特性来写。

3. GD25Q16驱动实现:从底层命令到读写封装

3.1 底层收发与基础命令:读ID、状态、写使能

驱动的基础是底层字节收发函数。用轮询标志位的方式在大多数场景足够,不需要中断和DMA。

static uint8_t spi2_read_write_byte(uint8_t byte) { while (spi_i2s_flag_get(SPI2, SPI_I2S_STS_TBE) == RESET); spi_transmit_data(SPI2, byte); while (spi_i2s_flag_get(SPI2, SPI_I2S_STS_RBNE) == RESET); return (uint8_t)spi_receive_data(SPI2); }

注意不同版本的AT32外设库,标志位名字可能略有差异,比如有的版本是SPI_I2S_STS_TBE,有的版本缩写不同。编译报错时去头文件里搜一下就行,逻辑都一样。

读JEDEC ID是整个驱动的第一步,也最推荐用来验证硬件连接。GD25Q16收到0x9F命令后,会连续返回3个字节:厂商ID(0xC8)、存储类型(0x40)、容量代码(0x15)。只要这个函数返回正确,硬件通路基本就确认没问题。

uint32_t gd25q16_read_id(void) { uint32_t id = 0; FLASH_CS_LOW(); spi2_read_write_byte(CMD_READ_JEDEC_ID); id = spi2_read_write_byte(0x00) << 16; id |= spi2_read_write_byte(0x00) << 8; id |= spi2_read_write_byte(0x00); FLASH_CS_HIGH(); return id; }

写使能和等待忙是成对出现的。GD25Q16在执行编程和擦除之前,必须先发送0x06写使能命令;之后Flash内部状态寄存器的WEL位会置1。而编程和擦除是需要时间的,完成后状态寄存器的WIP位会从1变回0。驱动里的wait_busy函数就是轮询WIP位,确保上一次操作彻底结束后再发下一条命令。

static void gd25q16_write_enable(void) { FLASH_CS_LOW(); spi2_read_write_byte(CMD_WRITE_ENABLE); FLASH_CS_HIGH(); } static void gd25q16_wait_busy(void) { uint8_t status; FLASH_CS_LOW(); spi2_read_write_byte(CMD_READ_STATUS); do { status = spi2_read_write_byte(0xFF); } while (status & 0x01); FLASH_CS_HIGH(); }

wait_busy这里有个容易踩的坑:发送读状态命令后,CS必须一直保持低电平,不能读完一个状态字节就拉高。因为Flash只有在这个命令周期内才会持续输出状态寄存器内容。有些人每读一个字节就拉一次CS,结果读到的永远是第一个字节后面的垃圾数据。我建议把CS拉低、发命令、循环读字节、再拉高,整个命令周期完整走完。

3.2 数据读取与页编程:0x03和0x02

读数据用0x03命令,后续跟3字节地址,然后Flash会按照地址从低到高连续输出数据。地址的格式是高字节在前:A16-A23、A8-A15、A0-A7。

void gd25q16_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; FLASH_CS_LOW(); spi2_read_write_byte(CMD_READ_DATA); spi2_read_write_byte((addr >> 16) & 0xFF); spi2_read_write_byte((addr >> 8) & 0xFF); spi2_read_write_byte(addr & 0xFF); for (i = 0; i < len; i++) { buf[i] = spi2_read_write_byte(0x00); } FLASH_CS_HIGH(); }

页编程是写入的核心命令,指令码0x02,后面接3字节地址和要写入的数据。GD25Q16的页大小是256字节,一次页编程最多只能写256字节,而且这256字节必须落在同一个页内。页边界以256为对齐,如果地址是0x100、0x200这种整页地址,写256字节没问题;但地址不是页对齐时,最多只能写到当前页的末尾。

以0x1FF地址为例,当前页是0x100到0x1FF,如果直接发0x02命令写2字节到0x1FF和0x200,Flash会从0x1FF写到0x1FF,再写到0x100(回到页开头),完全不是预期结果。这个特性是NOR Flash的通用规则,不是GD25Q16独有的。

页编程实现如下,写完数据后CS拉高,然后必须等待WIP清零:

void gd25q16_page_program(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; gd25q16_write_enable(); FLASH_CS_LOW(); spi2_read_write_byte(CMD_PAGE_PROGRAM); spi2_read_write_byte((addr >> 16) & 0xFF); spi2_read_write_byte((addr >> 8) & 0xFF); spi2_read_write_byte(addr & 0xFF); for (i = 0; i < len; i++) { spi2_read_write_byte(buf[i]); } FLASH_CS_HIGH(); gd25q16_wait_busy(); }

3.3 擦除流程与整片写入封装

NOR Flash的特性是只能把1写成0,不能把0写成1。想恢复成1只能靠擦除。GD25Q16支持4K扇区擦除(0x20)、32K块擦除(0x52)、64K块擦除(0xD8)和整片擦除(0xC7)。扇区擦除最常用,4K字节粒度对参数存储来说是刚好的。擦除也需要先写使能,然后等忙。

void gd25q16_erase_sector(uint32_t addr) { gd25q16_write_enable(); FLASH_CS_LOW(); spi2_read_write_byte(CMD_SECTOR_ERASE); spi2_read_write_byte((addr >> 16) & 0xFF); spi2_read_write_byte((addr >> 8) & 0xFF); spi2_read_write_byte(addr & 0xFF); FLASH_CS_HIGH(); gd25q16_wait_busy(); }

擦除地址必须是4K对齐的,传一个不对齐的地址进去,Flash不会报错,但行为不可预期。我在驱动里加过断言,地址按0xFFF掩码检查,不对齐直接返回错误码。

有了扇区擦除和页编程,就可以封装一个通用的写函数。用户只需要给起始地址、数据、长度,驱动自动处理跨页拆分。

void gd25q16_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t page_remain; while (len > 0) { page_remain = 256 - (addr & 0xFF); if (page_remain > len) { page_remain = len; } gd25q16_page_program(addr, buf, page_remain); addr += page_remain; buf += page_remain; len -= page_remain; } }

这个封装的精髓在于计算当前页剩余空间:256 - (addr & 0xFF)。addr的bit0到bit7就是页内偏移,用256减它就是当前页还剩多少字节能写。然后取这个值和剩余长度的较小值,作为本次页编程的长度。循环执行到全部写完。这个写法在任何SPI NOR Flash上都能直接用。

4. 主程序测试与疑难问题排查

4.1 快速测试流程:擦除、写入、回读校验

驱动写完,需要一套完整的测试来验证。测试流程分四步:初始化SPI2并读ID,擦除目标扇区,写入测试数据,回读并逐字节比较。

int main(void) { uint8_t wbuf[256]; uint8_t rbuf[256]; uint32_t id; uint32_t i; system_clock_config(); flash_spi_init(); id = gd25q16_read_id(); if (id != 0xC84015) { while (1); /* ID错误,硬件或驱动有问题 */ } /* 填充256字节测试数据 */ for (i = 0; i < 256; i++) { wbuf[i] = (uint8_t)(i + 1); } /* 擦除0地址所在扇区,写入,回读校验 */ gd25q16_erase_sector(0x000000); gd25q16_write(0x000000, wbuf, 256); gd25q16_read(0x000000, rbuf, 256); if (memcmp(wbuf, rbuf, 256) == 0) { /* 测试通过 */ } else { while (1); /* 校验失败 */ } while (1); }

更严谨的测试应该覆盖三类情况:跨页写入(比如从地址0x1F0写32字节,一定跨过0x200页边界)、跨扇区写入、非对齐擦除。只测整页对齐的256字节其实漏掉了最容易出问题的边界条件。我当时把跨页用例写成一等公民,驱动里的很多bug都是靠这类用例抓出来的。

4.2 调试中踩过的坑与排查思路

调试过程中我遇到了几个典型的坑,整理出来应该能帮大家省点时间。

现象一:读ID返回0xFFFFFF

这是SPI Flash调试最经典的问题。先不要怀疑芯片,先用示波器或逻辑分析仪看SPI2的SCK、MOSI、MISO波形。如果是完全无波形,检查GPIO复用配置和时钟使能。如果MOSI有波形但没有时钟,检查SPI2模式是否意外配成从模式。如果时钟和数据都有但MISO一直高,很大概率是CS没拉低或者Flash供电不对。

现象二:写进去的数据回读全是0xFF

这个问题通常有两个原因。第一个是扇区没有先擦除。NOR Flash在没有擦除的扇区上执行页编程,写不进去任何数据,读回来还是0xFF。第二个是写使能没有生效。GD25Q16在每次编程和擦除前必须发0x06,如果漏发或CS时序不对导致0x06没被芯片正确接收,操作不会执行。

现象三:写入超过256字节后,后半段数据错乱

这就是前面说过的页边界问题。假如从0x1F0开始写300字节,前16字节落在0x1F0到0x1FF,后284字节会从0x100开始覆盖本页开头。解法就是gd25q16_write里那个while循环,按页边界自动拆分,每页单独调一次页编程。

现象四:高频通信时偶尔读出错误数据

检查APB1总线时钟和SPI2分频系数。AT32F403A主频到240MHz时,APB1如果分成120MHz,SPI2的DIV4就是30MHz,这个频率对普通杜邦线连接来说太高了。PCB贴片连接可以试试,飞线测试老老实实用DIV16或DIV32。另外把Flash的WP和HOLD引脚都接VCC,也能减少意外进入保护状态的概率。

现象五:状态寄存器被保护导致擦写失败

GD25Q16的状态寄存器里有块保护位,如果之前被配置过,某些地址区域会拒绝编程和擦除。读状态寄存器如果发现块保护位不全为0,需要先写状态寄存器把这些位清零。不过出厂默认一般是全可写的,遇到大量擦写失败的板子再检查这一项。

我把这些经验整理成了速查表:

现象优先排查项次要排查项
读ID全FCS/供电/MOSI波形GPIO复用配置
写入回读全FF未擦除写使能失效
写入后半段错乱页边界跨页缓冲区指针偏移错误
高频随机出错SPI分频过高WP/HOLD悬空
擦写部分区域失败状态寄存器保护位扇区地址未对齐

写在最后的一些体会

这套驱动在项目里跑了小半年,除了读写参数,还承担了固件升级的临时存储。升到15MHz时钟后整片Flash读写稳定,没再出过问题。我个人比较满意的设计是把CS控制全部集中到宏定义里,后续如果改用其他引脚控制CS,只改两个宏就行,驱动其他部分完全不用动。

如果你要用在自己的项目里,建议从读ID开始验证,这是最快定位硬件问题的方式。另外,跨页写入的测试用例一定要写,千万别只测整页对齐的写入,很多隐藏问题都是边界条件暴露出来的。GD25Q16和市面上其他SPI NOR Flash指令高度兼容,这套代码只要改改宏定义和初始化引脚,就能移植到W25Q系列、MX25L系列上,算是非常通用的驱动框架了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询