简介:本资源是一套基于自抗扰控制(ADRC)算法实现的两轮自平衡车MATLAB仿真与嵌入式联合开发方案,面向自动控制、机器人学及智能车方向的本科生、研究生与工程实践者,解决经典倒立摆系统建模、控制器设计与实时闭环控制验证等核心问题。压缩包共28个文件,含11个C/C++头文件(如ADRC.h、ESO.h、TWIP.h)与9个源码文件(如ADRC.cpp、Motor.cpp、KalmanFilter相关实现),支撑状态观测、姿态解算与电机驱动;另有4个.zbak备份文件、2个说明类txt及.gitmodules配置,整体仅1.4MB,轻量易部署。目前已有66人学习下载,适合开展控制算法对比实验、从MATLAB仿真到嵌入式移植的进阶实践。读者可直接复现ADRC对扰动抑制与动态响应的优化效果,获取完整模块化代码结构、传感器融合逻辑(加速度计/陀螺仪)、以及AB平衡车与ADRC双方案对照参考。
1. 为什么两轮平衡车仿真不用PID而选ADRC?——MATLAB里跑通自抗扰控制的真实门槛
两轮平衡车不是玩具,是控制理论的试金石。很多初学者在MATLAB里搭完倒立摆模型、调好PID参数,一上真实小车就抖动失稳——不是模型不准,而是PID对电机延迟、编码器噪声、电池压降这些“看不见的扰动”束手无策。这套ADRC两轮平衡车MATLAB仿真资源,核心价值不在代码行数,而在它把自抗扰控制(ADRC)从论文公式落地为可调试、可替换、可对比的完整闭环:从扩张状态观测器(ESO)实时估计总扰动,到非线性反馈律生成控制量,再到与底层电机驱动接口的C++封装层(TWIP.cpp/Motor.cpp)。它不依赖Simulink物理建模,纯M文件+结构化C++混合架构,适合想吃透ADRC内核又需对接嵌入式硬件的工程师。如果你正卡在“仿真稳、实物飘”的阶段,或需要一份能拆解、能改参、能移植到STM32/FPGA的控制逻辑参考,这份资源比任何教程视频都更接近工程现场。
2. ADRC核心模块拆解:ESO如何实时估计总扰动?从MATLAB函数到C++类实现
ADRC的威力不在“抗扰”,而在“实时估计扰动”。传统PID靠经验凑Kp/Ki/Kd,ADRC则用扩张状态观测器(ESO)把系统内部摩擦、外部风阻、电源波动等所有未知扰动,统一建模为一个“总扰动量”并在线观测。这套资源中,ESO.h和ESO.cpp是理解ADRC本质的关键入口。
2.1 ESO数学结构与MATLAB验证脚本定位
资源包中的ADRC.m主函数调用eso_update()进行状态更新,其离散化形式为:
% ESO核心更新(简化示意,实际代码含滤波与饱和处理) z1(k) = z1(k-1) + h*(z2(k-1) - beta1*(z1(k-1)-y(k))); z2(k) = z2(k-1) + h*(-beta2*sign(z1(k-1)-y(k)));其中z1是被控量(倾角)的估计值,z2是总扰动及其导数的综合估计。beta1、beta2是观测器增益,直接决定响应速度与噪声敏感度。注意:beta1过大导致高频噪声放大,beta2过小则扰动跟踪滞后——这不是理论参数,而是必须在MATLAB里反复试出来的工程值。
提示:打开
main.m,找到% === ESO tuning section ===注释块。这里预置了三组beta参数组合([10,5]/[20,15]/[40,30]),运行时切换并观察scope_eso窗口中z2曲线的毛刺程度与跟踪延迟,比看论文推导更快建立直觉。
2.2 C++端ESO类与硬件耦合细节
MATLAB仿真最终要迁移到单片机,资源包中ESO.h定义了轻量级C++类:
class ESO { public: float z1, z2; // 状态估计 float beta1, beta2; // 观测器增益(需与MATLAB一致) float h; // 采样周期(单位:秒) float y; // 当前测量值(倾角,弧度) void update(float y_measured) { float e = z1 - y_measured; z1 += h * (z2 - beta1 * e); z2 += h * (-beta2 * sign(e)); // sign()函数在Motor.cpp中已重载 } };2.2.1 为什么用sign()而非线性增益?
sign(e)实现的是非线性观测器,对大误差快速收敛,对小误差抑制抖振。但sign()在数字系统中易引发高频振荡,因此Motor.cpp中实际使用带死区的平滑符号函数:
float sign_smooth(float x, float deadzone=0.001f) { if (x > deadzone) return 1.0f; if (x < -deadzone) return -1.0f; return x / deadzone; // 线性过渡区 }注意:此死区值
0.001f对应倾角约0.057°,需根据你的编码器分辨率(如12位ADC对应0.0879°/LSB)调整,否则ESO在平衡点附近持续震荡。
2.3 MATLAB与C++参数一致性校验表
| 参数名 | MATLAB位置 | C++位置 | 典型值 | 修改影响 |
|---|---|---|---|---|
h(采样周期) | main.m中Ts=0.01 | TWIP.h中#define CONTROL_PERIOD_MS 10 | 0.01s | 周期变短→ESO响应快但CPU负载高;需匹配ADC采样率 |
beta1 | ADRC.m中beta1=20 | ESO.cpp构造函数赋值 | 15~40 | 过大→z1噪声大;过小→z1跟踪慢,角度超调 |
beta2 | ADRC.m中beta2=15 | ESO.cpp构造函数赋值 | 10~30 | 过大→z2剧烈跳变;过小→z2无法跟踪突加扰动(如推车) |
b0(控制增益) | ADRC.m中b0=0.8 | ADRC.cpp中float b0 = 0.8f | 0.5~1.2 | 直接缩放控制量,影响电机响应强度 |
实操验证:在MATLAB命令行执行test_eso_convergence.m(资源包内附),输入不同beta组合,观察z1收敛时间(理想<0.5s)与z2稳态波动(应<0.02 rad/s²)。若波动超标,优先降低beta2而非beta1——这是ADRC调试的第一铁律。
3. 从MATLAB仿真到硬件部署:TWIP结构体与电机驱动接口映射
仿真再准,不连电机就是纸上谈兵。这套资源的亮点在于TWIP.h定义的硬件抽象层(HAL),它把MATLAB里的theta_dot、motor_pwm等变量,映射到真实小车的传感器读取与PWM输出流程。理解这个映射,是避免“仿真OK、实物飞车”的关键。
3.1 TWIP结构体:控制环路的数据中枢
TWIP.h中定义的核心结构体:
struct TWIP { // 传感器数据(来自Encoders.cpp/Gyroscope.cpp) float theta; // 倾角(弧度),融合陀螺仪与编码器 float theta_dot; // 角速度(rad/s),主要来自陀螺仪微分 float x; // 水平位移(米),由编码器累计 float x_dot; // 水平速度(m/s) // 控制输出 int16_t left_pwm; // 左轮PWM占空比(-1000~+1000) int16_t right_pwm; // 右轮PWM占空比(-1000~+1000) // 状态标志 bool is_balanced; // 平衡态标志(theta < 0.1rad) uint32_t timestamp; // 控制周期计时(ms) };3.1.1 为什么theta_dot不直接用编码器微分?
编码器每转脉冲数(如1000PPR)经齿轮比(如1:19)后,角度分辨率仅≈0.019°,微分后噪声极大。theta_dot实际由Gyroscope.cpp读取MPU6050的角速度原始值(gyro_z),经低通滤波(截止频率15Hz)后得到。验证方法:在main.cpp中临时添加printf("gyro:%.3f, enc_diff:%.3f\n", gyro_z, (theta-theta_prev)/h);,对比两者差异——你会看到编码器微分在静止时跳变±0.5 rad/s,而陀螺仪稳定在±0.02 rad/s。
3.2 电机驱动协议与PWM安全机制
Motor.cpp实现了双轮独立控制,并内置硬限幅:
void Motor::setPWM(int16_t left, int16_t right) { // 安全限幅:防止启动电流冲击 left = constrain(left, -800, 800); // 实际限制±800而非±1000 right = constrain(right, -800, 800); // PWM极性适配:部分驱动板高电平有效,部分低电平有效 if (MOTOR_POLARITY == 1) { analogWrite(LEFT_PWM_PIN, abs(left)); digitalWrite(LEFT_DIR_PIN, left >= 0 ? HIGH : LOW); } else { analogWrite(LEFT_PWM_PIN, 1023 - abs(left)); digitalWrite(LEFT_DIR_PIN, left >= 0 ? LOW : HIGH); } }提示:
MOTOR_POLARITY宏定义在Motor.h中,必须根据你所用驱动板(如L298N、TB6612FNG)的实际逻辑电平设置。错误会导致电机反转或不转——这是实物调试中最常卡住的环节。
3.3 MATLAB与C++数据流对照表
| MATLAB变量 | C++来源 | 更新时机 | 注意事项 |
|---|---|---|---|
theta | TWIP.theta←Fusion::update() | 每个控制周期(10ms) | 融合算法在Fusion.cpp中,权重alpha=0.98表示陀螺仪主导 |
theta_dot | TWIP.theta_dot←Gyroscope::read() | 同上 | 未使用编码器微分,避免噪声 |
left_pwm | ADRC::compute_control()→Motor::setPWM() | 同上 | 输出值需经constrain()限幅,否则烧毁MOSFET |
is_balanced | if (abs(theta) < 0.1) ... | 同上 | 0.1rad≈5.7°,是平衡态判定阈值,过大会导致频繁启停 |
实操步骤:
- 打开
main.cpp,定位while(1)循环内的twip.update_sensors();→adrc.compute(twip);→motor.setPWM(twip.left_pwm, twip.right_pwm); - 在
adrc.compute()前插入Serial.print("theta:"); Serial.println(twip.theta, 3); - 上电后用串口监视器观察
theta值:静止时应在0±0.02rad内波动,若持续漂移>0.1rad,说明陀螺仪零偏未校准(见Gyroscope.cpp中calibrate_gyro()函数)。
4. ADRC参数整定实战:三步法调出不抖不飘的平衡效果
ADRC参数不是“算出来”的,是“试出来”的。这套资源提供了完整的MATLAB/C++双平台调试路径,但新手常陷入盲目调参。以下三步法,基于真实调试记录提炼,可将调参时间从3天压缩到2小时。
4.1 第一步:冻结ESO,只调b0与omega_c
在ADRC.m中注释掉ESO更新,用理想扰动补偿:
% 注释掉原有ESO调用 % [z1,z2] = eso_update(y, z1, z2, beta1, beta2, h); z2 = 0; % 强制扰动为0,验证基础控制性能 u = b0 * ( -omega_c^2 * (y - y_ref) - 2*omega_c*y_dot ); % 二阶TD+NLSEF此时系统退化为带微分的PD控制。目标:让小车在无扰动下稳定直立,且无明显低频晃动(<1Hz)。
- 若小车缓慢倒下 → 增大
omega_c(如从15→25) - 若小车高频抖动(>5Hz) → 减小
b0(如从0.8→0.5) - 关键指标:用示波器抓取电机电流,纹波应<额定电流的15%,否则
b0过大。
4.2 第二步:激活ESO,分频段调beta1/beta2
恢复ESO后,用test_disturbance.m注入阶跃扰动(模拟推车):
% 在t=2s时施加0.1rad阶跃扰动 if t > 2 && t < 2.01 y = y + 0.1; end观察z2曲线:
- 低频段(0~2Hz):
z2应平滑跟踪扰动趋势 → 调beta1(增大加快跟踪) - 中频段(2~10Hz):
z2出现锯齿 →beta2过大,减小10%再试 - 高频段(>10Hz):
z2剧烈毛刺 →beta1过大,需同步降低
注意:
beta1与beta2存在耦合关系。经验公式:beta2 ≈ 0.6 * beta1。若按此比例仍抖动,检查h是否过小(如5ms)——采样率过高会放大量化噪声。
4.3 第三步:硬件联调中的“抖动-飘移”诊断树
| 现象 | 最可能原因 | 快速验证 | 解决方案 |
|---|---|---|---|
| 小车左右摇摆(1~3Hz) | omega_c过小或b0过小 | 在MATLAB中临时omega_c=30,观察仿真是否改善 | 增大omega_c至25~35,同步微调b0 |
| 小车缓慢前倾/后仰(<0.1Hz) | 陀螺仪零偏漂移 | 静止时读取gyro_z平均值,若≠0则修改Gyroscope.cpp中gyro_offset | 执行calibrate_gyro()并写入EEPROM |
| 启动瞬间猛冲 | b0过大或theta初始值偏差 | 断开电机,手动扶正小车,读取theta初始值 | 在main.cpp中添加if(!is_calibrated) theta = 0;强制归零 |
| 平衡时电机嗡嗡响 | PWM频率过低或ESO高频噪声 | 用示波器测PWM波形,频率应≥10kHz | 修改Motor.cpp中analogWriteFrequency()为31250 |
终极验证指令:在MATLAB命令行运行validate_adrc_stability.m,该脚本自动执行:
- 施加0.05rad正弦扰动(0.5Hz)→ 检查
theta响应相位滞后<30° - 突加0.1rad阶跃 → 检查超调量<15%,调节时间<1.2s
- 持续运行300s → 统计
theta标准差,应<0.008rad(≈0.46°)
若三项全通过,你的ADRC参数已达工业级可用水平——此时再把ADRC.h中的参数固化到C++代码,即可烧录到STM32。
5. 进阶技巧:用MATLAB实时绘图监控ESO状态,替代万用表查信号
调试实物平衡车时,最耗时的不是写代码,而是“猜”信号在哪出错。与其用万用表测电机引脚,不如用MATLAB的udp通信实时绘图——这套资源已预留接口,只需三步启用。
5.1 启用串口数据转发
修改main.cpp中setup()函数:
void setup() { Serial.begin(115200); // 确保波特率与MATLAB一致 // 添加:发送关键变量到MATLAB Serial.print("START:"); Serial.println(TWIP_VERSION); }并在主循环末尾添加:
void loop() { // ...原有控制逻辑... // 每100ms发一次数据(避免串口拥塞) static unsigned long last_send = 0; if (millis() - last_send > 100) { Serial.print("DATA:"); Serial.print(twip.theta, 3); Serial.print(","); Serial.print(twip.theta_dot, 3); Serial.print(","); Serial.print(adrc.z2, 3); Serial.print(","); Serial.print(twip.left_pwm); Serial.println(); last_send = millis(); } }5.2 MATLAB端实时接收与绘图
创建realtime_monitor.m:
% 初始化UDP接收 u = udp('LocalHost', 12345, 'LocalPort', 12345); fopen(u); % 创建实时图 figure('Name','ADRC Realtime Monitor'); ax1 = subplot(2,1,1); hold on; grid on; h1 = plot(NaN, NaN, 'b-', 'LineWidth',1.5); xlabel('Time (s)'); ylabel('Angle (rad)'); title('Theta & Theta_dot'); ax2 = subplot(2,1,2); hold on; grid on; h2 = plot(NaN, NaN, 'r-', 'LineWidth',1.5); xlabel('Time (s)'); ylabel('Z2 (disturbance)'); title('ESO Estimated Disturbance'); t_data = []; theta_data = []; theta_dot_data = []; z2_data = []; tic; while isvalid(u) && toc < 60 % 运行60秒 try data = fscanf(u, '%s'); % 读取一行 if startsWith(data, 'DATA:') parts = strsplit(data(6:end), ','); if length(parts) >= 4 t_data(end+1) = toc; theta_data(end+1) = str2double(parts{1}); theta_dot_data(end+1) = str2double(parts{2}); z2_data(end+1) = str2double(parts{3}); % 动态更新图形 set(h1, 'XData', t_data, 'YData', theta_data); set(h2, 'XData', t_data, 'YData', z2_data); drawnow limitrate; end end catch break; end end fclose(u);5.2.1 关键参数说明
fscanf(u, '%s'):按行读取,避免数据粘连drawnow limitrate:限制绘图帧率,防止MATLAB卡死t_data用toc而非millis():确保时间轴绝对准确
提示:若图形闪烁,将
limitrate改为nodrawnow,并在循环末尾加pause(0.01)。实时性稍降,但界面更稳。
5.3 用Z2曲线诊断真实扰动源
当小车平衡时,观察z2曲线:
- 平稳直线(≈0):系统干净,无持续扰动
- 缓慢上升斜线:电池电压下降(电机负载增加)→ 检查电源纹波
- 周期性尖峰(≈10Hz):机械共振(如轮轴松动)→ 拧紧螺丝或加阻尼
- 随机毛刺:编码器接触不良或电源干扰 → 检查地线连接
案例:某次调试中z2出现2Hz正弦波动,幅度0.15 rad/s²。用手机慢镜头拍摄发现车轮有0.5mm偏心——更换轮毂后z2回归平稳。这比用示波器查电源纹波快10倍。
至此,你已掌握从MATLAB公式推导、到C++硬件部署、再到实时信号诊断的全链路ADRC调试能力。下一步,可尝试将ESO.cpp中的sign_smooth()替换为saturation()函数,或引入KalmanFilter替代陀螺仪融合——这些进阶改造,代码骨架已在KalmanFilter子目录中备好。
本文还有配套的精品资源,点击获取