内容介绍
内容介绍
项目描述
这个使用TMC4361A-BOB&TMC2160-BOB板卡连接步进电机,配合ADMT4000实现一个线性执行器或者旋转执行器,ADMT4000接MCU,使用软件计算丢步情况,并用软件补相应步数。
软件流程图及各功能对应的主要代码片段及说明
系统硬件框图如下:

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, 终端打印出丢步和补偿信息。


具体演示可以参考视频。
对本活动的心得体会
时隔一年再次参加电子森林的活动,活动依旧创意十足,诚意满满!
附件下载
funpack4-3.zip
团队介绍
个人
评论
0 / 100
查看更多
猜你喜欢
Funpack3-5 基于BeagleBone® Black的步进电机控制该项目使用了BeagleBone Black开发板,实现了步进电机调速控制系统的设计,它的主要功能为:本项目实现的功能是使用 BeagleBone Black硬件接口功能实现步进电机控制,如 调速、正反转等功能。。
小屁孩06
207
Funpack4-3:TMC4361A-BOB & TMC2160-BOB 驱动步进电机使用TMC4361A-BOB&TMC2160-BOB板卡连接步进电机,使用ESP32S3结合circuitpython实现电机定速旋转、指定角度旋转、指定圈数旋转、正反转切换功能。
StreakingJerry
170
Funpack4-3 使用TMC4361A-BOB&TMC2160-BOB板卡连接步进电机实现电机控制该项目使用了TMC4361A-BOB&TMC2160-BOB,实现了电机控制的设计,它的主要功能为:使用TMC4361A-BOB&TMC2160-BOB板卡连接步进电机实现电机控制。
冲向天空的猪
44