从接线、时钟和单位开始
跳转到“从接线、时钟和单位开始”TMC4361A 负责生成运动轨迹,功率驱动器负责给电机绕组供电。STM32F103C8T6 可以通过 SPI1 配置 TMC4361A,再由后者输出 STEP/DIR。本文把两篇调试记录合并为这条控制链,并将原来重复、尚未实测的完整程序改为可核对的寄存器说明、通信核心和板级适配示例。
以下基准是:STM32 使用 8 MHz HSE、72 MHz SYSCLK/PCLK2,PA8 输出 HSE 给 TMC4361A,外接驱动器接收 STEP/DIR。电机绕组、驱动电流、使能端、电平转换和限位电路需要依据实际驱动器设计。文中代码不包含这些板级配置,也没有经过带电机运动的实机验证。
主要依据为 TMC4361A 数据手册 V1.26 的引脚、SPI、斜坡、同步、复位和寄存器章节。寄存器定义与 ADI 官方 TMC-API 固定版本 交叉核对;这里没有声称审读了整本手册或整个代码库。
1. 引脚和控制权
跳转到“1. 引脚和控制权”原始引脚图保留如下,接线时使用芯片封装的实际脚号:

| 功能 | TMC4361A 引脚 | 本例 STM32 连接或处理 |
|---|---|---|
| 主控片选 | NSCSIN,2 | PA4,GPIO 输出,空闲为高 |
| 主控 SPI 时钟 | SCKIN,3 | PA5 / SPI1_SCK |
| 主控写数据 | SDIIN,4 | PA7 / SPI1_MOSI |
| 主控读数据 | SDOIN,7 | PA6 / SPI1_MISO |
| 外部工作时钟 | CLK_EXT,38 | PA8 / MCO,8 MHz HSE |
| 硬件复位 | NRST,39 | 可接 PA1;低有效,复位时序按手册和板级电路安排 |
| 步进输出 | STPOUT,24 | 接驱动器 STEP,经需要的电平/隔离接口 |
| 方向输出 | DIROUT,23 | 接驱动器 DIR |
| 到达指示 | TARGET_REACHED,31 | 可接 PA3 输入;其含义仍是内部位置到达 |
| 同步开始 | START,20 | 默认是输出;本例不由 PA2 推挽驱动 |
| 冻结输入 | NFREEZE,19 | 低有效,正常工作时按板级设计保持非激活 |
| 测试模式 | TEST_MODE,34 | 正常应用接低 |
| 电源 | VCC,5/26/37;GND,6/15/25/36 | 按供电章节连接、去耦并处理裸露焊盘 |
| 内核稳压端 | VDD1V8,16/35 | 按参考电路去耦,不当作普通 3.3 V 输入 |
不要把两组 SPI 混接。SCKDRV(29)、SDODRV(27)、SDIDRV(28)、NSCSDRV(30) 属于芯片连接电机驱动器的 SPI;A_SCLK(40) 属于编码器接口。原稿的若干脚号整体错移,以及把 A_SCLK 当驱动器时钟的描述,均不适用。
只接 STEP/DIR 时,驱动器的报警不会自动通过 TMC 的驱动器 SPI 状态返回;报警线需要单独接入。接 TMC2130 等可通信驱动器时,还要核对其电源、SPI 格式、SPIOUT_CONF、电流与斩波设置,不能用一句“写 1 启用 SPI 输出”替代。
2. 时钟和 40 位 SPI
跳转到“2. 时钟和 40 位 SPI”CLK_FREQ(0x31) 是告诉运动计算单元实际时钟是多少,不会生成硬件时钟。若 PA8 输出 8 MHz,就写入 8000000。旧程序输出 36 MHz 的方案超出了手册给出的时钟范围,且 F103 HAL 并没有所写的 RCC_MCODIV_2 选项。手册不同位置出现 30 MHz 和 32 MHz 上限,应按具体器件版本确认上限;这里的 8 MHz 不依赖这处差异。
本例使用 SPI 模式 3:空闲高电平、下降沿移出、上升沿采样,8 位数据、MSB 先传。每次片选拉低期间恰好传 5 字节,之后拉高。PCLK2 为 72 MHz、预分频 64 时,SCK 是 1.125 MHz;原稿的 32 分频是 2.25 MHz,不能用于本例 8 MHz 芯片时钟所要求的四分之一上限。
| 项目 | 本例处理 |
|---|---|
| 地址字节 bit7 | 0 为读、1 为写;低 7 位是地址 |
| 后续四字节 | 32 位数据,大端顺序 |
| 同时返回的第一个字节 | 由 SPI_STATUS_SELECTION(0x0B) 选择的 8 个事件位 |
| 同时返回的四字节数据 | 前一次读请求对应的数据,或前一次写数据的回送 |
| 片选高间隔 | 按手册时序留余量,本例每帧之间至少 1 μs |
因此,读取 XACTUAL 需要先发 0x21 00 00 00 00,再发一个不会清事件的读请求来取回前一帧的数据。第二帧仍是合法请求;本例选择 STATUS(0x0F)。两帧之间不能插入其他调用,否则读回的数据会串位。写 VMAX 的首字节是 0xA4;0xA2 表示写 VACTUAL,而 VACTUAL 是只读寄存器,不能用它演示合法写入。
重复读取同一地址是常见做法,官方 API 也使用两帧读。这里选择 STATUS 作为第二帧,是为了避免读取 EVENTS 时再次触发事件清除。即使如此,EVENTS 仍有其固有的读取/清除边界,不能把它当作绝不丢事件的队列。
3. 模式、地址和数值表示
跳转到“3. 模式、地址和数值表示”3.1 RAMPMODE 不是正反转枚举
跳转到“3.1 RAMPMODE 不是正反转枚举”RAMPMODE 地址是 0x20。bit2 选择速度或定位,bit1:0 选择轨迹;0x35 是 X_HOME,不是模式寄存器。
| 值 | 工作模式 | 轨迹 |
|---|---|---|
| 0 | 速度 | 不使用斜坡 |
| 1 | 速度 | 梯形,可扩展 SixPoint |
| 2 | 速度 | S 形 |
| 4 | 定位 | 不使用斜坡 |
| 5 | 定位 | 梯形,可扩展 SixPoint |
| 6 | 定位 | S 形 |
速度模式由 VMAX 的符号决定方向;定位模式根据当前位置与 XTARGET 的关系决定运动方向。不能把 0、2、3 解释为保持、正转、反转。无斜坡模式会直接改变目标速度,不能默认适合带惯量的机械系统。

