HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

第六章 实战项目

6.1 智能小车项目

6.1.1 硬件系统

硬件清单:

核心控制: Arduino Mega 2560
电机驱动: L298N × 2
电机: TT直流减速电机 × 4 (12V 200RPM)
传感器:
  - HC-SR04超声波 × 4 (前、后、左、右)
  - 红外循迹模块 × 5 (TCRT5000)
  - MPU6050 IMU × 1
电源: 3S锂电池 11.1V 2200mAh
其他: 蓝牙模块HC-05、蜂鸣器、LED

硬件接线图:

Arduino Mega 2560
==================
[电机驱动1 - L298N]
IN1 → D7    (左前电机方向1)
IN2 → D6    (左前电机方向2)
ENA → D5    (左前电机PWM)
IN3 → D4    (右前电机方向1)
IN4 → D3    (右前电机方向2)
ENB → D2    (右前电机PWM)

[电机驱动2 - L298N]
IN1 → D8    (左后电机)
IN2 → D9    (左后电机)
ENA → D10   (左后电机PWM)
IN3 → D11   (右后电机)
IN4 → D12   (右后电机)
ENB → D13   (右后电机PWM)

[超声波传感器]
前: Trig→D22, Echo→D23
后: Trig→D24, Echo→D25
左: Trig→D26, Echo→D27
右: Trig→D28, Echo→D29

[红外循迹]
IR1→A0, IR2→A1, IR3→A2, IR4→A3, IR5→A4

[IMU - MPU6050]
SDA → D20 (I2C)
SCL → D21 (I2C)

[蓝牙 - HC-05]
RX → TX1 (D18)
TX → RX1 (D19)

[电源分配]
12V电池 → L298N (VCC)
       → DC-DC 5V → Arduino (VIN)
       → DC-DC 5V → 传感器

电路原理图(ASCII):

                    +12V Battery
                         |
                    [开关+保险丝]
                         |
              +----------+----------+
              |          |          |
          [L298N]    [L298N]   [DC-DC 5V/3A]
         电机驱动1   电机驱动2       |
              |          |          +---→ Arduino (5V)
         +----+----+ +---+----+     |
         |    |    | |   |    |     +---→ 传感器 (5V)
       LF_M RF_M  LR_M RR_M
       左前  右前   左后  右后

      [共地连接所有模块]

6.1.2 巡线算法实现

完整Arduino代码:

// ===== 引脚定义 =====
// 电机驱动
#define MOTOR_LF_1 7
#define MOTOR_LF_2 6
#define MOTOR_LF_PWM 5
#define MOTOR_RF_1 4
#define MOTOR_RF_2 3
#define MOTOR_RF_PWM 2

// 红外循迹传感器(数字输出: 0=检测到黑线, 1=白色)
#define IR_LEFT2 30
#define IR_LEFT1 31
#define IR_CENTER 32
#define IR_RIGHT1 33
#define IR_RIGHT2 34

// 超声波(前方避障)
#define TRIG_FRONT 22
#define ECHO_FRONT 23

// ===== PID参数 =====
float Kp = 30.0;
float Ki = 0.0;
float Kd = 20.0;

int base_speed = 150;  // 基础速度(0-255)
int max_speed = 200;
int min_speed = 50;

float last_error = 0;
float integral = 0;

// ===== 传感器权重 =====
int weights[5] = {-2, -1, 0, 1, 2};

void setup() {
  Serial.begin(9600);

  // 电机引脚初始化
  pinMode(MOTOR_LF_1, OUTPUT);
  pinMode(MOTOR_LF_2, OUTPUT);
  pinMode(MOTOR_LF_PWM, OUTPUT);
  pinMode(MOTOR_RF_1, OUTPUT);
  pinMode(MOTOR_RF_2, OUTPUT);
  pinMode(MOTOR_RF_PWM, OUTPUT);

  // 红外传感器
  pinMode(IR_LEFT2, INPUT);
  pinMode(IR_LEFT1, INPUT);
  pinMode(IR_CENTER, INPUT);
  pinMode(IR_RIGHT1, INPUT);
  pinMode(IR_RIGHT2, INPUT);

  // 超声波
  pinMode(TRIG_FRONT, OUTPUT);
  pinMode(ECHO_FRONT, INPUT);

  Serial.println("智能小车初始化完成");
}

// ===== 电机控制 =====
void setMotor(int motor, int speed) {
  // motor: 0=左前, 1=右前
  int pin1, pin2, pwm_pin;

  if (motor == 0) {
    pin1 = MOTOR_LF_1;
    pin2 = MOTOR_LF_2;
    pwm_pin = MOTOR_LF_PWM;
  } else {
    pin1 = MOTOR_RF_1;
    pin2 = MOTOR_RF_2;
    pwm_pin = MOTOR_RF_PWM;
  }

  if (speed > 0) {
    digitalWrite(pin1, HIGH);
    digitalWrite(pin2, LOW);
    analogWrite(pwm_pin, constrain(abs(speed), 0, 255));
  } else if (speed < 0) {
    digitalWrite(pin1, LOW);
    digitalWrite(pin2, HIGH);
    analogWrite(pwm_pin, constrain(abs(speed), 0, 255));
  } else {
    digitalWrite(pin1, LOW);
    digitalWrite(pin2, LOW);
    analogWrite(pwm_pin, 0);
  }
}

