☰
STM32精确控制行驶距离:开环脉冲与编码器闭环实战
2026/10/14 18:23:38 网站建设 项目流程

简介:面向嵌入式系统开发者与电机控制初学者,压缩包提供基于STM32的电机定距行驶完整工程源码,核心融合PID算法与编码器反馈,解决小车按预设距离精确停止的控制难题。包内共236个文件、约1.65MB,以121个.h头文件与107个.c源文件为主体,覆盖HAL库外设驱动、PID运算中间层、PWM输出与编码器脉冲捕获等模块,另含Keil工程配置、链接脚本与hex固件,便于直接编译烧录。已有1022人学习下载,在同类定距控制项目中具备一定参考热度。借助此源码,可熟悉定时器编码器接口设计、增量式PID参数整定流程,以及如何处理电机启停时的超调与振荡问题;既适合课程设计、电子竞赛中的闭环定位场景,也适用于工业小车等需要速度与行程控制的二次开发。工程内头文件与源码分层清晰,关键注释完整,可大幅降低闭环控制项目的入门门槛。

1. stm32 控制行驶指定距离:为什么例程里的「转多少圈」替换不了「走多远」

stm32 控制行驶指定距离,听起来是个入门级需求:让滑台或小车从 A 点出发,精确停在 500mm 外的 B 点。但真正上过手的人都知道,例程里教你的是「让电机转 N 圈」或者「输出 N 个脉冲」,而现场要的是「行驶到某个实际距离」。这两者之间隔着一层换算,而这层换算背后的硬件选型、闭环策略和标定方法,才是整个需求真实的工作量。开环发够脉冲不难,难的是走完发现偏了 3mm 你还不知道偏在哪。这篇文章就是把这些环节拆开讲透,适合做自动化滑台、机器人底盘、AGV 以及各种需要精确停位的装备的开发者。新手能照步骤跑通最小系统,熟手可以对照检查自己的参数边界和踩坑点。

2. 行驶距离控制的两条路线:开环脉冲数与编码器闭环选型

2.1 步进电机与直流减速电机的选择:毫米级精度靠什么保障

控制行驶距离,第一个选择是电机类型。常见做法是步进电机加驱动器,或者直流减速电机加编码器反馈。两者都能实现「行驶指定距离」,但精度来源完全不同。

步进电机靠的是步距角和细分:默认 1.8° 步距角时,电机转一圈需要 200 个整步;驱动器设置 16 细分后,转一圈需要 3200 个脉冲。只要不丢步,每个脉冲对应的角度是确定的,距离换算就是纯数学问题。直流减速电机不一样,它本身没有位置记忆能力,必须靠编码器脉冲数来推算电机轴转了多少圈,再经过减速比换算到轮子或丝杠的直线位移。

我的选型经验是:如果负载不大、速度要求不高、预算紧张,步进电机加驱动器是首选——控制简单,低速扭矩大,开环就能达到不错的重复精度。如果要求高速运行、频繁启停、负载波动大,或者需要堵转检测,那就选直流减速电机加编码器。还有一种情况必选闭环:开环步进一旦丢步,没有任何反馈,后续所有脉冲都白发了,整个位置全部错位。

用表格对比更直观。

对比项步进电机+驱动器直流减速电机+编码器
位置反馈无,开环定位有编码器,可闭环
精度来源步距角、细分、减速比编码器线数、减速比、倍频
低速扭矩大,但高速扭矩衰减快取决于减速比,整体平稳
丢步/堵转无感知,错位就全错可通过编码器检测
控制器负担只发脉冲,很轻需读编码器、跑闭环算法
毫米级重复精度不丢步时容易做到需要闭环算法和标定

这里有一个常见的误区:以为步进电机开环就能保证毫米级。实际上开环能保证的是「脉冲数准确」,不代表「行驶距离准确」。轮径、丝杠导程、皮带打滑都会让理论换算失真。所以无论哪种电机,想要让最终执行机构停在指定距离,标定这一步都跑不掉。

2.2 脉冲当量:从行驶距离换算成脉冲数的公式与示例

所谓脉冲当量,就是每一个脉冲或每一格编码器计数对应的实际行驶距离。它是整个距离控制的基准参数,所有指令最终都要换算成脉冲数或计数量。

