15浏览
查看: 15|回复: 0

[项目] ROS2雷达小车中篇:四层车身搭建与简易避障实现

[复制链接]
本帖最后由 云天 于 2026-8-6 11:42 编辑

从“能跑”到“会躲”,手搓一台带激光雷达的自主避障小车
【项目背景】
       在上一篇《ROS2雷达小车开篇》中,我们完成了树莓派4B的系统安装、ROS2 Humble环境搭建,以及RPLIDAR C1雷达驱动的编译和测试。小车已经能通过键盘控制跑起来了,但距离“自主”还有一步之遥。
       这一次,我们要做三件事:
  • 解决SSH远程登录问题——终于可以无线调试了。
  • 设计一套四层亚克力车身——把树莓派、Arduino、雷达、电池全部整合在一起。
  • 实现简易雷达避障——直走 → 遇障碍 → 看左右 → 转空旷方向。

ROS2雷达小车中篇:四层车身搭建与简易避障实现图8

【SSH服务配置】
       终于可以无线调试了
       在上一篇中,我们尝试SSH登录时遇到了 Permission denied (publickey) 的问题,始终无法用密码登录。折腾了好久,终于找到了根源。
       问题现象
  1. ssh ubuntu@ubuntu.local
  2. ubuntu@ubuntu.local: Permission denied (publickey)
复制代码
      问题根源
       Ubuntu Server默认通过PAM(可插拔认证模块)来验证密码,而PAM在SSH中走的是 KbdInteractiveAuthentication(键盘交互认证)通道。即使 PasswordAuthentication 设为 yes,如果 KbdInteractiveAuthentication 是 no,系统根本不会走到密码验证那一步。
       解决方案
       在树莓派本地终端执行:
  1. sudo nano /etc/ssh/sshd_config
