资讯动态

STM32读取多摩川TS5668N21绝对式编码器:RS485通信与角度换算

发布时间:2026/10/20 15:38:36 来源:尧图企业网站定制
简介一套面向STM32F4系列微控制器的多摩川编码器读取工程源码完整演示TS5668N21型号通过ADM485半双工协议获取位置与速度信息可应用于工业电机闭环控制、精密定位及机器人反馈等实时场景。资源包共263个文件主要包含HAL库应用层的C源码、头文件、启动汇编以及编译生成的十六进制烧录文件与调试中间产物压缩包4.79MB整体目录结构清晰支持直接导入Keil或IAR工程编译验证。源码基于STM32CubeMX的串口配置实现RS485方向控制和UART中断接收在边沿捕获中完成A、B相正交计数与方向判断同时利用Z相脉冲清零复位并加入上电稳定延迟、临界区锁存等抗丢步措施。附带的示例程序包含计数结果到角度速度的换算逻辑以及打印显示功能。目前已有4833人学习下载适合有一定嵌入式开发经验、需要快速掌握该编码器接入方法的工程师。1. 多摩川 TS5668N21 读取程序STM32 上把 17bit 绝对位置变成可用的角度值最近在搭一套伺服电机测试台电机尾部装的就是多摩川 TS5668N21 绝对式编码器。编码器本身不贵贵的是“读取程序”这一步——TS5668N21 输出的是 RS485 差分信号分辨率 17bit一圈 131072 个计数想在 STM32 上把它读出来并把位置换算成角度远不是串口发个命令、收一包字节那么简单。协议帧格式、应答窗口、校验拼接、收发方向切换每个环节都能让位置数据从正常变成“随机数”。这篇笔记按我实际调通的顺序从接线、DMA 接收、帧解析到踩坑逐条讲清楚给正在调位置环或者第一次碰多摩川编码器的你一个可以直接落地的参考。2. 读懂 TS5668N21 的通信基座从差分接线到应答帧格式2.1 绝对式编码器为什么走 RS485差分抗干扰是一条命脉TS5668N21 是绝对式编码器上电就能读出当前绝对位置不用像增量式那样找 Z 脉冲回零。它内部是 17bit 精度换算下来每圈 131072 个计数角度分辨率约 0.0027°这个精度在很多伺服应用里已经不需要再细分。问题是这种精度是建立在稳定的通信上的。编码器工作在伺服电机尾部旁边就是三相驱动线和逆变桥干扰环境很差。如果走普通 TTL 单端串口一两百毫伏的噪声就能让数据误码。TS5668N21 的 RS485 接口用差分方式传输A、B或 TX / TX-两线互为参考共模干扰在接收端被抵消掉这也是工业编码器普遍选差分传输的原因。从 STM32 侧看要么用内置的 USART 加一个 RS485 收发器要么用 UART 的 TTL 转 RS485 模块。我一般偏向前者直接把收发器放在控制板上毕竟编码器线上跑的是 2.5Mbps 甚至更高的波特率外挂转接模块容易引入额外的地环路问题。2.2 STM32 侧硬件清单收发器、终端电阻与方向控制数据手册里 TS5668N21 的引脚定义大致是电源通常 5V 或 3.3V具体看批次、GND、两根差分数据线DATA / DATA-。我这里以某开发者板上用的 MAX3485 为例它是 3.3V 供电的 RS485 收发器最大速率 10Mbps和 STM32 的电压域直接匹配。左端右端说明STM32 USART3_TXMAX3485 DI发送数据输入STM32 USART3_RXMAX3485 RO接收数据输出GPIO 方向控制脚MAX3485 DE / RE高电平发送低电平接收MAX3485 A编码器 DATA差分正端需接 120Ω 终端电阻MAX3485 B编码器 DATA-差分负端0.1μF 电容MAX3485 VCC / GND就近去耦方向控制脚是这整套电路里最容易翻车的地方。MAX3485 的 DE 是发送使能RE 是接收使能标准接法是 DE 和 RE 短接成一个脚用 STM32 的一个 GPIO 控制——拉高进入发送模式拉低进入接收模式。初始化代码如下void rs485_direction_init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio.Pin GPIO_PIN_1; gpio.Mode GPIO_MODE_OUTPUT_PP; /* 推挽输出方向控制脚需要足够的驱动能力 */ gpio.Pull GPIO_PULLDOWN; gpio.Speed GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(GPIOB, gpio); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); /* 默认进入接收模式 */ }逻辑说明方向脚默认置低让总线处于监听状态每次发送命令之前拉高命令发送完成后必须拉低否则收发器一直占着总线永远收不到编码器回包。参数上要重点检查 GPIO 速度2.5Mbps 以上速率时如果 GPIO 翻转太慢方向切换时间会吃掉第一个字节表现为“发命令之后收到的第一字节总是丢的”。2.3 命令帧与应答帧把协议表翻译成两个数组TS5668N21 的通信是典型的命令-应答模式主机发一帧位置请求命令编码器在几百微秒内返回一帧包含状态、位置数据的应答。不同批次、不同分辨率的手册里帧内字节偏移会有差异但结构上基本逃不出下面这个框架。typedef struct { uint8_t cmd[8]; /* 位置请求命令帧具体字节以规格书协议表为准 */ uint8_t rx_buf[64]; /* 应答帧缓存长度按最大帧长定义 */ } encoder_frame_t; encoder_frame_t enc; void encoder_send_request(void) { static const uint8_t pos_cmd[8] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; /* 常见做法全零帧作为位置请求部分批次需要一个特殊 ID 字节 */ RS485_DIR_SEND(); /* 拉高方向脚 */ HAL_UART_Transmit(huart3, (uint8_t*)pos_cmd, 8, 10); while (HAL_UART_GetState(huart3) HAL_UART_STATE_BUSY_TX); RS485_DIR_RECV(); /* 发送完成立即切回接收 */ }逻辑说明位置请求命令我习惯封装成数组留在源文件顶部以后拿到不同规格的编码器只需要改这一个数组。HAL_UART_Transmit 的最后一个参数是超时时间 10ms对 2.5Mbps 来说发 8 字节只需要约 32μs这个超时足够宽裕。注意 while 等待发送状态是因为 USART 的 TXE 中断标志和移位寄存器完成之间还有一个字节的空隙不等移位寄存器真正发完就切方向最后一个字节会被切断。2.4 应答帧结构状态字节、数据字节、校验字节的通用布局应答帧的解析不放在发送流程里而是单独写一个解析函数。以见到的多数 TS5668N21 应用为例应答帧通常前 1~2 字节是状态位温度、供电、通信自检结果中间 2~4 字节是位置计数值最后 1 字节是校验。位置值可能是 17bit 分布在 3 字节也可能一个 uint16 加一个溢出位这块没有统一的定论必须对照你手里那本规格书逐个 bit 核对。uint8_t encoder_parse_frame(uint8_t *buf, uint16_t len, int32_t *pos_out, uint8_t *status_out) { if (len 6) return 0; /* 帧长不合理直接丢弃 */ *status_out buf[0]; /* 状态字节按协议表定义 */ /* 此处是示例解析位置 第3字节左移8位 | 第4字节。 实际偏移必须对齐规格书的应答帧位置字段定义 */ int32_t raw ((int32_t)buf[2] 8) | buf[3]; *pos_out raw; return 1; }逻辑说明这个函数刻意做成“解析表驱动”的样子因为不同型号的应答帧布局差异不小与其把解析逻辑散在中断里不如集中到一个函数。参数里 len 必须由接收流程算出不能信 buf 里的任何长度字段DMA 的剩余计数在 3.3 节会详细说。3. 串口接收的微秒级窗口用 DMA 空闲中断把应答帧完整收下来3.1 为什么不能进一次串口中断收一个字节第一次调 TS5668N21 的时候我按最朴素的思路写USART 开 RXNE 中断每收到一个字节就进一次中断往数组里填填到一定长度就解析。结果位置输出像抽风一样偶尔正常偶尔丢帧。原因后来用示波器一抓就明白了2.5Mbps 波特率下 1 个字节只有 4μs 左右如果跑 10Mbps则只剩 1μs。应答帧按 8 字节算整个接收窗口也就 8~30μs。期间如果有任何一个更高优先级的中断插进来或者主循环里关中断的临界区太长RXNE 中断就会排队排队期间 USART 接收寄存器被后续字节覆盖于是丢字节。正确的做法是 DMA 空闲中断串口收到数据由 DMA 直接搬到内存不需要 CPU 每个字节都响应只有整帧结束总线空闲才触发一次中断。CPU 的中断压力从“每字节一次”降为“每帧一次”时序窗口问题基本消失。3.2 USART DMA 初始化HAL 库下三段关键配置以某项目采用的 STM32F407 为例编码器挂在 USART3DMA 通道用 DMA1_Stream1。初始化顺序有一个特别容易踩的坑必须先初始化 DMA再初始化 USART否则 USART 找不到绑定的 DMA 句柄。void encoder_uart_dma_init(void) { /* 第一步DMA 配置 */ hdma_uart3_rx.Instance DMA1_Stream1; hdma_uart3_rx.Init.Channel DMA_CHANNEL_4; hdma_uart3_rx.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_uart3_rx.Init.PeriphInc DMA_PINC_DISABLE; hdma_uart3_rx.Init.MemInc DMA_MINC_ENABLE; hdma_uart3_rx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_uart3_rx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_uart3_rx.Init.Mode DMA_CIRCULAR; hdma_uart3_rx.Init.Priority DMA_PRIORITY_VERY_HIGH; HAL_DMA_Init(hdma_uart3_rx); __HAL_LINKDMA(huart3, hdmarx, hdma_uart3_rx); /* 第二步USART 外设接收线也进入 DMA 模式 */ huart3.Instance USART3; huart3.Init.BaudRate 2500000; huart3.Init.WordLength UART_WORDLENGTH_8B; huart3.Init.StopBits UART_STOPBITS_1; huart3.Init.Parity UART_PARITY_NONE; huart3.Init.Mode UART_MODE_TX_RX; huart3.Init.HwFlowCtl UART_HWCONTROL_NONE; huart3.Init.OverSampling UART_OVERSAMPLING_8; HAL_UART_Init(huart3); }逻辑说明Mode 用 DMA_CIRCULAR 循环模式缓冲区满了自动从头写省去重复启动 DMA 的动作。OverSampling 用 8 倍采样而不是默认的 16 倍原因是高波特率下 16 倍过采样对时钟误差更敏感8 倍能在 2.5Mbps 下获得更宽的采样容忍窗口。如果后续跑 10Mbps这里建议保持 8 倍。参数说明BaudRate 2500000 对应 2.5Mbps。如果你的编码器支持 4Mbps 且线短、屏蔽好可以提上去但要注意 MAX3485 的驱动能力和 STM32 的 PCLK 频率是否足够分频得到整数波特率。3.3 空闲中断判定帧结束用 DMA 剩余计数算出收到多少字节初始化完成之后工作是 HAL_UARTEx_ReceiveToIdle_DMA当总线上超过一个字节时间没有新数据时HAL 库会触发回调同时告诉我们 DMA 缓冲区的“剩余计数”。static volatile uint16_t rx_len; static volatile uint8_t idle_flag; void encoder_start_receive(void) { HAL_UARTEx_ReceiveToIdle_DMA(huart3, enc.rx_buf, sizeof(enc.rx_buf)); } void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart huart3) { rx_len Size; /* HAL 库已算出本次有效数据长度 */ idle_flag 1; } }逻辑说明ReceiveToIdle_DMA 一旦使能DMA 就一直往数组搬运数据。每收到一帧总线出现空闲回调里 Size 就是这一帧实际字节数。主循环里轮询 idle_flag置位就立即解析解析完再调用一次 encoder_start_receive 继续接收。void encoder_poll(void) { if (idle_flag) { idle_flag 0; int32_t pos 0; uint8_t st 0; if (encoder_parse_frame(enc.rx_buf, rx_len, pos, st)) { /* 解析成功pos 就是 17bit 原始计数值 */ encoder_angle_update(pos); } else { /* 帧长不对或校验失败计数器加一用于统计丢帧率 */ encoder_fault_count; } encoder_start_receive(); } }这里有个细节要注意HAL_UARTEx_RxEventCallback 是在中断上下文执行的不要在回调里做大循环或调用 HAL_UART_Transmit否则会直接影响下一帧接收。我的习惯是只放两个赋值解析全部放主循环这是被一个半夜两点才想起来的中断死锁坑换来的经验。4. 从原始码到角度与圈数拼接、校验与零点偏移4.1 数据拼接高位在哪边决定了你的角度是连续还是乱跳TS5668N21 是 17bit 分辨率而一个 uint16_t 只有 16bit所以位置数据一定会拆到两个以上的字节。常见的排布有两种小端模式低字节在前和大端模式高字节在前。这一步看着简单实际错一个字节顺序角度就是满量程乱跳而且偶发正常偶发跳变排查起来极其恶心。int32_t encoder_raw_to_count(uint8_t *buf, uint8_t mode) { int32_t raw 0; if (mode ENCODER_ORDER_LITTLE_ENDIAN) { raw (int32_t)buf[0] | ((int32_t)buf[1] 8); if (buf[2] 0x01) { /* 17bit 的溢出位有些批次放在第3字节最低位 */ raw | (1 16); } } else { raw ((int32_t)buf[0] 8) | buf[1]; /* 大端模式同样需要按手册确认溢出位位置 */ } return raw; }逻辑说明这里把拼接逻辑独立出来是为了调试时能快速切换字节序而不影响其他代码。两个分支的区别只在哪个字节当高字节。注意 17bit 数据最高位对应的不是第 16 位就是某个状态位建议把第 0~16 位全部拿满后再和上一次读数做差值判断如果差值超过量程一半大概率是拼接错位而不是电机真的转那么快。4.2 校验与容错CRC8 查表实现与连续帧确认高速差分通信在电缆质量差的时候依然会有误码尤其是电机刹车瞬间的电流尖峰。TS5668N21 的应答帧末端通常带一个校验字节常见有 CRC8 和异或和两种。我们项目里用的是 CRC8查表法CPU 开销可以忽略。static const uint8_t crc8_table[256] { 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15 /* 其余表项由 CRC-8 多项式 0x07 生成篇幅原因省略 */ }; uint8_t encoder_crc8(uint8_t *buf, uint16_t len) { uint8_t crc 0; for (uint16_t i 0; i len; i) { crc crc8_table[(crc ^ buf[i]) 0xFF]; } return crc; }逻辑说明CRC 的入参是去掉校验字节的完整帧算出结果和帧末字节比对。查表法比逐位计算快一个数量级在 2.5Mbps、每帧几十微秒的节奏下属于必须做的优化。容错方面光靠 CRC 还不够。我加了“连续两帧位置一致才更新输出”的软保护因为电机静止时相邻两帧的原始计数应该完全相同如果两帧差一拍要么是电机真的动了要么就是干扰造成的假数据。int32_t encoder_filtered_pos(int32_t new_raw) { static int32_t last_raw -1; static int32_t last_valid -1; if (last_raw 0 new_raw last_raw) { last_valid new_raw; /* 两帧相同确认为有效位置 */ } last_raw new_raw; return last_valid; }参数说明这个滤波器只对静止状态有效。电机高速运行时位置每帧都会变所以实际项目里我改成“校验通过且当前命令响应正常”作为更新条件连续帧确认只用于上电校准阶段。4.3 角度换算与零点偏移把 131072 变成 360°拿到 17bit 原始计数后角度换算本身很简单真正的工程问题是零点对齐。编码器装在电机轴上时零位几乎不可能和机械原点一致所以需要在机械限位触发瞬间记录一个偏移量。#define ENCODER_COUNTS_PER_REV 131072.0f void encoder_angle_update(int32_t raw) { static int32_t offset 0; float angle_deg 0.0f; if (zero_set_flag) { /* 机械原点对齐时把当前 raw 存为偏移 */ offset raw; zero_set_flag 0; HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, (uint32_t)offset); } else { offset (int32_t)HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR1); } int32_t diff raw - offset; if (diff 0) diff 131072; /* 处理跨零翻转保证角度连续 */ angle_deg ((float)diff / ENCODER_COUNTS_PER_REV) * 360.0f; }逻辑说明offset 存储在 RTC 备份寄存器里掉电不丢又不需要每次上电都重新对齐。diff 为负说明编码器当前位置在零点之前加一圈补偿。这种处理比用浮点取模更稳浮点数在 131072 量级下做取模容易出现边界抖动。5. 避坑指南和 TS5668N21 通信的四条血泪经验5.1 位置偶发跳变波形显示沿上有毛刺现象静止状态下上位机读到位置偶尔跳一个很大的值下一秒又恢复正常用示波器抓 DATA 对地波形能在下降沿看到明显的振铃。原因差分线没有终端电阻。RS485 在 2.5Mbps 以上时如果 A、B 之间没有 120Ω 电阻信号在线的另一端反射叠加到主波形上形成假边沿接收器可能把噪声识别成数据。解决在 MAX3485 的 A、B 引脚之间跨接一个 120Ω 电阻且电阻尽量靠近 STM32 侧。如果编码器线超过 2 米再在编码器端也加一个 120Ω两端匹配反射最小。从那以后我每次做编码器接口都是先焊电阻再写代码。5.2 发送命令后收不到回应但示波器能看到编码器有输出现象代码逻辑看着没问题方向脚也切了但 rx_buf 永远是空的示波器接在 A、B 端却明显能看到编码器在发数据。原因收发器方向切换太早。HAL_UART_Transmit 返回时USART 的移位寄存器可能还没把最后一个字节完全送出去这时候如果立刻把方向脚拉低最后一个字节的一半被截断编码器收到不完整的命令帧自然不回应。解决发送完成检查改用 USART 的 TC传输完成标志而不只是 TXE。void encoder_send_request(void) { RS485_DIR_SEND(); HAL_UART_Transmit(huart3, (uint8_t*)pos_cmd, 8, 10); while (__HAL_UART_GET_FLAG(huart3, UART_FLAG_TC) RESET); RS485_DIR_RECV(); }这个坑在低速时不一定出现因为字节间隔足够长2.5Mbps 下多个 4μs 都会致命。检查 TC 标志后才切方向属于标准的 RS485 切换习惯。5.3 上电后前几帧状态异常位置输出为 0现象每次上电前 5~10 帧数据 CRC 校验失败或状态位显示异常之后恢复正常如果上电后立刻让电机跑前几步位置是错的。原因TS5668N21 内部有上电自检和初始化流程需要一定时间稳定另外部分伺服驱动器会给编码器供电而驱动器母线电容充电会让 5V 电源斜坡上升编码器在欠压下输出的第一帧就是无效帧。解决上电后不要立刻发位置请求先延时 500ms再发一帧状态查询如果返回状态异常间隔 20ms 重试直到状态正常后再读位置。5.4 高速运动时位置偶尔反向跳变现象电机正向匀速转位置曲线大部分时间线性增长但偶尔有一段反方向走然后又跳回来。原因应答帧中的多圈信息与单圈位置拼接错位。TS5668N21 有的批次会提供多圈计数单圈位置和多圈计数在不同字节如果两个字节不是同一个采样时刻电机高速旋转时就会拼出“前一时刻的多圈 后一时刻的单圈”这种混合数据。解决查规格书确认多圈与单圈是否来自同一锁存时刻如果硬件不支持锁存就在解析时限制相邻两帧的圈数变化不能超过 1超过就丢掉重读。int32_t last_turns 0; int32_t enc_turn_diff new_turns - last_turns; if (enc_turn_diff 1 || enc_turn_diff -1) { /* 圈数变化超过1圈判定为拼接错误丢弃本帧 */ return; } last_turns new_turns;这类问题最难查因为它不丢帧、校验也合格纯粹是内部数据采样不同步。我自己的习惯是先把圈数变化限制写到解析层再慢慢查硬件时序至少能保证位置输出不反向跳。6. 没有 USART 也能读把同一套逻辑移植到同步串行模式某些批次的 TS5668N21 是同步串行接口SSI走 CLOCK DATA 两根线没有异步串口帧这时上面整套 RS485 程序不直接适用。其实读法更简单主机每输出一个时钟脉冲编码器在 DATA 线上移出一个 bit发满 17 个时钟读完 17bit 位置数据。用 STM32 的两个 GPIO 就能模拟。uint32_t encoder_ssi_read(void) { uint32_t raw 0; GPIO_CLK_HIGH(); HAL_Delay_us(2); /* 时钟高电平稳定时间 */ GPIO_CLK_LOW(); for (int i 16; i 0; i--) { GPIO_CLK_HIGH(); /* 上升沿输出数据 */ if (GPIO_DATA_READ()) { raw | (1u i); } GPIO_CLK_LOW(); } return raw; }逻辑说明最高位在前逐位移入 raw。GPIO 翻转速度决定了最大读取频率想读更快就用 SPI 主模式替代 GPIO 模拟配置 CPOL/CPHA 让编码器和 SPI 的相位对齐。同步串行模式的抗干扰能力比 RS485 异步模式弱一些因为时钟线容易引入噪声实际项目里线长超过 30cm 就需要加大驱动力。验证这套移植是否成功的办法很简单让电机停在任意位置连续读 1000 组数据正常情况静止时 1000 组数据完全相同如果出现任何一位跳变说明时钟时序或者电平阈值需要调。从那以后我每次换编码器型号都会先做这个“静止读 1000 次”测试再进运动测试。这个习惯帮我挡掉了至少三次硬件改版和一次自以为协议解析对了、实际线接反了的尴尬。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