news 2026/10/26 13:27:55

STM32两轮自平衡小车:姿态解算与串级PID调参实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32两轮自平衡小车:姿态解算与串级PID调参实战

简介:面向自动控制课程设计、毕业设计与平衡小车入门实践的完整工程资料包,围绕陀螺仪与加速度计姿态采集、PID调节、卡尔曼滤波及状态观测等核心环节,帮助读者从理论走向可运行的嵌入式实现。压缩包共1420个文件,约36.17MB,以C源码和头文件为主体,辅以汇编启动文件、工程配置、链接脚本、HEX/AXF等构建产物,以及TXT说明、DOC/PDF文档和图片素材,覆盖编码、编译、下载与调试所需链路。已有1748人学习,适合需要对照真实工程理解控制算法落地细节的读者。包内可循Keil工程结构梳理传感器数据处理、电机控制与参数整定思路,也能借助说明文档和构建产物快速复现实验环境,是自控初学者和课程实践者查漏补缺、拆解平衡小车控制系统的实用参考。

1. 从一台摔倒在地的小车说起:平衡小车练的是什么功夫

给一台刚焊完的平衡小车上电,十有八九是同一个画面:轮子猛地一窜,车体朝某一侧栽下去,手还没摸到开关,它已经躺平了。这一秒的失控,正好覆盖了平衡小车全部的技术链路——从姿态传感器读到的角速度,到定时器里跑的三环 PID,再到驱动板给出的 PWM,任何一环没对齐,车就站不住。

这个包里装的是一台两轮自平衡小车的完整工程源码,主控是 STM32F103 一类的 Cortex-M3,姿态靠六轴传感器,驱动用双路 H 桥,配编码器测速、OLED 显示和串口调参。它适合两类人:刚学完单片机外设、想找个能把定时器、I2C、PWM、串口全串起来的综合练手项目的人;以及已经能把车立起来,但速度环一加就抖、转向一给就翻,想看清楚串级 PID 到底怎么叠的人。下面按硬件、姿态、控制、排障、改造的顺序拆,参数和代码都给到能直接抄的程度。

2. 硬件组成与工程结构:一台平衡小车由哪些模块拼起来

平衡小车这类项目,软件再漂亮也架不住硬件配错。我见过太多人卡在“代码明明照抄的,就是站不起来”,最后发现是电机驱动没接 STBY、编码器线序反了、或者电池电压一掉主控就复位。所以先把硬件最小集和工程目录理清楚,后面调参才有意义。

2.1 能跑起来的最小硬件集与引脚分配

一台能立起来的车,硬件上其实只要五样东西:主控、姿态传感器、电机驱动、带编码器的减速电机、电源。剩下的 OLED、蓝牙、按键、蜂鸣器都是调试辅助,可以先不焊,但强烈建议焊——后面调参你会感谢自己。

模块典型器件接口作用踩坑点
主控STM32F103C8T6 最小系统—跑姿态解算与控制环用 8M 晶振,别省
姿态六轴陀螺加速度计I2C1(PB6/PB7)输出角速度与加速度模块要装水平,别歪
电机驱动双路 H 桥TIM 互补 PWM + 4 根方向线驱动左右电机正反转STBY 必须拉高
电机带霍尔编码器减速电机TIM 编码器模式提供位移反馈左右轮子镜像安装
电源2 节锂电池串联 + 降压—给驱动和主控供电主控与电机共地

引脚分配上有两个经验:一是 PWM 尽量用同一个定时器的不同通道,这样两路频率天然一致,不会出现一边响一边不响;二是编码器一定用定时器的硬件编码器模式,不要用外部中断去数脉冲,电机一转几千赫兹,中断里光进光出就把 CPU 吃满了。我一般把左轮编码器接 TIM3、右轮接 TIM4,PWM 用 TIM1 的 CH1/CH4,方向线随便找两个普通 GPIO 就行。

