✨ 复制成功!

以文会友,打造好学人设!

📋 已复制到剪贴板:

古典也疯狂:PID控制让小车跑直线

创建时间:2026-08-10 更新时间:2026-08-10 阅读次数:1045 次

前面几章,我们用键盘操控机器人,扮演了“大脑”的角色。每一次按键,都是你亲自做出的决策。但真正的具身智能不能永远靠人类遥控。机器人需要自己做出决策,自己修正错误,自己保持稳定。

这一节,我们要认识自动控制领域最经典、最普适的算法——PID控制。它诞生于一个世纪以前,至今仍然控制着全世界超过90%的工业控制系统。无人机悬停靠它,恒温水壶靠它,巡航导弹靠它,你的扫地机器人直线行走也靠它。

我们将在一个仿真环境里,让一辆小车用PID算法沿着直线精准行驶。只需要不到30行核心代码,你就能直观地感受到比例、积分、微分三项组合起来的修正魔法。

问题定义:什么是“跑直线”?

想象一辆小车,四个轮子,两个驱动轮分别由左右电机驱动。你给两个电机发了同样的电压指令,理论上它应该笔直向前走。但现实永远不会如此完美——地面微微的不平整,左右轮胎磨损的细微差异,电机个体之间的性能误差,都会让小车慢慢跑偏。

跑直线的本质,就是一个连续纠偏的过程。小车需要时刻检测自己偏离了中心线多远,然后调整左右轮的转速差,把自己拉回来。检测偏多远是感知,调整转速差是行动,而中间“偏离多少该纠正多少”的决策逻辑,就是PID控制。

用数学语言来描述:设 $e(t)$ 为时刻 $t$ 的偏差,即小车当前位置与目标直线之间的横向距离。目标是将 $e(t)$ 维持在尽可能接近 $0$ 的状态。小车控制的输出是 $u(t)$,代表左右轮的转速差。$u(t) > 0$ 表示左转纠正,$u(t) < 0$ 表示右转纠正。

PID控制器的任务,就是根据 $e(t)$ 的历史、现在、未来趋势,计算出最合适的 $u(t)$。

拆解PID:三个字母,三种智慧

PID是三个英文单词的缩写:Proportional(比例)、Integral(积分)、Derivative(微分)。它们分别对应着三种修正策略,分别指向过去、现在和未来。

P——比例:现在有多偏?

比例的思路最直接:你偏离了多少,就往回纠正多少。偏离越大,纠正力度越大。公式为:

$$ u_P(t) = K_p \cdot e(t) $$

$K_p$ 是比例增益系数,是我们要调的参数。$K_p$ 越大,纠正越猛。

如果只用比例控制,小车会在中心线附近来回震荡。因为当它从偏离中被拉回中心线的瞬间,虽然偏差为零,但速度还在——它会冲过头,然后比例项又把它往回拉,如此往复。比例控制单独使用很难稳定收敛。

D——微分:速度有多快?

微分的思路是“预判未来”。它不看偏差本身,而看偏差的变化速率。如果偏差正在快速缩小,说明小车已经在回正的路上,这时候该收一点力,避免冲过头。公式为:

$$ u_D(t) = K_d \cdot \frac{de(t)}{dt} $$

$K_d$ 是微分增益系数,$\frac{de(t)}{dt}$ 是偏差随时间的变化率。在离散时间系统中,我们用相邻时刻的差值来近似微分:

$$ \frac{de(t)}{dt} \approx \frac{e(t) - e(t-1)}{\Delta t} $$

微分项起到了阻尼的作用——它抑制过快的运动,让系统更平稳。

I——积分:历史累计偏了多少?

积分的思路是“算旧账”。有些偏差不是瞬时的,而是持续存在的——比如小车因为路面总是微微向右倾斜,一直有个向右的偏移趋势。比例和微分对这种微小但持续的偏差无能为力。积分项把过去所有的偏差累加起来,只要偏差一直存在,积分值就不断增长,最终大到足以产生纠正动作。公式为:

$$ u_I(t) = K_i \cdot \int_0^t e(\tau) d\tau $$

$K_i$ 是积分增益系数。在离散系统中,积分用累加近似:

$$ \int_0^t e(\tau) d\tau \approx \sum_{k=0}^{n} e_k \cdot \Delta t $$

三项合一,PID控制器的完整输出为:

