☰
定时器触发SPI传输:从原理到STM32H743实践
2026/10/22 19:57:53 网站建设 项目流程

用定时器去触发SPI传输,这件事听起来好像就是两个外设之间连了一根线,但实际做起来,里面的门道比想象中多不少。我最初接到这个需求时,是在做一套电机控制器,需要周期性读取旋变解码芯片的角度值,那个芯片的读取窗口非常短,CPU轮流去发SPI命令虽然也能工作,但每到中断里切来切去,波形上总能看到抖动,延迟一大,角度数据就会出现肉眼可见的跳变。后来我把SPI传输改成由定时器硬触发之后,整个读取节奏变得异常精准,CPU占用也降了下来,这才算是真正体会到硬件触发链路的价值。

这篇内容适合谁看呢?如果你在做电机控制、高速数据采集、音频播放、传感器周期性读取,或者单纯想了解STM32H743这种高性能MCU上SPI外设的进阶用法,那这篇正好对口。我会把完整链路、寄存器级别的原理、CubeMX配置步骤、代码实现以及调试中踩过的坑都展开讲,尽量让刚接触这个玩法的朋友也能顺利跑起来。

1. 项目概述与整体设计思路

1.1 为什么要用Timer触发SPI

先说说不用Timer触发时,常见的SPI读取方式是什么样的。最粗暴的做法就是主循环里反复查询SPI状态寄存器,或者用定时器中断去发命令。中断方式看似可靠,但实际上有个致命问题:中断本身的响应时间是浮动的。如果定时器中断优先级不够高,刚好被其他中断打断,那SPI的实际启动时刻就会往后漂移几百纳秒甚至几微秒。对于角度编码器、ADC同步采样这类对时间确定性要求极高的场景,这种抖动足以让整个系统的性能失真。

还有一个更实际的问题:每次中断里要执行"拉低片选、写命令、等待、读数据、拉高片选"这一整套流程,这些指令本身要消耗几十个CPU周期,而且全都挤在中断上下文里。一旦SPI速率提高或者触发频率提高,CPU就会频繁被打断,实时性反而变差。

定时器触发SPI的方案就完全不同了。它把"什么时候开始传输"这件事完全交给硬件定时器,由定时器产生TRGO事件,事件信号直接送到SPI外设的触发输入口,SPI收到触发后自动启动一次传输。整个过程完全不经过CPU,所以触发时刻的精度就是定时器本身的精度,抖动量基本可以忽略不计。而且如果配合DMA使用,数据搬运也不占用CPU,CPU只需要在整块数据搬完之后做一次后处理就行。

1.2 触发链路全景:从定时器到SPI再到DMA

这条链路可以理解为一条流水线。定时器就好比一个非常守时的闹钟,到了设定时间就敲一下钟;SPI外设听到敲钟声,就立刻开始按设定好的参数在时钟线上输出波形,把数据移出去或者移进来;DMA则是那个负责搬运货物的机器人,SPI每移出一个字节,DMA就把内存里的下一个字节塞给SPI,或者把SPI收到的数据搬回内存。

具体到STM32H743上,链路是这样的:

  • 定时器选择更新事件或者某个比较匹配事件作为TRGO输出;
  • TRGO信号经过内部互连矩阵,连接到SPI的硬件触发输入引脚;
  • SPI配置为使能硬件触发模式,收到有效触发沿之后自动拉低NSS、产生SCK、传输数据;
  • 如果使能了DMA请求,SPI传输完成一个数据单元后会通过DMAMUX触达对应DMA通道,DMA自动搬运下一个数据;
  • 整块数据搬运完成,DMA传输完成中断通知CPU。

这条链路的妙处在于,从触发信号产生到SPI真正开始输出,延迟是固定的,而且非常短。这个固定延迟来自触发输入路径上的同步逻辑,在STM32H7系列中,触发到SCK起始沿的延迟通常只有几个外设时钟周期。只要这几个周期是恒定的,不管触发频率怎么变,系统的一致性都能保持住。

1.3 方案选型:查询、中断、DMA三种触发方式怎么选

同样是"Timer触发SPI",触发之后的数据搬运方式其实有三种选择。很多人在这一步就开始纠结,其实思路很简单:

触发后搬运方式实现难度CPU占用实时性适用场景
查询方式低极高差低速调试,不推荐
中断方式中中中单次小数据量,能接受中断延迟
DMA方式中高极低高高速、批量、周期传输,强烈推荐

我第一次实现时用的是中断方式,就是定时器触发后,进SPI接收中断,在中断里把数据读出来。这个方案在触发频率不高、数据量在几十字节以内时还行。但当时我那个旋变采样要做到10kHz以上,每个周期要读两个16位数据,中断方式就明显吃力了。

后来我改成DMA方式:定时器一触发,SPI自动开始SCK时钟,DMA自动把接收到的数据搬进缓冲区,整个过程CPU只需在DMA传输完成中断里处理一次数据。实测下来,同样10kHz周期采样,CPU占用从原来的百分之三四十降到了百分之五以内,而且波形上几乎看不到抖动。

所以如果你没有特殊理由,直接上DMA方式基本不会错。下面我会重点讲DMA方式的完整落地过程。

1.4 这个方案还能用在哪些地方

定时器触发SPI并不是只能用在电机控制上。我做过的项目里,还有几类场景也用了同样的思路:

  • 音频播放:用定时器以采样率频率触发SPI向DAC芯片发送音频数据,保证每个采样点之间的时间间隔高度一致,声音不会因为CPU调度抖动而产生爆音;
  • 多通道同步采样:多个ADC芯片通过SPI级联,用同一个定时器触发多个SPI同时启动采样,实现真正的同步采集;
  • 传感器周期性读取:IMU、气压计、陀螺仪等传感器,用固定频率触发SPI读取,数据时间戳的准确性大幅提升;
  • 通信协议时序模拟:一些特殊器件要求CS拉低后必须在一个精确时间内开始SCK,定时器触发可以保证这个时间稳定。

这套思路也不只在STM32上有,GD32、国民技术、HC32、ESP32等很多平台的外设都支持类似的触发机制。热词里有人提到"GD32 ADC Timer",其实GD32单片机上用定时器触发ADC采样也是同一类玩法。底层逻辑一旦通透了,换平台只是换寄存器名字而已。

2. STM32H743 SPI硬件触发机制深度解析

2.1 TRGO事件是怎么产生的

先说定时器侧。STM32H743的定时器非常灵活,每个高级定时器和通用定时器都有一个TRGO(Trigger Output)信号,它可以通过定时器控制寄存器2里的MMS位(Master Mode Selection)来选择输出什么事件。可选的事件通常包括:

  • 更新事件(Update Event,即计数器溢出);
  • 使能脉冲(Enable Pulse,即计数器使能时的脉冲);
  • 比较脉冲(OCxREF,即某个通道的比较匹配信号);
  • 多个通道的与/或逻辑组合。

对于触发SPI这种周期性传输的场景,最常用的是更新事件。因为定时器是自由运行加自动重装载的模式,计数器从0累加到ARR值,然后溢出归零,每次溢出就产生一次更新事件,这个事件映射到TRGO上输出。更新事件的周期完全由定时器时钟和ARR值决定,精度可以达到一个定时器时钟周期,非常稳定。

举个实际参数计算的例子。假设定时器挂在APB1定时器时钟上,STM32H743的定时器时钟如果配置为240MHz,想要10kHz触发频率,那么ARR值应该是:

ARR = 240000000 / 10000 - 1 = 23999

也就是说,计数器从0数到23999,总共24000个时钟周期,输出一个TRGO脉冲,对应时间正好是100微秒,触发频率10kHz。这个计算过程和用定时器产生周期中断完全没有区别,只是最后的输出目的地从中断控制器换成了TRGO引脚。

2.2 SPI侧怎么接收触发信号

SPI侧是很多人容易搞混的地方。STM32H743的SPI支持硬件触发模式,但这个模式并不是默认开启的。你需要在SPI配置寄存器里做三件事:

第一,使能触发功能。SPI_CFG2寄存器里的TRIGEN字段,默认是00表示关闭触发功能,必须改成01或者10来使能上升沿触发或下降沿触发。具体是哪个沿有效,取决于定时器TRGO的极性,一般用上升沿。