void stopMotors() {
  setMotor(0, 0);
  setMotor(1, 0);
}

// ===== 超声波测距 =====
float getDistance() {
  digitalWrite(TRIG_FRONT, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_FRONT, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_FRONT, LOW);

  long duration = pulseIn(ECHO_FRONT, HIGH, 30000);
  if (duration == 0) return 999;  // 超时

  return duration * 0.034 / 2;  // cm
}

// ===== 读取红外传感器 =====
int readLineSensors() {
  int sensors[5];
  sensors[0] = digitalRead(IR_LEFT2);
  sensors[1] = digitalRead(IR_LEFT1);
  sensors[2] = digitalRead(IR_CENTER);
  sensors[3] = digitalRead(IR_RIGHT1);
  sensors[4] = digitalRead(IR_RIGHT2);

  // 计算加权位置
  int sum = 0;
  int count = 0;

  for (int i = 0; i < 5; i++) {
    if (sensors[i] == 0) {  // 检测到黑线
      sum += weights[i];
      count++;
    }
  }

  if (count == 0) {
    // 丢线,保持上次方向
    return (last_error > 0) ? 2 : -2;
  }

  return sum / count;  // 返回位置偏差(-2到2)
}

// ===== PID循迹控制 =====
void lineFollowing() {
  // 读取传感器
  float position = readLineSensors();

  // PID计算
  float error = position;  // 0为理想中心
  integral += error;
  integral = constrain(integral, -100, 100);  // 抗饱和

  float derivative = error - last_error;
  float correction = Kp * error + Ki * integral + Kd * derivative;

  last_error = error;

  // 计算左右轮速
  int left_speed = base_speed - correction;
  int right_speed = base_speed + correction;

  // 限速
  left_speed = constrain(left_speed, min_speed, max_speed);
  right_speed = constrain(right_speed, min_speed, max_speed);

  // 输出到电机
  setMotor(0, left_speed);
  setMotor(1, right_speed);

  // 调试信息
  Serial.print("Position: ");
  Serial.print(position);
  Serial.print(" | Correction: ");
  Serial.print(correction);
  Serial.print(" | L: ");
  Serial.print(left_speed);
  Serial.print(" R: ");
  Serial.println(right_speed);
}

// ===== 避障逻辑 =====
void obstacleAvoidance() {
  float distance = getDistance();

  if (distance < 20) {  // 20cm以内
    Serial.println("检测到障碍物,避障中...");

    // 停止
    stopMotors();
    delay(500);

    // 后退
    setMotor(0, -100);
    setMotor(1, -100);
    delay(800);

    // 右转
    setMotor(0, 150);
    setMotor(1, -150);
    delay(600);

    // 继续前进
    setMotor(0, base_speed);
    setMotor(1, base_speed);
    delay(1000);

    // 左转回到路径
    setMotor(0, -150);
    setMotor(1, 150);
    delay(600);
  }
}

// ===== 主循环 =====
void loop() {
  // 避障优先
  obstacleAvoidance();

  // 循迹控制
  lineFollowing();

  delay(10);  // 100Hz控制频率
}

6.1.3 蓝牙遥控扩展

// 蓝牙命令处理
void bluetoothControl() {
  if (Serial1.available()) {
    char cmd = Serial1.read();

    switch (cmd) {
      case 'F':  // 前进
        setMotor(0, 150);
        setMotor(1, 150);
        break;

      case 'B':  // 后退
        setMotor(0, -150);
        setMotor(1, -150);
        break;

      case 'L':  // 左转
        setMotor(0, 50);
        setMotor(1, 200);
        break;

      case 'R':  // 右转
        setMotor(0, 200);
        setMotor(1, 50);
        break;

      case 'S':  // 停止
        stopMotors();
        break;

      case 'A':  // 自动模式(循迹)
        // 设置循迹标志
        break;
    }
  }
}

6.2 ROS机器人项目

6.2.1 硬件系统架构

硬件配置:

主控: 树莓派4B (4GB)
底层控制: Arduino Mega 2560
激光雷达: RPLIDAR A1
底盘: 差速驱动(两轮+万向轮)
电机: 12V直流减速电机 + 编码器
驱动: TB6612 × 2
传感器: MPU6050 IMU
电源: 3S 11.1V 5000mAh锂电池
      DC-DC降压至5V 3A (树莓派)
      DC-DC降压至5V 2A (Arduino+传感器)

系统框图:

[树莓派4B] ←→ [激光雷达 RPLIDAR A1]
    |               (USB)
    | UART
    ↓
[Arduino Mega]
    |
    +→ [TB6612] → [左电机+编码器]
    |
    +→ [TB6612] → [右电机+编码器]
    |
    +→ [MPU6050] (I2C)

软件架构:
树莓派: ROS Master
       - gmapping (SLAM)
       - move_base (导航)
       - rplidar_node (雷达驱动)
       - base_controller (底盘控制节点)

Arduino: 底层控制
        - 电机PWM控制
        - 编码器读取
        - 速度闭环(PID)
        - 串口通信

6.2.2 Arduino底层代码

// ===== 机器人参数 =====
#define WHEEL_RADIUS 0.05      // 轮半径(m)
#define WHEEL_BASE 0.3         // 轮距(m)
#define ENCODER_PPR 1000       // 编码器每转脉冲数
#define MAX_RPM 200            // 最大转速

