Funpack4-3:TMC4361A-BOB & TMC2160-BOB 配合ADMT4000编码器实现步进电机闭环控制
使用TMC4361A-BOB&TMC2160-BOB板卡连接步进电机,配合ADMT4000实现一个线性执行器或者旋转执行器,ADMT4000接MCU,使用软件计算丢步情况,并用软件补相应步数。
标签
步进电机
CircuitPython
Funpack
tmc
闭环控制
StreakingJerry
更新2026-02-02
42

项目描述

这个使用TMC4361A-BOB&TMC2160-BOB板卡连接步进电机,配合ADMT4000实现一个线性执行器或者旋转执行器,ADMT4000接MCU,使用软件计算丢步情况,并用软件补相应步数。

软件流程图及各功能对应的主要代码片段及说明

系统硬件框图如下:

image.png

ESP32-S3通过SPI连接TMC4361和ADMT4000, TMC2160通过SPI和EN, DIR连接到TMC4361,通过TMC4361的SPI PASSTHROUGH功能来对TMC2160进行配置。


我使用的开发环境是circuitpython,这样开发起来比较方便,可以快速测试寄存器写入读取的效果。


寄存器配置分为多个部分,第一部分是TMC4361。由于配置/控制/通信都使用SPI,因此我写了一个驱动来完成对TMC4361的控制。这个驱动和之前版本的最大区别是完善了SPI PASSTHROUGH部分:

    def initialize_spi_passthrough(self):
        """初始化TMC4361以启用与TMC2160的SPI passthrough功能"""
        print("正在初始化TMC4361的SPI Passthrough功能...")
        # 读取当前的SPI_OUT_CONF
        spi_out_conf = self.read_register(TMC4361_SPI_OUT_CONF_REGISTER)
        print(f"当前的 SPI_OUT_CONF: 0x{spi_out_conf:08X}")


        # 设置 spi_output_format 为 13 (b'1101') 以选择TMC2130/2160
        # 同时确保 COVER_DATA_LENGTH (bits 23:21)0
        spi_out_conf &= ~0x00E0000F # 清除 spi_output_format 和 COVER_DATA_LENGTH
        spi_out_conf |= 0x0000000C # 设置 spi_output_format = 12
        # spi_out_conf = 0x44401280 | 0x0C << 0
        print(f"将要写入的 SPI_OUT_CONF: 0x{spi_out_conf:08X}")
        self.write_register(TMC4361_SPI_OUT_CONF_REGISTER, spi_out_conf)


        # 再次读取以确认写入成功
        new_spi_out_conf = self.read_register(TMC4361_SPI_OUT_CONF_REGISTER)
        print(f"确认后的 SPI_OUT_CONF: 0x{new_spi_out_conf:08X}")
        if new_spi_out_conf == spi_out_conf:
            print("SPI Passthrough 初始化成功!")
        else:
            print("SPI Passthrough 初始化失败!")


    def spi_passthrough_transfer_v2(self, address, data=None):
        """
        修正后的SPI passthrough传输方法,增加了前置配置。
       
        Args:
            address: TMC2160寄存器地址 (7)
            data: 要写入的数据 (32)(None表示读取操作)
       
        Returns:
            读取的数据或状态 (32)
        """
        if data is not None:
            # 写操作
            tmc2160_address_byte = (address | 0x80) & 0xFF
           
           
            # 再写入地址到COVER_HIGH,触发传输
            self.write_register(TMC4361_COVER_HIGH_REGISTER, tmc2160_address_byte)
            # 先写入数据到COVER_LOW
            self.write_register(TMC4361_COVER_LOW_REGISTER, data)
            time.sleep(0.1)
            # 写操作通常返回0或状态,这里简化处理
            return 0
        else:
            # 读操作
            tmc2160_address_byte = address & 0x7F
           


            # 再写入地址到COVER_HIGH,触发传输
            self.write_register(TMC4361_COVER_HIGH_REGISTER, tmc2160_address_byte)
            # 对于读操作,先向COVER_LOW写入虚拟数据0
            self.write_register(TMC4361_COVER_LOW_REGISTER, 0)
           
            # 等待传输完成
            time.sleep(0.1)
           
            # 读取响应
            # 注意:根据数据手册,响应数据在COVER_DRV_LOW中,状态在COVER_DRV_HIGH
            response_data = self.read_register(TMC4361_COVER_DRV_LOW_REGISTER)
            response_status = self.read_register(TMC4361_COVER_DRV_HIGH_REGISTER)
           
            print(f"TMC2160响应 - 状态: 0x{response_status:08X}, 数据: 0x{response_data:08X}")
            return response_data


