最近在项目群里又看到有人发截图TMC5160的SPI读出来的寄存器全是0xFF用示波器看了一下波形SCK和MOSI都有输出MISO却一直高高在上最后怀疑芯片烧了。这场景我太熟了第一次调TMC5160时我也卡了整整一个晚上后来把芯片手册翻烂才发现问题根本不在代码而在SPI协议的几个细节和硬件连接上。TMC5160是Trinamic现在是ADI旗下的步进电机驱动芯片自带斜坡发生器、SpreadCycle静音防抖和闭环功能。SPI口承担的是“控制面”的职责——配置寄存器、读取状态、调整电流和速度参数真正的电机电流控制由芯片内部完成。所以SPI一旦配不好后面所有电机参数都无从谈起。这篇文章从芯片手册出发把TMC5160的SPI帧结构、硬件连接、CubeMX配置和寄存器读写代码完整串一遍重点讲我实际踩过的几个坑。适合正在用STM32F1/F4都适用调TMC5160的开发者尤其是第一次接触Trinamic芯片SPI的朋友。1. 开撸之前先把TMC5160的SPI帧结构背熟1.1 一次SPI访问包含5个字节不是4个很多第一次接触TMC5160的读者会踩同一个坑拿着一般SPI器件的思维去想地址和数据。普通SPI芯片一般是“先发地址再发数据”地址8位、数据8位或16位而TMC5160不一样它一次完整的SPI访问是固定的5个字节总共40位。这5个字节怎么分配第一字节高7位是寄存器地址最低1位是读写标志位。地址范围0x00~0x7F读写位为1表示读为0表示写。剩下4个字节是32位的寄存器数据先发最高位MSB优先。举个例子你要写地址0x10的寄存器那么第一个字节就是 0x10 左移一位末尾补0得到0x20如果你要读0x10寄存器第一个字节就是0x10左移一位后末尾补1得到0x21。后面四个字节在写操作时填要写入的32位数据在读操作时填dummy值一般填0。这个结构起初我也觉得反直觉。后来想通了Trinamic把寄存器设计成统一32位宽SPI帧直接对齐寄存器主控侧不用自己做地址和数据拼接反而更省事。如果你拿普通8位SPI器件的时序来套自然会对不上。1.2 Mode 3、MSB first两个参数定死TMC5160的SPI工作在Mode 3也就是CPOL1、CPHA1。这意味着什么CPOL1空闲时SCK时钟线为高电平。CPHA1数据在SCK的第二个边沿采样。很多STM32例程默认用SPI Mode 0CPOL0、CPHA1或者CPOL0、CPHA0都不对。Mode不对最典型的症状是两个读回来的寄存器值全是0x00或者数据移位错乱感觉每个bit都串位了。我建议拿到芯片的第一件事别急着写代码先翻手册里SPI时序图那一页看三分钟。看什么看SCK空闲电平是高还是低数据在上升沿还是下降沿采样MISO是在CSN拉低之后多长时间开始有数据。看完再回去配CubeMX很多问题直接就消失了。数据位顺序也定死MSB first也就是先发最高位再发低一些的位。STM32CubeMX里SPI默认就是MSB first一般不用改但如果是复制别人工程改来的一定要检查一下有没有人把First Bit改成LSB。1.3 写寄存器“上升沿锁存”读寄存器需要DummyTMC5160的写操作是CSN拉低后主机依次发出5字节数据全部发完后把CSN拉高芯片在CSN上升沿把寄存器内容锁存。这里有个关键点写操作过程中CSN不能提前拉高否则这次写入直接作废。所以代码里CSN的拉高一定要放在SPI传输完成之后不能把拉高写在启动传输之前。读操作和写操作不太一样。主机发完第一个字节地址读标志之后芯片并不会立刻在MISO上给数据而是需要主机继续发送4个dummy字节给从机提供时钟数据才会在后续的时钟周期内从MISO移出来。换句话说读操作必须一次性做满5字节的传输中途不能停停了就丢数据。有人会问那我先发1字节读命令再发4字节时钟MISO上的数据不就有了吗理论上是但实际用HAL库操作时如果你把读命令和dummy分成两次SPI调用中间就可能出现CSN或时钟的空档。稳妥做法是只调用一次全双工传输把5字节看作一整帧。后面给的代码就是这么写的。2. 硬件连接不通的根源往往不在代码在脚上2.1 SDI接MOSISDO接MISO命名别搞反TMC5160的SPI引脚命名和STM32不一样这是很多人把线接反的源头。TMC5160的SDI是芯片的数据输入要接STM32的MOSI引脚。TMC5160的SDO是芯片的数据输出要接STM32的MISO引脚。“输入”和“输出”都是站在芯片角度说的STM32的MOSIMaster Output Slave Input是从主控角度说的这两个“I”容易让人迷糊。我的记忆方法是MOSI和SDI都带字母I它们是一对MISO和SDO是一对。PCB上如果走线实在紧张SCK和MOSI反了程序还能调SDI/SDO反了基本没救只能飞线。实测中还遇到过一个情况从模块板上引出来的引脚丝印写的是“SDO”实际板上已经通过电平转换芯片转了一轮MISO线上有串阻或上下拉。如果发现MISO波形电平不对先别怀疑芯片看一下模块原理图确认信号方向有没有被模块内部倒装。2.2 CLK时钟来源SPI能通但电机不动的隐形原因TMC5160的CLK引脚用来提供芯片内部运动控制的时钟。它可以接外部时钟也可以用内部时钟但这有个隐藏的大坑SPI寄存器读写走的是IO时钟域电机运动控制走的是CLK时钟域。两个时钟域分开所以可能出现一种很迷惑的现象——SPI读寄存器完全正常写参数也能写进去但电机就是不动。我遇到过一次这种情况排查到最后发现CLK引脚悬空了芯片用内部时钟内部时钟虽然能跑但给Step/Dir模式做脉冲同步时精度不够斜坡发生器出来的速度曲线也乱。后来我把CLK接到STM32的MCO引脚输出16MHz电机才恢复正常。如果你的系统对运动控制精度有要求或者要用到闭环和微步插值CLK最好接外部16MHz时钟。从STM32的MCO引脚输出也是常见做法省一颗晶振但要注意MCO的输出频率和电平稳定性别在芯片上电前灌入毛刺。我的习惯是上电后延时50ms再开MCO输出确保TMC5160先完成内部复位。2.3 VCC_IO、ENN和CSN三个容易被忽略的电平问题TMC5160的VCC_IO引脚是数字IO电源决定SPI引脚和ENN等逻辑引脚的电平标准。VCC_IO接3.3V那么SDI/SDO/SCK/CSN都是3.3V逻辑接5V逻辑就是5V。STM32F103的IO一般是3.3V所以VCC_IO接3.3V最省事。如果你把VCC_IO接5V而MCU又是3.3V那就要加电平转换否则长期跑会不稳定。ENN引脚是驱动使能低电平有效。很多人会忽略它以为SPI配置好电机就会转结果电机纹丝不动。ENN悬空时大多数模块默认是禁用状态必须在初始化时拉低或者接下拉电阻。如果用的是现成模块查一下模块有没有把ENN默认拉到地没有的话就用一个GPIO控制上电先拉高初始化完成后再拉低。CSN建议用一个普通GPIO不要用硬件NSS原因后面专门讲。硬件NSS不是不能用但调试期最好不要给自己添堵。MISO线上还有一个容易忽略的小问题TMC5160在CSN为高时SDO引脚可能是高阻态MISO线上电平不确定。如果飞线特别长或者PCB走线周围干扰大建议在MISO上加上拉电阻4.7k到10k都可以。加完你会发现读寄存器的稳定性明显提升偶发错值的概率小很多。3. CubeMX配置和驱动代码一次写对的三种姿势3.1 CubeMX里这样配置SPI我用STM32CubeMX配置STM32F103的SPI2为例其他型号大同小异。SPI2挂载在APB1总线上F103的APB1时钟默认是36MHz这点先记住后面算分频要用。打开CubeMX把SPI2的Mode配成Full-Duplex Master也就是全双工主机模式。这里要注意如果你只需要写寄存器选Transmit Only Master也能跑但TMC5160读寄存器必须收MISO数据所以直接用Full-Duplex Master最稳。NSS片选那一项选择Software。这样CubeMX就不会强制把某个引脚当作硬件NSSCSN完全由你手上的GPIO控制。如果选Hardware后面传输时片选时序容易出幺蛾子。参数配置建议如下BaudRate Prescaler分频32。APB1是36MHz36MHz/321.125MHzSCK约1.125MHz稳定省心。Data Size8 Bits。First BitMSB First。CPOLHigh。CPHA2 Edge。算下来就是SPI Mode 3。实际中有人把分频设成2SCK跑到18MHzTMC5160手册虽然允许一定范围但飞线环境下高频串扰很容易导致偶发错值尤其是MISO上的数据毛刺根本看不清。1MHz左右足够TMC5160用了别图快。CSN引脚我习惯用PB12配成GPIO输出初始电平设为High。CubeMX里SPI2的默认引脚是PB13SCK、PB14MISO、PB15MOSI这三个引脚会被自动配成复用功能CSN自己补一下GPIO配置即可。GPIO Output Speed选High防止边沿太缓。3.2 阻塞式读写调试期最稳的姿势控制器和TMC5160的交互看起来简单写就是发5字节读就是“发5字节收5字节”。下面这套代码是调试期最稳的版本每一行都不玄乎。#define TMC5160_CSN_PORT GPIOB #define TMC5160_CSN_PIN GPIO_PIN_12 #define tmc5160_csn_low() (TMC5160_CSN_PORT-BRR TMC5160_CSN_PIN) #define tmc5160_csn_high() (TMC5160_CSN_PORT-BSRR TMC5160_CSN_PIN) void tmc5160_write_reg(uint8_t addr, uint32_t val) { uint8_t tx[5] {0}; uint8_t rx[5] {0}; tx[0] ((addr 0x7F) 1); // R/W 0写 tx[1] (uint8_t)(val 24); tx[2] (uint8_t)(val 16); tx[3] (uint8_t)(val 8); tx[4] (uint8_t)(val); tmc5160_csn_low(); HAL_SPI_TransmitReceive(hspi2, tx, rx, 5, 10); tmc5160_csn_high(); } uint32_t tmc5160_read_reg(uint8_t addr) { uint8_t tx[5] {0}; uint8_t rx[5] {0}; tx[0] ((addr 0x7F) 1) | 0x01; // R/W 1读 tx[1] 0x00; tx[2] 0x00; tx[3] 0x00; tx[4] 0x00; tmc5160_csn_low(); HAL_SPI_TransmitReceive(hspi2, tx, rx, 5, 10); tmc5160_csn_high(); return ((uint32_t)rx[1] 24) | ((uint32_t)rx[2] 16) | ((uint32_t)rx[3] 8) | ((uint32_t)rx[4]); }注意读操作里返回值用的rx[1]到rx[4]不是rx[0]因为rx[0]对应的是地址字节MISO上还没出有效数据。之前有人直接return rx[0]读回来永远对不上。还有一个容易犯的错把HAL_SPI_TransmitReceive的超时时间写得很小比如1msSPI在1.125MHz下传5字节大概35微秒1ms看起来够但如果在中断里被抢占或者DMA还没准备好就可能超时。我给10ms实际几乎不会触发但保险。3.3 DMA读取状态寄存器从阻塞到非阻塞阻塞式读写代码简单但在高频读取状态寄存器时有个问题HAL_SPI_TransmitReceive是同步的函数执行期间整个CPU都在等这对步进电机控制来说不友好。你一边要发脉冲一边还要卡在SPI读寄存器上时机不对就丢步。DMA方式可以解决这个问题。CubeMX里给SPI2的TX和RX都加上DMA请求方向分别是Memory_to_Peripheral和Peripheral_to_Memory数据宽度选Byte模式Normal优先级按需要调。生成代码后读写函数改成下面这种uint8_t tmc5160_tx[5]; uint8_t tmc5160_rx[5]; volatile uint8_t tmc5160_spi_busy 0; void tmc5160_read_reg_dma(uint8_t addr, uint32_t *result) { tmc5160_tx[0] ((addr 0x7F) 1) | 0x01; tmc5160_tx[1] 0x00; tmc5160_tx[2] 0x00; tmc5160_tx[3] 0x00; tmc5160_tx[4] 0x00; tmc5160_spi_busy 1; tmc5160_csn_low(); HAL_SPI_TransmitReceive_DMA(hspi2, tmc5160_tx, tmc5160_rx, 5); // 等DMA完成或者在其他地方用状态机轮询tmc5160_spi_busy while (tmc5160_spi_busy) { // 实际项目里不要空转可以切换任务或直接返回 } tmc5160_csn_high(); *result ((uint32_t)tmc5160_rx[1] 24) | ((uint32_t)tmc5160_rx[2] 16) | ((uint32_t)tmc5160_rx[3] 8) | ((uint32_t)tmc5160_rx[4]); } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI2) { tmc5160_spi_busy 0; } }DMA模式下片选拉高时机很关键。我见过有人把tmc5160_csn_high()写在HAL_SPI_TransmitReceive_DMA后面结果数据传输还没结束CSN已经拉高了寄存器值读出来全是错的。正确做法是等DMA完成回调再把CSN拉高或者至少在传输完成中断里做这个动作。另外DMA缓冲区必须是全局或静态变量不能是函数内的局部变量否则函数退出时缓冲区可能被回收DMA还在往那块地址写数据就是一个典型的悬垂指针问题。3.4 初始化序列先握手再配置硬件和驱动代码都就位之后别一股脑把所有寄存器都写上。我的习惯是分三步走。第一步上电后等待100ms让TMC5160内部完成复位。这个等待时间不能省尤其是VM和VCC_IO同时上电时芯片内部状态还没稳定就通信容易丢寄存器。第二步做一次通信握手。怎么握手读一个状态寄存器看读回来的值是否符合预期。比如读IOIN确认SPI链路是通的再继续往下走。如果握手失败先别查电机参数回去查SPI配置和硬件连接。第三步按顺序配置驱动电流、微步、速度模式等寄存器。TMC5160的寄存器很多但初始化一般用不了几个。注意很多寄存器有保留位写的时候最好按照手册里的字段说明做位运算不要整个寄存器直接赋值否则容易把保留位改成错误值。初始化完成后拉低ENN使能驱动然后给一个很小的速度指令验证电机有没有转。如果SPI握手没问题但电机不动排查重点放到CLK和ENN上基本就这两个原因。4. SPI通信不生效记录我的一次完整排查过程4.1 第一现场读什么都是0xFF有一次调试板子拿回来SPI怎么读都不对不管读哪个寄存器返回都是0xFFFFFFFF。0xFF这个现象很有代表性MISO线上一直为高主控什么都没收到。我当时的第一反应是查硬件因为SPI协议和代码已经在之前验证过了。用万用表量了MISO到TMC5160 SDO引脚的连通性发现模块上SDO焊盘虚焊看起来焊住了实际上没接触。重新补焊一下再去读寄存器值立刻正常。如果你也遇到读什么都是0xFF按这个顺序排查量MISO线上有没有波形。没有波形八成是SDO虚焊、线断、或者MISO接到了别的引脚。量CSN有没有在整个传输期间保持低电平。如果CSN是高电平TMC5160的SDO输出是高阻MISO一直被上拉拉高读回就是全1。确认SPI模式是Mode 3。如果模式不对数据虽然被时钟采出来了但主控采样点不对读回来可能是0x00。0xFF和0x00的区分很值得记读回全FF优先怀疑MISO链路和片选读回全00优先怀疑SCK相位和芯片没工作。4.2 用示波器/逻辑分析仪看波形排查SPI问题示波器和逻辑分析仪是少不了的。但很多人看波形只盯着SCK和MOSIMISO根本不去看这样找不到问题。抓TMC5160波形时至少要同时抓三根线SCK、CSN、MISO有条件再加MOSI。正确波形长这样CSN在传输开始前拉低整个5字节传输期间保持低传输完成后拉高。SCK空闲为高也就是CPOL1。读操作时CSN拉低后第一个字节期间MISO可能没有有效数据从第二个字节开始出现稳定的数据位。数据位在SCK的第二个边沿是稳定的如果数据变化沿和采样沿混在一起说明CPHA配错了。逻辑分析仪比示波器好用的地方是能直接解码SPI数据。但要注意逻辑分析仪解码出来的数据只是主机侧看到的数据它不知道TMC5160内部到底返回了什么。如果解码结果里MISO一直是0xFF那问题几乎可以锁定在硬件链路上。4.3 硬件片选 vs 软件片选调试期我坚持用GPIO很多人问CSN能不能直接接到STM32的硬件NSS上让SPI外设自动控制我的建议是能但调试期不要这么干。硬件NSS的主要问题是时序不透明。STM32F103的硬件NSS在单字节传输时表现还行但TMC5160一帧是5字节硬件NSS的拉低拉高时机跟着SPI外设状态走出问题的时候你不好判断到底是外设提前拉高了还是字节间有毛刺。软件GPIO控制的CSN代码在哪、什么时候拉高拉低一目了然。方案优点缺点建议软件GPIO片选时序可控多从机方便问题容易定位多占一个GPIO代码里要手动控制强烈建议硬件NSS少写拉高拉低代码时序不透明5字节帧下易出边界毛刺不推荐还有一个实际好处如果后续一个SPI总线上挂多片TMC5160每片用一个GPIO做CSN总线复用特别自然。硬件NSS在这种场景下反而难搞。4.4 读完后CSN拉高的时机非常关键CSN拉高时机这个坑看起来小踩的人特别多。有人用HAL_SPI_Transmit发送5字节作为读请求发完立刻拉高CSN完全不读MISO当然读不到数据。还有人用两次HAL SPI调用第一次发命令第二次接收中间CSN一直保持低但两次调用之间如果被更高优先级中断打断时钟就断了数据照样错。我推荐的做法只有一个读操作和写操作都用一次HAL_SPI_TransmitReceive一个函数调用里完成全双工5字节交换中间不给中断插入的机会。CSN拉高放在TransmitReceive返回之后绝不放前面。如果你用DMACSN拉高放在HAL_SPI_TxRxCpltCallback回调里这个顺序错不了。我之前为了省事在启动DMA后立刻拉高CSN结果读TMC5160的DRV_STATUS字段里面always one标志位都不对排查半天才发现是片选提前释放了。还有一个不太明显但值得留意的点TMC5160的写寄存器是在CSN上升沿锁存的。换句话说你写寄存器时CSN拉高那一下才真正生效。如果你在CSN拉高后马上读取该寄存器有可能会读到旧值因为芯片内部锁存也需要一小段处理时间。严谨的做法是写完寄存器后等一小会儿再读回校验或者读别的寄存器做缓冲。实际项目里这个延迟非常短但遇到偶发读不到刚写入的值时记得往这个方向想想。最后分享一个我自己的习惯用TMC5160调电机先别急着跑运动。上电后只做一件事读寄存器读回来默认值符合预期再往下配参数。这个习惯帮我省了很多时间也推荐给你。TMC5160这套SPI帧结构在Trinamic的TMC2130、TMC5161等芯片上基本一致换芯片时驱动层重写不了几行调通一次后面就是复制粘贴改地址的事。