电机减速比和编码器线数决定了速度环的换算系数,这个系数写错,速度环的 P 就会显得“怎么调都不对”。计算方式很直白:编码器线数 × 4(四倍频)× 减速比 = 输出轴转一圈的计数。

/* motor.h —— 编码器脉冲到物理速度的换算,改硬件只改这几个宏 */ #define ENC_LINE 13 /* 编码器每圈线数 */ #define GEAR_RATIO 30 /* 减速箱减速比 */ #define WHEEL_DIAMETER 65.0f /* 轮径,单位 mm */ #define PI_F 3.1415926f /* 输出轴转一圈的计数:13 * 4 * 30 = 1560 */ #define CNT_PER_ROUND (ENC_LINE * 4 * GEAR_RATIO) /* 每个计数对应的位移,单位 mm */ #define MM_PER_CNT (PI_F * WHEEL_DIAMETER / CNT_PER_ROUND)

这几行宏是整个工程里最该先确认的地方。ENC_LINE填错一倍,速度环的反馈就整体缩一半,表现出来就是“车往前跑得比设定快一倍”,很多人第一反应是去调 P,越调越乱。WHEEL_DIAMETER用卡尺量实际轮子,别用商品页标的标称值,差 3mm 在长距离行驶时就是明显的累积误差。

2.2 工程目录与启动链路

拿到源码包,第一件事不是编译,是看目录。平衡小车的工程结构基本是固定的分层:硬件抽象一层、算法一层、应用一层。照着这个结构读代码,比从头顺着 main 往下翻快得多。

car/ ├── USER/ │ ├── main.c 上电初始化、主循环、1s 心跳 │ ├── stm32f10x_it.c 定时器中断、串口中断 │ └── system_stm32f10x.c 时钟树配置(72MHz) ├── HARDWARE/ │ ├── motor.c/.h PWM 输出、方向控制、编码器读取 │ ├── mpu6050.c/.h I2C 读写、原始数据换算 │ ├── oled.c/.h I2C 屏显,调试时看角度和 PWM │ ├── encoder.c/.h 定时器编码器模式初始化 │ └── bluetooth.c/.h 串口收发,手机端改参数 ├── CONTROL/ │ ├── attitude.c/.h 互补滤波,输出可用角度 │ ├── balance.c/.h 直立环 PD │ ├── speed.c/.h 速度环 PI │ └── turn.c/.h 转向环 └── SYSTEM/ ├── delay.c/.h SysTick 延时 └── usart.c/.h 串口重定向 printf

读代码的顺序建议是:main.c看初始化顺序 →stm32f10x_it.c看中断里调了谁 →attitude.c→balance.c→speed.c。这条链路走通一遍,你脑子里就有了一张“数据从传感器流到电机”的图,剩下的边角代码都是外围。

这里有个容易忽略的点:初始化顺序不能乱。I2C、定时器、PWM、编码器全部初始化完之后,才能打开定时器中断开始跑控制环。我曾经把TIM_Cmd(TIM4, ENABLE)写在传感器初始化前面,结果中断一进来就去读 I2C,而 I2C 还没配好,直接卡死在 while 等待应答里,现象是“上电什么都不动,灯也不亮”,查了一晚上。

2.3 第一次编译、烧录与上电自检

工程一般是标准库或 HAL 库的 Keil 工程,也有少部分用 Makefile。用命令行的话,烧录这一段最省事:

# 编译(Keil 工程用 UV4 命令行,或者直接用 arm-none-eabi 工具链) make clean && make -j4 # 烧录,st-flash 适合 ST-Link,openocd 也行 st-flash write build/car.bin 0x08000000 # 串口看输出,波特率跟 usart.c 里保持一致 minicom -D /dev/ttyUSB0 -b 115200

烧完不要急着装车。上电前先做三步自检,能省掉后面一半的排查时间:

第一步,串口打印传感器 ID,正常应返回固定值(六轴芯片一般是 0x68)。返回 0xFF 说明 I2C 没通,先查上拉电阻和接线,别往下走。