复制代码
       找到并修改以下两行(确保值为 yes,且行首没有 #):
  1. PasswordAuthentication yes
  2. KbdInteractiveAuthentication yes   # ← 就是它!改成yes就能用密码登录了
复制代码
       保存退出(Ctrl+O,回车,Ctrl+X),重启SSH服务:
  1. sudo systemctl restart ssh
复制代码
       现在在电脑上就可以愉快地无线连接了:
  1. ssh ubuntu@ubuntu.local
复制代码
       经验总结:如果SSH连不上,除了检查 PasswordAuthentication,一定要看看 KbdInteractiveAuthentication。这两个要同时为 yes 才能用密码登录。
【车身结构设计】
       四层亚克力“三明治”
       为了让整车布局清晰、便于调试和穿线,我设计了一个四层亚克力车身结构,用激光切割机加工,层与层之间用铜柱支撑。
       硬件清单

层级
硬件
说明
第一层(底层)Arduino Uno R4 + L298P Motor Shield电机驱动,接收树莓派串口指令
4节18250锂电池(通过PWRIN供电)为电机提供独立电源
两个驱动轮(后轮)+ 一个万向轮(前轮)差速转向
第二层(中层)树莓派4BROS2主控
4路18650电池座(独立供电)为树莓派供电
第三层(过渡层)RPLIDAR C1 USB转接板用小铜柱支撑,层高低,避免裸露
第四层(顶层)RPLIDAR C1激光雷达360°扫描,安装在最高处
ROS2雷达小车中篇:四层车身搭建与简易避障实现图4




ROS2雷达小车中篇:四层车身搭建与简易避障实现图5


ROS2雷达小车中篇:四层车身搭建与简易避障实现图6


ROS2雷达小车中篇:四层车身搭建与简易避障实现图7


ROS2雷达小车中篇:四层车身搭建与简易避障实现图9


       设计要点
  • Arduino通过USB线连接到树莓派的USB口,传输串口指令。
  • 每层亚克力板上预留穿线孔,便于电源线、USB线、雷达线缆上下贯通。
  • 铜柱高度:底层与第二层之间用30mm,第二层与第三层之间用40cm,第三层与第四层之间用15mm小铜柱。
  • 整体尺寸约250mm × 200mm × 180mm(长×宽×高),紧凑且稳固。

       小贴士:亚克力板切割前,先用纸板打样确认孔位。铜柱螺柱规格统一选用M3,便于采购和安装。
【软件架构】
       树莓派 + Arduino 双控方案
       整车采用树莓派(决策层)+ Arduino(执行层)的经典架构:
  • 树莓派:运行ROS2节点,订阅 /cmd_vel 速度指令(来自键盘控制或自主避障节点),通过串口将左右轮PWM值发送给Arduino。
  • Arduino:接收串口指令,驱动L298P Motor Shield,控制两个直流电机的转速和方向。

       串口通信格式:
  1. M left_pwm right_pwm\n
复制代码
  • left_pwm 和 right_pwm 取值范围:-255 ~ 255,正负表示方向,绝对值表示速度。

【Arduino Uno R4 代码】
       Arduino负责接收串口指令并驱动L298P电机驱动板。L298P Shield的默认引脚为:电机A方向D12、PWM D10;电机B方向D13、PWM D11。
  1. // motor_control.ino - Arduino Uno R4 + L298P Motor Shield
  2. // 接收格式: "M left_pwm right_pwm\n",PWM 范围 -255 ~ 255
  3. // L298P Shield 默认引脚
  4. #define DIR_A 12
  5. #define PWM_A  10
  6. #define DIR_B 13
  7. #define PWM_B 11
  8. // 串口缓冲区
  9. const byte NUM_CHARS = 32;
  10. char receivedChars[NUM_CHARS];
  11. boolean newData = false;
  12. void setup() {
  13.   pinMode(DIR_A, OUTPUT);
  14.   pinMode(PWM_A, OUTPUT);
  15.   pinMode(DIR_B, OUTPUT);
  16.   pinMode(PWM_B, OUTPUT);
  17.   
  18.   analogWrite(PWM_A, 0);
  19.   analogWrite(PWM_B, 0);
  20.   
  21.   Serial.begin(115200);
  22.   Serial.println("Motor controller ready");
  23. }
  24. void loop() {
  25.   recvWithStartEndMarkers();
  26.   if (newData) {
  27.     parseCommand(receivedChars);
  28.     newData = false;
  29.   }
  30. }
  31. void recvWithStartEndMarkers() {
  32.   static boolean recvInProgress = false;
  33.   static byte ndx = 0;
  34.   char startMarker = 'M';
  35.   char endMarker = '\n';
  36.   char rc;
  37.   while (Serial.available() > 0 && newData == false) {
  38.     rc = Serial.read();
  39.     if (recvInProgress == true) {
  40.       if (rc != endMarker) {
  41.         receivedChars[ndx] = rc;
  42.         ndx++;
  43.         if (ndx >= NUM_CHARS) ndx = NUM_CHARS - 1;
  44.       } else {
  45.         receivedChars[ndx] = '\0';
  46.         recvInProgress = false;
  47.         ndx = 0;
  48.         newData = true;
  49.       }
  50.     } else if (rc == startMarker) {
  51.       recvInProgress = true;
  52.     }
  53.   }
  54. }
  55. void parseCommand(char* data) {
  56.   int left, right;
  57.   if (sscanf(data, "%d %d", &left, &right) == 2) {
  58.     setMotor(PWM_A, DIR_A, left);
  59.     setMotor(PWM_B, DIR_B, right);
  60.   }
  61. }
  62. void setMotor(int pwmPin, int dirPin, int speed) {
  63.   if (speed >= 0) {
  64.     digitalWrite(dirPin, HIGH);
  65.     analogWrite(pwmPin, speed);
  66.   } else {
  67.     digitalWrite(dirPin, LOW);
  68.     analogWrite(pwmPin, -speed);
  69.   }
  70. }
复制代码
【树莓派 ROS2 节点代码】
       1. 电机控制器节点(motor_controller.py)
       订阅 /cmd_vel,将速度指令转换为左右轮PWM值,通过串口发送给Arduino。
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
import serial
import time

class MotorController(Node):
    def __init__(self):
        super().__init__('motor_controller')
        
        # 参数配置
        self.declare_parameter('serial_port', '/dev/ttyACM0')
        self.declare_parameter('baud_rate', 115200)
        self.declare_parameter('wheel_base', 0.26)      # 轮距(米)
        self.declare_parameter('wheel_radius', 0.033)   # 轮子半径(米)
        self.declare_parameter('max_speed', 0.5)        # 最大线速度
        
        self.serial_port = self.get_parameter('serial_port').value
        self.baud_rate = self.get_parameter('baud_rate').value
        self.wheel_base = self.get_parameter('wheel_base').value
        self.wheel_radius = self.get_parameter('wheel_radius').value
        self.max_speed = self.get_parameter('max_speed').value
        
        # 初始化串口
        try:
            self.ser = serial.Serial(self.serial_port, self.baud_rate, timeout=0.1)
            self.get_logger().info(f'Serial port {self.serial_port} opened successfully')
            time.sleep(2)
        except Exception as e:
            self.get_logger().error(f'Failed to open serial port: {e}')
            return
        
        # 订阅 /cmd_vel
        self.subscription = self.create_subscription(
            Twist,
            '/cmd_vel',
            self.cmd_vel_callback,
            10
        )
        self.get_logger().info('Motor controller node started')
   
    def cmd_vel_callback(self, msg):
        vx = msg.linear.x
        vz = msg.angular.z
        
        vx = max(-self.max_speed, min(self.max_speed, vx))
        vz = max(-2.0, min(2.0, vz))
        
        # 差速运动学计算
        left_velocity = (vx - vz * self.wheel_base / 2.0) / self.wheel_radius
        right_velocity = (vx + vz * self.wheel_base / 2.0) / self.wheel_radius
        
        max_rad_speed = self.max_speed / self.wheel_radius
        left_pwm = int((left_velocity / max_rad_speed) * 255)
        right_pwm = int((right_velocity / max_rad_speed) * 255)
        
        left_pwm = max(-255, min(255, left_pwm))
        right_pwm = max(-255, min(255, right_pwm))
        
        # 最小PWM阈值(防止电机不转,尤其是转向时)
        MIN_PWM = 80
        if left_pwm != 0 and abs(left_pwm) < MIN_PWM:
            left_pwm = MIN_PWM if left_pwm > 0 else -MIN_PWM
        if right_pwm != 0 and abs(right_pwm) < MIN_PWM:
            right_pwm = MIN_PWM if right_pwm > 0 else -MIN_PWM
        
        self.send_motor_command(left_pwm, right_pwm)
        self.get_logger().debug(f'vx={vx:.2f}, vz={vz:.2f} -> L={left_pwm}, R={right_pwm}')
   
    def send_motor_command(self, left_pwm, right_pwm):
        try:
            cmd = f'M {left_pwm} {right_pwm}\n'
            self.ser.write(cmd.encode())
        except Exception as e:
            self.get_logger().error(f'Serial send error: {e}')
   
    def destroy_node(self):
        try:
            self.send_motor_command(0, 0)
            self.ser.close()
        except:
            pass
        super().destroy_node()

def main(args=None):
    rclpy.init(args=args)
    node = MotorController()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

6.2 自主避障节点(explorer.py)
订阅雷达 /scan 话题,实现“直走 → 遇障 → 看左右 → 转向”的简易避障逻辑。
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
import math

class WallFollower(Node):
    def __init__(self):
        super().__init__('wall_follower')
        
        self.declare_parameter('forward_speed', 0.2)
        self.declare_parameter('rotate_speed', 1.0)      # 提高转向速度
        self.declare_parameter('danger_distance', 0.5)
        self.declare_parameter('scan_angle', 30)
        
        self.forward_speed = self.get_parameter('forward_speed').value
        self.rotate_speed = self.get_parameter('rotate_speed').value
        self.danger_distance = self.get_parameter('danger_distance').value
        self.scan_angle = self.get_parameter('scan_angle').value
        
        self.state = 'FORWARD'
        self.turn_direction = 0
        self.turn_start_time = 0
        self.turn_duration = 2.1  # 120度 @ 1.0 rad/s
        
        self.scan_sub = self.create_subscription(LaserScan, '/scan', self.scan_callback, 10)
        self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)
        
        self.get_logger().info('Wall follower node started')
   
    def scan_callback(self, msg):
        if self.state == 'FORWARD':
            self.do_forward_behavior(msg)
        elif self.state == 'TURNING':
            self.do_turning_behavior()
   
    def do_forward_behavior(self, msg):
        front_min = self.get_min_distance_in_angle(msg, -30, 30)
        
        if front_min > self.danger_distance:
            self.move_forward()
            return
        
        self.stop()
        
        left_dist = self.get_min_distance_in_angle(msg, 30, 90)
        right_dist = self.get_min_distance_in_angle(msg, -90, -30)
        
        self.get_logger().info(f'Front: {front_min:.2f}m, Left: {left_dist:.2f}m, Right: {right_dist:.2f}m')
        
        if left_dist > right_dist:
            self.turn_direction = 1
            self.get_logger().info('Turning LEFT')
        else:
            self.turn_direction = -1
            self.get_logger().info('Turning RIGHT')
        
        self.turn_duration = 2.1 / self.rotate_speed  # 120度
        self.turn_start_time = self.get_clock().now().seconds_nanoseconds()[0]
        self.state = 'TURNING'
   
    def do_turning_behavior(self):
        elapsed = self.get_clock().now().seconds_nanoseconds()[0] - self.turn_start_time
        
        if elapsed < self.turn_duration:
            twist = Twist()
            twist.angular.z = self.turn_direction * self.rotate_speed
            self.cmd_pub.publish(twist)
        else:
            self.state = 'FORWARD'
            self.stop()
   
    def get_min_distance_in_angle(self, scan, start_deg, end_deg):
        start_rad = math.radians(start_deg)
        end_rad = math.radians(end_deg)
        angle_min = scan.angle_min
        angle_increment = scan.angle_increment
        min_dist = float('inf')
        
        for i, range_val in enumerate(scan.ranges):
            if range_val < 0.01:
                continue
            angle = angle_min + i * angle_increment
            if start_rad <= end_rad:
                if start_rad <= angle <= end_rad:
                    if range_val < min_dist:
                        min_dist = range_val
            else:
                if angle >= start_rad or angle <= end_rad:
                    if range_val < min_dist:
                        min_dist = range_val
        
        return min_dist if min_dist != float('inf') else 10.0
   
    def move_forward(self):
        twist = Twist()
        twist.linear.x = self.forward_speed
        self.cmd_pub.publish(twist)
   
    def stop(self):
        self.cmd_pub.publish(Twist())

