最近在给一台桌面级CNC做驱动升级,手头同时拿到了TMC2208和TMC2209两种步进电机驱动芯片,而且都需要通过UART模式接到STM32F103上控制。原本以为只是“一个贵一点、一个便宜一点”的区别,结果翻数据手册、看寄存器、调通信时序,折腾了整整两个晚上,才把TMC2209与TMC2208的UART模式差异彻底理清楚,也把F103的串口控制步进电机的代码跑稳定了。这篇文章就把整个实践过程写出来:从芯片差异、硬件接线、标准库代码到实际踩坑,尽量让同样在调驱动的朋友少走弯路。
如果你是第一次接触TMC系列驱动,或者已经在用TMC2208但想换TMC2209,尤其是想用STM32F103这类MCU通过UART去配置电流、微步、读取状态,这篇应该能帮上忙。开整。
1. 为什么要在TMC2208和TMC2209之间纠结
1.1 两颗芯片的定位和我的选型场景
TMC2208和TMC2209都是德国Trinamic(现在被ADI收购)推出的两相步进电机驱动芯片,主打静音、低发热、内建256微步细分。TMC2208是上一代“网红”驱动,大量用在3D打印机主板上;TMC2209则更像是针对TMC2208的痛点做了补强:多了StallGuard2失速检测、地址选择引脚、更完整的寄存器状态读取能力。
我的项目是一台三轴小型雕刻机,原来用A4988驱动,噪音大、电流要手动调电位器,而且一旦电机电流设小了就容易丢步。换成TMC2208之后噪音确实小很多,但在实际操作中发现,TMC2208的UART模式下地址固定,一根总线上只能挂一颗,调试时没法把X轴、Y轴驱动都挂在同一条串行线上。后来注意到TMC2209的AD0引脚可以选地址,一根UART总线最多挂两颗驱动,这就方便多了,于是决定把X/Y轴都用TMC2209,Z轴暂时用TMC2208测试。
1.2 UART模式到底解决了STEP/DIR模式解决不了什么问题
很多人以为TMC2208/TMC2209只是“带UART接口的步进驱动”,实际在电机运转层面,它们依然需要STEP脉冲和DIR方向信号,UART并不是用来发脉冲的。UART模式真正解决的是驱动参数在线配置的问题。
STEP/DIR模式下,电流大小靠电位器调,微步细分靠MS1/MS2引脚的电平组合,改一次参数就要动一次硬件。而UART模式下,可以通过寄存器直接设置IHOLD_IRUN运行电流、CHOPCONF微步、PWMCONF斩波参数、GSTAT错误标志,甚至实时读取失速检测结果。这点对于需要频繁调参的CNC和机器人项目来说,效率提升是立竿见影的。另外,UART模式下可以把TMC2209的StallGuard2功能用起来,通过检测机械负载突变来实现归零,节省一组限位开关。
提示:TMC2208支持UART但功能被砍掉了不少,尤其没有StallGuard2,不能直接把TMC2209的失速检测代码套上去。这个差异后面会详细说。
2. 寄存器级对比:TMC2209比TMC2208多了什么
2.1 基础寄存器功能对比
如果你在TMCL-IDE或手动写UART帧,会发现TMC2208和TMC2209的寄存器地址有很大一部分是重合的,比如GCONF地址都是0x00、IHOLD_IRUN地址都是0x10、CHOPCONF地址都是0x6C、PWMCONF地址都是0x70。也就是说,给TMC2208写的电流配置、微步配置代码,在TMC2209上基本也能跑,这是两者的兼容性基础。
但TMC2209增加了几组TMC2208完全没有的寄存器,最重要的是:
| 功能 | TMC2208 | TMC2209 |
|---|---|---|
| 地址选择 | 固定0x00 | AD0引脚选0x00或0x01 |
| StallGuard2失速检测 | 不支持 | 支持,SGTHRS寄存器配置 |
| TCOOLTHRS失速检测速度阈值 | 不支持 | 有,需配置才启用 |
| SG_RESULT负载结果读取 | 不支持 | 有,反馈当前机械负载 |
| DRV_STATUS错误细节 | 基本错误标志 | 更详细的开路/短路/温度标志 |
| 两线式/单线式UART | 单线 | 单线 |
这里有个容易忽略的点:TMC2208的UART地址是固定的,但TMC2209需要额外读一下AD0引脚,并且地址是包含在发送请求中的数据字节里的,所以如果换了芯片没改地址参数,驱动怎么叫都不应答。
2.2 地址机制:一颗2208总线上只能有一颗,2209可以挂两颗
TMC2208的数据手册写得很明确:UART从机地址为0x00,不支持地址修改。这意味着如果你在一条UART总线上同时接两个TMC2208,它们会对0x00地址的请求同时响应,总线冲突。在实际项目里,就得给每个驱动单独拉一根UART引脚,占用MCU的多个串口。
TMC2209则多了一个AD0引脚,接低电平时从机地址是0x00,接高电平时从机地址是0x01。这样一条UART上可以接两个TMC2209,只要软件里根据地址区分是X轴驱动还是Y轴驱动即可。如果项目有3个轴以上,那就需要多个UART或加模拟开关分时复用。我的方案很简单:STM32F103的USART1接X轴TMC2209(AD0=0)和Y轴TMC2209(AD0=1),USART2接Z轴TMC2208。
2.3 失速检测带来的归零方式变化
TMC2209的StallGuard2原理是监测电机反电动势和内部电流变化来判断负载突变。当机械结构运动到限位边界时,负载会突然变大,SG_RESULT数值会出现明显跳变。通过设置SGTHRS阈值,可以让驱动器在失速时给出标志,主控据此切断运动或跳回安全位置。
这个功能对CNC、3D打印机来说,最大价值就是可以省掉机械限位开关。原来归零时,平台撞到限位开关,开关触发电平变化,MCU停止脉冲输出。用了TMC2209后,不需要额外接线,只需在固件里周期读取SG_RESULT,当数值低于设置阈值时认为已经撞到边界,停止发脉冲即可。
TMC2208完全没有这个能力,想归零只能继续用限位开关,或者额外接光电/磁传感器。这是选型时最需要考虑的差异,而不是纠结那几十毫欧的内阻差异。
3. STM32F103与TMC驱动板的硬件接线
3.1 PDN_UART单线半双工总线怎么搭
TMC2208和TMC2209的UART模式都走PDN_UART引脚,这是一根单线双向数据线,硬件上类似1-Wire,但电气特性和UART又不完全相同。官方例子通常是让MCU的TX和RX通过1k电阻并在一起再连到PDN_UART,然后靠切换MCU的收发模式来工作。
STM32F103的USART支持半双工模式,在标准库中调用USART_HalfDuplexCmd(USART1, ENABLE),之后只需要接MCU的TX引脚即可,因为半双工模式下TX和RX在芯片内部已经连在一起了。我实际用的接法如下:
- STM32F103的PA9(USART1_TX)串一支1k电阻,然后连接到TMC2209的PDN_UART
- PDN_UART引脚外接一个10k上拉电阻到VCC_IO(3.3V)
- TMC2209的AD0引脚接GND,或者接3.3V,用来区分地址
- 驱动板的地与STM32F103的地必须共地
注意:TMC2208/TMC2209的逻辑电压VCC_IO接3.3V,PDN_UART上拉电阻也接3.3V,不要接到电机电源电压上。TMC2209的VCC_IO在3.3V直接连F103没有电平兼容问题,但如果驱动板上的VCC_IO接了5V,就需要加一个双向电平转换电路。
3.2 上拉、电容、这些容易翻车的小地方
PDN_UART空闲时应该被上拉到高电平,因为UART总线空闲为高。如果省掉10k上拉电阻,通信稳定性会变差,尤其是温度升高后,经常第一帧发送失败。
另外,PDN_UART对地需要一颗100nF左右的滤波电容吗?看具体驱动板设计,很多模块已经自带,建议先看模块原理图。如果PDN_UART到MCU的连线比较长,超过20cm,建议在靠近MCU侧加一个RC滤波,R取1k,C取100pF左右,可以有效减少电机线耦合过来的噪声干扰。不要加大电容,否则UART波特率高于115200时边沿会变缓,导致通信失败。
STM32F103的USART1_TX配置成复用开漏输出,因为半双工模式下需要边输入边输出。标准库初始化时GPIO配置为GPIO_Mode_AF_OD,速率GPIO_Speed_50MHz。如果配置成推挽输出,接收外部数据时会出问题。
4. STM32F103标准库实现TMC的UART帧
4.1 通信帧格式和CRC8计算
TMC2208/TMC2209的UART帧结构大致如下。
写寄存器时,主机发送:
0x05 | 从机地址 | 寄存器地址 | 数据data[3] | data[2] | data[1] | data[0] | CRC8其中从机地址、寄存器地址各占一个字节,数据是大端序,高字节在前。CRC8是前7个字节的校验值,多项式为0x07或0x8C,具体实现可以按官方推荐表来。
读寄存器时,主机发送:
0x07 | 从机地址 | 寄存器地址 | CRC8然后驱动会返回:
0x05 | 从机地址 | 寄存器地址 | 数据data[3] | data[2] | data[1] | data[0] | CRC8这里有个坑:TMC2208和TMC2209的地址虽然在硬件上固定/可选,但发送帧里的地址字段不是简单的0x00或0x01直接发,很多资料会强调需要左移一位或与读写位组合。我建议各位以自己手里数据手册的“UART接口”章节为准,我在代码里使用的是一个简单的地址值直接表示从机地址,实测TMC2209 AD0=0时地址用0x00可以通信,AD0=1时地址用0x01可以通信。
CRC8的C代码我用的是查表法:
static const uint8_t tmc_crc_table[256] = { 0x00, 0x91, 0xE3, 0x72, 0x07, 0x96, 0xE4, 0x75, 0x0E, 0x9F, 0xED, 0x7C, 0x09, 0x98, 0xEA, 0x7B, // ... 完整表略 }; uint8_t TMC_CRC8(const uint8_t *data, uint8_t len) { uint8_t crc = 0; while (len--) { crc = tmc_crc_table[(crc ^ *data++) & 0xFF]; } return crc; }实际项目里如果不方便把256字节表都拷进去,也可以用位运算写,但查表法在F103上更快,占用Flash也不大。注意CRC的对象是从机地址字节开始,包含寄存器地址和数据字节,不包含sync字节0x05/0x07。
4.2 初始化串口和写寄存器的代码
下面这段是标准库风格的初始化,使用USART1,波特率115200,8数据位、1停止位、无校验,并使能半双工模式:
void TMC_UART_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_TX | USART_Mode_RX; USART_Init(USART1, &USART_InitStructure); USART_HalfDuplexCmd(USART1, ENABLE); USART_Cmd(USART1, ENABLE); }写寄存器函数的要点是:先关掉接收中断,避免发送数据时收到自己返回的内容干扰状态判断;每发一个字节等TXE标志位置1;全部发完后等TC标志位置1,再留几十微秒延时再切换阶段。
void TMC_WriteReg(uint8_t addr, uint8_t reg, uint32_t dat) { uint8_t buf[8]; buf[0] = 0x05; buf[1] = addr; buf[2] = reg; buf[3] = (dat >> 24) & 0xFF; buf[4] = (dat >> 16) & 0xFF; buf[5] = (dat >> 8) & 0xFF; buf[6] = dat & 0xFF; buf[7] = TMC_CRC8(&buf[1], 6); for (int i = 0; i < 8; i++) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, buf[i]); } while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); delay_us(50); }这里buf[7]的CRC计算长度是6:从buf[1]到buf[6],正好包含地址、寄存器、4字节数据。如果数据手册上要求包含sync字节,需要调整表范围为buf[0]~buf[6]。
4.3 读寄存器时要注意的时序问题
读寄存器比写寄存器麻烦,因为要发完读请求后,引脚立刻变输入,等待驱动返回数据。标准库半双工模式下,USART1的RX和TX在内部已经连接,发完请求后如果直接启用了RX中断,理论上可以收,但实测有时会收到自己发送过来的字节,因为半双工切换不干净。
我的建议是:读操作不用中断,而是发送完请求后延时等待至少100微秒,然后一个字节一个字节地轮询读取,读满8字节或超时结束。这样代码简单,逻辑也稳定。
uint8_t TMC_ReadReg(uint8_t addr, uint8_t reg, uint32_t *dat) { uint8_t buf[4] = {0x07, addr, reg, TMC_CRC8(&addr, 2)}; uint8_t rx[12]; uint8_t rx_len = 0; for (int i = 0; i < 4; i++) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, buf[i]); } while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); delay_us(200); while (rx_len < 9) { uint8_t timeout = 100; while (--timeout && USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET); if (timeout == 0) break; rx[rx_len++] = USART_ReceiveData(USART1); } if (rx_len >= 9) { *dat = ((uint32_t)rx[3] << 24) | ((uint32_t)rx[4] << 16) | ((uint32_t)rx[5] << 8) | rx[6]; return 1; } return 0; }这里返回数据的位置是rx[3]~rx[6],是因为返回帧前两字节是sync和地址,第三字节是寄存器地址,之后才是数据。如果驱动返回帧不带寄存器地址,需要对应调整。我在TMC2209实测是带第三字节的,TMC2208也带。
5. 用UART把电机转起来的完整流程
5.1 上电后的寄存器初始化序列
电机不是一上电就能跑的。TMC2209/TMC2208上电后默认寄存器值是“保守模式”,电流、微步、PWM参数都属于最基础状态,必须写入初始化配置。我习惯按这个顺序:
- 清GSTAT复位标志,写0x01到0x00寄存器,把上电复位状态位读走
- 配置GCONF,寄存器地址0x00,把UART模式下需要用到的位打开,比如内部SENSE电阻补偿
- 配置IHOLD_IRUN,寄存器地址0x10,设置空闲电流和运行电流
- 配置CHOPCONF,寄存器地址0x6C,设置微步细分和斩波模式
- 如果是TMC2209,再配置SGTHRS和TCOOLTHRS
一个典型初始化代码:
TMC_WriteReg(0, 0x00, 0x00000001); // GCONF: 打开UART模式? 实际需确认 TMC_WriteReg(0, 0x10, 0x00030A10); // IHOLD_IRUN: 空闲电流10, 运行电流26 TMC_WriteReg(0, 0x6C, 0x000101D5); // CHOPCONF: 256微步, TOFF等这里给的寄存器值是一个参考,具体每一位的含义必须自己对着数据手册二进制拆解。IHOLD_IRUN的低8位是空闲电流IHOLD,接下来4位是IRUN运行电流,再上面4位是IHOLDDELAY。CHOPCONF的MRES位决定了微步,值为0时256微步,值为4时16微步。如果抄别人代码不核对位,很容易出现“写的电流没变化”的现象。
5.2 STEP/DIR脉冲与UART调节的配合
配置完成后,给驱动板的STEP引脚输入脉冲,电机就会按照配置的电流和微步运转。STM32F103要输出多少脉冲,可以用定时器PWM或GPIO翻转,这块我用的定时器1的PWM输出,频率控制为500Hz到20kHz,实际雕刻机常用10kHz左右。脉冲的每一路上升沿,电机走一个微步。
这里有个容易误解的地方:UART配置完成之后,电机运转完全是由STEP/DIR控制,如果UART线断开,电机照样能转,因为电流和微步已经写进驱动芯片寄存器。UART的作用顶多相当于你带电换了一个“虚拟电位器”,不是让电机通过UART命令来走位置。但你可以通过UART读取当前驱动状态、故障标志,或者在运行时只要发一条寄存器写命令,就能立刻调整运行电流,这在实际调试中非常好用。
如果想让电机按固定角度转动,需要在STM32F103里规划脉冲个数。比如一圈需要200步,电机驱动器细分为16,那么走一圈需要3200个脉冲。代码里定时器中断里计数,到了目标值就关PWM输出。
5.3 实时读取运行状态和错误标志
TMC2209比TMC2208强的一点是可以实时读取DRV_STATUS,知道芯片是不是过温、是不是开路、是不是堵转。我用以下方式:周期100ms读一次0x6F寄存器,检查bit位。
uint32_t status; if (TMC_ReadReg(0, 0x6F, &status)) { if (status & 0x00000001) { // 过温警告 } }如果是TMC2209,通过读SG_RESULT寄存器可以拿到当前负载数字量。这个值不是直接报错,而是需要设定阈值。我在CNC竖向轴调机时,先把机械限位拆掉,缓慢让丝杠顶到末端,观察SG_RESULT的变化范围,再设一个阈值,之后每次顶到末端时数值就会跳到阈值之下,MCU停止发脉冲。整个过程比限位开关调试直观很多。但要注意:失速检测只有在电机速度较快时才有意义,TMC2209数据手册中的TCOOLTHRS就是用来设定失速检测生效的最低速度,速度太低时SG_RESULT不可靠。
6. 实测中的坑:通信失败、电机异响、丢步
6.1 发送不返回:地址和CRC是最常见的原因
我一开始把TMC2209当TMC2208调,地址写0x00,结果驱动没响应,拿示波器点PDN_UART,能看到波形,但驱动就是不应答。后来检查发现我把TMC2209的AD0接在了3.3V上,应该用地址0x01。这算低级错误,但很典型。
第二个坑是CRC计算范围。一开始我从sync字节0x05开始参与CRC计算,结果连写寄存器都没成功。翻官方说明后才发现,sync字节不参与CRC计算,应该从地址字节开始。这个细节在TMC2208和TMC2209数据手册里写得很明白,但很多人习惯从数组下标0开始循环,一错就是一片。
如果判断自己的CRC没问题,可以用逻辑分析仪抓一下,看发送的数据是否和期望帧一致。PDN_UART空闲高电平,发送时波形是标准的UART低电平起始位,波特率可以从PC端计算。
6.2 驱动器一直处于待机状态怎么查
TMC2209有freewheeling和standstill两种状态。如果上电后什么都不动,IHOLD空闲电流会把电机锁住,但如果你发现电机完全软绵绵,可能是没正确配置IHOLD_IRUN,或者驱动进入了freewheeling(寄存器里对应位被置位)。
我出现过一次:初始化时往GCONF写了一个不兼容TMC2208的值,结果TMC2208直接“傻了”,电机不锁轴,UART还能通信但STEP怎么发都不转。后来用读寄存器方式读回GCONF,才发现数据根本没写进去,原因是我初始化序列里第一步清GSTAT的写操作把GCONF配置覆盖了。正确做法是先写GCONF,再清GSTAT,或者分开延时。
还要检查驱动板的EN引脚。很多模块默认不上拉,MCU复位期间EN引脚为低,驱动被禁用。我的板子上EN接了10k上拉到3.3V,需要输出低电平才能使能,这和我最初习惯的逻辑相反。建议硬件设计时把EN引脚用跳线引出,方便调试。
6.3 关于TMC2209“串口控制步进电机”的边界认知
最后聊一个概念问题。搜“TMC2209 UART STM32F103 串口控制步进电机”能看到很多帖子,但不少新手会误以为只要接一根串口线,就能像发指令那样让电机转动指定角度。实际上TMC2208/TMC2209的UART只是配置通道,不是运动控制接口,真正的运动还是要靠STEP/DIR脉冲源。你依然需要在STM32F103里用定时器产生步进脉冲,或者外接触摸屏控制器去发脉冲。
UART能做的,是在一个中央控制器上同时管理多颗驱动芯片,中央控制器通过地址区分X轴、Y轴、Z轴,然后各自下发电流、微步、失速检测阈值。运行时如果发现某轴负载异常,可以动态降低那轴的电流或直接停车。这种设计比纯STEP/DIR模式高了不只一个level,也是我这次升级驱动最核心的收获。
这个项目完成后,我再处理多轴设备一般优先考虑TMC2209而不是TMC2208,不是因为迷信“买新不买旧”,而是StallGuard2和地址选择确实能省线缆、省限位开关、调参方便。TMC2208在一些固定参数、不需要总线的场景仍然很合适,毕竟便宜且稳定。但如果你的STM32F103串口资源紧张,想一条UART管两颗电机,TMC2209基本是唯一解。