第二步,单独测电机。写个十行的测试函数让左轮正转 1 秒、反转 1 秒,确认方向线逻辑和 PWM 都通。这时候如果轮子只嗡不转,基本是 STBY 没拉高或者电池电压不够。

第三步,手动转轮子,看串口打印的编码器计数是不是一个加一个减。同向装的两个轮子,往前推车时一个计数为正一个为负,这是正常的,后面速度环里要把其中一个取反。这一步不做,速度环必然发散。

3. 姿态解算链路:六轴原始数据到可用倾角

控制环拿到手的不能是“传感器读数”,得是“车体相对地面的角度”和“角速度”。这两个量决定了直立环稳不稳。这一段链路是整个项目里最容易出“玄学问题”的地方——角度看着挺正常,车就是慢慢往一边倒,往往就是解算里的某个细节没处理干净。

3.1 传感器初始化与量程选择

初始化要配四个东西:电源管理、采样率分频、数字低通滤波、量程。量程选大了分辨率不够,选小了一抖就饱和,我一般用加速度 ±2g、陀螺 ±500°/s,对平衡小车够用。

/* mpu6050.c —— 上电初始化,返回芯片 ID 用于自检 */ #define MPU_ADDR 0x68 #define REG_SMPLRT_DIV 0x19 #define REG_CONFIG 0x1A #define REG_GYRO_CFG 0x1B #define REG_ACCEL_CFG 0x1C #define REG_PWR_MGMT_1 0x6B #define REG_WHO_AM_I 0x75 uint8_t MPU_Init(void) { I2C_WriteByte(MPU_ADDR, REG_PWR_MGMT_1, 0x00); /* 解除休眠,用内部时钟 */ I2C_WriteByte(MPU_ADDR, REG_SMPLRT_DIV, 0x04); /* 1k/(1+4) = 200Hz 输出 */ I2C_WriteByte(MPU_ADDR, REG_CONFIG, 0x03); /* 低通 44Hz,压电机振动 */ I2C_WriteByte(MPU_ADDR, REG_GYRO_CFG, 0x08); /* ±500dps,65.5 LSB/(°/s) */ I2C_WriteByte(MPU_ADDR, REG_ACCEL_CFG, 0x00); /* ±2g,16384 LSB/g */ return I2C_ReadByte(MPU_ADDR, REG_WHO_AM_I); /* 正常返回 0x68 */ }

REG_CONFIG这个低通滤波值是新手最容易忽略的参数。电机一转,车架上全是几十到几百赫兹的机械振动,这些振动会原封不动进到陀螺仪数据里,再被 D 项放大成 PWM 抖动。低通设到 44Hz 意味着 44Hz 以上的振动被压掉,而车身真正的晃动频率在 1~5Hz,完全不受影响。有人把低通设到 5Hz 想“更平滑”,结果车身响应的相位滞后变大,一推就振荡,这是典型的用力过猛。

REG_SMPLRT_DIV决定传感器内部输出率,设成 200Hz 是给后面 5ms 一次的控制节拍留了余量——控制环 200Hz 读一次,传感器 200Hz 出一次,节奏刚好对上,不会读到重复值。

3.2 互补滤波:陀螺积分与加速度观测的加权

陀螺仪短期准、长期漂;加速度计长期准、短期被振动干扰得厉害。互补滤波就是拿陀螺积分做主干,用加速度算出的角度做慢速修正,一行公式搞定,算力开销几乎为零,比卡尔曼好调太多。

/* attitude.c —— 每 5ms 调用一次,dt 固定 0.005s */ #define ALPHA 0.98f #define GYRO_LSB 65.5f /* ±500dps 时的灵敏度 */ #define RAD2DEG 57.29578f static float angle; /* 融合后的倾角,单位度 */ static float gyro_bias; /* 上电静态标定出的零偏 */ void Attitude_Update(float ax, float az, float gyro_raw) { /* 加速度算观测角:用 atan2 而不是 asin,全角度范围内都不会退化 */ float acc_angle = atan2f(ax, az) * RAD2DEG; /* 原始值先去零偏,再换算成 °/s */ float gyro_rate = (gyro_raw - gyro_bias) / GYRO_LSB; /* 陀螺积分做预测,加速度观测做修正 */ angle = ALPHA * (angle + gyro_rate * 0.005f) + (1.0f - ALPHA) * acc_angle; }

