第六章 实战项目
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, ¤t_vel_left, 4);
memcpy(buffer + 4, ¤t_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: 如何调试机器人硬件问题?
答案:
调试步骤:
- 分层测试
硬件层 → 驱动层 → 功能层 → 系统层
- 电源问题
# 检查电压
- 电池电压(万用表)
- 各模块供电电压
- 电流消耗
# 常见问题
- 压降过大
- 接触不良
- 功率不足
- 通信问题
# 串口测试
ls /dev/tty* # 查看设备
sudo chmod 666 /dev/ttyACM0
python -m serial.tools.miniterm /dev/ttyACM0 115200
# I2C测试
sudo i2cdetect -y 1 # 检测I2C设备
- 传感器问题
// 逐个测试传感器
void test_ultrasonic() {
float dist = getDistance();
Serial.print("Distance: ");
Serial.println(dist);
}
void test_encoder() {
Serial.print("Encoder: ");
Serial.println(encoder_count);
}
- 电机问题
// 单独测试每个电机
setMotor(0, 100); // 左电机正转
delay(2000);
setMotor(0, -100); // 反转
delay(2000);
setMotor(0, 0); // 停止
- 使用示波器
- PWM波形
- 编码器脉冲
- 通信信号
- 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仿真和实物有什么差异?
答案:
主要差异:
| 方面 | 仿真 | 实物 |
|---|---|---|
| 物理模型 | 理想化 | 真实摩擦/惯性 |
| 传感器 | 完美无噪声 | 噪声/漂移 |
| 延迟 | 可忽略 | 通信/处理延迟 |
| 环境 | 受控 | 复杂多变 |
| 电源 | 无限 | 电池限制 |
弥补差异:
- 添加噪声模型
<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>
- 调整物理参数
<gazebo>
<mu1>0.8</mu1> <!-- 摩擦系数 -->
<mu2>0.8</mu2>
<kp>1000000.0</kp> <!-- 接触刚度 -->
<kd>1.0</kd>
</gazebo>
- 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