$$ u(t) = K_p \cdot e(t) + K_i \cdot \sum_{k=0}^{t} e_k \Delta t + K_d \cdot \frac{e(t) - e(t-1)}{\Delta t} $$

用一句话总结:P看现在,I算旧账,D预判未来。三者配合,就能让小车平稳、快速、准确地跑在直线上。

环境搭建:你的第一辆虚拟小车

在动手之前,你需要安装一个轻量级的仿真环境。我们选用 gymnasium 和一个经典的强化学习基准环境,它内置了一个简单的小车跑道场景。打开终端:

pip install gymnasium numpy pygame

我们不用现成的强化学习算法,而是自己写PID控制器来控制小车。

为了让你专注于PID逻辑本身,我准备了一个简化版的仿真小车类。它模拟了小车的基本行为:给定左右轮转速,更新位置和朝向,并加入了一些随机噪声模拟现实世界的不完美。把以下代码保存为 simple_car.py

import numpy as np
import pygame
import math

class SimpleCar:
    """简化版小车仿真:给定左右轮转速差,在2D平面上运动"""
    def __init__(self, x=400, y=300, heading=0):
        self.x = x           # 横向位置(像素坐标)
        self.y = y           # 纵向位置
        self.heading = heading  # 朝向角(弧度)
        self.wheelbase = 30  # 轴距

    def step(self, left_speed, right_speed, noise_std=0.02):
        """根据左右轮转速更新小车状态"""
        # 加入随机噪声模拟真实世界的不确定性
        left_speed += np.random.normal(0, noise_std)
        right_speed += np.random.normal(0, noise_std)

        # 小车的线速度和角速度
        linear_velocity = (left_speed + right_speed) / 2.0
        angular_velocity = (right_speed - left_speed) / self.wheelbase

        # 更新位置和朝向
        self.heading += angular_velocity
        self.x += linear_velocity * math.cos(self.heading)
        self.y += linear_velocity * math.sin(self.heading)

    def get_state(self):
        return self.x, self.y, self.heading

这个简化模型已经包含了跑偏的核心原因:噪声。每一次 step 调用,左右轮的实际转速都会叠加一个微小的随机扰动,模拟真实世界中无法避免的不对称性。

核心实战:30行代码实现PID

现在我们写主程序。任务目标:小车从屏幕左侧出发,沿着一条水平的目标直线(比如 $y = 300$ 这条线)行驶。PID控制器根据小车当前位置与目标直线的偏差,计算出左右轮转速差,驱动小车修正方向。

import pygame
import numpy as np
import math
from simple_car import SimpleCar

# 初始化Pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("PID控制小车跑直线")
clock = pygame.time.Clock()

# 目标直线(屏幕坐标的y值)
target_y = 300

# 创建小车,初始在左侧,有一定偏航
car = SimpleCar(x=100, y=320, heading=0.0)

# PID参数
Kp = 0.8   # 比例增益
Ki = 0.005 # 积分增益
Kd = 2.0   # 微分增益

# PID状态变量
integral = 0.0
prev_error = 0.0
base_speed = 3.0  # 基础前进速度

# 轨迹记录
trajectory = []

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # 1. 计算偏差:小车y坐标与目标直线的距离
    error = target_y - car.y

    # 2. PID三项计算
    #   比例项:正比于当前偏差
    P = Kp * error

    #   积分项:累计历史偏差
    integral += error
    I = Ki * integral

    #   微分项:偏差的变化率
    derivative = error - prev_error
    D = Kd * derivative

    # 3. 合成控制输出
    control = P + I + D

    # 4. 将控制量映射为左右轮转速差
    left_speed = base_speed - control
    right_speed = base_speed + control

    # 5. 更新小车状态
    car.step(left_speed, right_speed)

    # 6. 记录轨迹
    trajectory.append((int(car.x), int(car.y)))

    # 7. 准备下一轮
    prev_error = error

    # ---- 可视化部分 ----
    screen.fill((30, 30, 30))

    # 画目标直线(红色虚线)
    for x in range(0, 800, 20):
        pygame.draw.circle(screen, (255, 80, 80), (x, target_y), 3)

    # 画小车轨迹
    if len(trajectory) > 1:
        pygame.draw.lines(screen, (100, 200, 100), False, trajectory, 1)

    # 画小车(一个带朝向的三角形)
    car_points = [
        (car.x + 12 * math.cos(car.heading), car.y + 12 * math.sin(car.heading)),
        (car.x + 8 * math.cos(car.heading + 2.5), car.y + 8 * math.sin(car.heading + 2.5)),
        (car.x + 8 * math.cos(car.heading - 2.5), car.y + 8 * math.sin(car.heading - 2.5)),
    ]
    pygame.draw.polygon(screen, (255, 255, 100), car_points)

    # 显示偏差值
    font = pygame.font.Font(None, 24)
    info_text = f"偏差: {error:.2f} | 控制量: {control:.3f} | P={P:.3f} I={I:.3f} D={D:.3f}"
    text_surf = font.render(info_text, True, (255, 255, 255))
    screen.blit(text_surf, (10, 10))

    pygame.display.flip()
    clock.tick(60)  # 60帧每秒

