扭转控制

参考

这一页解决什么问题

/cmd_vel 上的 geometry_msgs/Twist 是上层规划给出的意图:想让车以多快前进、 以多大角速度转弯。但车本身并不认识"角速度"这个量——方向盘只能转到某个角度, 油门只能给到某个速度,差速底盘只能控制左右轮各转多快。

于是中间缺一层换算:

规划层            Twists(v, ω)        "我想怎么走"
   │
   ├── 本页实现的转换 ──┐
   │                    │
执行层            转向角 δ、轮速 v_l / v_r   "车实际怎么动"

缺了这一层会发生什么:把 Twist 直接发给一辆阿克曼底盘的车,angular.z 会被忽略, 车只会直着开;即使某些控制器接受 Twist,它在低速时也会因为 ω / v 发散而输出 离谱的转角。本页给出的实现就是把这一层补齐。

自主实现:twist_to_control 包

对应源码:src/ground/twist_to_control。

运动学公式

把车沿后轴看成一个质点,前后轴之间距离为轴距 L,前轮转角为 δ, 则车体角速度与速度的关系是:

ω = v · tan(δ) / L

反过来,已知规划层给出的 (v, ω),先求曲率 k,再求转角:

k = ω / v
δ = atan(k · L)

曲率 k 的倒数就是转弯半径:k = 0.5 表示半径 2 米。这个量比转角更本质—— 它把"速度多快、转多急"两个量合并成了一个几何量,所以本实现内部始终以曲率为中心, 不同的底盘模型只是在曲率之后分岔:

底盘模型 由曲率到执行量的换算
bicycle δ = atan(k · L),输出一个前轮转角
ackermann δ_inner = atan(L·k/(1 - b·k/2))、δ_outer = atan(L·k/(1 + b·k/2))
diff_drive v_left = v - ω·b/2、v_right = v + ω·b/2

diff_drive 没有方向盘,/vehicle/control 里的转角字段仍按等效单车模型给出(方便与另 两种模型对照),真正执行请看 /vehicle/wheel_speed。

阿克曼几何中内侧轮必然比外侧轮转得更多(转弯半径更小),差速分配中转弯外侧轮 必然比内侧轮转得更快。这两个关系都写进了回归测试。

四个容易踩的坑,以及本实现的处理

1. 低速奇点。 k = ω / v 在 v → 0 时发散。速度 0.01 m/s、角速度 0.5 rad/s 就解出曲率 50(半径 2 厘米),任何实车都做不到。

处理:设死区 min_speed(默认 0.05 m/s),|v| 低于它时锁存上一帧曲率, 并在诊断话题里把"锁存"标志置 1。这比直接输出 0 更合理——车在缓慢挪动时, 方向盘应该保持刚才的角度,而不是突然回正。

2. 倒车方向。 v 取负值时 k = ω / v 自动反号,因此转角方向自动正确。 这一点不需要特殊分支,但很容易被"先取绝对值再算"的写法破坏,所以测试里专门 校验了倒车时转角为负。

3. 双级限幅。 一次超限可能是两种完全不同的原因:路径本身太弯(曲率超限), 或者方向盘转不到那么大(转角超限)。本实现分两级截断——先按 max_curvature 截断曲率,再按 max_steering 截断转角,control_info 的"饱和"位合并上报两者。

4. 指令抖动。 相邻两帧 Twist 稍有差异,转角就会跳变,实车表现为方向盘来回摆。 本实现同时限制加速度(max_accel)与转角变化率(max_steering_rate), 让执行量逐帧逼近目标,control_info 的"速率受限"位标记当前是否还在追赶。

参数

所有参数都在 config/twist_to_control.yaml 里给出默认值,并由 launch/main.launch 用 17 个带 doc 的 <arg> 暴露,可以命令行覆盖。