def main(args=None):
    rclpy.init(args=args)
    node = WallFollower()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()
【三个终端:启动命令】
       项目运行时,需要同时开启三个终端(通过SSH连接树莓派后,在电脑上开三个终端窗口即可)。
       终端1:启动雷达驱动
  1. ros2 run rplidar_ros rplidar_composition --ros-args -p serial_port:=/dev/ttyUSB0 -p serial_baudrate:=460800
复制代码
ROS2雷达小车中篇:四层车身搭建与简易避障实现图1

       终端2:启动电机控制器
  1. ros2 run motor_controller motor_controller
复制代码
ROS2雷达小车中篇:四层车身搭建与简易避障实现图2

       终端3:启动自主避障节点
  1. ros2 run motor_controller explorer
复制代码
      如果想让小车跑得快一点或慢一点,可以在启动时调整参数:
  1. ros2 run motor_controller explorer --ros-args -p forward_speed:=0.22 -p rotate_speed:=1.4
复制代码
ROS2雷达小车中篇:四层车身搭建与简易避障实现图3

【调试中的真实问题与解决
问题】
       问题1:SSH连不上
  • 现象:Permission denied (publickey)
  • 原因:KbdInteractiveAuthentication 为 no,PAM认证通道被关闭
  • 解决:改为 yes 后重启SSH服务

       问题2:键盘控制正常,但自主避障时转向无力
  • 现象:转向时电机发出蜂鸣声但不转
  • 原因:自主节点的 rotate_speed 默认只有0.5,计算出的PWM值仅±33,不足以克服地面摩擦力
  • 解决:将 rotate_speed 提高到 1.0 ~ 1.2,并在电机控制器中设置 MIN_PWM = 80

       问题3:小车一直向左转(死循环)
  • 现象:每次遇到障碍都判断左侧更空,无限左转
  • 原因:雷达安装方向与代码角度定义不匹配
  • 解决:检查雷达0度方向是否正对车头,或在代码中交换左右距离计算

【效果展示】

       经过调试,小车目前能够:
  • 在空旷区域直线前进,速度稳定。
  • 遇到前方障碍物(距离 < 0.5m)时停车,扫描左右。
  • 选择较空旷的方向转向(约120°),然后继续前进。
  • 在客厅环境中持续运行,不会卡死在墙角或桌腿旁。

【物料清单】

物料
规格
数量
树莓派4B4GB1
Arduino Uno R41
L298P Motor ShieldArduino兼容1
RPLIDAR C112m TOF雷达1
亚克力板3mm厚,激光切割4层
M3铜柱30mm / 15mm若干
18250锂电池3.7V4节
18650锂电池3.7V4节
直流电机(带编码器)2
万向轮1
USB转接板RPLIDAR C1配套1

【下一步计划】
  • 里程计:在Arduino端读取电机编码器,通过串口反馈给树莓派,实现简单定位。
  • SLAM建图:使用 slam_toolbox 构建环境地图。
  • 自主导航:引入Nav2,实现点到点的自主移动。


       中篇到此结束,下篇我们将进入SLAM建图和自主导航的世界。




您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail