做工业控制项目手里这颗GD32H759已经把RT-Thread跑起来了时钟树、GPIO、UART这些基础外设也都在前面几篇里趟过一遍。这一篇专门写SPI。SPI在工控里的出场率比很多人想象得高得多参数存储Flash、高精度ADC、绝对值编码器、LCD屏幕、IO扩展芯片全都是SPI的活儿。GD32H759这种主频600MHz的Cortex-M7核如果还在用IO口软件模拟SPI那基本等于浪费硬件资源。这篇把我在实际项目中用GD32H759加RT-Thread驱动SPI的完整套路讲清楚包括时序参数设计、设备框架接入、接W25Q64存储芯片、接ADS1256高精度ADC的完整代码以及在M7核上做SPI必须注意的Cache一致性问题。适合正在搞工控板卡、仪器仪表、数据采集系统的朋友参考尤其是那些想把RT-Thread设备框架用明白的人。1. 工控场景下为什么要选SPI以及这套方案的整体思路1.1 四类串行总线里的定位SPI凭什么是主力工控板上常见的串行总线无非UART、I2C、I2S、SPI这几类它们分工完全不同。UART是异步协议简单可靠但速率天花板低一般用来做调试串口和低速透传I2C只需要两根线还能挂多个从机地址但速率通常限制在400kHz到1MHz而且有ACK应答机制时序反而复杂I2S专门给音频数据用工控场景基本碰不到。SPI是同步全双工协议一根时钟线SCK一根主机输出从机输入MOSI一根主机输入从机输出MISO再加片选CS速率能跑到几十乃至上百Mbps天然适合大数据量交换。在工控现场SPI高频出现的原因有三条。第一是速率高速ADC采集、编码器角度读取、Flash存储固件参数都需要在短时间搬大量数据UART和I2C扛不住第二是全双工主机在读数据的同时还可以继续发命令协议简单透明调试方便第三是从机选通机制每个设备一根CS直接控制互不干扰这种确定性在工控里非常重要。比如我这次要用W25Q64做参数掉电保存用ADS1256做多路模拟量采集一个SPI总线挂4线设备每个从机单独片选逻辑非常清晰。1.2 整体方案架构两条SPI总线分别挂什么设备GD32H759片上集成了多组SPI外设完全没必要把所有设备挤到一条总线上。我的方案是SPI0挂W25Q64SPI1挂ADS1256两条总线各自独立配置。这么设计的原因很直接W25Q64可以跑几十MHzADS1256的SCLK上限通常不到2MHz如果挂在同一条总线上只能以最慢的那个从机为准高速Flash就被拖累了。分总线以后每条总线都能按自己的最优时序运行。另外一个重要决策是使用RT-Thread的SPI设备框架而不是直接在应用层操作寄存器。设备框架把总线、设备、片选Pin统一管理起来应用层通过rt_spi_send、rt_spi_recv这些API访问从机换一颗芯片或者换一个引脚改动范围被限定在驱动层应用代码基本不动。这对工控项目后期维护意义很大。RT-Thread从4.x到5.x版本一直维护GD32系列BSPGD32H759的驱动框架是现成的我们主要工作是配置引脚、挂设备、调时序参数。2. 硬件设计与时序分析动手前先搞懂这些参数2.1 硬件SPI和软件模拟SPI绝大多数情况选硬件很多从单片机转过来的朋友习惯用GPIO翻转模拟SPI因为灵活想用哪个引脚都行电平还能自己控制。但我要说清楚在GD32H759这种高主频M7核上模拟SPI有几个先天短板。第一GPIO翻转本身有延迟还要在每bit之间做延时控制同样收发1000字节模拟SPI占用的CPU时间可能是硬件SPI的几十倍第二模拟SPI没有FIFO没有错误标志没有DMA协同M7核跑着RT-Thread来了中断就得停时序一致性很难保证。第三模拟SPI的速率上去了时钟抖动也跟着上去了接高速Flash时信号质量很难看。我的原则很明确只要硬件SPI外设能用绝不模拟。GD32H759的SPI外设带收发FIFO配合DMA可以做到整块数据搬移不用CPU介入这才是M7该有的用法。只有一种情况可以考虑模拟就是某个从机对时序模式极其特殊硬件SPI调不出来那也只建议用在调试阶段。之前遇到一个项目从机要求SCK高电平空闲且在第二个边沿采样硬件SPI能配出Mode 3根本不需要模拟。2.2 SPI时序三件套极性、相位、位序SPI协议本身不复杂但四根线什么时候放数据、什么时候采数据全靠一对参数CPOL和CPHA。CPOL决定SCK空闲电平0是空闲低1是空闲高。CPHA决定数据采样边沿是第一个边沿还是第二个边沿。组合起来就是Mode 0到Mode 3很多从机的数据手册会直接写“支持Mode 0和Mode 3”意思是这两种模式都能用你选一种就行。判断从机模式有个最实用的办法看数据手册里时序图上“数据输出”和“数据采样”的标注。以Mode 0为例CPOL0SCK空闲是低电平第一个边沿是上升沿主机在这个上升沿采样从机在前一个下降沿把数据准备好。Mode 1就是CPOL0、CPHA1空闲低但是第二个边沿即下降沿才采样。ADS1256的数据手册写的是数据在SCLK下降沿输出、上升沿锁存对应模式就是Mode 1这个稍后实战部分会重点说。还有个参数是位序MSB还是LSB first。绝大多数SPI设备都是MSB first先发最高位GD32H759的SPI外设里对应配置。万一遇到LSB first的设备需要在SPI配置里单独设置否则读出数据就是反的。RT-Thread的配置结构体里这三个参数正好是对应的字段RT_SPI_MODE_0到RT_SPI_MODE_3、RT_SPI_MSB、RT_SPI_LSB。2.3 硬件片选还是软件片选我为什么全面倒向软件SPI外设一般都带有硬件NSS引脚传输数据时硬件自动拉低CS结束自动拉高。单从机场景这么用没问题但工控里很少只有一个从机而且很多时候一个从机的一次完整操作需要连续发多条消息中间CS必须一直保持低电平。典型就是读Flash数据先发命令字节和地址字节再连续收数据这期间CS不能断。如果用硬件NSSCS的拉高拉低完全由外设管控很难精确维持跨消息的持续时间。所以我把所有SPI设备的片选都配成普通GPIO用软件控制。CS作为GPIO输出低电平选中高电平释放时序完全由代码控制跨命令操作、多从机切换都很自由。GD32H759的GPIO翻转速度足够快对SPI这种协议来说完全不是瓶颈。相应的代价是每次访问从机前要先把CS引脚拉低操作完再拉高这个动作如果忘掉或者顺序错了就会得到一堆0xFF或者读回全乱码。后面实战部分我会给出带CS控制的标准封装写法。另外一个容易被忽略的点是CS高电平维持时间。从机数据手册里会有tSHSL之类的参数表示CS释放到下次选通的最小间隔通常是几十到几百纳秒。如果代码在一条消息结束后立刻拉低CS开始下一条消息大概率没问题但在高速模式下最好留一点余量。我的习惯是需要跨消息保持的操作用同一段CS低电平不需要的干脆拉高后再延时几百纳秒。3. RT-Thread SPI设备框架接入细节3.1 BSP驱动层的初始化和设备挂载RT-Thread的GD32 BSP在drv_spi.c中实现了SPI总线驱动应用层不用关心寄存器操作。首先要确认工程里已经使能了SPI驱动menuconfig里选上对应的选项然后确认板级初始化里为SPI外设分配了引脚。GD32H759的引脚复用比较灵活SPI0的SCK、MOSI、MISO能映射到多组引脚需要先在board或gpio配置里把复用功能设对否则代码层面怎么调都白搭。设备挂载采用rt_hw_spi_device_attach接口它把一根总线上挂的从机抽象成独立设备。例如把W25Q64挂在spi0总线上代码类似#include drv_spi.h static int rt_hw_spi_flash_init(void) { rt_hw_spi_device_attach(spi0, spi00, GET_PIN(C, 4)); return RT_EOK; } INIT_COMPONENT_EXPORT(rt_hw_spi_flash_init);这个函数做了三件事根据总线名找到SPI总线、创建设备节点、把CS引脚和从机设备绑定。第三个参数GET_PIN(C, 4)是RT-Thread的引脚编号宏表示GPIOC第4脚。如果CS引脚配置成了GPIO模式框架内部会自动管理这个引脚的输出高低应用层只需要知道从机设备名叫spi00就行。还有一点要注意INIT_COMPONENT_EXPORT让这段初始化在系统组件初始化阶段执行确保调用设备框架时驱动已经就绪。3.2 应用层访问SPI设备的正确姿势通过设备框架访问SPI从机最常用的是这几个API。rt_spi_configure用来配置设备参数包括主从模式、位宽、最大时钟和时序模式。rt_spi_send_then_recv最常用它先通过CS发送一个命令再在CS保持选中的情况下收数据非常适合“发命令读响应”这种操作。rt_spi_send_then_send则适合连续发两段数据的场景。直接收发数据的API底层都走消息结构体rt_spi_message里面有个关键概念是cs_take和cs_release。这两个标志位控制消息发送前后的片选动作。比如一条消息设置了cs_take1、cs_release0那么发送前自动拉低CS发送后不拉高接着下一条消息设置cs_take0、cs_release1就能实现整段操作CS保持低电平。理解这个机制以后你就能解释为什么单独调用rt_spi_send一次再调用rt_spi_recv一次和调用rt_spi_send_then_recv一次的结果可以完全不同了。我也见过不少人犯这个错在应用里直接用rt_device_find找到SPI设备后不做rt_spi_configure就发数据出来的结果当然不稳定。SPI不是即插即用设备每次从机上电后至少要配置一次mode、max_hz、data_width。而且配置的速率别一上来就顶满先低速验证通路再往高速调这是最基本的调试顺序。3.3 多线程访问SPI总线的锁管理RT-Thread是实时操作系统工控项目里必然有多个线程比如一个采集线程读ADC一个配置线程写Flash。如果两个线程同时访问同一根SPI总线消息会交错从机必然乱套。设备框架为此提供了总线锁机制rt_spi_take_bus和rt_spi_release_bus成对使用拿锁期间其他线程无法操作这条总线上的任何设备。设计多个线程共享SPI时我建议每个从机操作封装成完整函数内部先拿总线锁再拿片选等整段操作结束再释放片选、释放总线锁。别把锁放得太细比如发命令拿一次锁、读数据再拿一次锁中间CS已经释放从机状态机早就乱了。以读W25Q64为例一次完整读操作是发命令地址收数据必须是一把锁内的事务。另外要注意不要在中断服务函数里调用rt_spi_send这类带锁的API因为锁可能被别的线程占用中断里无法阻塞等待轻则丢失数据重则直接死锁。如果需要从中断触发SPI操作正确做法是中断服务里只释放信号量让等待信号量的业务线程去执行SPI读写。4. 实战一用W25Q64做参数掉电存储4.1 从读ID开始验证SPI通路W25Q64是国产工控板上最常见的SPI NOR Flash容量64Mbit指令集是通用的。第一步先读JEDEC ID命令是0x9F返回三个字节分别是厂商ID、设备类型、容量编码。这个操作最简单专门用来验证SPI四根线有没有接对SPI模式有没有配对。先配置SPI设备参数#include rtthread.h #include rtdevice.h #include drv_spi.h #define SPI_FLASH_DEV_NAME spi00 struct rt_spi_device *flash_dev RT_NULL; static int spi_flash_config(void) { flash_dev (struct rt_spi_device *)rt_device_find(SPI_FLASH_DEV_NAME); if (flash_dev RT_NULL) { rt_kprintf(flash device not found.\n); return -RT_ERROR; } struct rt_spi_configuration cfg; cfg.mode RT_SPI_MODE_MASTER | RT_SPI_MSB | RT_SPI_MODE_0; cfg.data_width 8; cfg.max_hz 1000000; /* 初始跑1MHz验证通过后再提高 */ rt_spi_configure(flash_dev, cfg); return RT_EOK; } INIT_APP_EXPORT(spi_flash_config);注意我用了RT_SPI_MODE_0W25Q64支持Mode 0也支持Mode 3。然后读IDstatic rt_err_t w25q_read_jedec_id(rt_uint8_t id[3]) { rt_uint8_t cmd 0x9F; rt_spi_send_then_recv(flash_dev, cmd, 1, id, 3); return RT_EOK; }rt_spi_send_then_recv内部会自动完成CS取用和释放所以这段代码看起来干净。如果读回来ID是0xFF 0xFF 0xFF那基本可以断定CS没选通、MOSI/MISO接反、速率太快或者引脚复用没配对。如果ID有效说明通路无误接下来就敢往Flash里写数据了。4.2 擦除、写使能和页编程的完整事务SPI NOR Flash写数据有个特点只能把1写成0要把0写成1必须先擦除。W25Q64擦除的最小单位是4KB扇区对应命令0x20。但擦除前必须发写使能指令0x06擦除或编程完成后还要轮询状态寄存器0x05里的WIP位等到硬件自己忙完。这些步骤少一步都不行。一个完善的扇区擦除函数是这样static rt_uint8_t w25q_read_sr(void) { rt_uint8_t cmd 0x05; rt_uint8_t sr 0; rt_spi_send_then_recv(flash_dev, cmd, 1, sr, 1); return sr; } static rt_err_t w25q_wait_busy(void) { while (w25q_read_sr() 0x01) { rt_thread_mdelay(1); } return RT_EOK; } static void w25q_write_enable(void) { rt_uint8_t cmd 0x06; rt_spi_send(flash_dev, cmd, 1); } static rt_err_t w25q_sector_erase(rt_uint32_t sector_addr) { rt_uint8_t cmd[4]; cmd[0] 0x20; cmd[1] (sector_addr 16) 0xFF; cmd[2] (sector_addr 8) 0xFF; cmd[3] sector_addr 0xFF; w25q_write_enable(); rt_spi_send(flash_dev, cmd, 4); return w25q_wait_busy(); }这里的要点是写使能和擦除命令必须是两次独立传输第一次发0x06CS拉高第二次发擦除命令CS再拉低。这是Flash器件的要求很多从单片机的判断固化成一步到位结果经常出错。页编程命令0x02最多一次写256字节如果数据跨页必须拆成多次操作。还有一点擦除粒度是4KB我习惯把参数区规划成多个独立的4KB分区每次更新只擦一个分区另一个分区留作备份掉电中途也不至于整份参数损毁。4.3 直接用SFUD组件Cant我们还是要省点事其实W25Q64这种标准SPI NOR Flash在RT-Thread里有现成的SFUD组件全称Serial Flash Universal Driver。只要在menuconfig里使能sfud然后指定SPI设备名系统启动后会自动识别Flash型号挂载成块设备配合FAL分区和文件系统就能像操作普通磁盘一样读写文件。这比自己写命令层省事太多。我用SFUD的经验是先把基础驱动跑通确认读ID正常然后打开SFUD让组件自己探测。SFUD对W25Q64这类常见型号自动识别成功以后再去看挂载上的块设备。工控项目最怕自己造轮子轮子还有bug能复用社区成熟的组件一定要复用。不过理解底层命令还是有必要的调试文件系统异常时比如擦写失败、分区超界你得知道文件系统底层到底在Flash上做了什么操作。4.4 传输提速的关键从1MHz到30MHz硬件SPI跑1MHz肯定不是目标调试阶段确认通路没问题后我会把max_hz逐步提高。GD32H759的SPI时钟源来自APB具体分频系数取决于驱动实现。比如APB给到150MHzSPI分频2就是75MHz分频4是37.5MHz分频8是18.75MHz。W25Q64官方标称支持上百MHz但实际板子上跑多少还得看布线。提速时的指标很简单反复读写几十次数据比对无错误。我的经验是正式工控板用短线布线30MHz基本稳定如果用杜邦线飞线调试超过10MHz就经常出现偶发错位。还有一个被忽略的因素是SPI Clock的高电平脉宽和低电平脉宽是否对称如果GPIO驱动能力和负载不匹配波形畸变会导致从机采样出错。调试中发现速率上不去先别急着怪芯片看看波形比什么都管用。5. 实战二ADS1256高精度ADC采集5.1 一个最容易被忽略的坑ADS1256不是Mode 0ADS1256是一颗8通道24位Delta-Sigma ADC内部还有个可编程增益放大器PGA增益可以设到64倍专门用于称重传感器、热电偶、多路模拟量采集这些高精度场景。它的控制寄存器要通过SPI配置转换结果也要通过SPI读出来但是它的SPI时序默认是Mode 1不是大多数人习惯的Mode 0。之前有朋友照着网上代码抄SPI明明配置成Mode 0结果是读回的数据全是乱的纳闷了很久。看ADS1256数据手册的时序图数据在SCLK下降沿输出在上升沿被锁存这就是CPOL0、CPHA1对应SPI Mode 1。所以配置要写成struct rt_spi_configuration adc_cfg; adc_cfg.mode RT_SPI_MODE_MASTER | RT_SPI_MSB | RT_SPI_MODE_1; adc_cfg.data_width 8; adc_cfg.max_hz 1920000; /* SCLK最高不能超过输入时钟的1/4 */ rt_spi_configure(adc_dev, adc_cfg);SCLK上限同样要提醒。ADS1256的输入时钟通常是7.68MHz晶振SCLK上限等于输入时钟的四分之一也就是1.92MHz。SPI总线配得太高从机根本处理不过来数据就会丢失或者错位。5.2 寄存器配置顺序和命令族ADS1256的控制命令是一组单字节命令常见的有WAKEUP、RDATA、RDATAC、SDATAC、WREG、RREG、SELFCAL。配置寄存器的流程一般是先发SDATAC退出连续读模式然后通过WREG命令把MUX、ADCON、DRATE寄存器写进去最后发SELFCAL做一次自校准。简单示意一下写ADCON寄存器的过程rt_uint8_t wreg[3]; wreg[0] 0x52; /* 11010010: WREG命令目标寄存器ADCON(0x02) */ wreg[1] 0x00; /* 寄存器数量减一只写1个寄存器 */ wreg[2] 0x00; /* ADCON默认值PGA1内部时钟输出关闭 */ rt_spi_send(adc_dev, wreg, 3);MUX寄存器用来选通道比如要测AIN0和AIN1之间的差分电压MUX写0x01要测单端AIN0相对于AINCOMMUX写0x0C。DRATE寄存器配置采样率具体数值要查数据手册的对应表不同主频下同一个值对应的SPS也不同。这几个寄存器不配置对后面读出来的数据要么是固定值要么数值大得离谱。配置完以后我习惯读一遍寄存器回来校验。RREG命令是0x10加上寄存器地址后面跟一个字节表示读几个寄存器减一然后从设备里读出数据。把写进去的值读回来比对一致再往下走能省很多排查时间。5.3 DRDY引脚与中断信号量配合的采集线程ADS1256有一个DRDY引脚低电平表示一次转换结果已经就绪。轮询这个引脚是最简单的但工控里讲究实时性转换完成到读取数据的延迟会影响有效采样率所以我用RT-Thread的信号量配合外部中断。先创建一个信号量并把DRDY引脚配置成下降沿中断static struct rt_semaphore drdy_sem; static void drdy_irq_callback(void *args) { rt_sem_release(drdy_sem); rt_interrupt_enter(); rt_interrupt_leave(); } void adc_drdy_init(void) { rt_sem_init(drdy_sem, drdy, 0, RT_IPC_FLAG_FIFO); rt_pin_attach_irq(DRDY_PIN, PIN_IRQ_MODE_FALLING, drdy_irq_callback, RT_NULL); rt_pin_irq_enable(DRDY_PIN, PIN_IRQ_ENABLE); }实际使用中外部中断回调里尽量不调复杂API我这里的写法是典型的RT-Thread风格中断里只释放信号量实际SPI读写放在业务线程里。读取一次转换结果的业务代码如下static rt_err_t adc_read_once(rt_uint8_t *stat, rt_uint8_t data[3]) { rt_uint8_t cmd 0x01; /* RDATA命令 */ rt_sem_take(drdy_sem, RT_WAITING_FOREVER); rt_spi_take_bus(adc_dev); rt_spi_configure(adc_dev, adc_cfg); rt_spi_take_cs(adc_dev); rt_spi_send(adc_dev, cmd, 1); rt_spi_recv(adc_dev, stat, 1); rt_spi_recv(adc_dev, data, 3); rt_spi_release_cs(adc_dev); rt_spi_release_bus(adc_dev); return RT_EOK; }这里特意先take_bus再配置是因为多线程并发时配置动作和发送动作必须是一体的不能让别的线程在这个线程配置完成后插入操作。返回值里有状态字节一般不用管但三字节有效数据是24位二进制补码格式转换公式是电压值 原始码 / 8388608.0 * (参考电压 / PGA增益)假设参考电压2.5VPGA1那么满量程就是±2.5V24位补码最高位符号位所以读回的值用int32_t处理后除8388608再乘满量程就行。5.4 多通道切换需要注意的事情ADS1256的8个输入通道通过MUX寄存器在转换前选通。我的做法是在需要切换通道时先退出连续读模式写MUX寄存器然后触发一次SELFCAL或者至少等待几个输出周期让内部滤波器稳定再开始读数据。如果一写完MUX立刻读大概率读到的还是上一个通道的残留数据。工控采集系统经常会遇到干扰问题尤其是ADC前端接近电机驱动或者开关电源时。PCB上SPI信号线要短DRDY这类状态引脚可以加个小上拉模拟输入端加RC滤波。软件上我还会做一步对连续多次读到的原始值做中值滤波或者滑动平均根据噪声水平调整。高精度ADC不是把芯片选好就完事全局的布局布线占一半以上成败。6. 踩坑实录与排查工具箱6.1 数据全是0xFF从最基础的链路开始查读SPI设备返回0xFF是最常见的故障现象也最容易让人一头雾水。我总结了一条排查链路。第一步用示波器或逻辑分析仪看CS引脚发送期间有没有拉低如果CS一直是高说明片选控制没生效检查设备挂载时的引脚编号以及GPIO是否被复用成了别的功能。第二步看SCK有没有时钟输出没时钟或者时钟纹波太大检查SPI外设时钟使能和GPIO复用配置重点看GD32H759的复用寄存器有没有设成AF功能。第三步看MOSI和MISOMISO始终高大概率是电平不匹配或从机没被激活。如果线路看起来都对把SPI速率降到100kHz试试很多从机在上电不稳或者时钟太快时会直接不响应低速一下就正常了。我习惯把所有SPI调试都从最低速率开始不是胆小而是为了把变量数降到最低。6.2 数据偶发错位、多一拍少一拍的疑难杂症全对的情况下偶发错位是最难定位的。有一种常见情况是CS拉高时机不对。有些SPI从机要求在最后一个SCK边沿之后再让CS变为高电平甚至要求保持几十纳秒。如果用多个rt_spi_send拼操作框架会在每条消息结束自动拉高CS那问题就来了。解决办法是手动构造含媒体将由一个消息结构体的工具类消息把整段时序放在一个CS事务里。还有一种错位来自SPI外设的接收FIFO。主机发送字节和从机返回字节并不是同一拍完成的对某些从机来说最后一个发送字节要从机响应的数据可能在CS拉高时才真正准备好软件立刻读FIFO就少了一个字节。解决办法是增加一个空闲时钟周期比如发送完命令后再伪发送一个0x00用来把从机的最后一个响应字节推出来。逻辑分析仪抓一下收发时序错位问题一般立即显形。6.3 Cortex-M7特有的Cache一致性坑GD32H759是Cortex-M7核带了D-Cache这是好事但对SPI加DMA传输来说是个大坑。CPU写数据到发送缓冲区可能只有一部分进了DMA能看到的主存其余还躺在Cache里DMA接收的数据同样可能只写进了Cache尚未同步到主存CPU去读缓冲内存时读到的就是旧数据或者垃圾。症状是SPI用轮询方式完全正常一换成DMA传输数据就偶发不对。解决办法分两种。一种是在DMA发送前调用清CacheDMA接收后先无效化Cache再读数据。另一种更彻底用MPU把DMA缓冲区所在内存区域配置成非缓存属性这样DMA和CPU访问同一块内存时不会经过Cache天然一致。工控项目里我倾向用MPU方式省得每次传输都要在代码里手动维护Cache减少出错概率。这个知识点在非M7核的单片机里根本碰不到是从STM32F4这类M4核转到GD32H759最容易踩的坑。6.4 排查工具和RT-Thread调试小技巧调试SPI最好用的工具是带协议解析的逻辑分析仪16通道以上最好采样率至少20MHz。抓线就抓CS、SCK、MOSI、MISO四根触发条件设成CS下降沿。然后根据抓到的波形核对三件事CS低电平时长、SCK空闲电平、数据翻转边沿和采样边沿。只要这三点和从机数据手册完全对应问题基本就缩小到具体寄存器配置了。在RT-Thread环境里我会在MSH命令行里注册一个简单的SPI测试命令比如读一次W25Q64的ID。命令函数里固定速率跑一遍然后把ID打印出来以后每次改动配置都可以快速验证不用重新烧录完整固件。用MSH的好处是可以实时操作方便对比不同速率下的现象。开发阶段多花半小时把调试命令做好后面排查问题的时间能省一大半。最后分享一个我自己的心得SPI调试中绝大多数问题都不是芯片坏了而是时序参数、片选控制和Cache一致性中的某一环没对上。所以遇到异常不要急着改硬件、换芯片、骂驱动老老实实拿逻辑分析仪把四根线的波形抓出来对着数据手册逐行核对问题通常都会很快水落石出。这个习惯帮我排掉了无数次工控板上的疑难杂症也希望能帮到你。