ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SPI NOR Flash驱动实战:AT32F403A SPI2与GD25Q16详解

SPI NOR Flash驱动实战:AT32F403A SPI2与GD25Q16详解 简介这份面向单片机与嵌入式硬件开发场景的驱动代码可帮助使用雅特丽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系列是MCUGD25系列是存储芯片两者虽然都是兆易创新的产品线但完全不是一回事。GD25Q16的关键参数页大小256字节扇区大小4K字节块大小64K字节擦写次数典型10万次数据保持能力20年。芯片通过标准的SPI接口访问支持Mode 0和Mode 3两种SPI模式我这次用的是Mode 0CPOL0, CPHA0。选这颗Flash的原因很直接2MB空间足够放固件副本和运行参数4K字节扇区对参数存储正好擦除粒度不会太大浪费空间。而且GD25Q16引脚兼容市面上主流的SPI Flash后期如果要换华邦、兆易其他型号硬件和驱动改动都很小。2. 硬件连接与SPI2外设初始化2.1 引脚规划与电路注意点AT32F403A的SPI2有多种引脚映射我选的是PB13SCK、PB14MISO、PB15MOSI片选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、软件NSSAT32F403A的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总线上如果系统主频配到240MHzAPB1分频后可能是60MHz或120MHz。我这边APB1跑60MHzDIV16就是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个字节厂商ID0xC8、存储类型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和0x200Flash会从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如果分成120MHzSPI2的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系列上算是非常通用的驱动框架了。本文还有配套的精品资源点击获取
返回列表