// ===== 引脚定义 =====
// 左电机
#define MOTOR_L_PWM 5
#define MOTOR_L_IN1 7
#define MOTOR_L_IN2 6
#define ENCODER_L_A 2
#define ENCODER_L_B 3

// 右电机
#define MOTOR_R_PWM 10
#define MOTOR_R_IN1 8
#define MOTOR_R_IN2 9
#define ENCODER_R_A 18
#define ENCODER_R_B 19

// ===== 全局变量 =====
volatile long encoder_left = 0;
volatile long encoder_right = 0;

float target_vel_left = 0.0;   // 目标速度(m/s)
float target_vel_right = 0.0;
float current_vel_left = 0.0;  // 当前速度
float current_vel_right = 0.0;

// PID参数
float Kp = 15.0, Ki = 5.0, Kd = 0.5;
float integral_left = 0, integral_right = 0;
float last_error_left = 0, last_error_right = 0;

unsigned long last_time = 0;
const unsigned long CONTROL_PERIOD = 20;  // 50Hz

void setup() {
  Serial.begin(115200);

  // 电机引脚
  pinMode(MOTOR_L_PWM, OUTPUT);
  pinMode(MOTOR_L_IN1, OUTPUT);
  pinMode(MOTOR_L_IN2, OUTPUT);
  pinMode(MOTOR_R_PWM, OUTPUT);
  pinMode(MOTOR_R_IN1, OUTPUT);
  pinMode(MOTOR_R_IN2, OUTPUT);

  // 编码器引脚
  pinMode(ENCODER_L_A, INPUT_PULLUP);
  pinMode(ENCODER_L_B, INPUT_PULLUP);
  pinMode(ENCODER_R_A, INPUT_PULLUP);
  pinMode(ENCODER_R_B, INPUT_PULLUP);

  // 中断
  attachInterrupt(digitalPinToInterrupt(ENCODER_L_A), leftEncoderISR, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENCODER_R_A), rightEncoderISR, CHANGE);

  last_time = millis();
}

// ===== 编码器中断 =====
void leftEncoderISR() {
  if (digitalRead(ENCODER_L_A) == digitalRead(ENCODER_L_B)) {
    encoder_left++;
  } else {
    encoder_left--;
  }
}

void rightEncoderISR() {
  if (digitalRead(ENCODER_R_A) == digitalRead(ENCODER_R_B)) {
    encoder_right++;
  } else {
    encoder_right--;
  }
}

// ===== 电机控制 =====
void setMotorSpeed(int motor, int pwm) {
  // motor: 0=left, 1=right
  // pwm: -255 to 255

  int pwm_pin, in1_pin, in2_pin;

  if (motor == 0) {
    pwm_pin = MOTOR_L_PWM;
    in1_pin = MOTOR_L_IN1;
    in2_pin = MOTOR_L_IN2;
  } else {
    pwm_pin = MOTOR_R_PWM;
    in1_pin = MOTOR_R_IN1;
    in2_pin = MOTOR_R_IN2;
  }

  if (pwm > 0) {
    digitalWrite(in1_pin, HIGH);
    digitalWrite(in2_pin, LOW);
    analogWrite(pwm_pin, constrain(pwm, 0, 255));
  } else if (pwm < 0) {
    digitalWrite(in1_pin, LOW);
    digitalWrite(in2_pin, HIGH);
    analogWrite(pwm_pin, constrain(-pwm, 0, 255));
  } else {
    digitalWrite(in1_pin, LOW);
    digitalWrite(in2_pin, LOW);
    analogWrite(pwm_pin, 0);
  }
}

// ===== PID速度控制 =====
int pidControl(float target, float current, float &integral, float &last_error) {
  float error = target - current;

  integral += error;
  integral = constrain(integral, -50, 50);

  float derivative = error - last_error;
  float output = Kp * error + Ki * integral + Kd * derivative;

  last_error = error;

  return constrain((int)output, -255, 255);
}

// ===== 速度计算 =====
void updateVelocity(float dt) {
  // 计算转速(rad/s)
  float omega_left = (encoder_left / (float)ENCODER_PPR) * (2 * PI) / dt;
  float omega_right = (encoder_right / (float)ENCODER_PPR) * (2 * PI) / dt;

  // 转换为线速度(m/s)
  current_vel_left = omega_left * WHEEL_RADIUS;
  current_vel_right = omega_right * WHEEL_RADIUS;

  // 重置编码器
  encoder_left = 0;
  encoder_right = 0;
}

// ===== 串口通信 =====
void parseCommand() {
  if (Serial.available() >= 8) {
    // 接收格式: [float linear, float angular]
    byte buffer[8];
    Serial.readBytes(buffer, 8);

    float linear, angular;
    memcpy(&linear, buffer, 4);
    memcpy(&angular, buffer + 4, 4);

    // 运动学逆解
    target_vel_left = linear - angular * WHEEL_BASE / 2;
    target_vel_right = linear + angular * WHEEL_BASE / 2;
  }
}

void sendOdometry() {
  // 发送格式: [float vel_left, float vel_right]
  byte buffer[8];
  memcpy(buffer, &current_vel_left, 4);
  memcpy(buffer + 4, &current_vel_right, 4);
  Serial.write(buffer, 8);
}

// ===== 主循环 =====
void loop() {
  unsigned long current_time = millis();

  if (current_time - last_time >= CONTROL_PERIOD) {
    float dt = (current_time - last_time) / 1000.0;

    // 更新速度
    updateVelocity(dt);

    // PID控制
    int pwm_left = pidControl(target_vel_left, current_vel_left,
                             integral_left, last_error_left);
    int pwm_right = pidControl(target_vel_right, current_vel_right,
                              integral_right, last_error_right);

    // 输出到电机
    setMotorSpeed(0, pwm_left);
    setMotorSpeed(1, pwm_right);

    // 发送里程计
    sendOdometry();

    last_time = current_time;
  }

  // 接收命令
  parseCommand();
}

6.2.3 ROS底盘控制节点

#!/usr/bin/env python
import rospy
import serial
import struct
import tf
from geometry_msgs.msg import Twist, Quaternion
from nav_msgs.msg import Odometry
import numpy as np

class BaseController:
    def __init__(self):
        rospy.init_node('base_controller')

        # 串口
        port = rospy.get_param('~port', '/dev/ttyACM0')
        baudrate = rospy.get_param('~baudrate', 115200)
        self.ser = serial.Serial(port, baudrate, timeout=0.1)

        # 机器人参数
        self.wheel_base = 0.3
        self.wheel_radius = 0.05

        # 里程计
        self.x = 0.0
        self.y = 0.0
        self.theta = 0.0
        self.last_time = rospy.Time.now()

        # 订阅/发布
        self.cmd_sub = rospy.Subscriber('cmd_vel', Twist, self.cmd_callback)
        self.odom_pub = rospy.Publisher('odom', Odometry, queue_size=10)
        self.tf_broadcaster = tf.TransformBroadcaster()

        rospy.loginfo("底盘控制节点启动")

    def cmd_callback(self, msg):
        """接收速度命令"""
        linear = msg.linear.x
        angular = msg.angular.z

        # 发送到Arduino
        data = struct.pack('ff', linear, angular)
        self.ser.write(data)

    def update_odometry(self):
        """更新里程计"""
        if self.ser.in_waiting >= 8:
            data = self.ser.read(8)
            vel_left, vel_right = struct.unpack('ff', data)

            # 正运动学
            v = (vel_left + vel_right) / 2.0
            omega = (vel_right - vel_left) / self.wheel_base

            # 时间间隔
            current_time = rospy.Time.now()
            dt = (current_time - self.last_time).to_sec()
            self.last_time = current_time

            # 位姿更新
            delta_x = v * np.cos(self.theta) * dt
            delta_y = v * np.sin(self.theta) * dt
            delta_theta = omega * dt

            self.x += delta_x
            self.y += delta_y
            self.theta += delta_theta

            # 发布里程计
            self.publish_odometry(v, omega, current_time)

    def publish_odometry(self, v, omega, current_time):
        """发布里程计消息"""
        # TF变换
        quat = tf.transformations.quaternion_from_euler(0, 0, self.theta)
        self.tf_broadcaster.sendTransform(
            (self.x, self.y, 0),
            quat,
            current_time,
            "base_link",
            "odom"
        )

        # Odometry消息
        odom = Odometry()
        odom.header.stamp = current_time
        odom.header.frame_id = "odom"
        odom.child_frame_id = "base_link"

        # 位置
        odom.pose.pose.position.x = self.x
        odom.pose.pose.position.y = self.y
        odom.pose.pose.orientation = Quaternion(*quat)

        # 速度
        odom.twist.twist.linear.x = v
        odom.twist.twist.angular.z = omega

        self.odom_pub.publish(odom)

    def run(self):
        rate = rospy.Rate(50)  # 50Hz

        while not rospy.is_shutdown():
            self.update_odometry()
            rate.sleep()

if __name__ == '__main__':
    try:
        controller = BaseController()
        controller.run()
    except rospy.ROSInterruptException:
        pass

6.2.4 完整启动文件

<launch>
  <!-- 底盘控制 -->
  <node name="base_controller" pkg="my_robot" type="base_controller.py" output="screen">
    <param name="port" value="/dev/ttyACM0"/>
    <param name="baudrate" value="115200"/>
  </node>

  <!-- 激光雷达 -->
  <node name="rplidar_node" pkg="rplidar_ros" type="rplidarNode" output="screen">
    <param name="serial_port" value="/dev/ttyUSB0"/>
    <param name="frame_id" value="laser"/>
  </node>

  <!-- SLAM建图 -->
  <include file="$(find my_robot)/launch/gmapping.launch"/>

  <!-- RVIZ可视化 -->
  <node name="rviz" pkg="rviz" type="rviz" args="-d $(find my_robot)/rviz/robot.rviz"/>
</launch>

6.3 机械臂抓取项目

6.3.1 硬件配置

6自由度机械臂:

舵机: MG996R × 6
控制板: PCA9685 16路PWM驱动
末端: 夹爪(舵机驱动)
视觉: RealSense D435深度相机
主控: 树莓派4B + ROS
电源: 5V 10A开关电源

关节配置:

Joint 1: 底座旋转 (0-180°)
Joint 2: 肩部俯仰 (0-180°)
Joint 3: 肘部俯仰 (0-180°)
Joint 4: 腕部俯仰 (0-180°)
Joint 5: 腕部旋转 (0-180°)
Joint 6: 夹爪开合 (0-90°)

6.3.2 MoveIt配置

URDF模型片段:

<?xml version="1.0"?>
<robot name="robot_arm">
  <!-- 底座 -->
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="0.05" length="0.05"/>
      </geometry>
    </visual>
  </link>

  <!-- 关节1 -->
  <joint name="joint1" type="revolute">
    <parent link="base_link"/>
    <child link="link1"/>
    <origin xyz="0 0 0.05" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
    <limit lower="-3.14" upper="3.14" effort="10" velocity="1.0"/>
  </joint>

  <link name="link1">
    <visual>
      <geometry>
        <box size="0.03 0.03 0.1"/>
      </geometry>
    </visual>
  </link>

  <!-- ... 其他关节和连杆 ... -->
</robot>

6.3.3 视觉定位(ArUco标记)

#!/usr/bin/env python
import rospy
import cv2
import numpy as np
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
from geometry_msgs.msg import PoseStamped

class ArUcoDetector:
    def __init__(self):
        rospy.init_node('aruco_detector')

        self.bridge = CvBridge()

        # 相机参数(标定得到)
        self.camera_matrix = np.array([
            [615.0, 0, 320.0],
            [0, 615.0, 240.0],
            [0, 0, 1.0]
        ])
        self.dist_coeffs = np.zeros(5)

        # ArUco字典
        self.aruco_dict = cv2.aruco.Dictionary_get(cv2.aruco.DICT_6X6_250)
        self.parameters = cv2.aruco.DetectorParameters_create()

        # 订阅/发布
        self.image_sub = rospy.Subscriber('/camera/color/image_raw',
                                         Image, self.image_callback)
        self.pose_pub = rospy.Publisher('/target_pose',
                                       PoseStamped, queue_size=1)

    def image_callback(self, msg):
        # ROS图像转OpenCV
        cv_image = self.bridge.imgmsg_to_cv2(msg, 'bgr8')

        # 检测ArUco
        gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
        corners, ids, rejected = cv2.aruco.detectMarkers(
            gray, self.aruco_dict, parameters=self.parameters)

        if ids is not None:
            # 估计位姿
            rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
                corners, 0.05,  # 标记尺寸5cm
                self.camera_matrix,
                self.dist_coeffs
            )

            for i, marker_id in enumerate(ids):
                # 绘制
                cv2.aruco.drawAxis(cv_image, self.camera_matrix,
                                  self.dist_coeffs, rvecs[i], tvecs[i], 0.03)

                # 发布位姿
                pose = PoseStamped()
                pose.header.stamp = rospy.Time.now()
                pose.header.frame_id = "camera_link"

                pose.pose.position.x = tvecs[i][0][0]
                pose.pose.position.y = tvecs[i][0][1]
                pose.pose.position.z = tvecs[i][0][2]

                # 旋转向量转四元数
                rot_matrix, _ = cv2.Rodrigues(rvecs[i])
                quat = self.rotation_matrix_to_quaternion(rot_matrix)

                pose.pose.orientation.x = quat[0]
                pose.pose.orientation.y = quat[1]
                pose.pose.orientation.z = quat[2]
                pose.pose.orientation.w = quat[3]

                self.pose_pub.publish(pose)

                rospy.loginfo(f"检测到标记ID={marker_id[0]}, "
                            f"位置=({tvecs[i][0][0]:.2f}, "
                            f"{tvecs[i][0][1]:.2f}, "
                            f"{tvecs[i][0][2]:.2f})")

        # 显示
        cv2.imshow('ArUco Detection', cv_image)
        cv2.waitKey(1)

    def rotation_matrix_to_quaternion(self, R):
        """旋转矩阵转四元数"""
        trace = np.trace(R)

        if trace > 0:
            s = 0.5 / np.sqrt(trace + 1.0)
            w = 0.25 / s
            x = (R[2, 1] - R[1, 2]) * s
            y = (R[0, 2] - R[2, 0]) * s
            z = (R[1, 0] - R[0, 1]) * s
        else:
            if R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
                s = 2.0 * np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2])
                w = (R[2, 1] - R[1, 2]) / s
                x = 0.25 * s
                y = (R[0, 1] + R[1, 0]) / s
                z = (R[0, 2] + R[2, 0]) / s
            elif R[1, 1] > R[2, 2]:
                s = 2.0 * np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2])
                w = (R[0, 2] - R[2, 0]) / s
                x = (R[0, 1] + R[1, 0]) / s
                y = 0.25 * s
                z = (R[1, 2] + R[2, 1]) / s
            else:
                s = 2.0 * np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1])
                w = (R[1, 0] - R[0, 1]) / s
                x = (R[0, 2] + R[2, 0]) / s
                y = (R[1, 2] + R[2, 1]) / s
                z = 0.25 * s

        return [x, y, z, w]

    def run(self):
        rospy.spin()

if __name__ == '__main__':
    detector = ArUcoDetector()
    detector.run()

6.3.4 抓取流程

#!/usr/bin/env python
import rospy
import moveit_commander
from geometry_msgs.msg import PoseStamped