ALPHA是这套滤波唯一的旋钮,物理含义是“多大程度相信陀螺”。取 0.98 对应的时间常数大约 0.25 秒,意味着加速度计的修正要花这么久才能把陀螺的漂移拉回来。调大到 0.995,车对角度变化的响应更“跟手”,但静止时会缓慢漂走;调小到 0.9,抗漂能力变强,但加速度计的振动噪声会渗进来,车身开始高频抖。我一般从 0.98 起步,车能站住之后再微调。

用atan2f而不是asin是个细节但很关键:asin在接近 ±90° 时对输入误差极度敏感,稍微超范围就返回 NaN,车会瞬间输出乱值扑倒。atan2f在整个圆周上都稳定,代价是多几个时钟周期,完全值得。

3.3 上电零偏标定与温漂处理

陀螺仪静止时输出不是 0,这是硬件特性,不标定就等着车慢慢往一边走吧。标定要在上电后、开中断前做,让车完全静止放在桌面上,别有人碰。

/* attitude.c —— 上电标定,1 秒内采 500 次求均值 */ void Attitude_Calibrate(void) { int32_t sum = 0; for (uint16_t i = 0; i < 500; i++) { sum += MPU_ReadGyroRaw(); delay_ms(2); /* 采 1 秒,期间车必须静止 */ } gyro_bias = (float)sum / 500.0f; }

采 500 次而不是 10 次,是因为单次读数的噪声在 ±3 LSB 左右,平均 500 次能把噪声压到 0.15 LSB,换算成角速度大约 0.002°/s,这个精度足够支撑几分钟的漂移。采太多次也没意义,1 秒已经够覆盖大部分噪声周期。

温漂是另一回事。传感器跑十几分钟会发热,零偏随温度缓慢变化,表现是“刚上电车是稳的,跑了一会儿开始慢慢往前跑”。正规做法是加温度补偿表,但平衡小车这个场景没必要——速度环本身就在不断修正缓慢的位移偏差,只要直立环调得够硬,温漂会被速度环吃掉。真正要担心的是别把传感器贴在电机驱动芯片旁边,那里是整个板子上最热的地方。

4. 串级 PID 控制核心:直立环、速度环、转向环怎么叠加

三环结构是平衡小车的标准答案,但“标准答案”不等于“照着写就能站”。三环之间的符号关系、输出限幅、调用频率,每一个都是能让车原地画圈或者高频抖动的开关。这一章把三个环单独拆开讲,最后讲怎么叠。

4.1 直立环:只有 PD,没有 I

直立环的任务是让角度误差归零,本质是一个倒立摆的镇定问题。倒立摆需要的是“回复力 + 阻尼”,对应 PD,加 I 反而会引入慢速振荡,所以这个环里坚决不要积分项。

/* balance.c —— 5ms 调用一次 */ #define KP_BALANCE 380.0f #define KD_BALANCE 1.2f #define ANGLE_MID 0.5f /* 机械中值,装配后必须实测重标 */ float Balance_Loop(float angle, float gyro_rate) { float err = angle - ANGLE_MID; /* 相对机械中值的偏差 */ /* P 项给回复力,D 项用角速度直接做阻尼,比角度差分更干净 */ return KP_BALANCE * err + KD_BALANCE * gyro_rate; }

ANGLE_MID是这台车“理论上完全竖直”时传感器读到的角度,因为传感器焊歪了一点、电池装偏了一点,这个值几乎永远不是 0。标定方法很土但很准:车能勉强站住之后,让它静止,串口读角度值,读几十次取平均,填进来。

