☰
C# 无人机飞控算法实战:分层架构、姿态解算与串口通信
2026/10/9 1:01:24 网站建设 项目流程

1. 项目缘起与整体架构拆解

1.1 为什么选择用 C# 来做无人机飞控

一提到无人机飞控,大多数人脑子里蹦出来的第一反应是 C 或者 C++,毕竟 Betaflight、PX4、ArduPilot 这些主流固件清一色都是 C/C++ 写的,跑在 STM32 这类资源受限的 MCU 上,对实时性和内存占用抠得极细。我一开始也是这个思路,直到真正动手做了一套基于 C# 的机载飞控算法之后,才发现这件事没有想象中那么“离经叛道”。

先说清楚这套东西到底解决什么问题。传统飞控开发有个很现实的痛点:算法验证周期长。你在 MATLAB/Simulink 里把姿态解算、PID 调参、路径规划跑通了,要移植到嵌入式 C 上,得重写一遍,还得处理定点数、内存对齐、中断优先级这些破事。改一个参数,编译、烧录、试飞,一轮下来半天没了。而 C# 跑在 .NET 运行时上,配合硬件在环仿真(HIL)和凤凰无人机模拟器这类工具,可以在 PC 上先把控制律跑通,再通过串口或者 UDP 把控制量下发给真实飞控板,形成“高层算法在 C#、底层执行在 MCU”的分层架构。

这套方案适合谁?我总结下来是三类人:一是做算法研究但不想深陷嵌入式细节的工程师;二是想快速验证视觉感知、路径规划这类计算密集型模块的开发者;三是教学场景下需要让学生直观看到控制算法每一步输出的老师。它不适合追求极致实时性(微秒级)的竞速穿越机,但对农业植保、航道巡查、果实采摘这类对实时性要求没那么变态的应用,完全够用。

1.2 分层架构的核心设计思路

整套系统的架构我拆成三层来看,这样理解起来最清晰。

最底层是执行层,由 STM32 或者类似的 MCU 负责,跑的是电机 PWM 输出、IMU 原始数据采集、遥控信号解析这些硬实时任务。这一层用 C 写,中断响应在微秒级,不能动。

中间是通信层,负责 C# 上位机和 MCU 之间的数据交换。我实测下来,串口通信(UART)在 921600 波特率下,一帧 32 字节的数据往返延迟大概在 2-3ms,对于 50Hz 的控制频率来说够用。如果对带宽要求更高,比如要传图像或者点云,那就走 UDP over WiFi,但延迟会跳到 10-20ms,得在控制律里做补偿。

最上层是算法层,也就是 C# 的主战场。姿态解算、位置控制、路径规划、视觉感知这些模块都在这里跑。.NET 的 JIT 编译器在运行时会做大量优化,实测一个 200Hz 的卡尔曼滤波器在 i5 上占用不到 5% 的单核资源,完全不是瓶颈。

注意:分层架构的关键是明确每一层的职责边界。我见过有人把 PID 放在 MCU 里,又把姿态解算放在 C# 里,结果两边数据不同步,飞起来像喝醉酒。我的建议是:所有涉及高频闭环的控制律要么全在 MCU,要么全在 C#,不要混着放。

1.3 开源生态的选型考量

既然是开源项目,选型就不能只看技术,还得看社区活跃度和可维护性。C# 这边我主要依赖几个库:MathNet.Numerics 做矩阵运算和滤波,System.IO.Ports 做串口通信,如果涉及视觉就上 OpenCvSharp。这些库在 NuGet 上都能直接拉,版本迭代也稳定。

对比一下 C++ 方案,C# 的优势在于开发效率。同样一个扩展卡尔曼滤波器,C++ 写下来加上调试大概要两天,C# 半天就能跑通,因为不用管内存管理和指针越界。劣势也很明显:.NET 运行时本身占内存,一个空的控制台程序启动就要 20MB 左右,而 STM32 整个固件可能才 100KB。所以这套方案的前提是机载计算机有足够的资源,比如树莓派或者 Jetson Nano 这类。