另一部分是ADMT4000的驱动,这一块比较简单,和之前的版本没有任何区别:

import time
from adafruit_bus_device.spi_device import SPIDevice


# Register addresses
_CNVPAGE = 0x01
_ABSANGLE = 0x03
_ANGLE = 0x05
_GENERAL = 0x10 # General Register, Page 0x02
_FAULT = 0x06 # Fault Register, Page Agnostic


# Bit masks for GENERAL register
# No direct software reset bit for GMR found in datasheet. Reset is physical.


class ADMT4000:
    """Driver for the ADMT4000 multiturn sensor."""


    def __init__(self, spi, cs, baudrate=10000000, polarity=0, phase=0):
        self.spi_device = SPIDevice(spi, cs, baudrate=baudrate, polarity=polarity, phase=phase)
        self._buffer = bytearray(4)


    def _read_register(self, address):
        """Read a 16-bit value from a register."""
        # Per datasheet Figure 18 (SPI Register Read Operation):
        # The first bit sent by the microcontroller is ignored.
        # The second bit is set low (0) to select the read register configuration.
        # The 6-bit register address A5 to A0 is then clocked into the ADMT4000.
        # This means the first byte of the 32-bit frame should be structured as:
        # [Ignored Bit (can be 0)][R/W Bit (0 for read)][A5][A4][A3][A2][A1][A0]
        # So, the command byte should be (address & 0x3F).
        # The remaining 3 bytes of the command frame are 0x00.
        self._buffer[0] = address & 0x3F
        self._buffer[1] = 0x00
        self._buffer[2] = 0x00
        self._buffer[3] = 0x00


        # Perform the 32-bit SPI transaction
        with self.spi_device as spi:
            spi.write_readinto(self._buffer, self._buffer)


        # The datasheet (Figure 18) indicates the response frame structure:
        # Byte 0: [Ignored Bit][R/W Bit (1)][A5][A4][A3][A2][A1][A0] (echo of command, but R/W=1)
        # Byte 1: [Data MSB (D15)][D14][D13][D12][D11][D10][D9][D8]
        # Byte 2: [Data LSB (D7)][D6][D5][D4][D3][D2][D1][D0]
        # Byte 3: [First Bit Following Data (always 1)][C1][C0][CRC4][CRC3][CRC2][CRC1][CRC0]
        # The 16-bit data is in self._buffer[1] and self._buffer[2].
        return (self._buffer[1] << 8) | self._buffer[2]


    def _write_register(self, address, value):
        """Write a 16-bit value to a register."""
        # Per datasheet Figure 19 (32-Bit Write Operation for 16-Bit Register):
        # The first bit sent by the microcontroller is ignored.
        # The second bit is set high (1) for a write.
        # The 6-bit register address A5 to A0 is then clocked into the ADMT4000.
        # This makes up the first byte of the 32-bit SPI frame.
        # Command byte: [Ignored Bit (can be 0)][R/W Bit (1 for write)][A5][A4][A3][A2][A1][A0]
        # So, the command byte should be (0x40 | (address & 0x3F)). (0x40 sets the R/W bit to 1)
        self._buffer[0] = 0x40 | (address & 0x3F)
        self._buffer[1] = (value >> 8) & 0xFF  # MSB of data
        self._buffer[2] = value & 0xFF         # LSB of data
        self._buffer[3] = 0x00 # The datasheet mentions 3 following bits are not specified, and then 5 bits CRC. For now, send 0.


        with self.spi_device as spi:
            spi.write(self._buffer)


    def gmr_reset(self):
        """Resets the GMR turn count sensor. This will set the turn count to 45.
        Requires a conversion sequence abort and restart after reset.
        Note: The GMR sensor reset is a physical action (e.g., applying a strong magnetic field
        at a specific angle or over-rotating the magnet). This method handles the software
        sequence (abort/restart conversion) that should follow the physical reset.
        """
        # Abort current conversion sequence
        # Set CNVPAGE bits 15:14 to 0b11 to abort
        current_cnvpage = self._read_register(_CNVPAGE)
        self._write_register(_CNVPAGE, (current_cnvpage | 0xC000)) # Set bits 15:14 to 11
        time.sleep(0.01) # Small delay for the device to process


        # Restart the conversion sequence
        # Set CNVPAGE bits 15:14 to 0b00 to restart
        self._write_register(_CNVPAGE, (current_cnvpage & 0x3FFF)) # Set bits 15:14 to 00
        time.sleep(0.01) # Small delay


    def reset_angle_registers(self):
        """Resets the ANGLE and ABSANGLE registers.
        The datasheet indicates that ANGLE and ABSANGLE are read-only registers.
        Their values are derived from sensor measurements. To 'reset' them,
        a GMR sensor reset is the primary mechanism, which sets the turn count to 45.
        This function will trigger the GMR reset sequence.
        """
        print("Warning: ANGLE and ABSANGLE registers are read-only. Performing GMR reset to affect turn count.")
        self.gmr_reset()


    @property
    def angle(self):
        """The single-turn angle in degrees."""
        raw_angle = self._read_register(_ANGLE)
        # The ANGLE register provides the upper 12 bits of the angle data [15:4].
        # Angle Resolution = 360°/4096.
        # The raw_angle read from the device is already the 16-bit value, where bits [15:4] are angle data.
        angle_data = raw_angle >> 4  # Shift right by 4 to get the 12-bit angle value
        return (angle_data / 4096.0) * 360.0


    @property
    def absolute_angle(self):
        """The absolute angle over multiple turns, in degrees."""
        raw_abs_angle = self._read_register(_ABSANGLE)
        # ABSANGLE[15:10] contains the number of whole turns (6 bits).
        # ABSANGLE[9:0] contains the angle information in straight binary with a resolution of 0.351° (10 bits).


        # Extract turn count (bits 15-10)
        turn_count_raw = (raw_abs_angle >> 10) & 0x3F  # Mask to ensure 6 bits


        # Extract angle part (bits 9-0)
        angle_part_raw = raw_abs_angle & 0x03FF  # Mask for lower 10 bits


        # Handle invalid turn count (0b110110 = 54) as per datasheet Table 11
        if turn_count_raw == 0b110110:
            return float("nan")


        # Handle two\'s complement for negative turns (0b110111 to 0b111111) as per datasheet Table 11
        if turn_count_raw >= 0b110111:
            turn_count = turn_count_raw - 64  # Convert 6-bit two\'s complement to negative integer
        else:
            turn_count = turn_count_raw


        # Calculate absolute angle: turn_count * 360° + angle_part * 0.351°
        return (turn_count * 360.0) + (angle_part_raw * 0.351)


    @property
    def turn_count(self):
        """The number of turns."""
        raw_abs_angle = self._read_register(_ABSANGLE)
        # ABSANGLE[15:10] contains the number of whole turns (6 bits).
        turn_count_raw = (raw_abs_angle >> 10) & 0x3F  # Mask to ensure 6 bits


        # Handle invalid turn count
        if turn_count_raw == 0b110110:
            return None


        # Handle two\'s complement for negative turns
        if turn_count_raw >= 0b110111:
            return turn_count_raw - 64
        else:
            return turn_count_raw