先看步进电机。以轮式底盘为例,设轮子直径 D = 80mm,轮周长 = 3.14159 × 80 ≈ 251.33mm。电机步距角 1.8°,驱动器 16 细分,电机转一圈需要 3200 个脉冲;中间还有一组 1:30 的减速箱,那么输出轴转一圈需要 3200 × 30 = 96000 个脉冲。脉冲当量 = 251.33 / 96000 ≈ 0.00262mm/脉冲。想走 500mm,需要的脉冲数 = 500 / 0.00262 ≈ 190840 个。

再看直流减速电机加编码器。设减速比 1:30,编码器线数 13 线,定时器做四倍频后电机轴每圈产生 13 × 4 = 52 个计数,再乘减速比 30,输出轴每圈 1560 个计数。轮径同样是 80mm 的话,每个计数对应的距离 = 251.33 / 1560 ≈ 0.1611mm。

这两个例子的对比很有说服力:步进方案理论精度能到 0.003mm 的当量,而普通直流减速电机编码器方案当量只有 0.16mm,想控制到 1mm 以内都接近极限。如果项目要求毫米级精度,直流方案就需要更高线数的编码器,或者配合更高减速比,而不是靠软件调能解决的。

实际开发中我不会在代码里直接写这么多除法,而是把脉冲当量固化成常量,目标距离乘以当量得到脉冲数,或者反过来用每毫米对应的脉冲数。

// 参数示例:步进电机 16 细分,1:30 减速比,轮径 80mm #define WHEEL_CIRCUMFERENCE_MM 251.33f #define PULSES_PER_REV 3200u #define GEAR_RATIO 30.0f // 每毫米对应的脉冲数 #define PULSES_PER_MM (PULSES_PER_REV * GEAR_RATIO / WHEEL_CIRCUMFERENCE_MM) // 目标距离转目标脉冲数 uint32_t distance_to_pulses(float dist_mm) { return (uint32_t)(dist_mm * PULSES_PER_MM + 0.5f); }

这里的关键是单位必须统一:轮径用毫米,距离就用毫米;轮径用米,距离就用米。我见过不少翻车现场,就是轮径单位写错,导致所有距离偏了 3.14 倍。脉冲数加 0.5 再取整是为了四舍五入,避免累计误差。

提示:脉冲当量用理论值算出来之后,必须用实际测量修正,具体方法在第 5 章展开。

2.3 开环最小实现:定时器输出指定数量脉冲的代码骨架

开环方案虽然不能修正误差,但作为最小实现能帮你把「发脉冲-走距离」的链路先打通。常见的做法是用定时器 PWM 模式输出脉冲,同时在更新中断里计数,发够数量就关掉定时器。

下面是一段用 HAL 库写的最小骨架。假设定时器时钟 72MHz,PSC 设为 71,得到 1MHz 计数频率;ARR 设为 999,PWM 频率就是 1MHz / 1000 = 1kHz,也就是每秒输出 1000 个脉冲。

// 定时器 PWM 输出指定数量脉冲的最小实现 #define PULSE_TIM (&htim1) #define PULSE_CHANNEL TIM_CHANNEL_1 static volatile int32_t pulse_remaining = 0; // 剩余脉冲数 static volatile uint8_t pulse_done = 1; // 1 表示发送完成 // 启动指定距离运动,参数为脉冲总数 void start_move_pulses(uint32_t total_pulses) { pulse_remaining = (int32_t)total_pulses; pulse_done = 0; __HAL_TIM_SET_PRESCALER(PULSE_TIM, 71); // 72MHz / (71+1) = 1MHz __HAL_TIM_SET_AUTORELOAD(PULSE_TIM, 999); // PWM 频率 1MHz / 1000 = 1kHz __HAL_TIM_SET_COMPARE(PULSE_TIM, PULSE_CHANNEL, 500); // 50% 占空比 HAL_TIM_PWM_Start_IT(PULSE_TIM, PULSE_CHANNEL); } // 每个 PWM 周期结束触发一次中断 void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance != PULSE_TIM->Instance) return; if (pulse_remaining > 0) { pulse_remaining--; if (pulse_remaining == 0) { // 脉冲数已发够,立即停止输出 HAL_TIM_PWM_Stop_IT(PULSE_TIM, PULSE_CHANNEL); pulse_done = 1; } } }

这段代码里 ARR 和 PSC 决定了脉冲频率,CCR 决定占空比。注意这种「每个 PWM 周期进一次中断」的方式,1kHz 频率下中断开销完全能接受;但如果把脉冲频率提到 20kHz,中断就会占掉大量 CPU,这时应该考虑定时器主从模式,用硬件自动停止输出,或者用 DMA 配合。另外,步进驱动器需要的是固定脉宽信号,占空比 50% 即可,不要在这里做调速——调速靠的是改变 PWM 频率,而不是占空比。