2. 核心算法模块的细节拆解

2.1 姿态解算:从原始 IMU 数据到欧拉角

姿态解算是飞控的地基,这块要是飘了,后面所有控制都是空中楼阁。我用的是 Mahony 互补滤波,相比卡尔曼滤波,它计算量小、参数少,在 C# 上跑 1kHz 毫无压力。

核心逻辑是这样的:陀螺仪积分得到角度,但会有零漂累积;加速度计测量重力方向,长期稳定但动态响应差、噪声大。互补滤波就是用高通滤陀螺仪、低通滤加速度计,然后加权融合。Mahony 算法的精妙之处在于它用四元数表示姿态,通过加速度计观测到的重力向量和预测重力向量的叉积来修正陀螺仪漂移。

具体到 C# 实现,几个关键点必须注意。第一,陀螺仪的零偏校准要在上电静止时做,采样 500 次取平均,我实测下来零偏能压到 0.01°/s 以内。第二,加速度计在电机振动大的时候噪声会淹没信号,必须加低通滤波,截止频率设在 20Hz 左右比较合适。第三,四元数归一化每帧都要做,不然数值误差累积会让姿态矩阵失去正交性。

// Mahony 互补滤波核心更新 public void Update(float gx, float gy, float gz, float ax, float ay, float az, float dt) { // 归一化加速度计测量值 float norm = MathF.Sqrt(ax * ax + ay * ay + az * az); ax /= norm; ay /= norm; az /= norm; // 估计重力方向 float vx = 2 * (q1 * q3 - q0 * q2); float vy = 2 * (q0 * q1 + q2 * q3); float vz = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3; // 误差叉积 float ex = ay * vz - az * vy; float ey = az * vx - ax * vz; float ez = ax * vy - ay * vx; // PI 修正陀螺仪 gx += Kp * ex + Ki * integralFB.x; gy += Kp * ey + Ki * integralFB.y; gz += Kp * ez + Ki * integralFB.z; // 四元数积分 float halfDt = 0.5f * dt; q0 += (-q1 * gx - q2 * gy - q3 * gz) * halfDt; q1 += (q0 * gx + q2 * gz - q3 * gy) * halfDt; q2 += (q0 * gy - q1 * gz + q3 * gx) * halfDt; q3 += (q0 * gz + q1 * gy - q2 * gx) * halfDt; // 归一化 norm = MathF.Sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); q0 /= norm; q1 /= norm; q2 /= norm; q3 /= norm; }

Kp 和 Ki 这两个参数我调了很久,最后定在 Kp=2.0、Ki=0.005 比较稳。Kp 太大姿态会跟着振动抖,太小收敛慢;Ki 用来消除稳态误差,但太大会引入低频振荡。

2.2 串口通信协议的设计与实现

C# 和 MCU 之间的通信协议,我踩过最大的坑就是帧同步。一开始用简单的“帧头+数据+校验”结构,结果偶尔会丢字节导致整个帧错位,飞控收到乱数据直接翻车。

后来改成固定长度帧加双字节帧头(0xAA 0x55),再加 CRC16 校验,稳定性大幅提升。帧结构设计如下:

字段长度(字节)说明
帧头2固定 0xAA 0x55
帧类型10x01 姿态数据,0x02 控制指令
数据长度1后续数据字节数
数据区N实际载荷
CRC162从帧类型到数据区的校验

C# 这边用SerialPort类,但有个坑必须注意:DataReceived事件是在后台线程触发的,不能在里面直接更新 UI 或者访问非线程安全的对象。我的做法是收到数据后先塞进一个ConcurrentQueue,主循环再取出来处理。