下面是主程序,硬件接线可以参考下面主程序部分。这块代码完成对三个硬件的初始化后我们就可以开始实验:

import time
import board
import busio
import pwmio
import digitalio
from adafruit_bus_device.spi_device import SPIDevice
import TMC4361_CircuitPython as TMC4361
from admt4000_CircuitPython import ADMT4000


# 外部时钟配置
clock_pin = board.GPIO9
clock_frequency = 8_000_000
pwm_clock = pwmio.PWMOut(clock_pin, frequency=clock_frequency, duty_cycle=2**15)


# SPI 配置
spi = busio.SPI(board.GPIO12, MOSI=board.GPIO11, MISO=board.GPIO13)
cs_pin = board.GPIO10


# 初始化 TMC4361 对象
tmc = TMC4361.TMC4361(spi, cs_pin)
tmc.begin(clock_frequency)


print("TMC4361 initialized successfully")


# 设置输出极性
tmc.set_outputs_polarity(step_inverted=False, dir_inverted=False)


# 设置输出时序
tmc.set_output_timings(step_length_us=5, dir_setup_time_us=5)


# 读取并打印配置信息
print("TMC4361 Version:", tmc.get_version())
print("TMC4361 Status:", hex(tmc.get_status_flags()))
print("Step Length and Dir Setup:", hex(tmc.read_register(TMC4361.TMC4361_STP_LENGTH_ADD)))