KD_BALANCE这里用的不是“角度误差的差分”,而是直接拿陀螺仪角速度。这两种写法数学上等价,但角速度是传感器的原始输出,噪声特性明确、没有差分放大,比(angle - last_angle) / dt干净一个数量级。这也是为什么很多人 D 项一调就抖——差分把量化噪声放大了。

符号问题必须在这里说清楚:陀螺仪读数方向要跟倾角变化方向一致。如果车往前倾时角度变大、但陀螺读数也是正,那 PD 两项同号,直接发散。验符号的办法是倾斜车体,看串口打印的角度和角速度是不是同增同减,不是就把陀螺读数取反。

4.2 速度环:PI 加输出限幅

速度环负责“车该不该往前走”,输出的是一个角度偏移量,叠加到直立环的目标上——也就是说,速度环不是直接给 PWM,而是通过“让车稍微前倾一点”来产生前进的加速度。这个层级关系理解了,速度环的参数就好调了。

/* speed.c —— 100ms 调用一次 */ #define KP_SPEED 120.0f #define KI_SPEED 0.8f #define SPEED_INT_LIMIT 3000.0f static float speed_int; float Speed_Loop(int16_t enc_l, int16_t enc_r, float target_mmps) { /* 左右轮取平均做线速度,注意其中一个轮子要取反(镜像安装) */ float v = (enc_l - enc_r) * 0.5f * MM_PER_CNT / 0.1f; /* mm/s,周期 0.1s */ float err = target_mmps - v; speed_int += err; if (speed_int > SPEED_INT_LIMIT) speed_int = SPEED_INT_LIMIT; if (speed_int < -SPEED_INT_LIMIT) speed_int = -SPEED_INT_LIMIT; return KP_SPEED * err + KI_SPEED * speed_int; }

速度环必须加积分限幅,这一点是血泪经验。车被手挡住的时候误差持续存在,积分项一路累加,等你松手,积分已经堆到几千,车会猛冲出去。限幅值取 PWM 满量程的一半左右比较合适,具体看你的电机功率。

速度环的频率比直立环低一个数量级,5ms 和 100ms 的搭配是常见做法。为什么不能同频?同频的话速度环的响应和直立环打架,两个环互相激励,表现是车在原地前后小幅高频晃动。10 倍频差让两个环解耦,直立环管“这一瞬间站不站得住”,速度环管“这一秒钟往哪走”。

enc_l - enc_r还是enc_l + enc_r,取决于轮子是不是镜像装的。同向装的轮子用加,镜像装的用减。这个符号搞错,车会一边转圈一边前进,看起来像“抽风”。

4.3 三环叠加与输出限幅

三个环的输出怎么合成两路 PWM,是整个控制里最需要想清楚的一步。

/* control.c —— 三环叠加,输出给电机 */ #define PWM_MAX 7200 /* 定时器 ARR 的 90%,留死区 */ static float speed_target, turn_target; void Control_Update(float angle, float gyro_y, int16_t enc_l, int16_t enc_r) { float out_balance = Balance_Loop(angle, gyro_y); float out_speed = Speed_Loop(enc_l, enc_r, speed_target); float out_turn = Turn_Loop(turn_target); /* 直立 + 速度同向叠加,转向左右反向叠加 */ float pwm_l = out_balance + out_speed + out_turn; float pwm_r = out_balance + out_speed - out_turn; Motor_Set(LIMIT(pwm_l, PWM_MAX), LIMIT(pwm_r, PWM_MAX)); }

叠加关系记住一句话:直立环和速度环的输出加在轮子上是“共同前进/后退”的,所以两轮同号;转向环是“一正一反”,所以两轮异号。这个结构决定了你可以单独调试——把turn_target设 0、speed_target设 0,车只要站住,两个环就都对了。

PWM_MAX留 10% 余量是给转向环留空间。如果直立环已经把 PWM 打满,转向环再叠加就被削平,表现出来就是“直行能走,一转弯就卡顿”。留余量之后转向才有调节余地。