class PickAndPlace:
    def __init__(self):
        moveit_commander.roscpp_initialize([])
        rospy.init_node('pick_and_place')

        # MoveIt接口
        self.robot = moveit_commander.RobotCommander()
        self.scene = moveit_commander.PlanningSceneInterface()
        self.arm_group = moveit_commander.MoveGroupCommander("arm")
        self.gripper_group = moveit_commander.MoveGroupCommander("gripper")

        # 设置参数
        self.arm_group.set_max_velocity_scaling_factor(0.5)
        self.arm_group.set_max_acceleration_scaling_factor(0.5)

        # 订阅目标位姿
        self.target_sub = rospy.Subscriber('/target_pose',
                                          PoseStamped, self.target_callback)

        self.target_pose = None

    def target_callback(self, msg):
        self.target_pose = msg

    def open_gripper(self):
        """打开夹爪"""
        self.gripper_group.set_named_target("open")
        self.gripper_group.go(wait=True)
        rospy.sleep(0.5)

    def close_gripper(self):
        """关闭夹爪"""
        self.gripper_group.set_named_target("close")
        self.gripper_group.go(wait=True)
        rospy.sleep(0.5)

    def go_to_home(self):
        """回到初始位置"""
        self.arm_group.set_named_target("home")
        self.arm_group.go(wait=True)

    def pick(self, target_pose):
        """抓取流程"""
        rospy.loginfo("开始抓取...")

        # 1. 打开夹爪
        self.open_gripper()

        # 2. 移动到目标上方
        pre_grasp_pose = PoseStamped()
        pre_grasp_pose.header = target_pose.header
        pre_grasp_pose.pose = target_pose.pose
        pre_grasp_pose.pose.position.z += 0.1  # 上方10cm

        self.arm_group.set_pose_target(pre_grasp_pose)
        plan = self.arm_group.go(wait=True)

        if not plan:
            rospy.logwarn("无法规划到目标上方")
            return False

        rospy.sleep(1)

        # 3. 下降到目标
        self.arm_group.set_pose_target(target_pose)
        plan = self.arm_group.go(wait=True)

        if not plan:
            rospy.logwarn("无法下降到目标")
            return False

        rospy.sleep(1)

        # 4. 关闭夹爪(抓取)
        self.close_gripper()

        # 5. 提升
        self.arm_group.set_pose_target(pre_grasp_pose)
        self.arm_group.go(wait=True)

        rospy.loginfo("抓取完成")
        return True

    def place(self, place_pose):
        """放置流程"""
        rospy.loginfo("开始放置...")

        # 1. 移动到放置位置上方
        pre_place_pose = PoseStamped()
        pre_place_pose.header = place_pose.header
        pre_place_pose.pose = place_pose.pose
        pre_place_pose.pose.position.z += 0.1

        self.arm_group.set_pose_target(pre_place_pose)
        self.arm_group.go(wait=True)

        # 2. 下降
        self.arm_group.set_pose_target(place_pose)
        self.arm_group.go(wait=True)
        rospy.sleep(1)

        # 3. 打开夹爪(释放)
        self.open_gripper()

        # 4. 提升
        self.arm_group.set_pose_target(pre_place_pose)
        self.arm_group.go(wait=True)

        rospy.loginfo("放置完成")
        return True

    def run(self):
        rate = rospy.Rate(1)

        while not rospy.is_shutdown():
            if self.target_pose:
                # 执行抓取
                success = self.pick(self.target_pose)

                if success:
                    # 定义放置位置
                    place_pose = PoseStamped()
                    place_pose.header.frame_id = "base_link"
                    place_pose.pose.position.x = 0.3
                    place_pose.pose.position.y = 0.2
                    place_pose.pose.position.z = 0.1
                    place_pose.pose.orientation.w = 1.0

                    # 执行放置
                    self.place(place_pose)

                    # 回到初始位置
                    self.go_to_home()

                self.target_pose = None

            rate.sleep()

if __name__ == '__main__':
    try:
        pnp = PickAndPlace()
        pnp.run()
    except rospy.ROSInterruptException:
        pass

6.4 Gazebo仿真

6.4.1 机器人模型(URDF)

