跳到主要内容Arduino BLDC 机器人 IMU 角度读取与 PID 互补滤波控制 | 极客日志C++算法
Arduino BLDC 机器人 IMU 角度读取与 PID 互补滤波控制
Arduino BLDC 机器人通过 IMU 获取姿态数据,利用互补滤波融合加速度计与陀螺仪信息以修正漂移,再经 PID 控制器计算电机输出实现动态平衡。文章涵盖基础平衡车、四轴飞行器及云台稳定系统的代码实现,详解滤波器参数整定、PID 调优策略及多传感器卡尔曼滤波方案,并提供硬件抗干扰与安全保护建议,适合嵌入式控制开发者参考实践。
静心1 浏览 Arduino BLDC 机器人 IMU 角度读取与 PID 互补滤波控制
基于 Arduino 平台实现 BLDC 机器人的姿态闭环控制,核心在于通过 IMU 获取角度数据,利用互补滤波融合传感器信息以修正漂移,再经 PID 控制器计算电机驱动力矩。这套架构是自平衡机器人、倒立摆或稳定云台的技术基石。
系统核心架构
在这个闭环系统中,各模块分工明确:
- 感知中枢(传感器融合):互补滤波解决了单一传感器的局限。它利用低通滤波提取加速度计的长期稳定分量(重力方向),同时用高通滤波处理陀螺仪的高频角速度数据。在嵌入式系统中,这通常简化为加权平均:
Angle = α * (Angle + Gyro_Rate * dt) + (1-α) * Accel_Angle。系数 α 一般取 0.95~0.98,决定了系统对陀螺仪的信任度。相比卡尔曼滤波,互补滤波计算量极小,非常适合资源受限的 Arduino。
- 决策大脑(PID 控制器):负责将姿态误差转化为电机指令。比例项(P)提供恢复力,决定系统刚度;微分项(D)提供阻尼,抑制超调和振荡;积分项(I)在此类快速动态环中通常设为 0 或极小值,以防累积过冲。
- 执行机构(BLDC 驱动):BLDC 电机功率密度高、响应快,适合跟随 PWM 指令产生恢复力矩。对于自平衡应用,通常工作在速度环模式。
应用场景
这套方案适用于多种场景:
- 两轮自平衡机器人:检测车身俯仰角,PID 计算左右轮目标转速,维持垂直平衡。
- 云台稳定系统:IMU 检测姿态,驱动电机反向旋转抵消载体晃动。
- 倒立摆实验:验证控制算法有效性。
- 双足/多足机器人基座:控制躯干俯仰和横滚角,防止倾倒。

