跳转至

导学案4 · 附加材料:飞控代码里的三个基础技巧(选读)

前置要求:导学案4之前(会 while / for / switch / if) 参考课程:无(实验室自制,配套 飞控主控代码) 预计用时:1 小时 本节目标:认识飞控代码里三个最基础的技巧:状态机、单次执行、计数器分频 选读说明:这是选读材料,看不懂不影响后续学习。目的是分享变成经验,让你产生概念。


附.0 先看一眼真实代码长什么样

飞控主控代码 是实验室真实飞控的主控任务:一键起飞 → 短悬停 → 飞四个航点画一个正方形 → 回到原点 → 降落。

它有两百多行、一堆没见过的函数名,看起来很吓人,而且它只是飞控众多代码文件中的一个单文件。但它的骨架只有三样东西,全部是你已经学过的:while 循环、switch、if。

这一节我们用终端代替传感器和电机("读高度"换成一个变量自加,"控制电机"换成 printf),把这三个技巧各演一遍,最后回去对照真实代码。


附.1 主循环:单片机的程序是怎么跑的

电脑上的程序"跑完就退出"。单片机上的程序永远不退出:

while (1) {
    /* 读传感器 -> 做决策 -> 输出控制 */
}

每圈之间隔几毫秒(飞控里常见 1ms、5ms、10ms),一圈叫一个周期。飞机在天上时你绝不允许程序退出,所以这个死循环是必需品,不是 bug——导学案4讲死循环时留下的那句话,在这里兑现。

在电脑上怎么演示一个"会退出的主循环"?加一个退出条件就行:

#include <stdio.h>

int main(void)
{
    int tick = 0;

    while (1) {
        tick++;
        printf("第 %d 圈:读传感器 -> 做决策 -> 输出控制\n", tick);
        if (tick >= 3) {
            break;          /* 电脑上演示用;单片机上不存在这一句 */
        }
    }

    return 0;
}

输出:

第 1 圈:读传感器 -> 做决策 -> 输出控制
第 2 圈:读传感器 -> 做决策 -> 输出控制
第 3 圈:读传感器 -> 做决策 -> 输出控制

后面要讲的三个技巧,全都发生在这个循环里面。


附.2 技巧一:状态机(switch + 状态变量)

问题:任务是一步一步的(先起飞、再悬停、再前进、再降落),但主循环是反复执行的。怎么让"起飞"这个阶段执行很多圈、完成之后自动进入下一阶段?

答案:用一个变量记录"现在在第几步",每圈用 switch 按它选择要执行的代码块;条件满足时把它改成下一步。这个变量叫状态变量,改它的动作叫状态迁移。

#include <stdio.h>

int main(void)
{
    int state = 0;        /* 0 起飞, 1 悬停, 2 前进, 3 降落, 99 结束 */
    int height = 0;       /* 模拟高度 cm */
    int front = 0;        /* 模拟前进距离 cm */
    int count = 0;        /* 每个状态自己的计数器 */

    while (1) {
        count++;

        switch (state) {
            case 0:                                  /* 起飞 */
                height += 20;
                printf("起飞中…… 高度 %d cm\n", height);
                if (height >= 100) {
                    state = 1;
                    count = 0;
                }
                break;

            case 1:                                  /* 悬停 */
                printf("悬停中…… 第 %d 圈\n", count);
                if (count >= 3) {
                    state = 2;
                    count = 0;
                }
                break;

            case 2:                                  /* 前进 */
                front += 50;
                printf("前进中…… 已飞 %d cm\n", front);
                if (front >= 150) {
                    state = 3;
                    count = 0;
                }
                break;

            case 3:                                  /* 降落 */
                height -= 25;
                printf("降落中…… 高度 %d cm\n", height);
                if (height <= 0) {
                    state = 99;
                }
                break;

            default:
                break;
        }

        if (state == 99) {
            printf("任务完成,程序退出\n");
            break;
        }
    }

    return 0;
}

输出:

起飞中…… 高度 20 cm
起飞中…… 高度 40 cm
起飞中…… 高度 60 cm
起飞中…… 高度 80 cm
起飞中…… 高度 100 cm
悬停中…… 第 1 圈
悬停中…… 第 2 圈
悬停中…… 第 3 圈
前进中…… 已飞 50 cm
前进中…… 已飞 100 cm
前进中…… 已飞 150 cm
降落中…… 高度 75 cm
降落中…… 高度 50 cm
降落中…… 高度 25 cm
降落中…… 高度 0 cm
任务完成,程序退出

读这段代码的方法:盯着 state 看。 它从 0 开始,每圈 switch 选中对应的 case;height >= 100 成立时 state = 1,下一圈就进入悬停。每个 case 里都是同一套结构:干活 → 判断条件 → 条件满足就迁移状态。

为什么不用 if - else if?也能写。但状态一多(真实代码有 7 个状态),switch 的表格感更清楚,而且每个 case 自带 break,不容易串。

对照真实代码:switch (OrdinalNum) 就是它。case 0 一键起飞、case 1 短悬停、case 2 ~ 5 是正方形的四条边(四个航点)、case 99 降落;OrdinalNum++ 就是状态迁移。骨架和上面这个 demo 一模一样,只是把 printf 换成了真的控制函数。