第二,选择触发源。SPI_CFG2里还有触发源选择字段,可以指定由哪个定时器的TRGO信号作为该SPI的触发输入。不同SPI外设能选择的触发源不一样,比如SPI1通常能选TIM1、TIM2、TIM3等高级或通用定时器。

第三,设置触发后到SCK启动之间的延迟。SPI_CFG2里的RTR字段就是干这个的。这个字段的全称是"Receive Trigger Response",它设置的是SPI收到触发信号后,延迟多少个PCLK周期再真正启动SCK。这个延迟参数非常关键,尤其是使用硬件NSS的时候。因为硬件NSS在收到触发后会被拉低,如果SCK启动太快,从设备可能还没准备好;如果SCK启动太慢,CS拉低时间太长,有些时序要求严格的从设备会直接超时。

这里我想特别强调一下RTR的取值。RTR是4位字段,也就是可以配置0到15个PCLK周期的延迟。更准确地说,它设置的是触发信号有效沿到SCK输出第一个时钟沿之间的等待时间。我当时调试时用的是8MHz的SPI外设时钟,RTR设为1到2个周期就能让大多数从设备正常响应。如果你的从设备对CS下降沿到SCK第一个上升沿的时间有明确要求,比如手册写的是最小100ns,那就把RTR乘以外设时钟周期算一下,比如60MHz的PCLK,一个周期约16.7ns,那RTR至少得设6才能满足100ns的要求。

2.3 DMA怎么和数据链路配合

SPI触发模式加DMA之后,整个数据流是这样一个节奏:定时器TRGO过来,SPI启动SCK,开始发送当前发送寄存器里的数据,同时接收移位寄存器里的数据。因为发送缓冲很快空了,SPI会产生DMA发送请求,DMA自动把内存里准备好的下一个字节写到SPI的发送寄存器。接收方向同理,每次收满一个字节,SPI产生接收DMA请求,DMA自动把数据读走。

如果SPI传输长度固定,你还需要在SPI配置里把传输长度设置好。注意,这里的"长度"指的是每个数据单元多少位,8位、16位还是32位,而不是DMA传输的字节总数。DMA的传输长度是你要搬运的数据块大小。

DMA通道映射在STM32H743上是通过DMAMUX1来实现的,也就是说,DMA请求信号并不是固定连到某个DMA通道的,而是通过DMAMUX1的通道选择寄存器把请求源映射到任意DMA流。这个灵活性很大,但也有个坑:你必须在启动DMA之前把映射配置对,否则DMA永远等不到请求。CubeMX里如果直接用图形化配置,它会自动帮你生成DMAMUX的配置代码,但如果你是手写寄存器操作,很容易漏掉这一步。

2.4 Cross Trigger Matrix:不止SPI能用这套触发逻辑

热词里出现了"cross trigger matrix"这个提法,确实值得展开说一下。在STM32H743里,定时器触发SPI只是硬件交叉触发矩阵的一个应用场景。同一个矩阵里,定时器还能去触发ADC采样、DAC输出、LPTIM启停等,多个外设之间可以形成复杂的联动关系,而不需要CPU在中间做协调。

比如你在做电机控制,同一个定时器更新事件同时触发ADC采样和SPI读取编码器数据,这两个动作就能做到严格同步。我用过不少平台之后,最大的感受是:H743的触发矩阵在实时系统中扮演的角色,比很多人想象的要重要得多。它相当于一个小型的硬件调度器,把周期性的、对时序敏感的动作全部从CPU里剥离出来,统一由硬件精确控制。

还有一点值得提,如果你之前用过GD32的ADC定时器触发,你会发现概念上和STM32H743的触发矩阵非常相似。所以即使你从STM32切到GD32,或者反过来,这套"外设由定时器触发"的思路都是相通的,差别只是寄存器和配置方式。

3. 从零到能跑:CubeMX配置与代码实现

3.1 硬件准备与6针SPI接线