# # 初始化TMC2160
# spi_bus = busio.SPI(board.GPIO18, MOSI=board.GPIO15, MISO=board.GPIO16)
# cs_pin = digitalio.DigitalInOut(board.GPIO17)  # CSN引脚
# tmc2160_device = SPIDevice(spi_bus, cs_pin, baudrate=100000, phase=1, polarity=1)


# # tmc2160寄存器读/写函数
# def read_reg(register):
#     # 读操作: 地址字节 = 0x80 | register (最高位为1)
#     address_byte = (0x80 | register) & 0xFF
#     data_bytes = bytearray(4)
#     with tmc2160_device as spi:
#         # 发送地址字节并读取4字节数据
#         spi.write(bytearray([address_byte]))
#         spi.readinto(data_bytes)
#     return int.from_bytes(data_bytes, 'big')


# def write_reg(register, data):
#     # 写操作: 地址字节 = 0x80 | register (最高位为0)
#     address_byte = (0x80 | register) & 0xFF
#     data_bytes = data.to_bytes(4, 'big')
#     with tmc2160_device as spi:
#         # 发送地址字节和4字节数据
#         spi.write(bytearray([address_byte, data_bytes[0], data_bytes[1], data_bytes[2], data_bytes[3]]))


# # 尝试读取IOIN寄存器以验证SPI通信
# print("Attempting to read IOIN register...")
# ioin_value = read_reg(0x04)  # IOIN寄存器地址为0x04
# print("IOIN register value: 0x{:08X}".format(ioin_value))


# # 配置电流寄存器 (IHOLD_IRUN, 地址0x10)
# # IRUN(bit 8-4)=10 (约1A), IHOLD(bit 3-0)=5 (0.5A), IHOLDDELAY=6
# write_reg(0x10, 0x000A0506)
# print("Configured IHOLD_IRUN")


# # 配置斩波器寄存器 (CHOPCONF, 地址0x6C)
# # TOFF=3 (使能斩波器), MRES=4 (16微步)
# write_reg(0x6C, 0x000100C3)
# print("Configured CHOPCONF")


# # 关键配置:启用外部STEP/DIR模式 (GCONF, 地址0x00)
# # 设置bit 3 (step_dir_mode)1
# write_reg(0x00, 0x00000008)
# print("Configured GCONF for STEP/DIR mode")


