ARTICLE DETAIL

资讯详情

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

S32K1 LPSPI驱动优化:中断模式与DMA模式对比及性能调优

S32K1 LPSPI驱动优化:中断模式与DMA模式对比及性能调优 1. 为什么LPSPI在S32K1上值得单独研究1.1 一块被低估的车规MCU和一个被低估的外设S32K1系列是NXP面向车载ECU的中坚力量S32K116、S32K142、S32K144、S32K146这几个型号在车身控制、电池管理、网关模块里出现频率非常高。原因是它兼顾了汽车级AEC-Q100认证、-40到125度的宽温工作范围以及一颗足够实用的Cortex-M4F核心主频最高能做到112MHzS32K148为120MHz。在电机控制器、BMS从板、T-BOX通信子板上S32K1几乎成了默认选项。但很多开发者把注意力放在CAN、FlexIO、ADC这些外设上LPSPI反而成了能用就行的角色。实际上LPSPILow Power SPI是NXP在Kinetis和S32K系列上力推的SPI外设相比传统DSPI它有几个关键变化寄存器布局重新设计、FIFO深度更大16字每字最大4字节、支持DMA请求信号单独使能、以及一个非常隐蔽的传输完成TC中断和FIFO空/满中断分离的机制。这些变化直接决定了你在做驱动时选择中断模式还是DMA模式代码写法和性能表现会完全不同。1.2 中断与DMA的根本差异谁在承担数据搬运先说结论中断模式下每一次收发都会触发CPU中断CPU需要停下来把数据从寄存器搬到内存或者从内存搬到寄存器。而DMA模式下CPU只需要在开始前配置好源地址、目的地址、传输长度然后eDMA控制器自己完成搬运全部结束后再通过中断通知CPU一次。这个差异放在低速、小数据量场景下几乎无感。比如通过LPSPI读取一颗温湿度传感器一次就读5个字节SPI时钟1MHz每秒读10次中断模式毫无压力。但当你面对以下场景时问题就出现了通过SPI接口驱动外部Flash一次写入256字节且通信频率较高。从AD7124这类高精度ADC连续读取采样数据数据量随采样率直线上升。用LPSPI与另一颗MCU做高速数据交互比如音频流或传感数据流。这种场景下中断模式会把CPU占用率拉到让人头疼的程度。每次中断进入和退出Cortex-M4F虽然只有12个周期的固定入口延迟但压栈、出栈、跳转、现场恢复的完整成本在50到80个周期。按112MHz主频算一个周期大约8.9ns一次中断的固定开销约500到700ns。如果每秒产生10万次中断仅中断固定开销就占掉5%到7%的CPU这还没算进入中断后实际搬运数据的时间。而DMA模式同样是10万次传输CPU只在最后收到一次完成中断。1.3 选型判断你自己的应用该走哪条路这里给一个相对粗暴但好用的判断标准。单次传输小于4字节传输频率低比如配置寄存器、读状态寄存器用中断模式就行DMA的配置开销反而更大。单次传输4到16字节但传输频率高每秒几千次以上建议DMA否则中断风暴会让系统时序变得非常难看。单次传输大于16字节无论频率高低都建议DMA因为LPSPI的FIFO只有16字深中断模式下CPU会被频繁打断DMA则能把数据连续喂给外设。如果SPI总线上挂了多个从设备CS频繁切换且每次通信时间极短这种情况下DMA的通道切换开销和描述符配置开销要仔细估算偶尔反而用中断更划算。这个判断逻辑我在实际项目里反复验证过绝大多数场景下DMA是值得优先考虑的方案。但DMA不是银弹后面会详细展开配置细节和那些文档里不会写的坑。2. S32K1平台环境准备与最小通信链路搭建2.1 开发环境选型S32DS还是MCUXpresso以及SDK版本的坑S32K1官方推荐的是S32 Design StudioS32DS编译工具链是GCC调试器可以接PE Micro或者J-Link。也有团队把S32K1搬到MCUXpresso里开发因为MCUXpresso的代码生成和调试体验在某些场景下更顺手。但我要提醒一句不要在这个问题上反复摇摆。S32K1的启动文件、链接脚本和时钟初始化跟通用MCUXpresso项目有些差异尤其是startup_M.s和system_MK*.c这两个文件必须确保来自NXP官方S32K1 SDK目前S32K1 SDK 2.0.0在GitHub可以拿到。另外一个容易被忽略的版本问题是S32K1 SDK各版本之间LPSPI驱动接口是变过的。老版本里有LPSPI_DRV_MasterInit、LPSPI_DRV_SlaveInit这种前缀新版本则统一成了LPSPI_MasterInit、LPSPI_SlaveInit。如果你在网上找到的参考代码是2020年之前的直接拷到新SDK里多半编译不过。这里有两个选择要么锁定SDK版本别升级要么动手改函数名后者工作量不大。2.2 时钟树配置PCC模块与LPSPI功能时钟S32K1的外设时钟管理和经典STM32完全不同。STM32的SPI时钟挂在APB总线上你要通过RCC去使能和分频。而S32K1上每个外设都有一个独立的时钟门控和分频机制这个机制叫做PCCPeripheral Clock Controller。在配置LPSPI之前需要明确两件事LPSPI使用哪个时钟源。S32K1的PCC为LPSPI提供的时钟源可以选择SIRCDiv、FIRCDiv、SPLLDiv或者SOSCDiv。默认情况下大多数参考设计会用SPLLDiv系统PLL分频后作为LPSPI的时钟源因为这样频率高且稳定。分频系数对应关系。PCC_LPSPI0寄存器的PCS字段选择时钟源而LPSPI自身的配置寄存器CCR里的SCKDIV字段决定最终SPI时钟频率。计算公式是SCK频率 LPSPI功能时钟 / (SCKDIV 2)这里有个经典陷阱很多人误以为SCKDIV是分频系数直接把值填对了却发现SPI时钟频率只有预期的一半。原因是SCKDIV字段实际是“除2再减1”的关系。比如你想从40MHz的LPSPI功能时钟得到10MHz的SCK正确配置是SCKDIV 3而不是4。不信可以拿示波器量量完你就记住了。我在实际配置时的标准动作/* 使能LPSPI0时钟 */ PCC-PCCn[PCC_LPSPI0_INDEX] PCC_PCCn_PCS(6) | PCC_PCCn_CGC_MASK; /* PCS6对应SPLL */ /* SPI时钟分频 */ LPSPI0-CCR LPSPI_CCR_SCKDIV(3) | LPSPI_CCR_DBT(0) | LPSPI_CCR_PCSSCK(3);DBT是片选延迟时间Delay Between TransfersPCSSCK是片选后到SCK第一个沿之间的延迟。这两个参数在对接不同从设备时可能需要微调但对通信性能影响不大。2.3 引脚与FIFO深度配置LPSPI不是普通SPIS32K1的LPSPI引脚复用是通过PCR寄存器配置的。以S32K144为例LPSPI0的SCK可以映射到PTC14SIN到PTC15SOUT到PTC16这是默认的Alternative 3功能。配置代码很简单PTC-PCR[14] PORT_PCR_MUX(3) | PORT_PCR_DSE_MASK; /* SCK */ PTC-PCR[15] PORT_PCR_MUX(3); /* SIN */ PTC-PCR[16] PORT_PCR_MUX(3) | PORT_PCR_DSE_MASK; /* SOUT */注意DSEDrive Strength Enable位如果需要SPI跑在10MHz以上且PCB走线较长建议打开这一位否则信号质量可能不够理想。但打开DSE会带来稍大的EMI车载应用里如果EMC要求严格可能需要权衡。FIFO配置上LPSPI的FIFO是收发各16字这里的字不是1字节而是可配置的1到4字节。通过TCR寄存器的FRAMESZ字段决定帧大小。这里要特别强调FIFO大小固定但水线Watermark可以配置RWF和TWF字段分别设置接收FIFO和发送FIFO的水线值。DMA操作中水线值决定了eDMA何时被触发这直接关系到性能。后面DMA部分会再细说。3. 中断模式与DMA模式的完整实现对照3.1 中断模式下中断服务函数里的最优解中断模式实现相对简单核心思路是传输过程中每次发送FIFO低于水线或者接收FIFO高于水线就触发中断CPU在中断函数里继续搬数据。先看发送端的实现。假设从设备是外部Flash需要写入256字节到命令寄存器之后的某个地址。中断发送的核心代码如下volatile uint8_t txBuffer[256]; volatile uint16_t txIndex 0; volatile uint16_t txLength 256; void LPSPI0_IRQHandler(void) { uint32_t status LPSPI0-SR; /* 发送FIFO未满继续填充数据 */ if ((status LPSPI_SR_TDF_MASK) (LPSPI0-FSR LPSPI_FSR_TXCOUNT_MASK) 8) { while (txIndex txLength (LPSPI0-FSR LPSPI_FSR_TXCOUNT_MASK) 16) { LPSPI0-TDR txBuffer[txIndex]; } } /* 接收FIFO非空读取数据 */ if (status LPSPI_SR_RDF_MASK) { while (LPSPI0-FSR LPSPI_FSR_RXCOUNT_MASK) { rxBuffer[rxIndex] LPSPI0-RDR; } } /* 传输完成 */ if (status LPSPI_SR_TCF_MASK) { LPSPI0-SR LPSPI_SR_TCF_MASK; /* 写1清中断标志 */ txComplete 1; } }这里有几个容易踩的坑发送中断触发条件必须设置正确。TCFTransfer Complete Flag是当TCR写入的数据全部发送完成后置位应当在发送数据开始时清除数据全部发完后在中断里置位。不要用TDF标志无限等FIFO空这样会白白浪费FIFO深度带来的性能优势。状态机处理才是对的。接收端务必一次性把RX FIFO读完否则下一个数据到达时RX FIFO满数据溢出会置位ROF标志然后这个标志如果不手动清除会一直影响后续接收。中断模式的优点是响应确定逻辑直观适合短小、低频率、时序敏感的控制类通信。缺点是CPU开销大特别在高频大数据场景下几乎吃掉全部CPU时间。3.2 DMA模式从DMAMUX到eDMA再到LPSPI的完整链路DMA模式看起来复杂拆开来看其实是一条数据通路LPSPI的DMA请求信号 → DMAMUX选择通道 → eDMA控制器负责搬运 → 搬运完成后触发中断。每一环都有对应配置。S32K1的eDMA模块与经典的Kinetis eDMA架构一致有32个通道具体看型号S32K116是16通道S32K144是32通道。每个通道可以配置主循环Major Loop和次循环Minor Loop。对于SPI驱动我们通常把一次完整传输比如128字节设为主循环把一次eDMA搬运的字长设为次循环。关键配置段如下/* 1. 配置DMAMUX将LPSPI0的TX请求映射到eDMA通道0 */ DMAMUX0-CHCFG[0] DMAMUX_CHCFG_SOURCE(15) | DMAMUX_CHCFG_ENBL_MASK; /* 2. 配置eDMA通道0源地址为txBuffer目的地址为LPSPI0_TDR */ EDMA0-TCD[0].SADDR (uint32_t)txBuffer; EDMA0-TCD[0].DADDR (uint32_t)LPSPI0-TDR; EDMA0-TCD[0].ATTR EDMA_ATTR_SSIZE(1) | EDMA_ATTR_DSIZE(1); /* 32位宽4字节对齐 */ EDMA0-TCD[0].NBYTES 4; /* 每次次循环搬运4字节 */ EDMA0-TCD[0].CITER 32; /* 当前主循环迭代次数32次次循环 */ EDMA0-TCD[0].BITER 32; EDMA0-TCD[0].SOFF 4; /* 源地址每次偏移4字节 */ EDMA0-TCD[0].DOFF 0; /* 目的地址固定 */ EDMA0-TCD[0].SLAST -256; /* 主循环结束后源地址回到起始 */ EDMA0-TCD[0].DLAST_SGA 0; EDMA0-TCD[0].CSR EDMA_CSR_INTMAJOR_MASK; /* 主循环完成触发中断 */这套配置里要特别注意NBYTES和ATTR的配合。ATTR里的SSIZE/DSIZE定义了一次读写的位宽NBYTES定义了一次次循环搬运的总字节数。当NBYTES 4SSIZE 32位时等于每次次循环触发一次32位读取和一次32位写入。在LPSPI侧发送DMA使能是通过DER寄存器的TDEN位控制的。配置了TDEN后当发送FIFO低于水线时LPSPI会向eDMA发出DMA请求。这个水线由CFGR1寄存器的TWFIELD字段决定。实际项目中除非特别追求极限吞吐量否则建议把发送DMA请求水线设为FIFO空TWFIELD 00也就是FIFO一个数据都不剩时再触发DMA。这样eDMA的搬运频率最低CPU被DMA完成中断打断的次数也最少。接收链路和发送对称区别在于DMA请求源是接收FIFO非空RDF事件目的地址是内存接收缓冲区源地址是LPSPI_RDR且RDR每次读取后自动清空FIFO项。3.3 收发方向同时开DMA双通道描述符的优先级与死锁风险如果SPI通信需要同时收发全双工比如与MCU之间做实时数据交换就需要同时占用两个eDMA通道。这时必须注意一个隐患当DMA的发送通道和接收通道同时工作时如果接收通道的优先级配置高于发送通道且接收数据量巨大发送通道可能一直得不到eDMA总线仲裁。反过来如果发送通道优先级始终高于接收接收FIFO可能溢出。解决思路是把两个通道配置成不同的优先级再配合LPSPI的FIFO水线设置让接收侧的DMA请求稍微滞后于发送侧。比如发送使用高优先级通道接收使用低优先级通道但接收水线设置得更低比如FIFO半满才触发DMA请求这样接收DMA不会频繁争抢总线。实测下来这种配置在大多数匀速传输场景下非常稳定。4. 实测性能对比中断延迟、CPU占用率与有效吞吐量4.1 测试环境与测试方法为了拿到真实数据我用S32K144定制板做了几组对比测试。板子主频112MHz代码运行在Flash变量在SRAM优化等级-O2。LPSPI配置为主模式时钟源用SPLLSCK频率分别设置了1MHz、5MHz和10MHz三档。通信对象是板载W25Q64外部Flash。测试内容包括单次写入256字节统计CPU占用率通过SysTick计数器记录空闲时间占比。单次读取4字节读状态寄存器场景统计中断响应时间。连续写入1KB数据统计总耗时和有效吞吐量。代码分别实现了中断模式和DMA模式中断模式用3.1节的实现DMA模式用3.2节的实现都经过功能验证。4.2 实测数据结果测试项中断模式1MHzDMA模式1MHz中断模式5MHzDMA模式5MHz中断模式10MHz单次256B写入CPU占用率约18%约2%约62%约6%接近100%单次4B读取中断响应时间约2.1us约1.2us(总完成延迟)约850ns约520ns—连续写入1KB总耗时约18.6ms约17.9ms约3.9ms约3.4ms约2.1ms有效吞吐量B/s约53KB/s约56KB/s约256KB/s约294KB/s约487KB/s完成中断次数256字节触发约8次1次256字节触发约8次1次256字节触发约8次看到数据你会发现一个有意思的现象在1MHz低SPI时钟下中断模式和DMA模式的总耗时差异很小差距主要体现在CPU占用率上。因为此时SPI传输本身是瓶颈CPU搬数据的时间远小于等待SCK翻转的时间。随着SCK频率升高中断模式的CPU占用率飙升而DMA模式依然保持低占用差距开始拉大。在10MHz SCK下中断模式已经无法稳定完成连续写入而不丢数据——CPU忙不过来这在实测中直接体现为传输中断或超时。而DMA模式在10MHz时依然游刃有余CPU占用率实测约7%左右。4.3 数据背后的原理为什么DMA在高速下完胜原因并不复杂。中断模式的本质是一场CPU和SCK时钟赛跑。SCK频率越高数据字节之间的时间窗口越窄。以10MHz SCK、全双工8位帧为例一个字节的传输时间是800ns。这800ns内CPU要做的事包括响应中断、处理现场保护、判断当前状态、从内存取一个字节、写入TDR然后退出中断。在112MHz主频下完成这一套流程需要约700ns到1us。也就是说当时钟升到10MHz中断模式的CPU已经完全没有余量了每个字节之间的800ns窗口几乎被占满。DMA模式则完全不同。eDMA的每次访问通过AHB总线矩阵直连SRAM和LPSPI不存在进入中断的开销。一次32位数据的搬运LPSPI TDR写入只需要2到3个总线周期也就是约25ns左右。10MHz SCK下每个字节800ns的窗口长度是eDMA搬运时间的30倍DMA有大量空闲等待时间CPU更是从头到尾只需要在最后等一个完成中断。还有一个很多人忽略的点DMA模式不仅仅是省CPU它还减少了代码层面的延迟抖动。中断响应时间受当前执行指令的影响某些紧耦合的循环可能让中断延迟跳到几微秒以上。DMA则完全不受这个影响因为它不依赖CPU的随机调度。在高实时性要求的场景比如用LPSPI控制外部DAC输出固定频率波形DMA的确定性优势比CPU占用率更值钱。5. 实际项目中遇到的坑与优化技巧5.1 中断风暴与优先级配置别让DMA完成中断变成另一种负担DMA模式虽然几乎免除了每字节中断但如果主循环完成中断配置不当高频、小批量传输会让DMA完成中断同样爆发吃掉CPU。我见过一个同事的项目用LPSPI连续发送16字节短帧DMA完成中断频率达到每秒5万次CPU占用反而比中断模式更高这是个很典型的负优化。解决思路有两种。第一种是合并传输。把多次小帧合并成一次大帧通过TCR中的CONT位Continue保持CS连续拉低一次性传输更多数据。但注意并非所有从设备都支持这种连续传输。第二种是用LPSPI的TCR里的RXMSK和TXMSK位做假传输。如果你只是需要产生一个时钟信号不需要真正接收数据可以设置RXMSK让接收不产生DMA请求只让发送DMA工作。同理如果只需要接收设置TXMSK。这样能减少一半的DMA通道冲突。5.2 缓冲区对齐问题eDMA的隐藏陷阱eDMA配置里ATTR字段的SSIZE与DSIZE如果设置成32位缓冲区必须严格4字节对齐。C语言里声明数组默认对齐通常没问题但你如果用了结构体或从某个协议栈里收到的指针地址不对齐的概率就非常大。不对齐的结果不是报错而是触发eDMA总线错误行为随机有时候会卡死在错误中断里特别难查。我的建议是缓冲区用__attribute__((aligned(4)))显式声明并且在使用前做指针地址校验uint8_t txBuffer[256] __attribute__((aligned(4))); uint8_t rxBuffer[256] __attribute__((aligned(4))); if ((uint32_t)rxBuffer % 4 ! 0) { /* 走到这里说明链接脚本或者代码有问题赶紧处理 */ while(1); }顺带说一句DMA接收缓冲区如果作为协议解析的输入解析时按字节处理没问题但如果你想用memcpy做快速拷贝源和目的地址都必须是4字节对齐否则memcpy内部的多字节拷贝优化路径会崩。5.3 FIFO水线与Burst配置吞吐量的最后一块拼图在DMA模式下LPSPI的发送FIFO水线直接决定eDMA的搬运节奏。水线设置得越高eDMA启动越早每次搬运的数据量越大但可能导致发送FIFO里始终保留部分数据从而影响最小延迟。水线设置得越低eDMA启动越晚每次搬运量越小CPU介入的机会越多。对于追求吞吐量的场景我的建议是发送DMA水线设为FIFO完全为空TXWATER 0TWFIELD 00。接收DMA水线设为FIFO半满RXWATER 8RWFIELD 01。这样做的逻辑是发送时eDMA只有等FIFO全空才搬下一批数据保证FIFO不会因残留数据阻塞新数据到来。接收时FIFO半满才触发eDMA读取这样eDMA每次读搬运量较大8字×4字节32字节减少DMA请求次数。实测这个配置在连续大数据块传输时吞吐量比默认配置大约提升5%到8%。5.4 时钟极性和相位CPOL/CPHA配置错误引起的诡异现象这是LPSPI驱动里出现频率最高的伪随机bug。现象是有时通信正常有时第一帧正常后面全部乱码或者特定延时时长下正常换一个板子就不正常。绝大多数情况下是时钟极性和相位配置错误。LPSPI的CPOL和CPHA是CFGR1寄存器的CSPOL和SCK极性相关的配置位具体字段为CFGR1的SPO和SPH。SPO决定SCK空闲电平SPH决定数据捕获沿。两个配置的组合决定了数据采样的窗口SPO0, SPH0SCK空闲低电平数据在第一个沿上升沿捕获。SPO0, SPH1SCK空闲低电平数据在第二个沿下降沿捕获。SPO1, SPH0SCK空闲高电平数据在第一个沿下降沿捕获。SPO1, SPH1SCK空闲高电平数据在第二个沿上升沿捕获。听起来很简单但问题在于SPI从设备的数据手册给出极性和相位的方式五花八门有的是说数据在SCK上升沿采样有的说数据在SCK下降沿移位有的直接用波形图不写文字。如果你只是照抄别的项目的配置而不查看从设备的数据手册极大概率踩坑。排查方法很简单却有效用示波器同时抓SCK、MOSI、MISO三条线对比从设备手册中的时序图逐项核对。看锁定数据是在SCK上升沿还是下降沿空闲电平是高还是低然后回填配置。这一步千万别省一旦配置错后面的分析都白搭。5.5 低功耗场景下的LPSPI处理车载项目里低功耗是常态。S32K1在Stop模式下LPSPI外设时钟会关闭DMA模块也可能停止工作。这时候设计驱动的注意点在于唤醒后LPSPI寄存器值可能部分丢失尤其是FIFO里的残留数据必须重新初始化。DMA描述符在低功耗模式下不能被访问唤醒后需要重新配置TCD。Stop模式下收到的SPI数据从设备主动发起不会唤醒CPU如果主控MCU是从机建议把LPSPI的接收中断单独配置成可唤醒源但这样会增加功耗。实际项目中我更倾向于总线事务设计上让外部主机只在系统活跃时发起通信低功耗时通过GPIO或者其他唤醒源先唤醒MCU再开始SPI传输。这块没有统一答案完全取决于系统的功耗预算和通信模型。我的建议是在驱动里实现一个状态机区分正常态和低功耗恢复态恢复后强制重新初始化LPSPI和DMA通道而不是依赖SDK自带的恢复机制。SDK的恢复机制通常做得比较保守恢复时间较长会拉高系统唤醒延迟。5.6 调试工具选择与实测信号质量最后说下调试工具。LPSPI跑在10MHz以上时逻辑分析仪的采样率至少50MHz否则还原出的波形边沿抖动很明显看不出真实时序。软件仿真用Saleae个人开发场景或Kingst的逻辑分析仪都可以应对这个带宽。示波器最好用100MHz以上带宽的看信号质量和CS时序会更直观。调试手段上除了常规的断点和打印外还有一个技巧在DMA完成中断里翻转一个GPIO同时用LPSPI的SOUT发一个特殊帧两路信号同时接到示波器上能非常直观地定位DMA延迟FIFO水线设置是否合理从设备响应慢导致CS空闲时间异常等问题。这个方法比看代码猜状态靠谱得多。写在最后的一个小习惯反复调LPSPI驱动的过程中我养成一个习惯任何涉及SPI的改动第一件事先写一段裸机测试代码只调用LPSPI最底层的寄存器操作跟外部设备做10MB数据的回环测试SOUT接SIN引脚做loopback也行或者跟Flash做写入读回校验。这段代码不经过SDK的抽象层可以非常快速验证硬件连接、时钟配置和基本寄存器操作是否正确。确认这层没问题之后再往上叠加DMA、中断、操作系统封装。很多看起来像驱动的问题最后发现其实是硬件连接或时钟配置的问题。先把地基打牢再谈优化永远不会错。
返回列表