基于Vision Board的电驱扩展板

通过Vision Board的图象处理, 识别物体颜色,检测路线的中心位置偏差, 甚至可识别红绿灯,根据识别结果通过UART/CAN口发送指令到开源电调VESC,其收到指令后完成速电机的速度设定或者进行占空比调节, 然后计算完成小车前进/后退/加速减速控制, 也能完成转向/旋转控制在配置两个电机的条件下,最终实现小车颜色跟踪/巡线/识别红绿灯等智能功能.

基于Vision Board的电驱扩展板封面
XiaoMing2025-11-03 16:05:33GPL 3.0
377
Star

PCBA

设计文件

KiCad图标基于Vision Board的VESC电驱扩展板V2.zip2.11MB

EDA查看器

复制嵌入代码

详细介绍

概述:

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的直流电机扩展板调试视觉处理软件.

基于视觉的巡线小车控制程序设计:

  1. 图像处理

    • 拍摄图像并二值化

    • 进行开运算和高斯滤波去噪

    • 通过线性回归检测巡线

  2. PID控制

    • 计算位置误差 rho_err 和角度误差 theta_err

    • 使用两个PID控制器分别处理位置和角度误差

  3. 电机控制

    • 根据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、华秋及所有相关组织者的辛勤付出与支持!