3.2 常用寄存器
跳转到“3.2 常用寄存器”下表的速度/加速度单位采用 GENERAL_CONF.direct_acc_val_en=0、direct_bow_val_en=0 的频率模式。直接模式的单位另依赖芯片时钟,不能套用同一编码。
| 地址 | 名称 | 数据意义 |
|---|---|---|
| 0x20 | RAMPMODE | 工作模式与轨迹 |
| 0x21 | XACTUAL | 有符号 32 位内部脉冲位置,可读写 |
| 0x22 / 0x23 | VACTUAL / AACTUAL | 有符号 32 位整数速度/加速度,只读 |
| 0x24 | VMAX | 有符号定点速度,8 位小数 |
| 0x25 / 0x26 / 0x27 | VSTART / VSTOP / VBREAK | 无符号速度,8 位小数,31 位字段 |
| 0x28 / 0x29 | AMAX / DMAX | 无符号加减速度,22 位整数加 2 位小数 |
| 0x2A / 0x2B | ASTART / DFINAL | 起始/末段加减速度,格式同 AMAX;作用依轨迹而定 |
| 0x2C | DSTOP | 特定软停止条件使用的减速度,不是 BOW1 |
| 0x2D–0x30 | BOW1–BOW4 | 四段加速度变化率,24 位无符号整数 |
| 0x31 | CLK_FREQ | 实际外部时钟频率,Hz |
| 0x37 | XTARGET | 目标位置,按有符号位置解释其 32 位字 |
手册前面的概要表与详细寄存器表存在位宽排版差异;读取 VACTUAL/AACTUAL 按详细表的完整 32 位有符号值解释。它们与具有小数位的 VMAX/AMAX 编码不同。
频率模式的编码是:
例如 v=3200 pulse/s 对应 VMAX=819200=0x000C8000;a=6400 pulse/s² 对应 AMAX=25600=0x00006400。速度左移 8 位是乘 256,不是乘 8;不能把“速度寄存器值的一半”直接当作加速度。
编码能装入字段不代表机械上可用。还需满足芯片的时钟相关上限、脉宽约束和电机能力;手册给出的加速度、加速度变化率上限分别与速度乘时钟、加速度乘时钟有关。小数精度、最大速度和脉冲频率也不能混为一谈。
3.3 一转需要多少脉冲
跳转到“3.3 一转需要多少脉冲”若电机步距角为 ,驱动器每整步接收 个微步脉冲,目标转速为 RPM,则:
1.8°、16 微步、60 RPM 得到每转 3200 脉冲和 3.2 kHz。原记录另写过“20 微步/整步”,那对应每转 4000 脉冲;它是另一个驱动设置,不能混入 16 微步例子。
STEP_CONF(0x0A) 的低四位不是任意细分数:0–8 依次代表 256、128、64、32、16、8、4、2、1 微步。16 微步编码为 4;bits15:4 还保存整步/转,1.8°电机为 200。保留其他已核验字段,仅更新这些字段可写成 (old & ~0xFFFFu) | (200u << 4) | 4u。只写 0x04 会把整步/转等字段一并清零。20 微步不能直接编码成 20;涉及编码器比例或内部电角度时尤其需要重新设计比例关系。
4. 脉宽、方向与使能
跳转到“4. 脉宽、方向与使能”
图中 312.5 μs 周期对应 3.2 kHz;若按显示的 0.04% 占空比估算,高电平只有约 125 ns。它说明当时测到了窄脉冲,不能证明所有驱动器都能识别。光耦输入驱动器尤其要核对最小高/低电平时间及电平幅度。
0x10 低 16 位是 STP_LENGTH_ADD,高 16 位是 DIR_SETUP_TIME,不是 SPI 状态寄存器。
例如 8 MHz 时设置 2 μs 脉宽和 2 μs 方向建立时间,对应低字段 15、高字段 16,整字为 0x0010000F。2 μs 只是计算示例,实际值必须满足所接驱动器的数据表。还需检查最高脉冲频率下低电平时间是否足够;芯片对方向切换与脉冲间隔也有约束,不能只看高电平宽度。
GENERAL_CONF 中 bit3 选择 STEP 有效极性,bit4 决定是否每次电平翻转都算一步;bit5 清零时 DIR 高表示正方向,默认 bit5=1 时 DIR 高表示负方向。bits7:6=0 才使用内部轨迹发生器;原稿设置 bit6 会选择外部 STEP 输入模式。bit9 控制外部 STEP 输入影响位置还是目标,不是 STEP 输出使能。bit23=0 使用 STEP/DIR 输出,置 1 则切换为 PWM 输出。
驱动器 ENA、抱闸和安全停机由实际硬件决定。GENERAL_CONF=1 只改动起始斜坡相关位,不会自动给电机“使能”。NFREEZE 也不能替代经设计验证的安全停机回路。
5. 有返回值的通信核心
跳转到“5. 有返回值的通信核心”下面是 C11 通信核心。transfer 必须完成一帧、检查底层结果、在失败时恢复片选为高,并满足时序;acquire/release 必须覆盖整个双帧读取,所有访问都走同一条路径。返回 TMC_IO 后,上层应中止本次控制流程并核对状态,不能假定失败写入完全没有生效,也不要直接重发运动命令。
读函数仅检查地址范围;调用者仍须根据手册选可读地址。写函数有意只开放本文需要的可写寄存器。数值辅助函数检查表示范围,机械行程与动态约束由上层另外检查。
#include <stdbool.h>#include <stddef.h>#include <stdint.h>#include <limits.h>
typedef enum { TMC_OK, TMC_ARGUMENT, TMC_BUSY, TMC_IO } TmcResult;typedef struct { void *context; bool (*acquire)(void *context); void (*release)(void *context); /* One complete 5-byte transaction; always restore CS high on failure. */ bool (*transfer)(void *context, const uint8_t tx[5], uint8_t rx[5]);} TmcBus;
enum { TMC_GENERAL_CONF = 0x00, TMC_START_CONF = 0x02, TMC_STEP_CONF = 0x0A, TMC_EVENTS = 0x0E, TMC_STATUS = 0x0F, TMC_STP_LENGTH_ADD = 0x10, TMC_RAMPMODE = 0x20, TMC_XACTUAL = 0x21, TMC_VACTUAL = 0x22, TMC_AACTUAL = 0x23, TMC_VMAX = 0x24, TMC_VSTART = 0x25, TMC_VSTOP = 0x26, TMC_VBREAK = 0x27, TMC_AMAX = 0x28, TMC_DMAX = 0x29, TMC_ASTART = 0x2A, TMC_DFINAL = 0x2B, TMC_BOW1 = 0x2D, TMC_BOW2 = 0x2E, TMC_BOW3 = 0x2F, TMC_BOW4 = 0x30, TMC_CLK_FREQ = 0x31, TMC_XTARGET = 0x37};
static bool bus_valid(const TmcBus *bus){ return bus != NULL && bus->acquire != NULL && bus->release != NULL && bus->transfer != NULL;}
static uint32_t unpack32(const uint8_t data[4]){ return ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | data[3];}
TmcResult tmc_read(TmcBus *bus, uint8_t address, uint32_t *value, uint8_t *spi_status){ if (!bus_valid(bus) || address > 0x7FU || value == NULL) return TMC_ARGUMENT; if (!bus->acquire(bus->context)) return TMC_BUSY; uint8_t request[5] = {address, 0, 0, 0, 0}; uint8_t dummy[5] = {TMC_STATUS, 0, 0, 0, 0}; uint8_t response[5]; bool ok = bus->transfer(bus->context, request, response); if (ok) ok = bus->transfer(bus->context, dummy, response); if (ok) { *value = unpack32(response + 1); if (spi_status != NULL) *spi_status = response[0]; } bus->release(bus->context); return ok ? TMC_OK : TMC_IO;}
/* Deliberately only the writable registers used in this article. */static bool example_writable(uint8_t address){ return address == TMC_GENERAL_CONF || address == TMC_START_CONF || address == TMC_STEP_CONF || address == TMC_STP_LENGTH_ADD || address == TMC_RAMPMODE || address == TMC_XACTUAL || (address >= TMC_VMAX && address <= TMC_CLK_FREQ) || address == TMC_XTARGET;}
TmcResult tmc_write(TmcBus *bus, uint8_t address, uint32_t value){ if (!bus_valid(bus) || !example_writable(address)) return TMC_ARGUMENT; if (!bus->acquire(bus->context)) return TMC_BUSY; uint8_t request[5] = { (uint8_t)(address | 0x80U), (uint8_t)(value >> 24), (uint8_t)(value >> 16), (uint8_t)(value >> 8), (uint8_t)value }; uint8_t response[5]; bool ok = bus->transfer(bus->context, request, response); bus->release(bus->context); return ok ? TMC_OK : TMC_IO;}
/* Defined conversion from the wire's two's-complement word. */int32_t tmc_signed32(uint32_t value){ if (value <= INT32_MAX) return (int32_t)value; return (int32_t)(-(int64_t)(UINT32_MAX - value) - 1);}
/* These encoders check representation, not a particular motor's limits. */bool tmc_velocity_raw(int32_t pulses_per_second, uint32_t *raw){ int64_t value = (int64_t)pulses_per_second * 256; if (raw == NULL || value < INT32_MIN || value > INT32_MAX) return false; *raw = (uint32_t)(int32_t)value; return true;}
bool tmc_acceleration_raw(uint32_t pulses_per_second2, uint32_t *raw){ if (raw == NULL || pulses_per_second2 > 0x3FFFFFU) return false; *raw = pulses_per_second2 * 4U; return true;}
bool tmc_relative_target(int32_t current, int32_t delta, int32_t *target){ int64_t sum = (int64_t)current + delta; if (target == NULL || sum < INT32_MIN || sum > INT32_MAX) return false; *target = (int32_t)sum; return true;}
bool tmc_step_timing(uint32_t clock_hz, uint32_t pulse_ns, uint32_t direction_ns, uint32_t *raw){ if (raw == NULL || clock_hz == 0 || pulse_ns == 0) return false; /* Widen before multiplication; all uint32_t inputs fit this sum. */ uint64_t high = ((uint64_t)clock_hz * pulse_ns + 999999999ULL) / 1000000000ULL; uint64_t setup = ((uint64_t)clock_hz * direction_ns + 999999999ULL) / 1000000000ULL; if (high == 0 || high > 65536 || setup > 65535) return false; *raw = (uint32_t)((setup << 16) | (high - 1)); return true;}6. STM32F103 的板级适配
跳转到“6. STM32F103 的板级适配”下面与上段核心放在同一个编译单元使用;工程配置需启用 HAL SPI、RCC、GPIO、DMA 模块,并使用真实 STM32F1 HAL/CMSIS 头文件。该适配只适用于一个主循环拥有总线、ISR 不访问 SPI的示例。RTOS 下应把 owned 改为整个事务共用的互斥锁,不能把普通布尔量当跨线程锁。
系统时钟、HSE 电路和 HAL 毫秒时基由工程先初始化;SPI1 不重映射,HAL_SPI_MspInit 不得把这里的引脚改成其他用途。下面没有配置 USART2,不会与 PA2/PA3 的串口复用发生隐式争用;若要输出诊断日志,另行规划串口引脚。
#include "stm32f1xx_hal.h"#include <stdbool.h>#include <stdint.h>#include <string.h>
/* Include after tmc_core.c in this compact demonstration translation unit. */typedef struct { SPI_HandleTypeDef *spi; bool owned; /* Exactly one main-loop caller; no ISR access. */} TmcStm32;
static bool delay_one_us(void){ if (SystemCoreClock != 72000000U) return false; uint32_t start = DWT->CYCCNT; for (uint32_t attempt = 0; attempt < 10000U; ++attempt) { if ((uint32_t)(DWT->CYCCNT - start) >= 72U) return true; } return false; /* Disabled/stalled cycle counter. */}
static bool stm32_acquire(void *context){ TmcStm32 *port = context; if (port == NULL || port->spi == NULL || port->owned) return false; port->owned = true; return true;}
static void stm32_release(void *context){ TmcStm32 *port = context; port->owned = false;}
static bool stm32_transfer(void *context, const uint8_t tx[5], uint8_t rx[5]){ TmcStm32 *port = context; uint8_t sending[5]; memcpy(sending, tx, sizeof sending); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); if (!delay_one_us()) return false; HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); if (!delay_one_us()) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); return false; } HAL_StatusTypeDef result = HAL_SPI_TransmitReceive( port->spi, sending, rx, 5U, 5U); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); bool gap_ok = delay_one_us(); return result == HAL_OK && gap_ok;}
/* The board has already enabled HSE=8 MHz, SYSCLK=PCLK2=72 MHz. */bool tmc_stm32_prepare(SPI_HandleTypeDef *spi){ if (spi == NULL || SystemCoreClock != 72000000U || HAL_RCC_GetPCLK2Freq() != 72000000U || __HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) == RESET) return false; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); GPIO_InitTypeDef pins = {0}; pins.Pin = GPIO_PIN_4; pins.Mode = GPIO_MODE_OUTPUT_PP; pins.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &pins); pins.Pin = GPIO_PIN_5 | GPIO_PIN_7; pins.Mode = GPIO_MODE_AF_PP; HAL_GPIO_Init(GPIOA, &pins); pins.Pin = GPIO_PIN_6; pins.Mode = GPIO_MODE_INPUT; pins.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &pins); HAL_RCC_MCOConfig(RCC_MCO1, RCC_MCO1SOURCE_HSE, RCC_MCODIV_1); spi->Instance = SPI1; spi->Init.Mode = SPI_MODE_MASTER; spi->Init.Direction = SPI_DIRECTION_2LINES; spi->Init.DataSize = SPI_DATASIZE_8BIT; spi->Init.CLKPolarity = SPI_POLARITY_HIGH; spi->Init.CLKPhase = SPI_PHASE_2EDGE; spi->Init.NSS = SPI_NSS_SOFT; spi->Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; spi->Init.FirstBit = SPI_FIRSTBIT_MSB; spi->Init.TIMode = SPI_TIMODE_DISABLE; spi->Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spi->Init.CRCPolynomial = 7U; if (HAL_SPI_Init(spi) != HAL_OK) return false; CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0U; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; return delay_one_us();}
TmcBus tmc_stm32_bus(TmcStm32 *port){ TmcBus bus = {port, stm32_acquire, stm32_release, stm32_transfer}; return bus;}连接方式是在主循环启动前创建并清零 SPI_HandleTypeDef 和 TmcStm32,令后者的 spi 指向前者;tmc_stm32_prepare 成功后,用 tmc_stm32_bus 得到 TmcBus。HAL 的 5 ms 超时依赖其毫秒时基持续前进,不能在关闭该中断的临界区或 ISR 中调用。DWT 延时有有限次检查,计数器未运行会返回失败;这仍不是实机时序证明,最终应以逻辑分析仪核对。
7. 参数准备与运动操作
跳转到“7. 参数准备与运动操作”7.1 上电后先做静止通信检查
跳转到“7.1 上电后先做静止通信检查”- 保持功率驱动器禁用,确认接线、供电、电平、TEST_MODE 与 NFREEZE。启动并测量 8 MHz 时钟,按手册完成 NRST 硬件复位。
- 建立 SPI,读回已知配置并检查回读,不能把全 0 或全 1 当作通信成功。写
CLK_FREQ=8000000后读回核对。 - 核对 GENERAL_CONF 的内部轨迹、STEP/DIR、方向、单位设置;核对 STEP_CONF 的细分与整步/转,并设置 0x10 脉宽。保留与板级连接有关的其他字段。
- 本例先使用
START_CONF=0,即参数立即生效。先写VMAX=0,确认VACTUAL=0;只在静止并有明确位置基准时初始化 XACTUAL 和 XTARGET。 - 配置参考输入、限位极性与滤波,低速验证真实驱动器方向和故障反馈,再允许带负载试验。
硬件复位是 NRST 拉低;软件复位字为向 0x4F 写 0x52535400,它不包含在上面受限写函数中。若进入 FROZEN 状态,手册要求硬件复位,软件复位不能解除。原稿把 50 ms 写成通用必要复位时间没有依据;板级时间应按手册要求和实测时钟/供电稳定过程确定。
7.2 先验证梯形,再扩展 S 形
跳转到“7.2 先验证梯形,再扩展 S 形”下面的量是便于核算的示例,不是已调试好的电机参数。每次写入都检查返回值;多寄存器配置期间保持单一控制任务所有权。进入定位模式之前,先让 XTARGET 与静止位置一致,避免沿用旧目标意外启动。
| 目标 | 参数与顺序 |
|---|---|
| 梯形速度控制 | 静止配置 VSTART=VSTOP=VBREAK=0,AMAX=DMAX=25600(6400 pulse/s²),RAMPMODE=1;最后写 VMAX=819200(3200 pulse/s)开始加速 |
| 反向速度 | 按已验证的减速/反向策略,将 VMAX 设为负值;不能对无符号寄存器字直接取绝对值来决定模式 |
| 绝对定位 | 静止准备 RAMPMODE=5,VMAX 使用正的速度上限;最后写有符号 XTARGET。XACTUAL 是内部计数,不会自动等于编码器位置 |
| 相对定位 | 静止读 XACTUAL,用 tmc_relative_target 在宽类型中检查 current+delta,再执行绝对定位;运动途中“相对当前位置”涉及快照时间,需另定语义 |
| 速度模式正常停车 | 保持已有梯形或 S 形模式,写 VMAX=0,按期限监测 VACTUAL 归零;无斜坡模式下这会直接改变速度 |
| 定位过程停车 | 不将 XTARGET 硬改成当前值冒充受控急停。按手册及机械需求选择停止输入/软停止或明确的模式转换策略,实测制动距离 |
| 改变位置基准 | 仅在静止且控制流程明确时写 XACTUAL;这不是移动指令,也不是自动回零 |
VBREAK=0 对应不使用分段切换的梯形。SixPoint 在此基础上加入 VSTART/VSTOP 及 ASTART/DFINAL,不同速度区间的加减速不同;使用非零 VBREAK 时必须满足它与 VSTOP 等值的约束。

