从零构建无人机大脑:STM32与卡尔曼滤波的协同艺术

在无人机的世界里,每一秒的稳定飞行都离不开一个精密的大脑——飞行控制系统。这个系统需要从嘈杂的传感器数据中提取出精准的姿态信息,就像一位技艺精湛的数据雕塑家,从粗糙的大理石中雕琢出精美的艺术品。对于嵌入式开发者和无人机爱好者来说,构建这样一个系统既是一项技术挑战,也是一次创造之旅。

想象一下,当你站在实验室里,面对着一架自己组装的四旋翼无人机,手中握着调试器,眼前是实时波动的数据曲线。这一刻,你不仅是开发者,更是一位与物理世界对话的艺术家。STM32微控制器作为硬件核心,卡尔曼滤波算法作为数据处理灵魂,二者的完美结合将决定无人机能否在天空中优雅起舞。

1. 硬件架构设计与核心组件选型

构建无人机控制系统的第一步是选择合适的硬件组件。这个选择过程需要平衡性能、功耗、成本和实际应用需求。

主控制器选择:STM32F103系列微控制器是入门级无人机的理想选择。它基于ARM Cortex-M3内核,主频可达72MHz,具备丰富的外设接口包括多个定时器、ADC模块和通信接口。更重要的是,它拥有足够的计算能力来处理实时传感器数据和运行控制算法,同时保持较低的功耗。

传感器组合:MPU6050是常见的惯性测量单元(IMU),集成了三轴加速度计和三轴陀螺仪。在实际应用中,我发现这个传感器虽然成本低廉,但噪声水平较高,需要优秀的滤波算法来提取有用信号。对于更高级的应用,可以考虑MPU9250,它额外集成了磁力计,能够提供更完整的姿态信息。

通信模块:ESP8266 WiFi模块为无人机提供了无线通信能力。通过简单的AT指令配置,我们可以建立无人机与地面站之间的数据链路,实时传输飞行状态和接收控制指令。在实际调试中,这个模块的价值不言而喻——你不再需要通过有线连接来调整参数,大大提高了开发效率。

提示:选择组件时不仅要考虑性能参数,还要注意供电需求和物理尺寸。无人机的重量分配对飞行稳定性有显著影响。

2. 开发环境搭建与基础配置

搭建一个高效的开发环境是项目成功的关键。我推荐使用STM32CubeIDE,它集成了STM32CubeMX配置工具和成熟的开发环境,大大简化了外设初始化和代码生成过程。

环境配置步骤

  1. 安装STM32CubeIDE并配置所需的软件包
  2. 创建新项目,选择正确的STM32系列和具体型号
  3. 使用图形化工具配置时钟树、GPIO和外设
  4. 生成初始化代码并导入到开发环境中

关键外设配置:I2C接口用于与MPU6050通信,需要正确配置时序参数。在我的经验中,标准模式(100kHz)通常足够满足需求,但如果你需要更高的数据更新率,可以考虑快速模式(400kHz)。

// I2C初始化示例代码
void MX_I2C1_Init(void)
{
  hi2c1.Instance = I2C1;
  hi2c1.Init.ClockSpeed = 100000;
  hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
  hi2c1.Init.OwnAddress1 = 0;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress2 = 0;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {
    Error_Handler();
  }
}

电源管理:无人机的电源稳定性至关重要。我在多个项目中发现,电机启动时的电流尖峰会严重影响传感器读数。建议为控制电路和动力系统使用独立的稳压器,并在电源输入端添加适当的滤波电容。

3. 传感器数据采集与预处理

获取高质量的传感器数据是一切算法的基础。MPU6050通过I2接口提供原始数据,但这些数据包含各种噪声和误差,需要经过精心处理才能使用。

传感器初始化:MPU6050需要正确配置才能正常工作。这包括设置量程、采样率和唤醒设备。我通常将加速度计量程设为±4g,陀螺仪设为±500°/s,这样在保证精度的同时避免数据溢出。

// MPU6050初始化函数
uint8_t MPU6050_Init(I2C_HandleTypeDef *hi2c)
{
  uint8_t check;
  uint8_t data;
  
  // 检查设备ID
  HAL_I2C_Mem_Read(hi2c, MPU6050_ADDR, WHO_AM_I, 1, &check, 1, 1000);
  if (check != 0x68) return 1;
  
  // 电源管理设置
  data = 0x00;
  HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, PWR_MGMT_1, 1, &data, 1, 1000);
  
  // 配置加速度计
  data = 0x08;  // ±4g
  HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, ACCEL_CONFIG, 1, &data, 1, 1000);
  
  // 配置陀螺仪
  data = 0x08;  // ±500°/s
  HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, GYRO_CONFIG, 1, &data, 1, 1000);
  
  // 设置采样率
  data = 0x07;  // 1kHz/(7+1)=125Hz
  HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, SMPLRT_DIV, 1, &data, 1, 1000);
  
  return 0;
}