开环的局限也很明确:发出去的脉冲数是精确的,但执行机构实际动了多少,系统不知道。这也是第 3 章要用编码器闭环的原因。

3. 编码器闭环:让行驶距离真正收敛到目标值

3.1 编码器正交解码与回绕处理:读准距离的第一步

闭环的前提是能读到准确的位置。增量编码器输出 A、B 两相信号,用定时器的编码器接口模式可以自动做四倍频和方向判别,MCU 不需要在中断里手动判相。常见的初始化方法如下。

// 编码器模式下读取位置,需要先初始化定时器 TIM_Encoder_InitTypeDef enc_cfg = {0}; enc_cfg.EncoderMode = TIM_ENCODERMODE_TI12; // A/B 两相都计数,四倍频 enc_cfg.IC1Polarity = TIM_ICPOLARITY_RISING; // A 相上升沿计数 enc_cfg.IC2Polarity = TIM_ICPOLARITY_RISING; // B 相上升沿计数 enc_cfg.IC1Filter = 0x0F; // 输入滤波,滤毛刺 enc_cfg.IC2Filter = 0x0F; HAL_TIM_Encoder_Init(&htim3, &enc_cfg); __HAL_TIM_SET_COUNTER(&htim3, 0); // 位置清零 HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);

配置好之后,直接读计数器就能拿到带方向的累计值。但这里有一个绝大多数例程不讲的大坑:定时器计数器是 16 位的,范围 0 到 65535。正转到 65535 再继续就回绕到 0,反转到 0 再继续就回绕到 65535。如果直接用__HAL_TIM_GET_COUNTER的原始值参与运算,行程超过一定长度后位置会突然反向跳变。

处理回绕的标准做法是记录上一次的原始值,每次读取时做一次差量计算,再把差量累加到 32 位变量上。

// 读取编码器增量,处理 16 位计数器回绕 int32_t encoder_read_delta(void) { static uint16_t last_raw = 0; uint16_t raw = __HAL_TIM_GET_COUNTER(&htim3); // 16 位相减得到带符号增量,再扩展为 32 位 int32_t delta = (int32_t)(int16_t)(raw - last_raw); last_raw = raw; return delta; } // 全局位置累计,单位:编码器计数 static int32_t encoder_total = 0; void encoder_poll(void) { encoder_total += encoder_read_delta(); }

这里(int16_t)(raw - last_raw)的写法自动处理了回绕:比如 raw 从 65530 变到 10,相减结果是负的大数,转成 int16 后正好是 +20,方向就正确了。我在实际项目里把这段放在一个 1ms 周期的定时器中断里调用,位置刷新率足够。要注意编码器总线的滤波强度,线长超过 30cm 时信号边沿会变差,IC1Filter调节不当会造成计数毛刺,这一点在第 4 章还会展开。

3.2 位置 PID 的增量式实现与参数初值

有了连续、可靠的位置读数,下一步是闭环控制器。位置闭环最常见的是「位置外环 + 速度内环」或者简化为直接位置误差算输出。这里先给一个工程上够用的增量式位置 PID,输出直接作为速度命令。