先说硬件。STM32H743最小系统板一块,SPI从设备可以是任何你手头有的SPI器件,我这里以AD2S1210旋变解码芯片为例,它正好也是热词里提到过的。除此之外,还需要一个逻辑分析仪或者示波器,用来确认时序对不对。这个非常重要,因为你光看代码跑没跑通不能完全放心,只有看到SCK、MOSI、MISO、NSS四根线的波形和器件手册对得上,才真正算成功。

接线方面,SPI最少有4根信号线,再算上电源和地就是6根,所以热词里说的"6针SPI"就是指这种带电源地的标准接法。我在测试时用的接线如下:

信号STM32H743引脚从设备引脚说明
SCKPA5(SPI1_SCK)SCLK时钟线
MOSIPA7(SPI1_MOSI)SDI主出从入
MISOPA6(SPI1_MISO)SDO主入从出
NSSPA4(SPI1_NSS)CS片选,硬件控制
VCC3.3VVDD电源
GNDGNDGND共地

片选这里有两种选择:硬件片选和软件片选,我们后面专门开一小节讲,这里先按硬件片选接线。

3.2 CubeMX里定时器怎么配

打开STM32CubeMX,选择STM32H743芯片后,先配置时钟树,把定时器时钟提上来。这里我建议把APB1和APB2的定时器时钟都配置成240MHz,保证定时器精度。

然后找到TIM1,在Mode里选择Internal Clock,在Trigger Output(TRGO)Parameters里把Master/Slave Mode配置为Trigger Mode,Master Mode Selection选择Update Event。这样TIM1的更新事件就会通过TRGO输出。

参数区里,Prescaler设为0,Counter Period设为你想要的触发周期减一。例如想要10kHz触发频率:

Counter Period = 240000000 / 10000 - 1 = 23999

如果你是用HAL库,这个参数最后会写进htim1.Init.Period。Period的单位是定时器时钟周期个数,不是秒,这个不要搞混。

还需要在NVIC设置里打开TIM1全局中断,因为后面DMA链路的终点是CPU做后处理,如果不需要中断可以不开,但建议开,方便调试时加断点确认传输完成。

3.3 CubeMX里SPI怎么配

以SPI1为例,在CubeMX的SPI1配置页面里:

  1. Mode选择Full-Duplex Master;
  2. Hardware NSS Signal选择NSS Output,也就是硬件片选输出;
  3. Parameter Settings里,Baud Rate尽量设低一点起步,比如8Mbps,等验证通过再往上调;
  4. CPOL设为Low,CPHA设为1 Edge,也就是SPI Mode 0,大多数SPI从设备默认都是这个时序;
  5. Data Size设为8Bits或16Bits,取决于你的从设备寄存器宽度;
  6. 重点来了,在Advanced Parameters里找到Hardware Trigger,置为Enabled,然后Trigger Source选择TIM1_TRGO。

如果你用的IDE和我一样是CubeMX,这几个选项基本都在图形界面里能直接点出来。RTR这个参数在CubeMX里有时不会直接暴露,可能需要你在代码里手动修改SPI_CFG2寄存器,我会在代码部分说明。

3.4 CubeMX里DMA怎么配

还是在SPI1配置页面,往下找DMA Settings,添加一个SPI1_RX的DMA请求,Direction选择PeripheralToMemory,Mode选择Circular或Normal都行,看你的需求。

  • Normal模式:每次触发SPI传输一块数据,传输完成后DMA停止,需要重新启动才能处理下一次触发;
  • Circular模式:DMA会自动回绕,整块缓冲区持续接收,适合连续周期采样。

我做10kHz连续采样时用的是Circular模式,配合一个双缓冲区,CPU在后台处理一块数据的同时DMA往另一块写,效率最大化。不过Circular模式有个细节要小心,就是缓冲区覆盖问题,需要自己判断哪些数据是新的哪些是旧的。

3.5 主程序代码实现

CubeMX生成工程之后,代码逻辑其实非常简洁。核心思想是:先准备好要发送的数据缓冲区,启动DMA接收,再启动定时器,之后一切交给硬件。

下面给出一个最小可运行的示例代码,基于HAL库:

/* 定义缓冲区 */ #define SPI_RX_BUFFER_SIZE 16 #define SPI_TX_BUFFER_SIZE 16 uint8_t aRxBuffer[SPI_RX_BUFFER_SIZE]; uint8_t aTxBuffer[SPI_TX_BUFFER_SIZE]; volatile uint8_t spi_block_complete = 0; /* 发送数据初始化:示例为向从设备地址0x00写入 */ void PrepareTxData(void) { aTxBuffer[0] = 0x00; /* 寄存器地址 */ aTxBuffer[1] = 0x01; /* 数据高位 */ aTxBuffer[2] = 0x02; /* 数据低位 */ /* 其余字节按需填充 */ for (int i = 3; i < SPI_TX_BUFFER_SIZE; i++) { aTxBuffer[i] = 0x00; } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_SPI1_Init(); MX_TIM1_Init(); PrepareTxData(); /* 启动SPI接收DMA,使能SPI外设 */ HAL_SPI_TransmitReceive_DMA(&hspi1, aTxBuffer, aRxBuffer, SPI_TX_BUFFER_SIZE); /* 启动定时器,开始产生TRGO触发信号 */ HAL_TIM_Base_Start(&htim1); while (1) { if (spi_block_complete) { spi_block_complete = 0; /* 在这里处理aRxBuffer里的数据 */ } } } /* SPI接收DMA传输完成回调 */ void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { spi_block_complete = 1; } }

注意,上面的代码默认使用Normal模式的DMA。如果SPI传输长度只有16字节,那么每触发一次,SPI收到16个字节后DMA就完成,回调里置标志位。下一次定时器触发来的时候,DMA必须已经重新启动,否则只能做单次触发。所以在实际连续采样场景中,你需要在回调里再次调用HAL_SPI_TransmitReceive_DMA重装DMA。

如果你是Circular模式,就不需要重装了,DMA会自动循环搬运,你只需要根据DMA半传输和全传输中断来切块处理数据。

还有一个细节:CubeMX生成的SPI初始化函数里,默认是不开启硬件触发模式的,你需要手动补一句。修改MX_SPI1_Init函数,在SPI初始化之后加上:

void MX_SPI1_Init(void) { hspi1.Instance = SPI1; /* 原有Init结构体配置... */ HAL_SPI_Init(&hspi1); /* 使能SPI硬件触发,选择TIM1_TRGO作为触发源 */ SPI1->CFG2 |= SPI_CFG2_TRIGEN_0; /* 使能触发模式 */ SPI1->CFG2 &= ~SPI_CFG2_TRIGEN_1; /* 上升沿触发 */ /* 根据实际从设备时序,设置触发后延迟为2个PCLK周期 */ SPI1->CFG2 = (SPI1->CFG2 & ~SPI_CFG2_RTR) | (2U << 28); }

这里的寄存器操作是H743特有的。SPI_CFG2的TRIGEN位域在bit8和bit9,RTR位域在bit28到bit31。不同系列的SPI寄存器布局不一样,如果你是其他型号的芯片,务必翻一下对应参考手册,不要照抄。

3.6 硬件片选和软件片选该选哪个

片选这个问题在定时器触发SPI模式下尤其关键,这里单独拿出来说。

硬件片选的意思是说,NSS引脚由SPI外设自动控制。收到触发信号后,SPI自动把NSS拉低;传输完成或者触发后延迟结束时,自动拉高。好处是完全不需要CPU干预,从设备看到的就是干净利落的一次片选拉低、时钟传输、片选拉高的脉冲。

坏处是,硬件片选信号的有效时间、拉低和拉高的时机都由SPI外设内部的逻辑决定。有些从设备要求CS拉低后必须保持一定时间才能拉高,如果SPI配置不当,硬件NSS的脉冲太短,从设备会识别失败。

软件片选就简单多了,NSS引脚配置为普通GPIO输出,你在代码里手动拉低、传输、再拉高。但在定时器触发场景下,手动拉低片选会引入一个麻烦:片选拉低动作本身需要CPU去执行,如果在中断里做,又会出现中断延迟抖动。

所以我个人建议是:一旦决定用定时器硬触发,尽量用硬件NSS,把整个片选、SCK、数据传输全部交给硬件。如果从设备对CS时序确实有特殊要求,那就用RTR参数来调整CS拉低到SCK启动之间的时间。这个方法比CPU手动干预要准得多。