数据读取与转换:原始数据是16位有符号整数,需要根据量程设置转换为物理量。这个转换过程看起来简单,但在实际应用中,我发现了几个容易出错的细节:

参数 量程设置 转换系数 注意事项
加速度计 ±2g 16384 LSB/g 温度影响显著
加速度计 ±4g 8192 LSB/g 推荐设置
加速度计 ±8g 4096 LSB/g 噪声增加
陀螺仪 ±250°/s 131 LSB/°/s 高精度模式
陀螺仪 ±500°/s 65.5 LSB/°/s 平衡选择
陀螺仪 ±1000°/s 32.8 LSB/°/s 高速运动

温度补偿:MPU6050内置温度传感器,读数可以帮助补偿温度漂移。在我的项目中,温度补偿使姿态估计精度提高了约30%。具体实现是通过建立温度与零偏的关系模型,实时调整校准参数。

4. 卡尔曼滤波:从噪声中提取真实信号

卡尔曼滤波是无人机姿态估计的核心算法。它通过融合加速度计和陀螺仪的数据,利用各自的优点,弥补各自的缺点。陀螺仪提供短期精度但存在漂移,加速度计提供长期稳定性但噪声较大。

滤波器初始化:卡尔曼滤波器需要设置过程噪声协方差Q和测量噪声协方差R。这些参数需要根据实际传感器特性进行调整,在我的经验中,这是一个需要反复试验的过程。

typedef struct {
  float q;    // 过程噪声协方差
  float r;    // 测量噪声协方差
  float x;    // 估计值
  float p;    // 估计误差协方差
  float k;    // 卡尔曼增益
} KalmanFilter;

void Kalman_Init(KalmanFilter *kf, float q, float r, float initial_value)
{
  kf->q = q;
  kf->r = r;
  kf->x = initial_value;
  kf->p = 1.0f;  // 初始估计误差
  kf->k = 0.0f;
}

滤波算法实现:卡尔曼滤波包含预测和更新两个阶段。预测阶段基于系统模型估计当前状态,更新阶段利用新的测量值修正估计。这个算法的美妙之处在于它能够根据系统的不确定性自动调整对模型和测量的信任度。

float Kalman_Update(KalmanFilter *kf, float measurement)
{
  // 预测阶段
  kf->p = kf->p + kf->q;
  
  // 更新阶段
  kf->k = kf->p / (kf->p + kf->r);
  kf->x = kf->x + kf->k * (measurement - kf->x);
  kf->p = (1 - kf->k) * kf->p;
  
  return kf->x;
}

参数整定技巧:经过多个项目的积累,我总结出一些参数整定的经验:

  • 开始时将Q设得较小,R设得较大,观察滤波器响应
  • 如果估计值对测量值响应过慢,适当增大Q或减小R
  • 如果估计值噪声过大,适当减小Q或增大R
  • 在实际飞行中微调参数,观察无人机的实际响应

注意:卡尔曼滤波参数对性能影响巨大,但往往需要通过实际测试来调整。建议记录飞行数据,在地面分析软件中反复调试。

5. 姿态解算与四元数表示

从滤波后的传感器数据到实际姿态角需要经过复杂的数学变换。虽然欧拉角直观易懂,但存在万向节锁问题,因此我推荐使用四元数表示姿态。

四元数基础:四元数由四个分量组成,可以表示任意三维旋转。与欧拉角相比,四元数计算更高效,且避免了奇异性问题。在我的实现中,四元数更新频率达到500Hz,而欧拉角转换只在需要时进行。

姿态更新算法:基于陀螺仪数据的四元数更新算法:

void Quaternion_Update(float *q, float gx, float gy, float gz, float dt)
{
  float norm;
  float vx, vy, vz;
  float ex, ey, ez;
  
  // 标准化加速度计读数
  norm = sqrt(ax * ax + ay * ay + az * az);
  ax /= norm;
  ay /= norm;
  az /= norm;
  
  // 估计方向重力
  vx = 2.0f * (q[1] * q[3] - q[0] * q[2]);
  vy = 2.0f * (q[0] * q[1] + q[2] * q[3]);
  vz = q[0] * q[0] - q[1] * q[1] - q[2] * q[2] + q[3] * q[3];
  
  // 误差是交叉积 between estimated and measured direction
  ex = (ay * vz - az * vy);
  ey = (az * vx - ax * vz);
  ez = (ax * vy - ay * vx);
  
  // 积分误差比例增益
  exInt += ex * Ki;
  eyInt += ey * Ki;
  ezInt += ez * Ki;
  
  // 调整后的陀螺仪读数
  gx += Kp * ex + exInt;
  gy += Kp * ey + eyInt;
  gz += Kp * ez + ezInt;
  
  // 四元数导数
  float q0 = q[0], q1 = q[1], q2 = q[2], q3 = q[3];
  q[0] += (-q1 * gx - q2 * gy - q3 * gz) * (0.5f * dt);
  q[1] += (q0 * gx + q2 * gz - q3 * gy) * (0.5f * dt);
  q[2] += (q0 * gy - q1 * gz + q3 * gx) * (0.5f * dt);
  q[3] += (q0 * gz + q1 * gy - q2 * gx) * (0.5f * dt);
  
  // 标准化四元数
  norm = sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
  q[0] /= norm;
  q[1] /= norm;
  q[2] /= norm;
  q[3] /= norm;
}

四元数到欧拉角转换:虽然内部使用四元数,但控制算法通常需要欧拉角。转换公式如下:

void Quaternion_To_Euler(float *q, float *roll, float *pitch, float *yaw)
{
  *roll = atan2f(2.0f * (q[0] * q[1] + q[2] * q[3]), 
                  1.0f - 2.0f * (q[1] * q[1] + q[2] * q[2]));
  *pitch = asinf(2.0f * (q[0] * q[2] - q[3] * q[1]));
  *yaw = atan2f(2.0f * (q[0] * q[3] + q[1] * q[2]), 
                 1.0f - 2.0f * (q[2] * q[2] + q[3] * q[3]));
}

在实际应用中,我发现俯仰角接近±90°时,滚转角和偏航角的计算会出现奇异点。这种情况下,需要采用特殊处理方法,或者直接使用四元数进行控制计算。

6. PID控制算法实现与调参

PID控制器是飞行控制的核心,它根据期望姿态和实际姿态的差异计算电机控制量。我推荐使用串级PID结构,内环控制角速度,外环控制角度,这种结构提供了更好的抗干扰性能。

控制器结构定义

typedef struct {
  float kp;           // 比例系数
  float ki;           // 积分系数
  float kd;           // 微分系数
  float integral;     // 积分项
  float prev_error;   // 上一次误差
  float output_limit; // 输出限幅
} PIDController;

void PID_Init(PIDController *pid, float kp, float ki, float kd, float limit)
{
  pid->kp = kp;
  pid->ki = ki;
  pid->kd = kd;
  pid->integral = 0.0f;
  pid->prev_error = 0.0f;
  pid->output_limit = limit;
}

控制算法计算

float PID_Compute(PIDController *pid, float setpoint, float measurement, float dt)
{
  float error = setpoint - measurement;
  
  // 比例项
  float proportional = pid->kp * error;
  
  // 积分项(抗积分饱和)
  pid->integral += error * dt;
  if (pid->integral > pid->output_limit) pid->integral = pid->output_limit;
  if (pid->integral < -pid->output_limit) pid->integral = -pid->output_limit;
  float integral = pid->ki * pid->integral;
  
  // 微分项(采用测量值微分)
  float derivative = pid->kd * (measurement - pid->prev_error) / dt;
  pid->prev_error = measurement;
  
  // 总和输出
  float output = proportional + integral - derivative;
  
  // 输出限幅
  if (output > pid->output_limit) output = pid->output_limit;
  if (output < -pid->output_limit) output = -pid->output_limit;
  
  return output;
}

参数整定经验:PID参数整定是一个需要耐心的过程。我通常采用以下步骤:

  1. 初始化所有参数为零,逐步增加比例系数直到系统开始振荡
  2. 记录振荡时的比例系数,将其设置为临界值的60-70%
  3. 增加微分系数来抑制超调和振荡
  4. 最后调整积分系数来消除稳态误差

在实际调试中,我发现角速度环的参数相对固定,而角度环参数需要根据具体飞行器的惯性特性进行调整。重无人机需要更小的比例系数和更大的微分系数。

7. 无线通信与地面站交互

ESP8266模块为无人机提供了与地面站的无线连接。这个连接不仅用于传输遥测数据,还允许实时调整控制参数,极大提高了开发效率。

WiFi模块初始化

