第 3 篇:把逻辑写清楚——控制流、函数与枚举

发布时间:2026/7/19 7:16:12

第 3 篇:把逻辑写清楚——控制流、函数与枚举 第 3 篇把逻辑写清楚——控制流、函数与枚举回顾前两篇的「小车工厂模型」第 1 篇看了工厂大门、工具箱、开工点名和永不下班的流水线第 2 篇看了仓库里的储物盒变量和怎么选尺寸合适的盒子数据类型。本篇走进车间看「工人到底按什么规矩干活」遇到分叉怎么选路控制流、怎么把一串动作打包成班组函数、怎么让小车随时清楚自己正在干啥枚举状态机以及怎么安全地摆弄文字字符串函数。0. 引子工人手里的操作流程图光有存放数据的储物盒变量还不够程序得有一套干活规则。把代码逻辑比作车间操作图纸遇到不同情况走不同路线、重复的动作循环执行、一整套固定操作打包统一调用。通俗来讲控制流决定程序什么时候走哪条路、什么时候重复干活函数把重复操作打包成独立班组枚举给小车每种运行状态起专属名字方便管理。1. 控制流工人干活的路线规则1.1 if / else if / else多岔路口范围判断专用最基础的判断语句适合判断区间、多段复杂条件。生活化比喻骑小车出门速度大于 100 减速、速度 0 停车、其余正常行驶分多种区间处理。我们项目里ApplyMotionControl就是用一长串if / else if判断小车当前运动模式再计算四个麦轮的转速。适用场景数值区间判断、逻辑条件比较复杂的场景。1.2 switch-case固定选项查表干净清爽如果同一个变量只有几种固定取值用 switch 比一长串 if 好看太多。比喻遥控器按键对照表按键1前进、按键2后退、按键3原地旋转一目了然不用一层层判断。项目里大量使用HandleNRFCommands接收遥控指令切换小车运动模式屏幕打印小车状态数字对应文字根据当前工况分别计算平移、旋转速度。适用场景单一变量、固定离散数值判断编译器还会优化运行速度。1.3 for提前知道要循环多少次比喻给屏幕补空格固定要填充16个空格次数写死跑完直接结束。代码里OLED_ShowTrim使用 for 循环补空格消除屏幕上一帧残留文字。适用场景写代码时就能确定循环总次数。1.4 while不知道循环几次满足条件就一直干比喻不断读取摄像头数据只要还有数据就持续读取缓存空了立刻停下。我们最熟悉的while(1)主循环就是无限 while读取串口数据也会用 while 读完所有缓存字节。适用场景循环次数不确定靠条件判断是否停止。一句话快速区分固定几个选项判断用 switch区间、复杂条件用 if次数已知用 for次数未知靠条件终止用 while。2. 函数把动作打包成班组2.1 为什么要把代码拆成函数看main.c真正干活的几乎都是一个个static void函数HandleNRFCommands收遥控、ApplyMotionControl算速度、SendReply回应答、AppendToDisplay拼显示、OLED_ShowTrim刷屏……比喻每个函数是一个班组只负责一件明确的事。main像车间主任只负责调度不用管班组内部细节。好处是可读性高、能复用、好改——改显示逻辑只动OLED_ShowTrim不用翻遍整个文件。2.2 static 函数本车间私有的内部班组这些函数名前面都带static定义在 main.c:95、144、161、173、208、230即AppendToDisplay/ModeName/OLED_ShowTrim/SendReply/HandleNRFCommands/ApplyMotionControl的定义处。比喻static函数 这个车间私有的班组别的车间别的.c文件叫不到它。一来避免和别处同名函数打架二来等于告诉读代码的人“这函数只在这个文件内部用别处不用管”。注意区分第 2 篇讲过函数内的 static 变量是让局部变量保持值这里的 static 是文件内可见是两码事。2.3 const 参数只可读不可改的封条OLED_ShowTrim(uint8_t row, uint8_t col, const char* s)main.c:161的最后一个参数有const。比喻const等于在参数上贴了张此物只可看、不可改的封条。函数向你保证我不会动你传进来的字符串。调用的人更放心编译器也能帮你抓不小心改了只读数据的低级错误。2.4 extern在别车间挂个号第 2 篇说过全局变量。有些全局变量定义在 A 文件、用在 B 文件B 文件里就要用extern声明一下这东西在别处这儿只是挂个号externfloatyaw;// 航向角在角度环文件里定义 (main.c:53)externvolatileuint32_tTimer7_5ms_Count;// 节拍计数在 Timer7 文件里定义 (main.c:54)比喻extern像在公告栏贴本厂共用物资存放在 3 号仓库——不用搬过来谁要用谁去那儿取。这就是多文件协作的基本套路。伏笔提示第 2 篇说过全局变量常带volatile还提到static、extern都和多文件/多任务有关。本篇把static函数、extern讲完了但volatile 中断是另一座大山——主循环和中断抢同一个变量会出什么乱子、volatile怎么破局我单独放到第 4 篇专讲。3. 枚举状态机让小车随时清楚自己正在干啥3.1 什么是枚举小车有前进、后退、左移、右移、旋转、停车、发射好几种工况。代码里没有用数字 0/1/2 去记而是用了枚举main.c:117-126typedefenum{MODE_STOP,MODE_CANNON,MODE_FORWARD,MODE_BACKWARD,MODE_LEFT,MODE_RIGHT,MODE_ROTATE_LEFT,MODE_ROTATE_RIGHT}MotionMode;staticMotionMode current_modeMODE_STOP;// 当前工况 (main.c:127)比喻枚举就像工厂墙上的一排工位指示灯每个灯名字对应一个明确状态。你看到MODE_FORWARD立刻懂是前进要是看到个光秃秃的2还得翻表才知道啥意思。3.2 状态机按指令切状态每帧按状态行动遥控发指令→HandleNRFCommands里switch把current_mode翻到对应模式main.c:214-224每帧主循环→ApplyMotionControl按current_mode算出四个轮子目标速度main.c:230-295要显示时→ModeName把模式转成 “FWD”/“BACK” 这种短名打在 OLED 上main.c:144-158。比喻这整套就是状态机——current_mode是块当前工况记事板。遥控是调度室发个命令就翻到对应工况工人主循环每轮看记事板决定这轮怎么干。好处很明显逻辑一眼看清、加个新工况只要添一个MODE_XXX、不容易出现状态混乱的诡异 bug。4. 字符串函数摆弄文字的专用工具小车要跟 OpenMV 摄像头、遥控器交换文字二维码、状态信息C 语言的string.h、stdio.h提供了一堆工具。注意main.c里处理字符串很克制能限长度的地方一律用带n的限度版本sprintf也只用在缓冲区明确够大的地方4.1 strncpy限量拷贝strncpy((char*)NRF24L01_TxPacket[1],openmv_tx_buf,30);// 最多拷 30 个 (main.c:182)比喻普通strcpy像整箱倒倒多了就溢出来strncpy多了个n参数最多倒 30 个倒满就停绝不越界。4.2 sprintf按模板填空sprintf((char*)NRF24L01_TxPacket[1],M%d S%d %d %d %d,...);// 拼状态帧 (main.c:199)比喻像一张填空模板M__ S__ __ __ __把几个变量填进去自动生成一串文字。OLED 显示也用它main.c:506、516。4.3 strncmp只比前 n 个字符if(strncmp(last_openmv_line,last_beep_line,RX_LINE_WIDTH)!0)// 去重 (main.c:474)比喻只比对前 N 个字母判断两行文字一不一样避免整串比到天荒地老或越界。4.4 memset批量清零memset(display_lines[current_line],0,RX_LINE_WIDTH1);// 清空一行缓冲 (main.c:101)比喻把一整排储物盒一次性清空准备装新东西。安全提醒单片机内存小、还没有操作系统兜底缓冲区溢出会直接覆盖别的变量、甚至程序跑飞。所以能限长度的strncpy/strncmp/memset一律用带n的版本绝不碰strcpy、gets这类无上限的危险函数sprintf本身不检查长度本项目里只把它用在目标缓冲区明确够大的地方。5. 本篇小结记住这页车间操作手册知识点通俗理解if / else多岔路口适合区间、复杂条件判断switch-case固定选项查表多离散分支更清爽for / whilefor 循环次数固定while 靠条件控制启停static函数本文件私有的内部班组const参数只可读不可改的封条extern在别文件挂个号跨文件共用变量enum枚举工位指示灯状态一目了然strncpy/strncmp/memset限度版、sprintf摆弄文字的安全工具strncpy等带n限长度sprintf只用于够大的缓冲区6. 第2篇小练习答案练习1把int16_t Speed1改成uint16_t小车运行时会出什么异常轮子速度有正有负——前进是正、后退是负。而uint16_t只能装 0~65535。一旦小车后退产生负速度比如 -50存进uint16_t会按补码规则绕成 65536-50 65486 这种巨大正数。后果PID 算出来以为轮子该以接近满速正转电机反向狂转、小车乱冲速度显示也变成几万彻底失控。所以轮子速度必须用int16_t这种能表示负数的类型。练习2同样存储 0~200 的数字分别用int和uint8_t各占多少内存哪种更适合单片机在 32 位 STM32 上int固定占4 字节32 位uint8_t只占1 字节8 位。0~200 完全落在uint8_t的 0~255 范围内用uint8_t足够且省下 3/4 的内存。单片机 RAM 很金贵通常才几 KB 到几十 KB能省则省所以这种小数优先选uint8_t。本篇趣味思考答案见第4篇末尾如果把MotionMode枚举删掉改用#define MODE_FORWARD 1这类宏来表示模式和用枚举相比哪种更容易埋下 bug为什么提示想想枚举让编译器帮你做了哪些检查。代码里为什么坚持用strncpy/sprintf而不是strcpy如果把 OpenMV 发来的不定长文本直接strcpy进一个固定大小的数组在单片机上可能出什么事7. 交流我目前也是嵌入式自学新手本篇内容是结合自己麦轮小车项目实操总结而来部分知识点理解可能存在偏差讲解也有不够严谨的地方。技术大佬如果发现文中存在错误、逻辑漏洞欢迎评论区指正我会及时修正文章零基础小白有看不懂的知识点、实操踩坑问题都可以留言提问我看到会逐条回复大家也可以分享自己做智能小车的经验、代码优化思路一起交流学习共同进步祝愿各位大佬项目开发一路顺畅调试零bug思路源源不断祝愿所有入门小白少踩弯路快速吃透STM32早日做出属于自己的智能小车下篇预告《第 4 篇volatile 与中断——嵌入式 C 最关键的坑》。我们会讲中断到底是什么、为什么主循环和中断抢同一个变量会出乱子volatile 怎么破局以及中断里查标志→处理→清标志的标准动作。

相关新闻