概述:
1.扩展板基于STM32F4控制器,DRV8302 MOSFET 驱动器,IRFS7530 MOEFET.
2. 开源电调VESC,引出UART的TX/RX,适配Vision Board的UART2(RPI TXD2/RPI RXD2)发送/接收指令,完成速度和位置控制。
3.Vision board 接收路径线条数据,检测路线的中心位置偏差,计算转向和前进/后退指令。 电驱的模块专门用于电机执行和驱动的功能, 这样VisionBoard开发板让其专注于机器视觉处理如颜色跟踪等任务
硬件:
原理图如图, 参考自VESC6的开源原理图
四层板, PCBA也打样回来了,通过开源的上位机VESC tool确认板子的功能正常.




软件:
在硬件确认完毕之前,曾通过基于L298P的直流电机扩展板调试视觉处理软件.


基于视觉的巡线小车控制程序设计:
图像处理:
拍摄图像并二值化
进行开运算和高斯滤波去噪
通过线性回归检测巡线
PID控制:
计算位置误差
rho_err和角度误差theta_err使用两个PID控制器分别处理位置和角度误差
电机控制:
根据PID输出调整左右轮速度实现差速转向
无线条时原地旋转寻找线路
代码在OPENMV 基于micro python快速原型的. 并由Deep Seek优化后测试的
from math import pi, isnan
from machine import Pin
import utime
import sensor, image, time
INVERSE_LEFT = False # 左轮反转 INVERSE_RIGHT = False # 右轮反转
THRESHOLD = (0, 20, -10, 10, -10, 10) MIN_MAGNITUDE = 8 # 最小检测可信度 SEARCH_SPEED = 30 # 搜索速度 BASE_SPEED = 50 # 基础速度 MAX_OUTPUT = 40 # 最大输出限制
RHO_PID_PARAMS = {'p': 0.5, 'i': 0.01, 'd': 0.1} THETA_PID_PARAMS = {'p': 0.8, 'i': 0.02, 'd': 0.15}
========== 硬件初始化 ==========
class HardwareController:
def __init__(self):
self._init_pins()
self._init_pwm()
self._init_sensor()
def _init_pins(self):
"""初始化所有GPIO引脚"""
self.ledb = Pin(("BLUE", 0x0102))
self.ain1 = Pin(('A1', 0x002))
self.ain2 = Pin(('A2', 0x004))
self.bin1 = Pin(('B1', 0x005))
self.bin2 = Pin(('B2', 0x009))
self.tim1 = Pin(('T1', 0x006))
self.tim2 = Pin(('T2', 0x003))
def _init_pwm(self):
"""初始化PWM控制器"""
self.pwm_controller = MultiSoftwarePWM(frequency=1000)
self.ch1_id = self.pwm_controller.add_channel(self.tim1)
self.ch2_id = self.pwm_controller.add_channel(self.tim2)
def _init_sensor(self):
"""初始化摄像头传感器"""
sensor.reset()
sensor.set_vflip(True)
sensor.set_hmirror(True)
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQQVGA) # 80x60
sensor.skip_frames(time=2000)
print("Sensor initialized")========== 软件PWM实现 ==========
class MultiSoftwarePWM:
def __init__(self, frequency=1000):
self.frequency = frequency
self.period_us = 1000000 // frequency
self.channels = []
self.last_time = utime.ticks_us()
def add_channel(self, pin):
channel = {
'pin': pin,
'duty_cycle': 0,
'high_time': 0
}
self.channels.append(channel)
return len(self.channels) - 1
def pulse_width_percent(self, channel_id, percent):
"""设置PWM占空比"""
if 0 <= channel_id < len(self.channels):
percent = max(0, min(100, percent)) # 限制范围
self.channels[channel_id]['duty_cycle'] = percent
self.channels[channel_id]['high_time'] = (self.period_us * percent) // 100
def update(self):
"""更新PWM输出(需要在主循环中频繁调用)"""
current_time = utime.ticks_us()
elapsed = utime.ticks_diff(current_time, self.last_time)
if elapsed >= self.period_us:
self.last_time = current_time
# 周期开始,所有引脚置高
for channel in self.channels:
channel['pin'].value(1)
else:
# 检查每个通道是否应该置低
for channel in self.channels:
if elapsed >= channel['high_time']:
channel['pin'].value(0)========== PID控制器 ==========
class PID:
def __init__(self, p=0, i=0, d=0, imax=0):
self._kp = float(p)
self._ki = float(i)
self._kd = float(d)
self._imax = abs(imax)
self._integrator = 0
self._last_error = 0
self._last_derivative = float('nan')
self._last_t = 0
self._RC = 1 / (2 * pi * 20) # 低通滤波器
def get_pid(self, error, scaler=1):
"""计算PID输出"""
tnow = utime.ticks_ms()
dt = tnow - self._last_t
# 时间处理
if self._last_t == 0 or dt > 1000:
dt = 0
self.reset_I()
self._last_t = tnow
delta_time = float(dt) / 1000.0
# 比例项
output = error * self._kp
# 微分项(带低通滤波)
if abs(self._kd) > 0 and dt > 0:
if isnan(self._last_derivative):
derivative = 0
self._last_derivative = 0
else:
derivative = (error - self._last_error) / delta_time
# 低通滤波
derivative = self._last_derivative + \
((delta_time / (self._RC + delta_time)) * \
(derivative - self._last_derivative))
self._last_error = error
self._last_derivative = derivative
output += self._kd * derivative
output *= scaler
# 积分项
if abs(self._ki) > 0 and dt > 0:
self._integrator += (error * self._ki) * scaler * delta_time
# 积分限幅
self._integrator = max(-self._imax, min(self._imax, self._integrator))
output += self._integrator
return output
def reset_I(self):
"""重置积分项"""
self._integrator = 0
self._last_derivative = float('nan')========== 巡线控制器 ==========
class LineFollower:
def __init__(self):
self.hw = HardwareController()
self.rho_pid = PID(**RHO_PID_PARAMS)
self.theta_pid = PID(**THETA_PID_PARAMS)
self.clock = time.clock()
self.state = "SEARCHING"
self.lost_counter = 0
self.max_lost_frames = 30
def process_image(self, img):
"""图像处理流水线"""
img.binary([THRESHOLD])
img.open(1) # 开运算去噪
img.gaussian(1) # 高斯滤波
return img.get_regression([(100, 100)], robust=True)
def calculate_errors(self, line, img_width):
"""计算位置和角度误差"""
rho_err = abs(line.rho()) - img_width / 2
if line.theta() > 90:
theta_err = line.theta() - 180
else:
theta_err = line.theta()
return rho_err, theta_err
def run_motors(self, left_speed, right_speed):
"""控制电机运行"""
# 方向处理
if INVERSE_LEFT:
left_speed = -left_speed
if INVERSE_RIGHT:
right_speed = -right_speed
# 设置方向
self.hw.ain1.value(left_speed >= 0)
self.hw.ain2.value(left_speed < 0)
self.hw.bin1.value(right_speed >= 0)
self.hw.bin2.value(right_speed < 0)
# 设置PWM
self.hw.pwm_controller.pulse_width_percent(
self.hw.ch1_id, abs(left_speed))
self.hw.pwm_controller.pulse_width_percent(
self.hw.ch2_id, abs(right_speed))
def handle_line_detected(self, line, img):
"""处理检测到线条的情况"""
rho_err, theta_err = self.calculate_errors(line, img.width())
# 绘制检测线
img.draw_line(line.line(), color=127)
if line.magnitude() > MIN_MAGNITUDE:
# PID控制
rho_output = self.rho_pid.get_pid(rho_err, 1)
theta_output = self.theta_pid.get_pid(theta_err, 1)
output = rho_output + theta_output
# 输出限制
output = max(-MAX_OUTPUT, min(MAX_OUTPUT, output))
self.run_motors(BASE_SPEED + output, BASE_SPEED - output)
self.state = "FOLLOWING"
# 修复的print语句 - 使用传统格式化
print("FOLLOWING: Rho_err=%.1f, Theta_err=%.1f, Output=%.1f, Mag=%d" %
(rho_err, theta_err, output, line.magnitude()))
else:
print("Low confidence, stopping")
self.run_motors(0, 0)
self.state = "UNCERTAIN"
def handle_no_line(self):
"""处理未检测到线条的情况"""
self.lost_counter += 1
if self.lost_counter > self.max_lost_frames:
self.state = "LOST"
print("Line completely lost! Aggressive search")
self.run_motors(SEARCH_SPEED, -SEARCH_SPEED) # 原地旋转
else:
self.state = "SEARCHING"
self.run_motors(SEARCH_SPEED // 2, -SEARCH_SPEED // 2) # 温和搜索
# 修复的print语句
fps = self.clock.fps()
print("%s: FPS=%.1f, Lost frames=%d" %
(self.state, fps, self.lost_counter))
def main_loop(self):
"""主控制循环"""
print("Starting line follower...")
self.hw.ledb.value(1) # 开启状态指示
try:
while True:
# 更新PWM
self.hw.pwm_controller.update()
# LED状态指示
self.hw.ledb.value(not self.hw.ledb.value())
# 图像处理
self.clock.tick()
img = sensor.snapshot()
line = self.process_image(img)
# 状态处理
if line:
self.lost_counter = 0
self.handle_line_detected(line, img)
else:
self.handle_no_line()
# 短暂延时,保持PWM更新频率
utime.sleep_ms(5)
except KeyboardInterrupt:
print("Program interrupted by user")
except Exception as e:
print("Unexpected error: " + str(e))
finally:
self.cleanup()
def cleanup(self):
"""清理资源"""
print("Cleaning up...")
self.run_motors(0, 0) # 停止电机
self.hw.ledb.value(0) # 关闭LED
print("Cleanup completed.")在硬件确认后,结合视觉处理软件,调试VISION BOARD 和VESC通过UART/CAN的协同工作,UART数据发出后VESC响应还有些问题,后续更新中。
========== VESC通信协议 ==========
def vesc_set_rpm(rpm: int):
"""设置转速"""
payload = struct.pack('>i', rpm)
cmd = 0x08 msg = bytes([cmd]) + payload msg_len = len(msg)
data = bytes([msg_len >> 8, msg_len & 0xFF]) + msg
checksum = crc16(data)
packet = b'\x02' + data + struct.pack('>H', checksum) + b'\x03'
uart.write(packet)
def vesc_stop():
"""停止电机"""
vesc_set_duty(0.0)
ledb.value(0)
========== 电机测试 ==========
def motor_test():
"""简单电机测试"""
print("电机测试开始")
# 正转测试
print("正转1000 RPM - 10秒")
ledb.value(1)
vesc_set_rpm(1000)
for i in range(5, 0, -1):
time.sleep(1)
# 停止
vesc_stop()
time.sleep(1)
# 反转测试
print("反转1000 RPM - 10秒")
ledb.value(1)
vesc_set_rpm(-1000)
for i in range(5, 0, -1):
time.sleep(1)
# 最终停止
vesc_stop()
print("电机测试完成")总结:
基于Vision Board的电驱扩展板 凭借其合适的模块化设计,主要负责电机驱动的任务, Vision Board 集成的OPENMV 和MicorPython 可以快速调试摄像头采集图片并处理图片,分析路线角度并调整电机前进方式,是前进 左右转还是停止. Vision Board 是做巡线小车的得力助手, 可以高效的完成图像处理和小车前进方向的识别.
感谢:
衷心感谢RT-Thread与华秋携手组织本次富有意义的活动,为我们广大开发者提供了宝贵的交流与实践平台。
同时,我们也期待RT-Thread未来能推出更多让开发者深度参与活动,在现有线上形式基础上,逐步拓展至线下交流,通过与开发者多元互动,增添行业活力..
再次感谢RT-Thread、华秋及所有相关组织者的辛勤付出与支持!