void ESP8266_Init(UART_HandleTypeDef *huart)
{
  char cmd[64];
  
  // 重启模块
  sprintf(cmd, "AT+RST\r\n");
  HAL_UART_Transmit(huart, (uint8_t*)cmd, strlen(cmd), 1000);
  HAL_Delay(1000);
  
  // 设置为Station模式
  sprintf(cmd, "AT+CWMODE=1\r\n");
  HAL_UART_Transmit(huart, (uint8_t*)cmd, strlen(cmd), 1000);
  HAL_Delay(500);
  
  // 连接WiFi
  sprintf(cmd, "AT+CWJAP=\"%s\",\"%s\"\r\n", WIFI_SSID, WIFI_PASSWORD);
  HAL_UART_Transmit(huart, (uint8_t*)cmd, strlen(cmd), 1000);
  HAL_Delay(3000);
  
  // 启用多连接
  sprintf(cmd, "AT+CIPMUX=1\r\n");
  HAL_UART_Transmit(huart, (uint8_t*)cmd, strlen(cmd), 1000);
  HAL_Delay(500);
  
  // 创建TCP服务器
  sprintf(cmd, "AT+CIPSERVER=1,%d\r\n", TCP_PORT);
  HAL_UART_Transmit(huart, (uint8_t*)cmd, strlen(cmd), 1000);
  HAL_Delay(500);
}

数据协议设计:我设计了一个简单的二进制协议来传输数据,包含数据头、时间戳、姿态数据和校验和。这种设计既保证了传输效率,又提供了足够的数据完整性检查。

#pragma pack(push, 1)
typedef struct {
  uint8_t header[2];      // 数据头 0xAA, 0x55
  uint32_t timestamp;     // 时间戳 ms
  float roll;             // 滚转角 deg
  float pitch;            // 俯仰角 deg
  float yaw;              // 偏航角 deg
  float altitude;         // 高度 m
  uint16_t checksum;      // 校验和
} TelemetryData;
#pragma pack(pop)

地面站软件:使用Python和PyQt开发了一个简单的地面站软件,可以实时显示姿态数据、绘制曲线和调整参数。这个工具在调试过程中发挥了重要作用,让我能够直观地观察算法效果。

在实际项目中,无线通信的稳定性是一个挑战。我采用了以下措施来提高可靠性:

  • 数据包添加序列号和重传机制
  • 关键参数使用确认机制
  • 定期检查连接状态,必要时重新连接

8. 系统集成与实战调试

将各个模块整合成一个完整的系统是项目中最有挑战性的部分。这个阶段需要处理时序问题、资源冲突和性能优化。

主循环设计:我采用了基于定时器中断的多任务架构,确保关键任务按时执行。姿态估计和控制计算放在高优先级中断中,而数据记录和通信放在低优先级任务中。

// 主循环结构
int main(void)
{
  // 硬件初始化
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_USART2_UART_Init();
  MX_TIM1_Init();
  
  // 模块初始化
  MPU6050_Init(&hi2c1);
  ESP8266_Init(&huart2);
  Kalman_Init(&kf_roll, 0.001f, 0.03f, 0.0f);
  Kalman_Init(&kf_pitch, 0.001f, 0.03f, 0.0f);
  PID_Init(&pid_roll, 1.2f, 0.05f, 0.01f, 400.0f);
  
  // 主循环
  while (1)
  {
    // 低优先级任务
    if (telemetry_timer >= TELEMETRY_INTERVAL)
    {
      Send_Telemetry_Data();
      telemetry_timer = 0;
    }
    
    // 空闲时处理通信数据
    Process_Communication();
    
    // 系统状态指示灯
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    HAL_Delay(100);
  }
}

// 高优先级定时器中断
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM1)  // 1kHz中断
  {
    // 读取传感器数据
    Read_MPU6050(&hi2c1, accel, gyro);
    
    // 姿态估计
    roll = Kalman_Update(&kf_roll, gyro[0], accel[0]);
    pitch = Kalman_Update(&kf_pitch, gyro[1], accel[1]);
    
    // 姿态控制
    roll_output = PID_Compute(&pid_roll, target_roll, roll, 0.001f);
    pitch_output = PID_Compute(&pid_pitch, target_pitch, pitch, 0.001f);
    
    // 电机控制
    Motor_Output(roll_output, pitch_output, 0, 0);
    
    // 更新计时器
    telemetry_timer++;
  }
}

调试技巧:在系统集成过程中,我遇到了许多问题并积累了一些调试经验:

  • 使用逻辑分析仪检查I2C时序和中断频率
  • 添加状态指示灯来监控系统运行状态
  • 实现数据记录功能,将关键变量保存到内存中,事后分析
  • 逐步增加功能,每步都进行充分测试

性能优化:STM32F103的资源有限,需要精心优化代码。我采用了以下优化措施:

  • 使用查表法代替复杂数学函数
  • 将频繁访问的变量定义为register类型
  • 使用DMA传输数据,减少CPU开销
  • 优化算法计算顺序,减少重复计算

经过多次迭代和优化,最终的系统能够在72MHz的主频下稳定运行,完成所有实时计算任务,为无人机提供可靠的控制信号。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