参数 默认值 说明
model bicycle 底盘模型:bicycle / ackermann / diff_drive
wheelbase 2.7 轴距 L(米)
track_width 1.5 轮距 b(米)
max_steering 0.55 前轮最大转角(弧度,约 31.5°)
max_curvature 1.0 最大曲率(1/m),对应最小转弯半径 1 米
max_speed 5.0 最大速度(m/s),正反向都限
max_accel 2.0 纵向加速度上限(m/s²)
max_steering_rate 2.0 转角变化率上限(rad/s)
min_speed 0.05 曲率死区(m/s)
timeout 0.5 /cmd_vel 超时(秒)
publish_hz 20.0 发布频率(Hz)
use_mock_odom false 是否启用内置运动学模拟

话题

话题 类型 方向 内容
/cmd_vel geometry_msgs/Twist 订阅 linear.x、angular.z
/vehicle/control std_msgs/Float64MultiArray 发布 [转角, 速度, 曲率]
/vehicle/wheel_speed std_msgs/Float64MultiArray 发布 [左轮速, 右轮速],仅差速模型
/vehicle/control_info std_msgs/Float64MultiArray 发布 [请求转角, 实发转角, 饱和, 锁存, 速率受限]
/odom nav_msgs/Odometry 发布 内置模拟位姿,仅 use_mock_odom:=true

运行步骤

# 构建
cp -r src/ground/twist_to_control ~/catkin_ws/src/
cd ~/catkin_ws && source /opt/ros/melodic/setup.bash && catkin_make
source devel/setup.bash

# 终端 1
roslaunch twist_to_control main.launch

# 终端 2:发指令(持续发布,低于 timeout 的频率会被判为零速度)
rostopic pub -r 10 /cmd_vel geometry_msgs/Twist '{linear: {x: 1.0}, angular: {z: 0.5}}'

# 终端 3:看输出
rostopic echo /vehicle/control

速度 1.0 m/s、角速度 0.5 rad/s、轴距 2.7 m 时,稳态曲率为 0.5、转角约为 0.933 rad (超过默认的 max_steering = 0.55,因此会被限幅并上报饱和)。把角速度降到 0.2 即可得到约 0.495 rad 的转角,落在限幅之内。

想看车真的按指令跑起来,不需要 CARLA:

roslaunch twist_to_control main.launch use_mock_odom:=true

节点会自己按运动学积分位姿并发布 /odom,rostopic echo /odom 能看到 位置沿圆弧推进。

离线回归测试

cd <仓库根目录>
python3 tests/test_twist_to_control_local.py

测试只用标准库,覆盖参数默认值与读取、曲率换算(含倒车与死区)、转角与双级限幅、 阿克曼内外轮关系、差速轮速守恒、逐帧推进(加速度限制、转角速率限制、锁存、饱和)、 内置模拟(直线、定圆、倒车、dt = 0)、四元数往返,以及工程文件约定 (launch 入口、yaml 键名、核心逻辑不依赖 rospy 等)。

与已有模块的关系

本包与 vehicle_pygame_teleop、waypoint_target_switcher 是同一条链路的相邻环节:

vehicle_pygame_teleop   键盘 ──→ /cmd_vel ┐
waypoint_target_switcher 路径点 ──→ /cmd_vel ┤
                                                 ├──→ twist_to_control ──→ 转角 / 轮速

前两个模块负责产生 Twist,本包负责把 Twist 翻译成执行量,三者可以串起来跑, 也可以单独使用。本包不修改上述任一模块。

常见问题

Q:/vehicle/control 一直是 0? A:/cmd_vel 需要持续发布。节点在 timeout(默认 0.5 s)收不到指令时按零速度处理, 所以 rostopic pub -r 1 会被判为零速。

Q:转角一直是 0,但速度不为 0? A:angular.z 为 0。曲率只由 angular.z / linear.x 决定。

Q:低速时转角不动? A:|v| 低于 min_speed 时进入死区锁存,control_info 第 4 位为 1。

Q:is neither a launch file in package? A:新终端没 source 工作空间,先 /opt/ros/melodic/setup.bash 再 devel/setup.bash。

说明

本模块使用大模型辅助编写代码、测试与文档,提交内容由本人负责。