S 形模式控制加速度逐步改变:BOW1 将加速度提升,BOW2 在接近目标速度时收回加速度,BOW3 建立减速度,BOW4 收回减速度。平滑起止的基本配置可从 VSTART=VSTOP=ASTART=DFINAL=0 和四个正 BOW 值开始;例如 BOW1…4=64000 pulse/s³ 在加速度 6400 pulse/s² 下对应 0.1 s 的名义加速度变化时间。短行程可能达不到设定的峰值速度或加速度,不能用这几个值反推完整轨迹总时长。
图中把四个 BOW 地址范围写成 0x2C…0x30 会包含 DSTOP;正确的 BOW1–4 是 0x2D…0x30。非零 VSTART/VSTOP 会改变起止段,不能说它们对所有 S 形轨迹都必须非零;DFINAL 在定位 S 形中的作用也不能照搬速度模式。
7.3 监控要有结束条件
跳转到“7.3 监控要有结束条件”上层以固定周期读取 STATUS、XACTUAL、VACTUAL,使用不溢出的时间差检查总期限,并在通信故障、限位或驱动器报警时进入故障处理。不要在主循环中无限 while (!target_reached),也不要把写命令成功当成运动成功。
| 读数 | 可以说明什么 | 不能据此断言什么 |
|---|---|---|
| STATUS bit0 | 内部 XACTUAL 与 XTARGET 相等 | 电机没有失步、机械位置已精确到达 |
| STATUS bits4:3 | 当前速度状态 | 外部驱动器所有故障都已排除 |
| STOPL/STOPR 等状态 | 已配置参考输入/停止条件状态 | 接线极性和安全功能无需验证 |
| XACTUAL | 内部脉冲位置 | 自动获得真实编码器反馈 |
| VACTUAL | 内部轨迹速度 | 电机轴一定达到该转速 |
有符号读取使用 tmc_signed32,相对位置计算先扩展到 64 位。特别避免对 INT32_MIN 直接取负,以及用 32 位有符号数计算 current+delta。
8. 事件、中断和同步功能
跳转到“8. 事件、中断和同步功能”INTR_CONF=0x0D,EVENTS=0x0E,STATUS=0x0F。STATUS 是只读的当前状态,不能向它写 0xFFFFFFFF 清标志。EVENTS 默认在读请求后清除;EVENT_CLEAR_CONF(0x0C) 对相应事件置 1 可保护其不被读取清除,再按手册用写 1 清除。改变清除策略前应明确哪个任务拥有事件消费权。
SPI 的首字节只是 0x0B 选出的最多 8 个事件,INTR 则由 0x0D 所选事件组合决定,默认低有效;它们与 32 位 STATUS 是三个不同层次。不能拿一个 8 位字节直接套 STATUS 的全部位定义。
| 功能 | 正确含义与使用前提 |
|---|---|
| START 同步 | START_CONF bits4:0 决定哪些参数等内部开始信号才生效,bits8:5 选择触发源;只写 1 是延后 XTARGET 生效,并不是“立即启动” |
| START 引脚输入/输出 | 外部触发选择位决定它成为输入;默认是输出。手册同步章节中一处输出条件文字与配置表矛盾,应以完整寄存器表核对,不能直接推挽连接两个输出 |
| 阴影寄存器 | 预装下一组轨迹参数,在内部开始信号到来时切换;单层全参数与双层 S 形映射不同,不能按同一地址表混用 |
| 目标流水线 | X_PIPE0–7 在触发后依次推进,可按配置回写形成循环;启用多个目标类型会改变每条流水线的深度 |
| U-Turn | 改变 VMAX 符号或改到反方向目标会触发反转轨迹,TZEROWAIT 可加入停留;S 形在 BOW2=BOW4 时还有连续穿越零速的特例 |
| 重复定位 | 到达目标后清 XACTUAL 再重复,与圆周坐标不是同一个功能 |
| 圆周运动 | X_RANGE 限定 [-X_RANGE, X_RANGE) 并处理跨界,定位通常选短路径;阻挡区、方向与小数圈长需按应用配置 |
| 编码器闭环 | 需要实际编码器接线、分辨率/比例与控制参数;只读 XACTUAL 或设置 ENC_IN_CONF 不会自动完成闭环 |
这些功能应在单次低速运动已验证之后逐个启用。原记录中的“重复各圆周运动、阴影寄存器、内部参数流水线”保留为学习入口,不把未调试的组合配置包装成通用烧录程序。四种影子映射、触发跳过次数与 BOW 换算等待详见 START 同步与影子斜坡寄存器。
9. 验证范围与来源
跳转到“9. 验证范围与来源”本页通信核心用于验证 5 字节顺序、双帧数据延迟、事件副作用、故障返回与数值边界。STM32 适配需要结合工程编译,硬件检查至少包括实际 MCO/SCK、片选间隔、STEP 高低电平、方向建立时间、驱动报警、停止距离和失步检测。离线测试与编译不能代替这些测量。
原文中绝对/相对定位、速度与加速度、停车、状态监控、串口观察、限位、驱动器 SPI 和编码器扩展等意图均在相应章节保留;重复程序中的错误常量和虚构“完整验证”结论不再沿用。
来源:TMC4361A 使用原记录、TMC4361A 阅读原记录、原厂数据手册、ADI TMC-API、STM32F1 HAL 官方源码。