限幅函数本身也要注意:不要用简单的if > max return max,那样在接近限幅时会有一小段非线性。正确做法是上下都限,且限幅值取正负对称。

4.4 控制节拍:5ms 中断里的执行顺序

所有控制都在一个定时器中断里跑,dt 的稳定性直接决定积分和微分的准确性。中断里的执行顺序不是随便排的,传感器读取得排最前面。

/* stm32f10x_it.c —— TIM4 更新中断,5ms 一次 */ void TIM4_IRQHandler(void) { static uint8_t div_cnt = 0; if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); MPU_ReadRaw(); /* I2C 读,约 1ms,先做 */ Attitude_Update(ax, az, gyro_raw); /* 姿态解算 */ Control_Update(angle, gyro_y, Enc_Read(TIM3), Enc_Read(TIM4)); /* 20 次中断 = 100ms,跑一次速度环相关的外围逻辑 */ if (++div_cnt >= 20) { div_cnt = 0; Speed_Reset_If_Stopped(); /* 停车时清积分 */ OLED_Refresh(); /* 刷屏只在这里做 */ } } }

执行顺序的理由:传感器读最慢(I2C 一次要 1ms 左右),放最后会挤压控制计算的时间窗口;姿态解算必须在控制之前,因为控制要的是这一拍的角度;OLED 刷屏一定要放到低频分支里,I2C 刷一屏 128×64 要十几毫秒,放 5ms 中断里直接把控制节拍撑爆。

中断服务函数本身要短。所有printf、delay、浮点除法能挪就挪到主循环。我见过有人在中断里调printf打调试信息,串口一堵,中断出不来,车直接失控——这种问题排查起来非常费劲,因为现象是“偶尔抽一下”,没有规律。

5. 常见问题排查:调了三天还是原地画圈的五种原因

平衡小车的调试过程,本质上是“用现象反推哪一环错了”。下面五条是我自己踩过、也帮别人定位过的典型问题,每条都按“现象 → 原因 → 解决”写,对照着看能省不少时间。

5.1 现象:一上电就朝固定方向猛冲

这是新手最常见的第一个坎。车通电瞬间不看角度、直接满 PWM 朝一个方向冲出去,撞墙为止。

原因基本是符号错了。三种可能:一是直立环的 P 项符号反了,误差为正时输出也为正,车一倾就被推得更倾,正反馈;二是陀螺仪方向反了,D 项变成“加速倾斜”;三是电机方向线接反了,代码认为在往前推,实际在往后拉。

解决办法是分步验符号:先把 Kp 设成一个很小的值(比如 20),Kd 设 0,手扶着车倾斜,看轮子转向是不是朝着“扶正”的方向。不对就把KP_BALANCE取负。P 对了之后再加 Kd,同样手扶测试,阻尼感应该是“推得快阻力大”。两步都对,再把车放地上。

5.2 现象:能站住,但一直缓慢往一个方向跑

车立住了,姿态也稳,就是十几秒之后开始匀速往一边走,越走越快,最后撞东西。

原因有两个层面。表层是速度环没生效或者积分太小,车的位置偏差没有被修正。深层原因是姿态的机械中值ANGLE_MID标偏了——车以为自己“竖直”的时候其实是前倾两度,于是持续输出一个小的前进分量。

解决办法是先标中值再调速度环。让车勉强站住,串口连续读角度,手动把车调到“轮子完全不动”的位置,此时的角度值就是真实中值,填进ANGLE_MID。然后再开速度环,Ki 从 0.1 开始往上加,加到车被推动后能回到原位为止。速度环的 P 太大会让车前后点头,I 太大会低频摆动。

5.3 现象:车身高频抖动、电机嗡嗡响

车能站住,但一直在高频震颤,声音像蜜蜂,手放上去能感到明显震动。