private ConcurrentQueue<byte[]> _rxQueue = new ConcurrentQueue<byte[]>(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { int count = _serialPort.BytesToRead; byte[] buffer = new byte[count]; _serialPort.Read(buffer, 0, count); _rxQueue.Enqueue(buffer); } // 主循环中解析 private void ProcessRxQueue() { while (_rxQueue.TryDequeue(out byte[] data)) { _parser.Append(data); while (_parser.TryGetFrame(out Frame frame)) { HandleFrame(frame); } } }

提示:串口波特率不是越高越好。我试过 2M 波特率,结果因为线材质量一般,误码率飙升。921600 是个比较稳妥的选择,配合 50Hz 的控制频率,带宽绰绰有余。

2.3 位置控制与路径规划的实现

位置控制这块,我用的是串级 PID:外环位置、内环速度。外环根据目标位置和当前位置算出期望速度,内环再根据期望速度和实际速度算出期望姿态角,最后交给姿态控制器。

路径规划我选了 A* 算法做全局规划,局部用动态窗口法(DWA)避障。A* 在 C# 上跑,栅格地图 200x200 的情况下,一次规划大概 10-20ms,对于农业植保这种场景完全够用。如果是航道巡查这种长距离任务,我会把地图分块,只规划当前块和相邻块,减少计算量。

这里有个经验:路径规划的输出不要直接给位置控制器,中间要加一个轨迹生成器,把离散的路径点平滑成连续的位置、速度、加速度曲线。我用的是五次多项式插值,保证加速度连续,这样电机不会频繁正反转,飞起来更稳。

3. 实操过程与关键环节实现

3.1 开发环境搭建与依赖配置

先把环境搭起来。我用的 Visual Studio 2022 社区版,.NET 6 运行时。新建一个控制台项目,然后通过 NuGet 装这几个包:

dotnet add package MathNet.Numerics dotnet add package System.IO.Ports dotnet add package OpenCvSharp4 dotnet add package OpenCvSharp4.runtime.win

MathNet 用来做矩阵运算,姿态解算里的旋转矩阵、卡尔曼滤波的协方差矩阵都靠它。System.IO.Ports 在 .NET Core 之后需要单独装,不像 .NET Framework 那样自带。OpenCvSharp 是给视觉感知模块用的,如果只做飞控可以不装。

MCU 那边我用的是 STM32F405,跑 ChibiOS 实时系统。固件里实现了一个简单的通信状态机,收到 C# 发来的控制指令后,直接映射到电机 PWM。这里要注意,MCU 端必须做失控保护:如果超过 500ms 没收到 C# 的指令,自动切回手动模式或者悬停。

3.2 硬件在环仿真的搭建步骤

硬件在环(HIL)是这套方案的精髓,能在不炸机的前提下验证算法。具体步骤:

  1. 把真实飞控板通过 USB 转串口接到 PC,电机不接桨叶。
  2. C# 程序读取飞控板传来的 IMU 数据,跑姿态解算和控制律。
  3. 控制量通过串口发回飞控板,驱动电机。
  4. 飞控板的 IMU 会感受到电机振动,形成闭环。

但这样有个问题:飞控板被固定着,姿态不会真的变化。所以更完整的做法是接入凤凰无人机模拟器,让模拟器根据控制量计算虚拟姿态,再把虚拟 IMU 数据发给 C#。这样整个闭环都在虚拟环境里跑,安全且可重复。

我实测下来,HIL 模式下姿态控制的响应和真机试飞非常接近,PID 参数可以直接迁移,省去了大量试飞调参的时间。

3.3 参数整定的实操记录

PID 参数整定我走的是“先内后外、先 P 后 I 再 D”的路子。以姿态角速度环为例:

第一步,把 I 和 D 置零,P 从 0.5 开始加,每次加 0.5,直到出现等幅振荡。我这边 P 加到 3.0 开始振荡,于是取 2.0 作为基准。

第二步,加 D 抑制振荡。D 从 0.01 开始,每次翻倍,直到振荡消失且响应不迟钝。最后 D 定在 0.08。

第三步,加 I 消除稳态误差。I 从 0.001 开始,每次加 0.001,直到稳态误差在 0.5° 以内。最后 I 定在 0.005。

整个过程在 HIL 下大概花了两个小时,如果真机试飞,估计得摔好几次。

控制环PID说明
角速度环2.00.0050.08内环,响应快
角度环4.00.010.15外环,输出角速度期望
速度环1.50.020.05位置控制内环
位置环0.80.0050.1位置控制外环

注意:不同机架的参数差异很大。上面这组参数对应的是 450mm 轴距的四轴,载重 1.5kg。如果你的机架更大或者载重更高,P 要相应减小,否则容易超调。

4. 常见问题与排查技巧实录

4.1 姿态解算漂移的排查思路

姿态漂移是最常见的问题,表现是无人机悬停时慢慢往一个方向偏。排查顺序我总结成一张表:

现象可能原因排查方法解决
缓慢单向漂移陀螺仪零偏未校准静止时看角速度输出重新校准,采样 500 次
振动时姿态跳变加速度计噪声大看振动频谱加低通滤波,做减振
快速旋转后漂移四元数未归一化检查代码每帧归一化
温度变化后漂移陀螺仪温漂冷热环境下对比加温度补偿

我遇到过一次特别诡异的漂移,排查了半天发现是电机振动通过机架传到了 IMU,导致加速度计输出饱和。后来在 IMU 下面垫了减振棉,问题解决。所以硬件减振和软件滤波要一起上,缺一不可。

4.2 串口通信丢包的定位方法

丢包的表现是控制指令偶尔不响应,或者姿态数据跳变。定位方法很简单:在 C# 端统计每秒收到的帧数和 CRC 错误数。如果帧数明显低于发送频率,说明丢包;如果 CRC 错误多,说明线路干扰。

我遇到过两种情况。一种是 USB 转串口芯片质量差,换了个 FT232 的模块就好了。另一种是电机电源线和信号线捆在一起,电磁干扰导致误码,把线分开走就解决了。

4.3 控制延迟的补偿技巧

从 C# 算出控制量到 MCU 执行,中间有通信延迟。这个延迟如果不补偿,高频控制时会相位滞后,导致振荡。我的做法是在控制律里加一个延迟补偿项,根据实测延迟时间(我这边是 3ms)对期望值做超前预测。

具体来说,如果当前时刻是 t,控制量要在 t+3ms 执行,那我就用 t 时刻的状态外推 3ms 后的状态,用外推后的状态算控制量。外推用一阶保持就行,简单有效。

4.4 视觉感知模块的集成坑点

如果要做农业病虫害识别或者果实采摘,视觉模块的集成有几个坑。第一,OpenCvSharp 在 .NET 6 下需要手动拷贝 native DLL,不然运行时报找不到 dll。第二,图像采集和处理很吃 CPU,建议单独开一个线程,不要和飞控主循环抢资源。第三,识别结果到控制指令的映射要做平滑,不然识别框一跳,无人机就跟着抖。

我一般把视觉处理频率降到 10Hz,中间用卡尔曼滤波做目标跟踪,这样既保证了识别精度,又不会拖累控制频率。

5. 从这套方案延伸出去的几个方向

这套 C# 飞控算法跑通之后,我发现它的扩展性比想象中好。比如农业植保场景,可以在 C# 层加一个病虫害识别模型,识别到病害区域后自动生成喷洒路径,直接下发给飞控。又比如航道巡查,可以把 AIS 数据和视觉感知融合,做更智能的避障。

还有一个我觉得很有意思的方向是集群控制。C# 在网络编程方面比 C++ 舒服太多,用 gRPC 或者 MQTT 做多机通信,几十行代码就能搭起一个集群控制框架。我试过用三台无人机做编队,C# 端跑一致性算法,效果还不错。

最后分享一个我在实际项目中总结的小技巧:C# 的Stopwatch类精度比DateTime高得多,做控制循环计时一定要用它。我一开始用DateTime.Now算 dt,结果精度只有 15ms 左右,控制循环抖得厉害。换成Stopwatch之后,dt 精度到微秒级,控制平滑了很多。这个坑不大,但很隐蔽,希望能帮你省点调试时间。

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

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

立即咨询