typedef struct { float kp; // 比例增益 float ki; // 积分增益 float kd; // 微分增益 float integral; // 积分累计 float last_error; // 上次误差 } pid_t; // 增量式位置 PID:输入目标位置和当前位置,输出速度命令 float pid_update(pid_t *pid, float target, float current) { float error = target - current; float p_out = pid->kp * error; pid->integral += error; // 积分限幅,防止深度饱和 float i_out = pid->ki * pid->integral; if (i_out > 500.0f) i_out = 500.0f; if (i_out < -500.0f) i_out = -500.0f; float d_out = pid->kd * (error - pid->last_error); pid->last_error = error; return p_out + i_out + d_out; }

增量式相对位置式的好处是计算量小、不需要累计输出的历史值,输出的是「相对上一次的变化量」还是「绝对速度命令」取决于你怎么接电机驱动。我一般把返回值直接当速度设定值,再经过 PWM 输出给驱动器。

参数初值没有万能公式,但有可靠的经验起点。对于一套普通直流减速电机小车底盘,采样周期 1ms,位置单位用 mm,我的起步参数是 kp = 0.5、ki = 0.01、kd = 0.05,然后按「先 P 后 D 再 I」的顺序调:P 太小走不到位,P 太大到位后抖动;D 用来压过冲,但太大会引入噪声;I 负责消掉静态误差——没有积分项,电机需要有持续力矩时位置总是差一点。

注意:调试 PID 时每次只动一个参数,改动幅度不要超过一倍。一次改三个参数调出来的状态,回头看根本不知道是哪项在起作用。

3.3 梯形加减速与到位判定:不冲过头也不抖动的关键

PID 解决了「到位精度」,但解决不了「过程冲击」。如果一上来就按最大速度发命令,步进会丢步、直流电机会过冲。工程上普遍用梯形加减速曲线:加速段、匀速段、减速段三段拼成一个运动过程。

最简单的实现思路是每个控制周期根据当前位置计算剩余距离,距离小于刹停距离时进入减速段。

// 每个周期判断是否进入减速段,单位:mm #define MAX_SPEED_MMS 200.0f #define DECEL_MMS2 400.0f float planned_speed_mms(float remaining_dist, float current_speed) { // 刹停距离 = v^2 / (2 * a) float brake_dist = (current_speed * current_speed) / (2.0f * DECEL_MMS2); if (remaining_dist <= brake_dist) { // 进入减速区,把速度线性降下来 float v = sqrtf(2.0f * DECEL_MMS2 * remaining_dist); return v; } return MAX_SPEED_MMS; }

这段逻辑的物理含义是:只要剩余距离小于当前速度下的刹停距离,就必须立刻开始减速,否则到目标点停不下来。sqrtf那行算出的速度保证刚好在目标点刹停,工程上通常再乘以 0.9 留点余量。实际代码里还要限制加速度上升速率,避免速度跳变,但最小实现用上面这段就够了。

到这里闭环的框架就齐了:编码器读数喂给 PID,PID 输出接 PWM 调速,加减速规划决定每个周期的速度命令。下一步要讲的是这套系统在真机上最常翻车的位置。

4. 避坑:距离控制翻车的高频原因与排查方法

4.1 丢步与堵转:起步就被卡死的现象、原因与解决

现象:步进电机接到走 500mm 的指令,结果只走了 400mm,或者电机发出嗡嗡声但机体完全不动。还有一种情况是低速没事,速度稍微调高就丢步。

原因:步进电机的启动频率有上限,专业叫法是「突跳频率」。电机轴带着负载直接从一个静止状态跳到每秒几千个脉冲,转子和定子磁场跟不上,就会丢步甚至堵转。很多人直接把速度设成目标值就跑,忽略了起动过程。另一个常见原因是加速段太陡,扭矩需求超过电机的输出能力。

解决:起步频率设低一点,我一般从 200Hz 到 300Hz 起步,用 300ms 到 500ms 线性加速到目标频率。如果是直流电机出现堵转,检查减速箱和导轨的静摩擦,起步瞬间给一个短时间的过压或者额外力矩脉冲往往能解决。血泪经验是:开环步进丢步后系统没有任何提示,下次上电前最好加一个限位开关回原点,否则丢步造成的错位会一直带进下一轮运动。

4.2 过冲与震荡:PID 参数怎么试才不会来回摆

现象:快到目标位置时冲过头,然后退回来,再冲过去,来回几次才停住;或者到位后一直在目标点左右抖动,肉眼可见。

原因:kp 太大,系统在目标点附近变成欠阻尼;积分项累积过大,导致输出命令一直在目标两侧来回摆动;到位判定只看了瞬时位置,一旦编码器有毛刺或者机械振动就让系统反复启停。

解决:第一步把 kp 降一半,看震荡幅度是否收敛;第二步给微分项加一点权重,抑制超调;第三步给积分项加限幅,也就是第 3.2 节代码里i_out限幅的作用。高频率抖动还有一个工程技巧——用到位窗口判定,而不是精确等于目标值:

// 连续 10 次误差小于 1mm 才判定到位,相当于 10ms 稳定 static uint8_t window_cnt = 0; #define WINDOW_MM 1.0f #define WINDOW_CNT 10 int8_t position_on_target(float error_mm) { if (error_mm < WINDOW_MM && error_mm > -WINDOW_MM) { window_cnt++; if (window_cnt >= WINDOW_CNT) { window_cnt = 0; return 1; // 到位 } } else { window_cnt = 0; } return 0; }

这个窗口不能设太大,也不要小于编码器分辨率的两倍。比如 3.1 节那个当量 0.16mm/计数的系统,窗口设 1mm 就是 6 个计数,已经留了足够余量。窗口时间太短会误判,太慢又影响节拍,10ms 是一个比较均衡的起点。

4.3 轮径与打滑:机械误差为什么比算法误差更致命

现象:用理论轮径算出来的距离指令,实际走完后总是偏 1%-3%;而且同一段路反复走,每次误差不一样。

原因:塑胶轮子的实际直径和标注值经常差 1-2mm,加上地面压扁形变,有效周长和理论周长差得更多。打滑则是闭环系统感知不到的问题——编码器装在电机轴上,测的是电机转了多少,轮子在地面打滑时电机照转、车身不动。这种情况再精确的 PID 也无法修正。

解决:轮径参数必须实测标定,方法在第 5 章详细展开。如果地面条件复杂、载重变化大,靠轮径标定只能是修修补补。换到丝杠、导轨、同步带这类刚性传动机构,重复精度会高一个量级。这里要有个清醒的预期:轮式底盘想在任意地面上做到毫米级定位,从机械结构上就是难事,软硬结合加地面参考(磁条、色带、光电开关)才是正路。

4.4 编码器读数异常:抖动、跳变与回绕处理

现象:正转时计数偶尔跳成负值;或者正常行驶中位置突然变了一大截;还有的在电机停止时计数还在缓慢变化。

原因:先把回绕处理做了——很多例程不处理 16 位计数器回绕,行程超过一定长度必然出现方向反转的假象;然后是接线问题,A/B 相接反时计数方向和实际方向相反,有些人靠把 PID 输出取反来硬凑,凑完发现停止时读数乱跳;长线传输干扰和 GND 没有共地会导致边沿毛刺,造成额外计数。

解决:接好 A/B 相后用示波器看两路波形,确认相位关系;检查所有 GND 是否共地,电机驱动的地和控制板的地之间不要留电位差;然后把定时器输入滤波打开,增强抗干扰能力。这一步排查到最后往往就是「玄学」——看起来都正常,最后发现是某根杜邦线接触不良。

5. 实测校准与验证:把理论系数修正到毫米级

5.1 单点标定:反推实际脉冲当量的操作步骤

理论换算必然有误差,所以每一套设备装配完成后必须做一次单点标定。做法很简单:让执行机构走一段已知距离,实测实际走了多少,用比值修正换算系数。

操作步骤:

  1. 把滑台或小车放在起点,编码器位置清零。
  2. 发指令让它走 1000mm(或滑台全行程的某个整数距离)。
  3. 用卷尺或激光测距实测实际行驶距离,记为 S。
  4. 理论每毫米计数设为 C0,新系数 C1 = C0 × (1000 / S)。
  5. 用 C1 重新发一次同样距离,验证误差是否在可接受范围。

如果第一次标定后误差从 3% 降到 0.5%,说明是轮径等系统误差主导;如果误差没有明显变化,就要怀疑有打滑或者机械间隙,标定救不了随机误差。

标定代码其实很简单,核心就是更新每毫米计数这个常量:

// 标定逻辑:目标 1000mm,实测 1016mm,编码器累计计数 N // 理论 C0 = 6.2 counts/mm #define C0_COUNTS_PER_MM 6.2f float calibrate_counts_per_mm(float measured_mm, int32_t encoder_counts) { // 实际每毫米计数 float c1 = (float)encoder_counts / measured_mm; return c1; } // 之后所有距离换算一律使用标定后的 c1 uint32_t dist_to_counts(float dist_mm, float counts_per_mm) { return (uint32_t)(dist_mm * counts_per_mm + 0.5f); }

标定后有一个关键动作:重新验证。不要标定一次就认为万事大吉,先在同方向走三次,确认重复精度,再反向走三次,看回差。

5.2 往返误差测试:判断是系统性误差还是随机误差

标定完做往返测试,目的是把误差来源分类。记录正向走 1000mm 的误差和反向走 1000mm 的误差,各做三次。

典型的测试结果有两种形态。如果三次都是正向误差 -2mm、反向误差 +2mm,方向固定、大小对称——这是机械回差,典型来源是减速齿轮箱间隙和联轴器背隙。如果三次都是正向 -3mm、反向 -3mm,方向相同、大小稳定——这是系统误差,说明标定系数还有偏差,重新修正即可。

测试次数正向 1000mm 误差反向 1000mm 误差判断方向
第 1 次-3mm-3mm系统误差,重新标定
第 2 次-3mm-2mm系统误差为主
第 3 次-3mm-3mm系统误差,修正系数后复测
第 1 次-2mm+2mm机械回差,标定修正无效
第 2 次-2mm+2mm回差,考虑预紧或补偿
第 3 次-2mm+2mm回差,方向与运动方向相关

机械回差基本没法靠标定消除。工程上有两个缓解方向:一是选择低背隙减速箱或者把联轴器锁紧;二是在软件层面做单向定位——最终停位时永远从同一个方向逼近目标点,回差就变成固定值,可以被标定吃掉。这个技巧在很多定位设备上都能看到。

5.3 速度-载重边界:参数调好后哪些条件会打破它

标定和 PID 参数都是在特定工况下得出的,换一个运行条件可能全部失效。我的习惯是把边界条件列出来写进程序注释,防止同事改参数时踩坑。

速度是最常见的变量。步进电机超过一定速度后扭矩急剧下降,丢步概率上升;直流电机超过最大速度时编码器采样跟不上,位置环会紊乱。加速度同理:静摩擦大的系统需要更长的加速段,否则起步即堵转。载重变化直接改变系统惯量,惯量大了 PID 容易出现低频抖动,原来的 kp、kd 就没法用了。电池电压下降时,直流电机输出力矩下降,同一组 PID 参数的响应会变慢。

所以调好的参数要按最恶劣工况留 20% 到 30% 的余量。载重允许 5kg,测试时就按 5kg 调;速度允许 300mm/s,调试就按 300mm/s 跑过再降。只在轻载慢速条件下调出来的参数,上产线就是给自己挖坑。这些边界数据建议做成表格写进项目文档里,我见过太多「程序调好了换台机器全乱套」的情况,最后查下来都是工况参数没有同步迁移。

6. 到位窗口、限幅与堵转检测:位置环的三件套

到此为止,开环和闭环的基本框架已经完整。最后这个章节要给一套在真实产线上能直接用的组合技巧:把第 3 章和第 4 章的内容合成三个互相配合的保护机制,让位置环在异常情况下不至于毁掉整个工序。

// 组合:到位窗口 + 积分限幅 + 堵转检测,1ms 周期调用 #define WINDOW_MM 1.0f #define STALL_SPEED_MMS 5.0f // 低于该速度视为未移动 #define STALL_COUNT_MAX 100 // 100ms 无位移则判定堵转 static float target_mm = 0; static float integral_out = 0; static int32_t stall_cnt = 0; static float last_pos = 0; void position_protect_loop(float current_pos) { float err = target_mm - current_pos; // 1. 到位窗口:误差在 1mm 以内且持续 10ms if (err < WINDOW_MM && err > -WINDOW_MM) { // 进窗口后停止积分,防止停下后还在累积 integral_out = 0; set_speed(0); return; } // 2. 堵转检测:命令速度不为 0,位置却几乎没变 float delta = current_pos - last_pos; last_pos = current_pos; if (delta > -STALL_SPEED_MMS && delta < STALL_SPEED_MMS) { stall_cnt++; if (stall_cnt >= STALL_COUNT_MAX) { // 连续 100ms 不动,视为堵转,急停并报错 set_speed(0); while (1); // 或进入错误状态机 } } else { stall_cnt = 0; } // 3. PID 计算 + 输出限幅 integral_out += err; if (integral_out > 300.0f) integral_out = 300.0f; if (integral_out < -300.0f) integral_out = -300.0f; float speed_out = 0.8f * err + 0.02f * integral_out - 0.1f * (err - last_err); if (speed_out > 200.0f) speed_out = 200.0f; if (speed_out < -200.0f) speed_out = -200.0f; set_speed(speed_out); }

这里的三个保护各有定位:到位窗口是防止机械振动引起反复启停;积分限幅是防止大误差起步时积分饱和导致过冲;堵转检测则是对付「轮子卡死但命令还在跑」的恶性故障。搭好了这套骨架后,剩下的就是调 PID 参数了,把调试日志打开,记录目标位置、实际位置、输出速度三条曲线,一眼就能看出是 P 不够还是 D 过大。有一次我调了一整天过冲问题,最后发现是日志里位置单位写错,把 mm 当 cm 在调,闹了个大笑话。所以现在我的习惯是:任何参数进代码之前,先写清单位;任何现象复现,先看日志再动手改。这个习惯帮我省了很多时间,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询