4. 调试记录与常见问题排查

4.1 触发后完全没有SCK波形

这是我第一次调这个方案时遇到的问题。定时器明明在跑,TRGO配置也设了,但用示波器看SCK引脚,什么波形都没有。

排查下来发现是SPI_CFG2里的TRIGEN没有正确设置。我当时用的HAL库,CubeMX的SPI配置界面里虽然能看到触发选项,但生成的代码并没有把触发模式写进寄存器,需要手动补。如果你也遇到这种情况,首先检查SPI的CFG2寄存器,确认TRIGEN位不是0。

另外要注意,SPI外设必须处于使能状态。硬件触发只能在SPE位置1之后才能响应。如果SPI使能了但DMA还没准备好,触发信号来了也不会产生SCK。

还有一个常见问题:TRGO信号根本没输出。检查定时器的Master Mode Selection,必须把MMS设成Update或者其他有效事件,同时定时器得真正启动。很多人只配置了定时器参数却忘了调用HAL_TIM_Base_Start,TRGO当然不会有信号。

4.2 数据对不上位,错位或者全是FF

SCK波形正常了,但读回来的数据永远是0xFF,或者数据整体错位了一个字节。这种情况十有八九是SPI时序参数和从设备不匹配。

首先是CPOL和CPHA。SPI有四种模式,从设备手册里会写明它支持哪种。如果从设备要求模式1(CPOL=Low, CPHA=1 Edge),你却配成了模式0(同样的CPOL和CPHA),读回来的数据就会整帧错位。我经历过一次印象特别深,AD2S1210明明手册上一开始说的是模式0,但实际读出来总是低字节和高字节颠倒,后来发现是数据宽度设置成了8位而不是16位。

其次是数据宽度。STM32H743的SPI支持4到32位的数据宽度,如果你用16位宽度去读一个8位寄存器,读回来的数据会整体左移,看起来像错位。务必核对从设备的寄存器位宽和SPI配置里的Data Size保持一致。

还有一个很隐蔽的问题:硬件NSS的脉冲宽度。如果从设备比较慢,CS拉低后需要一点点时间稳定内部状态,而你的RTR设得太小,SCK启动太快,从设备还没来得及采样,回来的数据就是乱的。这时候可以把RTR调大一些再试。

4.3 触发频率提升后出现丢帧

当我把触发频率从10kHz往上调到100kHz时,开始出现数据帧丢失。排查下来发现原因有两个。

第一个是DMA重装不及时。Normal模式下,每次传输完成,DMA会停止。下一次触发来之前,你必须重新调用HAL_SPI_TransmitReceive_DMA把DMA重新武装好。如果中断回调处理得慢,定时器触发又来得快,就会有一帧数据被SPI忽略掉,因为DMA没准备好,SPI收进来的数据无处可放。

解决办法:改用Circular模式,或者把DMA重装的代码放到效率更高的位置,比如DMA传输完成中断里只做一件事,就是重新启动DMA,数值后处理放到主循环里去做。

第二个原因是触发频率超过了SPI最大吞吐能力。SPI传输一小块数据需要时间,这个时间取决于SPI波特率和数据量。如果定时器触发间隔比传输一次需要的时间还短,SPI还在忙着上一帧,新的触发来了也只能排队,造成数据积压。计算很简单:

  • SPI位速率8Mbps,一帧16字节等于128bit;
  • 一帧传输时间 = 128 / 8000000 = 16微秒;
  • 对应最大触发频率 = 1 / 0.000016 = 62.5kHz。

所以如果你的采样率想上到100kHz,SPI至少得配到13Mbps以上,同时保证DMA准备时间足够短。这个计算我建议每个用定时器触发SPI的人都动手算一算,能避免大量无谓的调试时间。

4.4 触发沿选择和触发串扰问题

有时候你会在调试时发现,SPI本来只该在一个定时器事件时启动,却出现了额外的SCK脉冲。这个问题多半是触发沿极性搞反了。定时器TRGO默认输出的是脉冲信号,但如果你配置了下降沿触发,同时TRGO信号本身存在高点平时间较长的情况,就可能出现重复触发。