基础实战:两轮自平衡机器人
我们先从最经典的倒立摆开始。这里需要读取 MPU6050 数据,进行互补滤波,再通过 SimpleFOC 库驱动 BLDC 电机。
#include <Wire.h>
#include <MPU6050.h>
#include <SimpleFOC.h>
MPU6050 mpu;
BLDCMotor motor(7);
Encoder encoder(2, 3);
float Kp = 40.0, Ki = , Kd = ;
targetAngle = ;
previousError = , integral = ;
alpha = ;
dt = ;
filteredAngle = ;
{
Serial.();
Wire.();
mpu.();
mpu.(MPU6050_GYRO_FS_250);
mpu.(MPU6050_ACCEL_FS_2);
motor.();
encoder.();
motor.(&encoder);
}
{
ax, ay, az, gx, gy, gz;
mpu.(&ax, &ay, &az, &gx, &gy, &gz);
accelAngle = (ay, az) * RAD_TO_DEG;
gyroRate = gx / ;
gyroAngle = ;
gyroAngle += gyroRate * dt;
filteredAngle = alpha * (filteredAngle + gyroAngle * dt) + ( - alpha) * accelAngle;
error = targetAngle - filteredAngle;
integral += error * dt;
derivative = (error - previousError) / dt;
output = Kp * error + Ki * integral + Kd * derivative;
previousError = error;
motor.((output, , ));
Serial.();
Serial.(filteredAngle);
Serial.();
Serial.(output);
(dt * );
}
10.0
0.5
float
0.0
float
0
0
float
0.98
float
0.01
float
0
void setup()
begin
9600
begin
initialize
setFullScaleGyroRange
setFullScaleAccelRange
init
init
linkEncoder
void loop()
int16_t
getMotion6
float
atan2
float
131.0
static
float
0
1
float
float
float
move
constrain
-10
10
print
"Angle: "
print
print
" Output: "
println
delay
1000
进阶场景:四轴飞行器姿态控制
如果是四轴飞行器,我们需要同时处理俯仰(Pitch)和滚转(Roll)。这里使用舵机模拟电机输出,逻辑类似但涉及混合矩阵。
#include <Wire.h>
#include <MPU6050.h>
#include <Servo.h>
MPU6050 mpu;
Servo motors[4];
float Kp_pitch = 1.2, Ki_pitch = 0.05, Kd_pitch = 0.8;
float Kp_roll = 1.2, Ki_roll = 0.05, Kd_roll = 0.8;
float targetPitch = 0, targetRoll = 0;
float alpha = 0.95;
float pitchAngle = 0, rollAngle = 0;
void setup() {
Serial.begin(115200);
Wire.begin();
mpu.initialize();
for (int i = 0; i < 4; i++) {
motors[i].attach(5 + i);
motors[i].write(90);
}
}
void computeAngles() {
int16_t ax, ay, az, gx, gy, gz;
mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
float accelPitch = atan2(-ax, sqrt(ay * ay + az * az)) * RAD_TO_DEG;
float accelRoll = atan2(ay, az) * RAD_TO_DEG;
static float gyroPitch = 0, gyroRoll = 0;
gyroPitch += gy / 131.0 * 0.01;
gyroRoll += gx / 131.0 * 0.01;
pitchAngle = alpha * (pitchAngle + gyroPitch) + (1 - alpha) * accelPitch;
rollAngle = alpha * (rollAngle + gyroRoll) + (1 - alpha) * accelRoll;
}
void loop() {
static unsigned long lastTime = 0;
unsigned long now = millis();
if (now - lastTime >= 10) {
lastTime = now;
computeAngles();
static float iTerm_pitch = 0, iTerm_roll = 0;
float errorPitch = targetPitch - pitchAngle;
float errorRoll = targetRoll - rollAngle;
iTerm_pitch += errorPitch * 0.01;
iTerm_roll += errorRoll * 0.01;
float outputPitch = Kp_pitch * errorPitch + Ki_pitch * iTerm_pitch;
float outputRoll = Kp_roll * errorRoll + Ki_roll * iTerm_roll;
int throttle = 1100;
int m1 = throttle + outputPitch + outputRoll;
int m2 = throttle - outputPitch + outputRoll;
int m3 = throttle - outputPitch - outputRoll;
int m4 = throttle + outputPitch - outputRoll;
for (int i = 0; i < 4; i++) {
int val = i == 0 ? m1 : i == 1 ? m2 : i == 2 ? m3 : m4;
motors[i].write(constrain(map(val, 1000, 2000, 0, 180), 0, 180));
}
}
}
云台稳定系统(单轴)
云台需要更平滑的控制,通常目标角度是固定的(如水平 90 度)。这里我们增加了 dt 的动态调整和安全限制。
#include <Wire.h>
#include <MPU6050.h>
#include <SimpleFOC.h>
MPU6050 mpu;
BLDCMotor motor(7);
Encoder encoder(2, 3);
float Kp = 2.0, Ki = 0.1, Kd = 0.05;
float targetAngle = 90.0;
float alpha = 0.92;
float filteredAngle = 0;
void setup() {
Serial.begin(115200);
Wire.begin();
mpu.initialize();
motor.init();
encoder.init();
motor.linkEncoder(&encoder);
}
void loop() {
static unsigned long lastTime = 0;
unsigned long now = millis();
float dt = (now - lastTime) / 1000.0;
if (dt > 0.1) dt = 0.1;
uint16_t ax, ay, az, gx, gy, gz;
mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
float accelAngle = atan2(ay, az) * RAD_TO_DEG;
float gyroRate = gx / 131.0;
static float gyroAngle = 0;
gyroAngle += gyroRate * dt;
filteredAngle = alpha * (filteredAngle + gyroAngle * dt) + (1 - alpha) * accelAngle;
static float integral = 0;
float error = targetAngle - filteredAngle;
integral += error * dt;
float derivative = (error - (lastTime ? (targetAngle - (alpha * (filteredAngle + gyroAngle * dt) + (1 - alpha) * accelAngle)) : 0)) / dt;
float output = Kp * error + Ki * integral + Kd * derivative;
motor.move(output);
if (now - lastTime >= 50) {
Serial.print("Target: ");
Serial.print(targetAngle);
Serial.print(" Actual: ");
Serial.print(filteredAngle);
Serial.print(" Output: ");
Serial.println(output);
lastTime = now;
}
}
高级方案:自适应与卡尔曼滤波
当环境复杂时,简单的互补滤波可能不够。我们可以引入自适应系数或扩展卡尔曼滤波(EKF)来提高精度。
自适应互补滤波
根据运动状态动态调整 α。静止时用 Madgwick 滤波校准,运动时用互补滤波保证实时性。
#include <SimpleFOC.h>
#include <MPU6050_tockn.h>
#include <MadgwickAHRS.h>
struct AdaptiveFilter {
float alpha = 0.98f;
float alpha_min = 0.90f;
float alpha_max = 0.995f;
float beta = 0.1f;
float innovation = 0.0f;
float innovation_threshold = 0.1f;
float q0 = 1.0f, q1 = 0.0f, q2 = 0.0f, q3 = 0.0f;
float roll = 0.0f, pitch = 0.0f, yaw = 0.0f;
float angularVelocity[3] = {0, 0, 0};
};
AdaptiveFilter adaptiveFilter;
void adaptiveComplementaryFilter(float ax, float ay, float az, float gx, float gy, float gz, float dt) {
float acc_mag = sqrt(ax * ax + ay * ay + az * az);
float acc_confidence = 1.0f;
if (abs(acc_mag - 1.0f) > 0.2f) {
acc_confidence = 0.1f;
}
float dynamic_alpha = adaptiveFilter.alpha;
if (acc_confidence < 0.5f) {
dynamic_alpha = 0.995f;
} else {
dynamic_alpha = 0.98f;
}
dynamic_alpha = constrain(dynamic_alpha, adaptiveFilter.alpha_min, adaptiveFilter.alpha_max);
float acc_roll = atan2(ay, az);
float acc_pitch = atan2(-ax, sqrt(ay * ay + az * az));
float gyro_roll = adaptiveFilter.roll + gx * dt;
float gyro_pitch = adaptiveFilter.pitch + gy * dt;
adaptiveFilter.roll = dynamic_alpha * gyro_roll + (1.0f - dynamic_alpha) * acc_roll;
adaptiveFilter.pitch = dynamic_alpha * gyro_pitch + (1.0f - dynamic_alpha) * acc_pitch;
adaptiveFilter.yaw = gyro_yaw;
adaptiveFilter.angularVelocity[0] = gx;
adaptiveFilter.angularVelocity[1] = gy;
adaptiveFilter.angularVelocity[2] = gz;
}
扩展卡尔曼滤波 (EKF)
对于高精度需求,可以使用 EKF 融合多传感器数据(加速度计、磁力计、陀螺仪),并估计零偏。
#include <SimpleFOC.h>
#include <BasicLinearAlgebra.h>
#include <math.h>
using namespace BLA;
class ExtendedKalmanFilter {
private:
static const int STATE_DIM = 7;
static const int MEAS_DIM = 6;
Matrix<STATE_DIM, 1> x;
Matrix<STATE_DIM, STATE_DIM> P;
Matrix<STATE_DIM, STATE_DIM> F;
Matrix<STATE_DIM, STATE_DIM> Q;
Matrix<MEAS_DIM, STATE_DIM> H;
Matrix<MEAS_DIM, MEAS_DIM> R;
float dt = 0.01f;
public:
ExtendedKalmanFilter() { init(); }
void init() {
x = {1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
P.Identity(); P *= 0.1f;
Q.Identity(); Q(0, 0) = Q(1, 1) = Q(2, 2) = Q(3, 3) = 1e-6f;
Q(4, 4) = Q(5, 5) = Q(6, 6) = 1e-8f;
R.Identity(); R(0, 0) = R(1, 1) = R(2, 2) = 0.1f;
R(3, 3) = R(4, 4) = R(5, 5) = 0.5f;
updateJacobians();
}
void update(float gx, float gy, float gz, float ax, float ay, float az, float mx, float my, float mz, float dt) {
this->dt = dt;
predict(gx, gy, gz);
updateMeasurement(ax, ay, az, mx, my, mz);
}
void getEulerAngles(float &roll, float &pitch, float &yaw) {
float q0 = x(0), q1 = x(1), q2 = x(2), q3 = x(3);
float norm = sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
q0 /= norm; q1 /= norm; q2 /= norm; q3 /= norm;
roll = atan2(2.0f * (q0 * q1 + q2 * q3), 1.0f - 2.0f * (q1 * q1 + q2 * q2));
pitch = asin(2.0f * (q0 * q2 - q3 * q1));
yaw = atan2(2.0f * (q0 * q3 + q1 * q2), 1.0f - 2.0f * (q2 * q2 + q3 * q3));
}
};
ExtendedKalmanFilter ekf;
关键要点总结
- 互补滤波机制:核心公式是
angle = α × (上次角度 + 陀螺积分) + (1-α) × 加速度计角度。α=0.98 表示信任陀螺仪为主,但需配合零漂补偿策略。
- IMU 校准:静态校准必不可少。设备静止时采集样本计算零偏,必要时加入温度补偿和尺度校准。
- PID 整定:建议采用'由小到大'的试凑法。先设 I、D 为 0,增大 P 直到临界振荡,再加入 D 抑制超调。对于平衡车,D 项至关重要。
- 实时性保障:严禁在主循环中使用
delay() 阻塞。应使用硬件定时器中断或 millis() 精确控制采样周期(建议 ≥100Hz)。
- 安全保护:必须设置输出限幅和紧急停止机制。例如检测到角度超限或 IMU 数据异常时,立即切断电机电源。
实际应用中,代码需要根据具体硬件配置(如电机类型、传感器型号)进行调整。务必确认引脚连接正确,并在通电前检查电源隔离情况,避免大电流干扰导致复位。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online