# # 读取DRV_STATUS寄存器(0x6F)进行诊断
# drv_status = read_reg(0x6F)
# print("DRV_STATUS: 0x{:08X}".format(drv_status))
# # 分析DRV_STATUS值:
# # - bit 31 (stst): 1表示电机待机,0表示运动
# # - bit 0 (ola): A相开路
# # - bit 1 (olb): B相开路
# # - bit 4 (s2ga): A相短接到地
# # - 其他位参考数据手册


tmc.initialize_spi_passthrough()
tmc.spi_passthrough_transfer_v2(0x10, 0x000A0506)
tmc.spi_passthrough_transfer_v2(0x6C, 0x000100C3)
tmc.spi_passthrough_transfer_v2(0x00, 0x00000008)



spi3 = busio.SPI(board.GPIO5, MOSI=board.GPIO6, MISO=board.GPIO7)
cs3 = digitalio.DigitalInOut(board.GPIO4) # Example: use D5 as CS pin
sensor = ADMT4000(spi3, cs3)


# 设置TMC4361的运动参数
round = 360 / 1.8 * 16 * 16
tmc.set_accelerations(round*10, round*10, 0, 0)


# # 设置目标位置
# tmc.set_ramp_mode(TMC4361.TMC4361.RampMode.POSITIONING_MODE, TMC4361.TMC4361.RampType.TRAPEZOIDAL_RAMP)
# tmc.set_max_speed(round * 1.9)
# target = int(round * 3)
# tmc.set_target_position(target)
# print("Target position set")


# # 设置目标速度
# tmc.set_ramp_mode(TMC4361.TMC4361.RampMode.VELOCITY_MODE, TMC4361.TMC4361.RampType.TRAPEZOIDAL_RAMP)
# tmc.set_max_speed(round * 1.9)
# print("Target velocity set")


# close-loop
start_angle = sensor.angle
set_angle = 360 * 35
tmc.set_ramp_mode(TMC4361.TMC4361.RampMode.POSITIONING_MODE, TMC4361.TMC4361.RampType.TRAPEZOIDAL_RAMP)
tmc.set_max_speed(1.0*round)
# tmc.set_target_position(int(set_angle*(round/360)))


功能展示及说明

实验我们使用位置控制模式,实时打印出丢步数据和补充步数。

# 主循环
while True:
    current_speed = tmc.get_current_speed()
    current_position = tmc.get_current_position()
    target_reached = tmc.is_target_reached()
   
    # print(f"Speed: {current_speed}, Position: {current_position}, Target Reached: {target_reached}")


    # Read single-turn angle
    angle = sensor.angle
    # print(f"Single-Turn Angle: {angle:.2f} degrees")


    # Read absolute angle (multi-turn)
    absolute_angle = sensor.absolute_angle
    # print(f"Absolute Angle: {absolute_angle:.2f} degrees")


    # Read turn count
    turn_count = sensor.turn_count
    # print(f"Turn Count: {turn_count}")


    if str(absolute_angle) != "nan":
        if int(absolute_angle) != set_angle:
            angle_diff = set_angle - int(absolute_angle)
            pulse_need = int(angle_diff * (round/360))
            print(f"丢步角度: {angle_diff}, 补偿步数:{pulse_need}")
            tmc.set_target_position(current_position + pulse_need)
        else:
            tmc.set_target_position(current_position)


    # if target_reached:
        # print("Target reached!")
        # if current_position >= 0:
        #     tmc.set_target_position(-target)
        # else:
        #     tmc.set_target_position(target)
   
    # time.sleep(0.1)

接线上如文章最开始的硬件框图所示,TMC4361和TMC2160之间通过SPI, STEP和DIR连接,TMC4361使用脉冲来控制TMC2160。单片机通过SPI连接TMC4361和ADMT4000, 终端打印出丢步和补偿信息

image.png

image.png

具体演示可以参考视频。

对本活动的心得体会

时隔一年再次参加电子森林的活动,活动依旧创意十足,诚意满满!

附件下载
funpack4-3.zip
团队介绍
个人
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号