原因通常是 D 项过大或者传感器低通没配。D 项放大的是角速度的高频成分,电机振动、车架共振全都从这里进来。另一个可能是控制周期不稳,中断被别的高优先级任务打断,dt 忽长忽短,微分项算出来的值乱跳。

解决办法先把KD_BALANCE砍一半,看抖动是否消失,消失说明就是 D 太大。如果砍完站不住,说明传感器数据本身脏,把REG_CONFIG的低通从 0x03 再往下调(比如 0x04 对应 21Hz)。还不行就查中断里有没有耗时操作,把 OLED 刷新和串口打印全挪出去。

5.4 现象:接上蓝牙模块就复位或者屏幕花屏

不接蓝牙一切正常,接上就时不时重启,或者 OLED 显示乱码。

原因是供电和总线冲突。蓝牙模块发射瞬间电流能到几十毫安,从主控的 3.3V 稳压上取电会把电压拉低,触发欠压复位。另一个可能是模块和 OLED 抢同一个 I2C 地址,或者共用了同一路串口。

解决办法是给蓝牙单独一路 LDO,或者在电源脚并一个 100μF 的电解电容吸收瞬时电流。I2C 地址冲突就换一路 I2C,或者改成软件模拟 I2C 挂在其他引脚上。串口冲突的话,把蓝牙挂到 USART2,调试打印留在 USART1。

5.5 现象:换了块电池,参数全变了

同一台车,一块电池满电时站得稳稳的,换一块电压低一点的就站不住,或者抖动明显。

原因是 PWM 的有效增益跟电池电压正相关。同样的占空比,8.4V 时电机得到的平均电压比 7.0V 时高 20%,等于直立环的等效 P 变大了 20%,本来调好的参数就越界了。

解决办法有两个方向:讲究一点的做法是在控制输出上做电压前馈补偿,实时采电池电压,把 PWM 值按V_ref / V_bat缩放;省事的做法是把电池用到 7.4V 就换,别等掉到 7.0V 以下,并且调参时用一块中等电压的电池做基准。我一般两个都做,前馈补偿也就十来行代码,能省掉很多“昨天还好好的今天怎么不行”的困惑。

6. 二次开发改造:把平衡车当底盘往上叠功能

车能稳稳站住之后,这个工程的价值才刚开始体现——它本质上是一个“能精确控制位置和朝向的两轮底盘”,往上加东西的空间很大。下面几个方向是我实际做过或者见人做成的,按改动量从小到大排。

改造方向主要改动点风险与注意
上位机波形调参串口按固定帧格式发角度/PWM/速度,电脑端画曲线帧率别超过 50Hz,否则串口带宽不够
蓝牙遥控解析手机发来的速度/转向指令,替换speed_target指令要做超时保护,断连自动停车
里程计与航向估计编码器积分算位移,陀螺积分算航向角航向角会漂,长距离要配地磁或视觉
避障加超声波/红外,检测到障碍时把speed_target置 0避障逻辑要放在速度环外层,别塞进中断
视觉跟随摄像头模块串口输出目标框中心,映射成转向量帧率低,转向环要做低通

最值得先做的是串口波形。调试三环的时候,光看数字根本判断不出是振荡还是漂移,把角度、目标角度、PWM 三路数据打成 CSV 发到电脑上画出来,一眼就能看出相位关系和超调量。帧格式我一般用最简单的二进制帧:帧头 0xAA 0x55 + 三个 int16 + 校验和,解析代码二十行搞定。

/* debug.c —— 每 20ms 发一帧,主循环里调用,不要放中断 */ void Debug_Send(float angle, float target, int16_t pwm) { uint8_t buf[10]; buf[0] = 0xAA; buf[1] = 0x55; int16_t a = (int16_t)(angle * 100); /* 放大 100 倍保两位小数 */ int16_t t = (int16_t)(target * 100); buf[2] = a >> 8; buf[3] = a & 0xFF; buf[4] = t >> 8; buf[5] = t & 0xFF; buf[6] = pwm >> 8; buf[7] = pwm & 0xFF; buf[8] = buf[2] ^ buf[3] ^ buf[4] ^ buf[5] ^ buf[6] ^ buf[7]; /* 异或校验 */ buf[9] = 0x0D; USART_SendArray(USART1, buf, 10); }