pygame.quit()

运行这段代码。你会看到一个小车从屏幕左侧出发,起初它可能偏离了中心的红线,但很快就开始做出修正——车头摆向红线,轨迹弯曲回来,然后在红线附近稳定下来。屏幕左上角实时跳动着偏差值和控制量,P、I、D三项的数值一目了然。

这就是PID控制的直观效果:偏差出现,控制响应,偏差归零,系统稳定。

调参实验:亲手感受三个参数的作用

一个PID控制器好不好,很大程度上取决于三个增益参数的设置。现在是时候亲手调参,感受每个参数的独特作用了。

实验一:纯P控制

KiKd 都设为 0,只保留 Kp。观察小车的表现。你会发现它在红线附近来回震荡,怎么也停不下来。这是典型的比例控制欠阻尼震荡。

实验二:加上D项

保持 Kp 不变,逐渐增大 Kd。你会发现震荡幅度变小了,小车更快地稳定在红线上。D项提供了阻尼,抑制了过冲。

实验三:引入I项

将小车初始偏离调大(比如 y=350),不加I项时,你可能会发现小车最终稳定在红线附近——但不是正好在红线上,而是有一个微小的稳态误差。现在给 Ki 一个小的正数,观察积分项如何慢慢累积,最终把那个微小的残余偏差消除。

实验四:调过头

Kp 调到 3.0,保持 Kd 不变。你会看到小车剧烈摆头,轨迹像锯齿一样——这是过调的表现。再降到 0.1,小车几乎不修正方向,慢慢悠悠地越偏越远——这是欠调。

参数 偏小 偏大 适中
$K_p$ 响应迟缓,修正无力 过冲震荡,不稳定 快速响应,适度修正
$K_i$ 稳态误差难以消除 积分饱和,超调加重 静差归零,平稳累积
$K_d$ 过冲明显,震荡持续 对噪声敏感,抖动 阻尼适度,平稳收敛

调参是PID控制中最“玄学”也最有乐趣的环节。没有一个放之四海皆准的参数组合,它取决于你的小车物理特性、噪声大小、基础速度等等。工程师在真实项目里,往往要在仿真里花大量时间反复调试这三个数字。你今天体验的,正是他们日常工作的核心内容。

小结:一个世纪前的智慧,至今闪耀

在这一节,你用不到30行核心代码,实现了一个PID控制器,让虚拟小车跑出了直线。你在屏幕上亲眼看到了比例项如何响应偏差,微分项如何抑制震荡,积分项如何消灭残余静差。

PID的意义远不止让小车跑直线。在后面的章节里,你会发现机械臂的关节伺服控制、无人机的姿态稳定、甚至某些强化学习中的baseline控制器,底层都是PID或其变体。它是自动控制的ABC,也是具身智能大厦最底层的一块砖。

更重要的是,PID提供了一种通用的问题解决视角:感知偏差、计算响应、施加修正、循环反馈。 这不仅是控制算法,更是具身智能体与物理世界持续交互的根本范式。

在下一节,我们将升级难度——让机械臂用视觉信息自动追踪并抓取物体。从直线到抓取,从单自由度到多关节协同,你离一个完整的具身智能系统越来越近了。

本教程共21节,当前为第14节!
本教程最新修订时间为:2026-08-19 23:29:19

📌 面试天下网:一款便捷的大模型学习口袋书,让富士康流水线的打工人也能摸到改命逆天的机会!
📌 网站公告:【悬赏 200 元/篇】寻找“大模型面试战场”的一手回忆录
📌 网站公告:【大模型/Agent面试陪练1V1指导】正式启动......

评论专区

评论加载中...