<?xml version="1.0"?>
<robot name="mobile_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <!-- 参数定义 -->
  <xacro:property name="chassis_length" value="0.4"/>
  <xacro:property name="chassis_width" value="0.3"/>
  <xacro:property name="wheel_radius" value="0.05"/>

  <!-- 底盘 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="${chassis_length} ${chassis_width} 0.1"/>
      </geometry>
      <material name="blue">
        <color rgba="0 0 1 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="${chassis_length} ${chassis_width} 0.1"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="10.0"/>
      <inertia ixx="0.1" ixy="0" ixz="0" iyy="0.1" iyz="0" izz="0.1"/>
    </inertial>
  </link>

  <!-- 左轮宏 -->
  <xacro:macro name="wheel" params="prefix reflect">
    <link name="${prefix}_wheel">
      <visual>
        <geometry>
          <cylinder radius="${wheel_radius}" length="0.04"/>
        </geometry>
        <material name="black">
          <color rgba="0 0 0 1"/>
        </material>
      </visual>
      <collision>
        <geometry>
          <cylinder radius="${wheel_radius}" length="0.04"/>
        </geometry>
      </collision>
      <inertial>
        <mass value="0.5"/>
        <inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
      </inertial>
    </link>

    <joint name="${prefix}_wheel_joint" type="continuous">
      <parent link="base_link"/>
      <child link="${prefix}_wheel"/>
      <origin xyz="0 ${reflect*0.18} -0.05" rpy="0 0 0"/>
      <axis xyz="0 1 0"/>
    </joint>

    <!-- Gazebo插件 -->
    <gazebo reference="${prefix}_wheel">
      <material>Gazebo/Black</material>
    </gazebo>
  </xacro:macro>

  <!-- 实例化轮子 -->
  <xacro:wheel prefix="left" reflect="1"/>
  <xacro:wheel prefix="right" reflect="-1"/>

  <!-- 激光雷达 -->
  <link name="laser">
    <visual>
      <geometry>
        <cylinder radius="0.03" length="0.04"/>
      </geometry>
      <material name="red">
        <color rgba="1 0 0 1"/>
      </material>
    </visual>
  </link>

  <joint name="laser_joint" type="fixed">
    <parent link="base_link"/>
    <child link="laser"/>
    <origin xyz="0.15 0 0.1" rpy="0 0 0"/>
  </joint>

  <!-- Gazebo差速驱动插件 -->
  <gazebo>
    <plugin name="differential_drive_controller" filename="libgazebo_ros_diff_drive.so">
      <rosDebugLevel>Debug</rosDebugLevel>
      <publishWheelTF>true</publishWheelTF>
      <robotNamespace>/</robotNamespace>
      <publishTf>1</publishTf>
      <publishWheelJointState>true</publishWheelJointState>
      <alwaysOn>true</alwaysOn>
      <updateRate>100.0</updateRate>
      <leftJoint>left_wheel_joint</leftJoint>
      <rightJoint>right_wheel_joint</rightJoint>
      <wheelSeparation>0.36</wheelSeparation>
      <wheelDiameter>0.1</wheelDiameter>
      <broadcastTF>1</broadcastTF>
      <wheelTorque>30</wheelTorque>
      <wheelAcceleration>1.8</wheelAcceleration>
      <commandTopic>cmd_vel</commandTopic>
      <odometryFrame>odom</odometryFrame>
      <odometryTopic>odom</odometryTopic>
      <robotBaseFrame>base_link</robotBaseFrame>
    </plugin>
  </gazebo>

  <!-- Gazebo激光雷达插件 -->
  <gazebo reference="laser">
    <sensor type="ray" name="rplidar">
      <pose>0 0 0 0 0 0</pose>
      <visualize>false</visualize>
      <update_rate>10</update_rate>
      <ray>
        <scan>
          <horizontal>
            <samples>360</samples>
            <resolution>1</resolution>
            <min_angle>-3.14159</min_angle>
            <max_angle>3.14159</max_angle>
          </horizontal>
        </scan>
        <range>
          <min>0.15</min>
          <max>12.0</max>
          <resolution>0.01</resolution>
        </range>
      </ray>
      <plugin name="gazebo_ros_rplidar_controller" filename="libgazebo_ros_laser.so">
        <topicName>/scan</topicName>
        <frameName>laser</frameName>
      </plugin>
    </sensor>
  </gazebo>
</robot>

6.4.2 Gazebo world文件

<?xml version="1.0"?>
<sdf version="1.6">
  <world name="default">
    <!-- 光照 -->
    <include>
      <uri>model://sun</uri>
    </include>

    <!-- 地面 -->
    <include>
      <uri>model://ground_plane</uri>
    </include>

    <!-- 障碍物 -->
    <model name="box1">
      <pose>2 0 0.5 0 0 0</pose>
      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>1 1 1</size>
            </box>
          </geometry>
        </collision>
        <visual name="visual">
          <geometry>
            <box>
              <size>1 1 1</size>
            </box>
          </geometry>
        </visual>
      </link>
    </model>

    <!-- 墙壁 -->
    <model name="wall">
      <static>true</static>
      <pose>5 0 1 0 0 0</pose>
      <link name="link">
        <collision name="collision">
          <geometry>
            <box>
              <size>0.1 10 2</size>
            </box>
          </geometry>
        </collision>
        <visual name="visual">
          <geometry>
            <box>
              <size>0.1 10 2</size>
            </box>
          </geometry>
        </visual>
      </link>
    </model>
  </world>
</sdf>

6.4.3 启动仿真

<launch>
  <!-- Gazebo空世界 -->
  <include file="$(find gazebo_ros)/launch/empty_world.launch">
    <arg name="world_name" value="$(find my_robot)/worlds/my_world.world"/>
    <arg name="paused" value="false"/>
    <arg name="use_sim_time" value="true"/>
    <arg name="gui" value="true"/>
  </include>

  <!-- 加载机器人描述 -->
  <param name="robot_description" command="$(find xacro)/xacro '$(find my_robot)/urdf/robot.urdf.xacro'"/>

  <!-- 生成机器人 -->
  <node name="spawn_urdf" pkg="gazebo_ros" type="spawn_model"
        args="-urdf -model mobile_robot -param robot_description"/>

  <!-- RVIZ -->
  <node name="rviz" pkg="rviz" type="rviz" args="-d $(find my_robot)/rviz/simulation.rviz"/>
</launch>

6.5 高频面试题

