简介:面向 STM32 标准库开发的完整工程资源,基于 STM32F429IGT6 单片机,演示 SPI5 与 W25Q256JV Flash 芯片的通信,并通过 DMA 实现数据收发。工程在 Keil MDK5.32 下构建,整合 SysTick 延时、LED/按键控制、Flash 扇区读写及仿 printf/scanf 的可变参数宏,适合需要深入理解 SPI 同步通信、DMA 仲裁与中断配合的中高级嵌入式学习者。压缩包共 126 个文件,以 .c 与 .h 源码为主,包含 57 个头文件与 55 个 C 文件,另有 uvprojx/uvoptx 工程配置、hex 固件、sct 分散加载描述等,整体仅 616KB,目录结构清晰。已有 3103 人学习下载。资源中重点展示了 TX/RX DMA 优先级设置与传输完成中断处理,以及软件控制 NSS 的时序逻辑,并给出 Flash 输入输出结构体的共用体设计,可直接移植到同类 SPI Flash 项目中,减少底层调试时间。 前段时间在改一个工业数据记录仪的项目,要存连续半小时的原始波形数据,芯片内部Flash只有64KB,算下来根本不够用。方案没什么悬念,外挂一颗W25Q256JV——256Mbit,足足32MB空间,存日志、存波形、做OTA固件备份都够了。真正花时间的是把SPI、DMA、Flash读写、串口重定向这几块串起来,尤其是4字节地址和DMA全双工收发这两个地方,网上的资料要么只讲W25Q128,要么就是HAL库,标准库的老项目得自己一点一点搬。这篇就把完整方案拆开写,从硬件接线、SPI初始化、DMA收发,到W25Q256JV的读写指令、printf/scanf重定向,全部跑通后的代码思路和踩坑记录都放在下面,适合正在用标准库做数据存储、日志记录、固件升级这类需要外挂Flash的STM32开发者参考。
1. 整体架构与模块拆分:先看清这套方案的边线
1.1 这套方案到底解决了什么问题
我手头的场景是:主控是STM32F103RCT6,Flash总共256KB,内部还要放代码和文件系统,真正能自由使用的大块存储空间非常有限。而记录仪要求连续保存半小时以上的原始波形,每秒几十KB的数据量,光靠内部存储根本不现实,所以必须外挂NOR Flash。
选型上我直接定了W25Q256JV。为什么不选W25Q128?因为128Mbit只有16MB,当时考虑后续要存长时间数据和远程升级固件包,32MB一步到位,价格差不了太多。芯片本身是SPI接口,4根信号线加电源就能工作,不像并行NOR Flash那样接线复杂。
我用的软硬件环境如下:STM32F103RCT6,72MHz主频,标准外设库V3.5版(也就是大家常说的标准库),Keil MDK5编译,Flash芯片是华邦W25Q256JV-IQ。
这套方案分四个功能块:SPI负责物理通信,DMA负责搬运数据,W25Q256JV读写是最终目的,printf/scanf重定向是为了让调试和交互变得方便。四个功能拆开看都不难,组合起来容易出兜兜转转的问题,后面每一节我都会按"为什么这么做"来展开。
1.2 各模块的分工关系
整体上我把它分成三层:
- 底层:SPI1外设初始化 + DMA1通道映射,提供字节传输能力。
- 中间层:W25Q256JV驱动,通过SPI命令实现读ID、擦除、页编程、读数据、等待忙状态等操作。
- 上层:应用逻辑,比如擦除一个扇区、写入一页数据、读回校验,以及通过串口终端交互。
串口重定向跟Flash读写没有直接关系,它是独立的一个辅助模块,用来打印调试信息和接收用户输入。文章标题里把它和SPI、DMA、Flash并列,是因为实际项目里没有调试打印的话,读写Flash出了问题很难定位,有了printf和scanf,整个调试体验完全不一样。
2. 硬件连接与SPI初始化:两处埋雷必须先排掉
2.1 接线方式与引脚选择
我用的SPI1外设,默认引脚如下:
| 信号 | STM32引脚 | 连接W25Q256JV |
|---|---|---|
| SCK | PA5 | CLK |
| MISO | PA6 | DO(数据输出) |
| MOSI | PA7 | DI(数据输入) |
| CS | PB0 | CS(片选) |
| WP | - | 直接接3.3V |
| HOLD | - | 直接接3.3V |
CS用软件片选,也就是GPIO手动控制,不依赖SPI外设的NSS硬件管理。做法是初始化时把PB0配置为推挽输出,默认拉高,操作Flash前拉低,操作完拉高。软件片选的好处是时序完全可控,不会因为NSS自动管理模式导致中途片选跳变。
WP和HOLD这两个引脚是新手最容易忽略的雷。WP拉低会使Flash进入硬件写保护状态,HOLD拉低则会让Flash冻结当前通信,外部时钟和数据都不响应。我遇到过HOLD引脚悬空导致读ID始终返回0xFF的情况,查了好久才发现是HOLD低电平在干扰。所以这两个引脚不用时一定要接高电平,不要悬空。
2.2 SPI模式:为什么选CPOL=1、CPHA=2Edge
W25Q256JV数据手册里明确说明,支持SPI Mode 0和Mode 3。Mode 3对应CPOL=1、CPHA=2Edge,也就是空闲时时钟为高,第二个边沿采样数据。我习惯选Mode 3,因为跟手册时序图对照起来最直观,也避免了某些芯片在Mode 0下表笔不稳的问题。
标准库下SPI1初始化如下:
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_SPI1, ENABLE); // SCK, MOSI, CS推挽复用输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); // MISO浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // CS引脚,软件控制 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }注意SPI_Direction用了2Lines_FullDuplex,这是关键点。SPI从设备是收一个字节回一个字节的,主机要想读数据,必须同时具备发送和接收能力。即使某次操作只是读,也要往总线上发送字节来产生时钟。
SPI_BaudRatePrescaler我选了4分频,APB2时钟72MHz,4分频之后SPI_SCK为18MHz。W25Q256JV最高支持133MHz时钟,18MHz完全在范围内。但当时我着急测试,发现板子上长走线波形差,偶尔读到错误数据,改成8分频后就稳定了。后来我把分频系数做成宏,量产固件统一用8分频,稳妥第一。
2.3 片选时序的隐性要求
片选不仅仅是拉低拉高这么简单。一次完整的Flash操作期间,CS必须全程保持低电平,不能在发命令和发地址之间抖动。我的做法是封装CS_LOW()和CS_HIGH()宏,所有Flash驱动函数里严格保证操作前后各拉一次,中间绝不动CS。
DMA模式下尤其要注意,因为数据搬运是异步的,DMA传输没完成之前不能提前拉高CS。否则Flash会发现片选提前失效,本次操作直接中断,数据就丢了。
3. SPI+DMA收发:读操作必须收发两条DMA同时跑
3.1 DMA通道映射:这个不能随便选
STM32F103的DMA1有7个通道,每个通道的外设请求映射是固定的。SPI1_TX对应DMA1_Channel3,SPI1_RX对应DMA1_Channel2,这是芯片设计决定的,不能改到其他通道。初始化之前先查一下参考手册的DMA请求映射表,省得代码写错了找不到原因。
之所以用DMA而不是中断收发,是因为读写Flash经常是大块数据。比如读4KB的扇区,如果用中断方式,每收到一个字节就要进一次中断,CPU全耗在搬运上了。DMA把数据从SPI数据寄存器搬到内存,传输完成才产生一次中断,CPU可以腾出来干别的事。
3.2 发送DMA配置
发送方向典型场景是写入命令和地址。下面的代码把buf中的数据通过SPI1发送出去:
void SPI_SendArray_DMA(uint8_t *buf, uint16_t len) { DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = len; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); DMA_Cmd(DMA1_Channel3, ENABLE); while (DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET); // 等待传输完成 DMA_ClearFlag(DMA1_FLAG_TC3); }发送完成后等待DMA的传输完成标志,然后清除。DMA_Mode用Normal,因为每次都是固定长度的单次传输,不需要循环模式。
3.3 接收DMA与全双工模型
读Flash是这套方案里最容易卡住的一步。SPI是同步全双工总线,主机每个时钟周期同时发送一位、接收一位。读操作时,主机在发命令和地址的阶段,MISO上回来的其实是垃圾数据;之后主机需要不断发送占位字节,MISO上才会源源不断地返回Flash内部数据。
所以读数据不能用单独的接收DMA。如果只开接收DMA,SPI没有发送动作就没有时钟,接收DMA会一直等不到数据,整个程序卡死在等待标志位那里。正确的做法是收发两条DMA同时开启。
我封装的读函数长这样:
void SPI_Receive_DMA(uint8_t *rx_buf, uint16_t len) { uint8_t dummy[256]; DMA_InitTypeDef DMA_InitStructure; uint16_t i; for (i = 0; i < sizeof(dummy); i++) { dummy[i] = 0xFF; // 占位数据,用于产生SPI时钟 } // 接收DMA:SPI1_RX -> 内存 DMA_DeInit(DMA1_Channel2); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = len; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel2, &DMA_InitStructure); // 发送DMA:内存中的0xFF -> SPI1_DR,产生时钟 DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)dummy; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = len; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); DMA_Cmd(DMA1_Channel3, ENABLE); DMA_Cmd(DMA1_Channel2, ENABLE); // 等待接收完成 while (DMA_GetFlagStatus(DMA1_FLAG_TC2) == RESET); DMA_ClearFlag(DMA1_FLAG_TC2 | DMA1_FLAG_GL2); DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_GL3); }发送dummy数组时,MemoryInc设成Disable,意思是DMA始终从dummy[0]读取,重复发送0xFF。这样可以省掉一个大型数组占用的内存,测试下来完全没问题。
3.4 写Flash可以只开发送DMA
写操作跟读操作不一样。向Flash写数据时,主机只管把命令、地址、数据发出去,MISO上回来的内容在写命令阶段没有意义,所以写函数只需要发送DMA,不需要接收DMA。这也是很多人一开始搞不清楚的地方:同样都在SPI总线上,为什么读要两条DMA、写只要一条。
从本质上来讲,SPI永远是"发一个、收一个",但只有你关心MISO上返回的内容时,接收数据才有意义。发送DMA是产生时钟的唯一来源,不管读还是写,发送方向必须存在。
4. W25Q256JV读写:4字节地址是最大分水岭
4.1 从W25Q128迁移过来的差异
如果你之前用过W25Q64、W25Q128这类小于等于16MB的Flash,直接迁移到W25Q256JV时最容易踩的坑就是地址宽度。
W25Q128容量16MB,用24位地址刚好覆盖。W25Q256JV容量32MB,地址必须扩展到32位。但芯片默认状态下,常用的03h、02h、20h这些指令仍然是24位地址模式,也就是只能访问前16MB区域。如果直接写地址0x1000000以上的位置,Flash会按24位地址回卷,读回来的数据全是0xFF,看起来就像Flash坏了。
解决方式有几种:
| 方案 | 做法 | 优缺点 |
|---|---|---|
| 使用4字节地址指令变体 | 读用13h,写用12h,扇区擦除用21h | 不改变芯片全局状态,最安全 |
| 进入4字节地址模式 | 发B7h,之后03h/02h/20h自动按32位地址解释 | 掉电恢复默认,上电要重新进入 |
| 读芯片寄存器确认状态 | 读状态寄存器2的ADS位 | 结合第二种方案使用 |
我强烈推荐第一种:直接使用带4字节地址语义的指令变体。原因是它不改变芯片内部状态,不会因为某次意外掉电导致后续所有命令错乱。
4.2 读JEDEC ID验证通信
排查SPI通信是否正常,第一步永远是读JEDEC ID,不要一上来就做擦除写操作。读ID发0x9F,之后连续读6个字节。W25Q256JV正常返回的ID是:
EF 40 19有些批次会返回EF 40 19 00 00 00,这是正常的。如果读回来的不是这个值,八成是接线、HOLD引脚、SPI模式、片选时序其中一项有问题。
读ID的代码可以不用DMA,直接用查询方式发指令收数据就行,因为就几个字节,不值得走DMA。我把普通收发函数也封装了一份,短数据传输用普通函数,大块数据用DMA,两种方式共存不冲突。
4.3 页编程、扇区擦除与等待忙状态
写Flash前必须先擦除。NOR Flash擦除后,所有位变成1,也就是读出来是0xFF。最小擦除单位是扇区,W25Q256JV的扇区大小是4KB。A1擦除指令0x21,带上4字节地址。
页编程是写入的最小单位,一次最多写256字节。如果超过256字节,Flash内部地址会回卷到当前页开头,把已经写入的数据覆盖掉。比如从页偏移0xF0开始连续写300字节,写到0xFF之后会回到0x00覆盖之前的内容。
所以写大块数据时要按页拆分。核心逻辑是每次写入前计算当前地址到页末端的剩余字节数,取这个剩余值和待写入长度的较小值作为本次页编程长度:
uint16_t remain = 256 - (write_addr % 256); uint16_t len_this_time = (num_to_write > remain) ? remain : num_to_write;擦除和编程都是耗时操作。页编程典型时间是0.4ms到3ms,扇区擦除典型时间45ms到400ms。执行完指令不能立刻做下一步,必须轮询状态寄存器1的BUSY位(bit0),等到它变成0,Flash才算真正忙完。
等待函数:
void W25Q256_WaitBusy(void) { uint8_t status; CS_LOW(); SPI_SendByte(0x05); // Read Status Register 1 do { status = SPI_ReceiveByte(); } while (status & 0x01); CS_HIGH(); }4.4 写入后的数据校验
写完数据我习惯马上读回来比对,这是最快暴露问题的方式。对比方式很简单,读回的数据和原始数据逐字节比对。实际测试中我发现,如果SPI时钟频率过高或者HOLD引脚没处理好,写入阶段可能成功,但读回时出现个别字节错误,这种问题靠肉眼很难发现,必须依赖代码校验。
5. 仿printf和scanf重定向:把串口变成标准输入输出
5.1 为什么选择重定向而不是自己封装UART_Printf
不少人喜欢在自己写的驱动里放一个UART_Printf函数,内部用vsprintf格式化字符串再发送。这种方案能用,但有个麻烦:vsprintf依赖C运行库,缓冲区长度需要自己管理,浮点格式化支持也跟编译器设置有关。与其自己造轮子,不如直接重定向标准库的printf和scanf,让标准C库帮我们处理格式化逻辑。
重定向的核心思路很朴素:C库的printf最终通过fputc逐字符输出,scanf通过fgetc逐字符读取。只要实现这两个函数并绑定到串口,标准输入输出就自动变成了串口收发。
5.2 Keil微库与fputc/fgetc实现
在Keil工程里,最简单的做法是勾选Use MicroLib。微库体积小,重定向printf后不会进入半主机模式,不需要额外处理调试通道。如果不用微库,默认标准库的printf会尝试调半主机模式,这种情况下要额外实现_sys_exit等函数,否则程序会卡死。
我的实现:
#include <stdio.h> int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; } int fgetc(FILE *f) { uint8_t ch; while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET); ch = USART_ReceiveData(USART1); USART_SendData(USART1, ch); // 回显 while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; }fgetc里的回显很重要。串口终端没有本地回显的话,用户输入一个字符,程序立刻把同一个字符发回去,屏幕上才能看到自己打了什么。这直接决定交互体验。
5.3 scanf使用的几个注意点
重定向了fgetc之后,标准scanf就能用了。但实际用起来有几个细节要注意。
scanf是个阻塞函数。如果程序跑到scanf等着输入,而终端没有任何提示,很容易让人以为程序死机了。所以scanf之前一定要用printf打印提示信息,比如printf("please input cmd: ");
另外scanf("%s", buf)不检查缓冲区长度,输入多长的字符串都会往buf里写,容易越界。要么用scanf("%19s", buf)这种限制宽度写法,要么手动做一层输入解析。我在项目里只是做一个命令交互菜单,输入数字或长度小于20的字符串,所以限制宽度就够了。
如果程序里printf和scanf交替频繁,建议在scanf前清空串口接收缓冲,避免之前的残余字符影响本次读取:
void USART_ClearRxBuf(void) { while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET) { USART_ReceiveData(USART1); } }这一步不是必须的,但能避免很多输入错乱的怪问题。
6. 综合测试与调试实录
6.1 串口调试菜单设计
全部模块打通后,我做了一个简单的命令交互菜单,用来验证Flash扇区擦除、页编程、读回校验几个主要功能。主流程大致如下:
- 初始化USART1和SPI1;
- printf输出版本信息;
- 读JEDEC ID并打印,确认SPI通信正常;
- 进入scanf等待输入命令;
- 根据命令执行擦除、写入、读取并打印结果。
测试时终端输出类似这样:
==== W25Q256JV Test Menu ==== 1. Read JEDEC ID 2. Erase sector 3. Write test data 4. Read and verify Please select:输入数字后程序执行对应操作并回显结果。整个交互完全依赖重定向后的printf和scanf,没有自己写任何命令解析协议。
6.2 实测中踩过的坑汇总
这次调试前后遇到不少问题,我把最值得注意的几个列出来:
| 现象 | 根因 | 解决办法 |
|---|---|---|
| 读ID返回0xFF | HOLD引脚悬空,Flash不响应 | HOLD接3.3V |
| 读地址超过16MB全是0xFF | 用了24位地址指令03h | 改用13h或进入4字节地址模式 |
| 程序卡在DMA等待标志 | 只开接收DMA,SPI没有发送时钟 | 同时开启发送DMA发送0xFF |
| 第二次DMA传输数据错乱 | DMA完成标志未清除 | 等待完成后清除TC和GL标志 |
| 大块写入时部分数据丢失 | 页编程跨过256字节边界回卷覆盖 | 按页边界拆分长度 |
| 高时钟频率下读回个别字节错误 | SPI_SCK频率过高,走线信号质量差 | 分频系数从4改为8 |
最后一个坑最隐蔽。18MHz时钟在短距离下通常没问题,但板子上Flash离主控比较远,过孔也多,信号完整性问题就暴露了。表现为读回的数据偶尔错一个字节,不是每次都错,很难复现。后来我用示波器看MISO波形,发现上升沿有振铃,降低SPI时钟后问题消失。如果你的板子PCB走线比较长,建议直接选8分频甚至16分频,Flash操作本身不追求极限速度,稳定压倒一切。
6.3 关于写保护的一个补充
W25Q256JV上电后的状态寄存器默认值通常是0x00,也就是不保护任何区域。但我也遇到过个别芯片被设置过写保护,具体表现是写使能命令06h执行后,状态寄存器1的WEL位能置1,但页编程或擦除指令执行完,数据没变,读出来还是0xFF。
遇到这种问题,检查一下状态寄存器1的高位,重点是BP4和BP0这几个高低位。如果发现有保护,需要先发送写使能,再向状态寄存器1写0x00解除写保护:
void W25Q256_DisableWriteProtect(void) { CS_LOW(); SPI_SendByte(0x06); // Write Enable CS_HIGH(); CS_LOW(); SPI_SendByte(0x01); // Write Status Register 1 SPI_SendByte(0x00); // 解除所有写保护 CS_HIGH(); W25Q256_WaitBusy(); }这条命令会修改非易失性状态寄存器,执行一次就够了,掉电后仍然保持。批量生产时最好在出厂检测里把这一步加进去。
整套方案最后交付时,代码结构并不复杂,但每一层都有它的讲究。SPI是物理基础,DMA让大块数据搬运不占CPU,W25Q256JV驱动把指令细节封装好,printf/scanf重定向让调试体验接近PC端控制台。如果你跟我一样还在维护标准库的老工程,这套思路可以直接抄过去。以后再做需要外挂Flash的项目,我会优先考虑这套组合,尤其是4字节地址处理和DMA双通道同步这两个点,搞清楚了,后面就一路顺畅。
本文还有配套的精品资源,点击获取