导学案4 · 附加材料:飞控代码里的三个基础技巧(选读)¶
前置要求:导学案4之前(会 while / for / switch / if) 参考课程:无(实验室自制,配套 飞控主控代码) 预计用时:1 小时 本节目标:认识飞控代码里三个最基础的技巧:状态机、单次执行、计数器分频 选读说明:这是选读材料,看不懂不影响后续学习。目的是分享变成经验,让你产生概念。
附.0 先看一眼真实代码长什么样¶
飞控主控代码 是实验室真实飞控的主控任务:一键起飞 → 短悬停 → 飞四个航点画一个正方形 → 回到原点 → 降落。
它有两百多行、一堆没见过的函数名,看起来很吓人,而且它只是飞控众多代码文件中的一个单文件。但它的骨架只有三样东西,全部是你已经学过的:while 循环、switch、if。
这一节我们用终端代替传感器和电机("读高度"换成一个变量自加,"控制电机"换成 printf),把这三个技巧各演一遍,最后回去对照真实代码。
附.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;
}
输出:
后面要讲的三个技巧,全都发生在这个循环里面。
附.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;
}
输出:
真实代码里三样都在: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-电气-赵煦华学长