问题1: Arduino和树莓派如何选择和配合?

答案:

见第2章问题4的详细解答。核心要点:

  • Arduino: 实时底层控制
  • 树莓派: ROS高层算法
  • 串口通信桥接

问题2: 如何调试机器人硬件问题?

答案:

调试步骤:

  1. 分层测试
硬件层 → 驱动层 → 功能层 → 系统层
  1. 电源问题
# 检查电压
- 电池电压(万用表)
- 各模块供电电压
- 电流消耗

# 常见问题
- 压降过大
- 接触不良
- 功率不足
  1. 通信问题
# 串口测试
ls /dev/tty*           # 查看设备
sudo chmod 666 /dev/ttyACM0
python -m serial.tools.miniterm /dev/ttyACM0 115200

# I2C测试
sudo i2cdetect -y 1    # 检测I2C设备
  1. 传感器问题
// 逐个测试传感器
void test_ultrasonic() {
    float dist = getDistance();
    Serial.print("Distance: ");
    Serial.println(dist);
}

void test_encoder() {
    Serial.print("Encoder: ");
    Serial.println(encoder_count);
}
  1. 电机问题
// 单独测试每个电机
setMotor(0, 100);  // 左电机正转
delay(2000);
setMotor(0, -100); // 反转
delay(2000);
setMotor(0, 0);    // 停止
  1. 使用示波器
  • PWM波形
  • 编码器脉冲
  • 通信信号
  1. ROS调试
# 查看话题
rostopic list
rostopic echo /odom

# 查看TF树
rosrun tf view_frames
evince frames.pdf

# 查看日志
rosrun rqt_console rqt_console

问题3: 如何提高机器人的稳定性?

答案:

1. 机械结构

  • 降低重心
  • 增加轮距
  • 使用减震结构
  • 紧固螺丝

2. 电源管理

// 电压监测
if (battery_voltage < 10.0) {
    reduce_max_speed();  // 降低最大速度
}

// 软启动
for (int speed = 0; speed <= target; speed += 10) {
    setMotor(motor, speed);
    delay(50);
}

3. 控制算法

# 限制加速度
def limit_acceleration(current, target, max_accel, dt):
    max_change = max_accel * dt
    change = target - current

    if abs(change) > max_change:
        return current + np.sign(change) * max_change
    return target

# 低通滤波
alpha = 0.2
filtered = alpha * new_value + (1 - alpha) * old_value

4. 异常处理

class SafetyMonitor:
    def check_safety(self):
        # 检查倾斜
        if abs(imu.roll) > 30:
            emergency_stop()

        # 检查卡死
        if motor_current > threshold:
            backward_escape()

        # 检查通信超时
        if time.time() - last_cmd > 1.0:
            stop_motors()

5. 传感器融合

  • 多个传感器互相验证
  • 卡尔曼滤波去噪
  • 异常值检测

问题4: Gazebo仿真和实物有什么差异?

答案:

主要差异:

方面仿真实物
物理模型理想化真实摩擦/惯性
传感器完美无噪声噪声/漂移
延迟可忽略通信/处理延迟
环境受控复杂多变
电源无限电池限制

弥补差异:

  1. 添加噪声模型
<gazebo reference="laser">
  <sensor type="ray" name="laser">
    <ray>
      <noise>
        <type>gaussian</type>
        <mean>0.0</mean>
        <stddev>0.01</stddev>
      </noise>
    </ray>
  </sensor>
</gazebo>
  1. 调整物理参数
<gazebo>
  <mu1>0.8</mu1>  <!-- 摩擦系数 -->
  <mu2>0.8</mu2>
  <kp>1000000.0</kp>  <!-- 接触刚度 -->
  <kd>1.0</kd>
</gazebo>
  1. Sim2Real转移
  • 参数保守设计
  • 实物测试验证
  • 增加鲁棒性

问题5: 如何进行机器人项目的性能优化?

答案:

1. 计算性能

# 降低分辨率
costmap.resolution = 0.1  # 从0.05增大

# 降低更新频率
update_frequency = 2.0  # 从5.0降低

# 使用更快的算法
# Dijkstra → A*
# EKF → 互补滤波

2. 通信优化

# 降低发布频率
rate = rospy.Rate(10)  # 从50降低

# 压缩图像
image_transport compressed

# 减少消息大小
# LaserScan → 稀疏点云

3. 电源优化

// 降低电机功率
max_pwm = 200;  // 从255降低

// 睡眠模式
if (idle_time > 60) {
    enter_low_power_mode();
}

// 动态调速
if (distance_to_goal < 1.0) {
    max_speed = 0.3;  // 接近目标减速
}

4. 算法优化

# 使用查找表
sin_table = [np.sin(i * 0.01) for i in range(628)]

# 避免重复计算
dist_sq = dx*dx + dy*dy  # 不开方

# 提前退出
for item in list:
    if condition:
        break  # 不遍历全部

5. 多线程/多进程

# 并行处理
import multiprocessing

# 传感器处理
sensor_process = multiprocessing.Process(target=sensor_handler)

# 控制循环
control_process = multiprocessing.Process(target=control_loop)

6. 性能监控

# CPU使用
htop

# ROS节点性能
rosrun rqt_top rqt_top

# 话题带宽
rostopic bw /scan

# TF延迟
rosrun tf tf_monitor