这段代码的要点是“放大 100 倍转整型”,浮点直接按字节拆会有精度和对齐问题,转成int16覆盖 ±327 度、±327 的 PWM,够用且不会溢出。校验用异或足够,平衡小车的串口环境很干净,不需要 CRC。发送放在主循环里按 20ms 调度,绝不放进 5ms 中断——串口发送是阻塞的,一卡就把控制节拍拖垮。

转向环的参数整定有个小技巧:先把turn_target设成一个阶跃值,看车原地转 90 度需要多久、有没有超调。理想状态是 1 秒左右转完、不来回摆。转向环本质也是个 PD,用的是陀螺仪的 Z 轴角速度做阻尼,和直立环是同一套思路,参数整定顺序也一样,先 P 后 D。

最后说个我自己的教训。刚做这个项目的时候,我习惯“改一个参数、烧一次、看一次”,一次调参要烧十几次固件,一晚上过去车还在原地抖。后来强制自己先把所有参数做成可以从串口在线改的变量,加了set kp 380这样的命令解析,调参效率直接翻几倍。从那以后我每次做控制类项目,第一件事都是把参数通道和波形通道搭好,再动手调——这个习惯比任何调参口诀都管用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 13:27:55

AI自我加速时代:从Agent架构到智能爆炸的工程实践

1. 这条联名到底在说什么&#xff1a;从“AI造AI”到智能爆炸的真实含义 第一次看到“Hinton、Bengio等22位巨头重磅联名”这个标题&#xff0c;我下意识以为是又一份关于AI风险的公开信。但仔细读完内容之后发现&#xff0c;这次的重点不在“AI会不会毁灭人类”这种老生常谈&a…

作者头像 李华
网站建设 2026/10/26 13:27:09

龙珠超61集视频处理技术与动画制作分析

1. 项目背景解析 "dragonballsuper_061-1"这个标题看起来像是《龙珠超》动画的某一集编号。作为资深动漫爱好者&#xff0c;我第一时间想到的是《龙珠超》第61集的相关内容。这集在粉丝圈中有着特殊地位&#xff0c;因为它包含了关键的剧情转折点。 从命名格式来看&…

作者头像 李华
网站建设 2026/10/26 13:26:36

MATLAB/Simulink船舶动力定位模拟器安装与闭环调试指南

简介&#xff1a;面向船舶工程、自动控制与航海类教学场景的动力定位仿真资料包&#xff0c;围绕MATLAB/Simulink构建船舶六自由度运动与推力控制闭环&#xff0c;可用于理解PID控制、状态空间建模及复杂海况下的定位保持问题。压缩包共13个文件、约58KB&#xff0c;以mat数据文…

作者头像 李华
网站建设 2026/10/26 13:26:34

AI技术在罕见病诊断中的突破与应用前景

1. 罕见病诊断的现状与挑战罕见病诊断一直是全球医疗体系面临的重大难题。根据医学界定义&#xff0c;罕见病是指患病率低于1/2000的疾病&#xff0c;目前已知的罕见病种类超过7000种。这类疾病虽然单个病种患者数量少&#xff0c;但整体影响人群庞大——全球约有3亿罕见病患者…

作者头像 李华
网站建设 2026/10/26 13:26:33

HC32L136/F030/L130远程IAP升级与Bootloader设计

简介&#xff1a;面向使用华大单片机&#xff08;HC32L136、HC32F030、HC32L130 等&#xff09;进行产品固件远程升级的嵌入式开发者&#xff0c;这份 DEMO 提供了可落地的 IAP 在线升级方案&#xff0c;包含 BOOT 程序、APP 程序与上位机三部分&#xff0c;可用于解决设备出厂…

作者头像 李华