还有一个相关的问题是触发信号上的毛刺。在电磁环境比较差的场合,长走线上可能会耦合进来噪声,产生额外的触发沿。这时候需要做两件事:第一,把触发信号线尽量走短,远离功率电路;第二,在SPI_CFG2里把触发模式配置成正确的极性,避免像下降沿触发那样容易被噪声拉低而造成误触。

4.5 常见问题速查表

现象排查方向处理建议
SCK无波形TRIGEN未使能、SPE未置位、TRGO无信号检查SPI_CFG2.TRIGEN,定时器MMS,确认已启动定时器
数据全为FF或00时序模式错误、数据宽度不匹配核对CPOL/CPHA,检查SPI Data Size
数据错位MOSI/MISO接反、数据宽度错误、DMA缓冲区错位先查接线,再查数据宽度和DMA地址
偶发性丢帧DMA重装不及时、触发间隔小于传输时间用Circular模式,提高SPI波特率
无法重复触发DMA模式设为Normal但未重新启动在回调中重新调用DMA接收函数
CS时序不符合预期RTR设置过小根据从设备手册增加RTR值
触发误动作噪声毛刺、触发极性错误缩短走线,调整触发沿极性

4.6 调试工具和高效观测方法

调这类硬件触发链路,光看代码是看不出问题的。我的经验是,逻辑分析仪是必备工具。把SCK、MOSI、MISO、NSS四根线挂上,用高采样率的逻辑分析仪抓一段波形,然后对照从设备的SPI时序图逐帧比对。

有一个操作技巧我每次调试都会用:先把SPI波特率降到很低,比如1Mbps,确认整个链路逻辑正确,再把波特率逐步往上调。这样可以避免一开始就被高频信号的各种反射和串扰干扰,把问题复杂化。低速下先把功能打通,高速下再去解决物理层问题,这个顺序能节省大量时间。

另外,如果条件允许,可以用DAC或者GPIO在DMA完成中断里翻转一个IO口,用示波器看这个IO翻转的周期是否和理论触发周期一致。这样能快速验证整个链路是否真的做到了稳定周期触发,也方便对比随机抖动大小。

4.7 HAL库和寄存器混用时的几个小坑

在实际代码里,我混用了HAL库和直接寄存器操作,有几个坑值得单独提醒。

首先是HAL库的SPI句柄状态。HAL_SPI_TransmitReceive_DMA函数在启动DMA传输时会检查句柄的State字段,如果上一次传输还在进行中,返回就是HAL_BUSY。我在调试时遇到过在回调里重新启动DMA但总是失败的情况,后来发现是因为我在传输完成中断里做太多事情,导致句柄状态没有及时清零。解决办法是把重启动DMA的操作放在回调的最前面,做完之后再做数据处理。

其次是触发源的配置时序。H743的SPI_CFG2寄存器里,有些位只能在SPI禁用状态下修改,有些可以在运行状态下修改。TRIGEN和RTR我建议在SPI还未使能时一次性配好,否则可能出现配置不生效的现象。

还有个非常隐蔽的坑:如果同时用到了硬件NSS和DMA,NSS的输出时序会受到SPI_CFG2里MSTR、SSOE这些位的影响。我在调试时发现CS拉低的时间比预期长了很多,后来发现是SSOE位配置不对,导致NSS在一帧传输结束后没有立刻拉高。这时候参考手册里NSS输出的时序图特别重要,一定要逐条对照,别想当然。


最后再分享一个小实操经验。调试定时器触发SPI时,不要一上来就追求高性能,先用一个最简单的从设备,比如一片SPI Flash,跑通"定时器触发+SPI读ID"这个流程。SPI Flash的读命令时序非常宽容,对RTR、片选延迟几乎不挑,很适合做功能验证。一旦这套流程稳定了,再换到AD2S1210这类时序敏感的器件,你就能更容易分辨出是硬件链路的问题还是从设备本身时序要求的问题。这个顺序帮我省了不少时间,至少三次调试项目都是靠这个思路快速定位的。

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

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

立即咨询