附.3 技巧二:单次执行(进入状态时只做一次的事)

问题:主循环每圈都会跑 case 里的代码。但有些事只该做一次——进入悬停状态时把计时器清零、切换模式时发一条通知。写成普通语句,就会每圈都做一遍。

办法:一个标志变量,初值 1;进来时判断它,成立就把要做的事做完、然后立刻把标志关掉。于是这段代码在整个任务期间只会执行一次。

#include <stdio.h>

int main(void)
{
    int flag = 1;         /* 单次执行标志 */
    int count = 0;

    while (count < 5) {
        count++;

        if (flag == 1) {
            flag = 0;
            printf("第 %d 圈:进入任务,做一次初始化(这句只会出现一次)\n", count);
        }

        printf("第 %d 圈:执行任务的常规动作\n", count);
    }

    return 0;
}

输出:

第 1 圈:进入任务,做一次初始化(这句只会出现一次)
第 1 圈:执行任务的常规动作
第 2 圈:执行任务的常规动作
第 3 圈:执行任务的常规动作
第 4 圈:执行任务的常规动作
第 5 圈:执行任务的常规动作

真实代码里每个 case 的开头都是这个形状,一模一样:

/* 片段:飞控主控代码.md 中 case 1(短悬停)的开头 */
if (SFlag == 1)
{
    SFlag = 0;                 /* 关掉标志,下次进来就不成立了 */
    TaskTimer_ms = 0;          /* 只执行一次的初始化 */
    TaskConfirmCount = 0;
}

类型与命名小注:真实代码用 unsigned char SFlag(1 字节,省内存),单片机代码里常写成 u8;变量名也多是 SFlag、TaskConfirmCount 这类缩写,属于嵌入式的老习惯。这些都不用模仿——教学阶段统一用 int 和导学案2 的命名规则,道理完全一样;等你真上单片机,再按项目规范切换。


附.4 技巧三:计数器分频(一句话版)

主循环每 1ms 跑一圈,但不是所有事都要每圈做:有的要"每 100ms 发一次数据",有的要"连续确认 200 次到位才算真到位"(防止传感器偶尔抽风造成误判)。办法就是数圈数。三种常见写法:

想做的事 写法
每 N 圈做一次 if (tick % N == 0) { ... }
累计够 T 毫秒做一次 timer += dT_ms; if (timer >= T) { ...; timer = 0; }
连续 N 次满足条件才认 if (条件) count++; else count = 0; 然后 if (count >= N) { ... }
#include <stdio.h>

int main(void)
{
    for (int tick = 1; tick <= 10; tick++) {
        if (tick % 3 == 0) {
            printf("第 %2d 圈:发送一次数据\n", tick);
        }
    }
    return 0;
}

输出:

第  3 圈:发送一次数据
第  6 圈:发送一次数据
第  9 圈:发送一次数据

真实代码里三样都在:TaskTimer_ms += dT_ms; 配 if (TaskTimer_ms <= 5000) 是"悬停满 5 秒";ZGS_tm1 = TaskTimer_ms / 100; 是把毫秒换算成 0.1 秒发上去显示;if (TaskConfirmCount >= 200) 是"连续 200 次确认到位才切下一个航点"。


附.5 三个技巧合起来:回去看真实代码

我们的 demo 飞控主控代码.md 里 是什么
int state unsigned char OrdinalNum 状态变量
switch (state) switch (OrdinalNum) 状态机
state = 1; OrdinalNum++; 状态迁移
int flag = 1; unsigned char SFlag = 1; 单次执行标志
count++ / count >= 3 TaskConfirmCount++ / >= 200 计数器
printf(...) LZY_DataSand_F3(0x01) 等 "输出":真实代码是往串口发数据
height、front rplidar_st.x / y / yaw "输入":真实代码是激光雷达的读数

动手做一件事:打开 飞控主控代码,找到 case 1:(短悬停)那一段,逐行认一认——哪句是单次执行?哪句在数时间?哪句是状态迁移?认出这三个,这份两百行的文件对你就不再是天书。

再动手改一改(选做):给附.2 的 demo 加一个 state = 4 的"返航"状态(front 从 150 一路减回 0),返航完成后再进入降落。加完你就亲手写过一个 5 状态的状态机了。

所谓飞控主任务,就是"一个死循环里跑一个状态机"。 你导学案4学的 while + switch + if,已经够写出它的骨架了。剩下的只是把 printf 换成真的硬件操作——那是下一门课的事。


附.6 小结

读完本节,你应该能做到:

  • 说清单片机主循环为什么是 while (1),以及一圈里做的三件事
  • 用自己的话说清状态机三要素:状态变量、switch 分派、状态迁移
  • 写出"单次执行"的三行模板,并说清标志为什么先置 1、用完立刻置 0
  • 认出计数器分频的三种写法,知道"连续 N 次确认"是防什么
  • 在 飞控主控代码.md 里指出状态变量、单次执行标志、计数器分别是谁

这一节没有新语法。你看到的每一个符号,都来自导学案1到导学案4。

但把它们拼在一起,就是一架飞机能自己飞完一条航线的全部秘密。

作者:Mike-lin300

最后更新:2026-09-25

更新说明:主控代码框架参考来自于实验室 24-电气-赵煦华 学长