马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
期末复习 C 语言时,很多同学会遇到一个很典型的情况:if、for、结构体单独看都会,放到一份单片机工程里就不知道程序从哪里开始读。做题时也容易只记住“语法长什么样”,却说不清变量为什么这样定义、边界为什么要判断。 我这次整理了一份脱敏的 STM32 光控窗帘工程资料。工程里有光照采集、阈值设置、OLED 显示、舵机开合和手动/自动模式切换。它不是为期末考试专门写的例题,但刚好把 C 语言最容易考、也最容易用错的知识点连在了一起。下面的代码片段均按原工程的逻辑重新整理,适合拿来复习,不依赖具体开发板也能看懂。 一、先用主循环看懂一个 C 程序的骨架" N+ ]" R3 f* j# z7 [/ c0 q
嵌入式程序通常不是“运行一次就结束”,而是在 while(1) 中不断采集、判断和执行。光控窗帘工程的核心链路可以概括为: 初始化 → 读取光照/温湿度 → 判断当前模式 → 按阈值控制舵机或继电器 → 回到采集 初始化中能看到串口、OLED、BH1750 光照传感器、继电器、SG90 舵机、按键和定时器。主循环里先把光照值写入 SensorData.LightVal,再调用 Mode_Decide()。这个顺序值得记住:先更新输入,再做判断。如果把采集放到判断后面,控制逻辑读到的可能就是上一轮的旧数据。 - while (1)$ K# R3 D& H' `# b, B. U4 x4 L
- {
, l) G/ L {* P, a2 g - SensorData.LightVal = Light_Intensity();
; L; ?, z3 H0 O: \( P8 B% Q: U6 S - DHT11_Read_TempAndHumidity(&dht11Data);
+ J7 C4 ?7 g) t - Mode_Decide();
! h4 V) d& T% f8 l" `2 _ - }
复制代码这段很适合考“函数调用顺序”和“循环体的执行过程”。不要把 while(1) 理解成死循环错误;在单片机里,它就是持续响应外部环境的主循环。 二、结构体:把一组有关的数据装进一个名字里
* o0 C; q2 R/ v- v; Y3 B7 R+ D项目没有把温度、湿度、光照阈值、模式开关零散地定义成一长串全局变量,而是用 SENSOR、THRESHOLD、SYSTEM、TIMING 这几类结构体归类。复习结构体时,重点不是背定义格式,而是理解它解决了什么问题。 - typedef struct {5 X0 Q7 X7 n) d
- float LightVal;
/ D9 A7 A% g! H" D" H9 o2 Y - } SENSOR;
# M8 v0 ?7 z9 x2 c+ L - typedef struct {
- {* M0 r3 q* z3 O a0 g# a - int TempMax;
% W$ ~9 g, V% ]" D - int HumiMax;
- K0 J' K& y2 z. j - int LightMin;9 e8 ~5 P$ v+ Y
- } THRESHOLD;0 \1 J$ E9 N9 ]* k
- SENSOR SensorData;
/ G, V! C6 n ^+ I4 } - THRESHOLD Threshold;
复制代码 ) g& U/ ^5 g. w
这样写之后,Threshold.LightMin 一眼就能看出是“光照下限”,不会和温度、湿度的阈值混在一起。原工程的初始值为温度 35、湿度 55、光照 150(单位由显示界面标为 lux)。考试中如果题目给出“学生、商品、传感器”等多个字段,优先考虑能否用结构体组织;访问成员时用点运算符 .。 一个常见失分点是把“结构体变量”和“结构体类型”混为一谈:THRESHOLD 是类型名,Threshold 才是变量名。两者大小写不同,C 语言会严格区分。 三、指针和取地址:函数为什么能改掉外面的阈值
. T' K# m8 E! v/ k$ C+ ?' v* N
" B& {+ i; @& k6 x工程的阈值初始化函数写成 Threshold_Init(&Threshold)。这里的 & 不是“取值”,而是取变量的地址;函数接收到地址后,才能直接修改原来的结构体成员。 - void Threshold_Init(THRESHOLD *p)
9 J9 [ T4 [* ~+ W1 A1 k; A2 h- P - {
1 D+ m" Y% \) ^, k. Y! M- ^ - p->TempMax = 35;
1 t% G( {. d( j R5 L - p->HumiMax = 55;
! ^+ z; r7 t/ K* Z+ @ - p->LightMin = 150;
, P4 }& x8 d: V) Z9 F4 I - }1 Q# a# [/ P) h7 Y, I0 p; o3 r
- Threshold_Init(&Threshold);
复制代码这段可一次复习三个点: 1. THRESHOLD *p 表示“指向 THRESHOLD 类型的指针”; 2. 调用时的 &Threshold 取得实参地址; 3. p->LightMin 等价于 (*p).LightMin,但前者更常用、更清楚。 如果把形参写成 void Threshold_Init(THRESHOLD p),函数里改的是结构体副本,函数结束后外部的 Threshold.LightMin 仍可能保持旧值。这正是“值传递”和“地址传递”的区别。期末选择题常把 .、->、&、* 放在一起考,建议把这段短代码抄一遍再默写。 四、if/else 不只是判断,关键是把边界条件补齐
/ j! q$ o" d' X5 X& E1 ]# v! y
0 W* T+ f% ?' l5 @ z& p自动控制中,程序比较实时光照和阈值:低于 150 时给舵机 500 微秒的控制脉宽,达到或超过阈值时给 2000 微秒脉宽。具体开、合方向要以机械安装为准,但“比较—分支—执行”的 C 语言逻辑是明确的。 - if (SensorData.LightVal < Threshold.LightMin). K* \4 U2 k0 K/ N) \5 E; k6 s
- {! d8 o4 e* f6 d E6 D
- System.Switch1 = 0;
2 |& w$ T) Q$ L8 G( V- R - Control_SG90(500);
: z& L9 \5 [- x! e - }
9 f2 Z# S+ X3 n' x1 S - else' E. {; v. x" O+ s3 k. s& n- Q4 \9 z
- {
6 S3 [& O, m3 S c; V. {# ? - System.Switch1 = 1;
7 @ d& Q, Z% K; e& O - Control_SG90(2000);; F4 x5 Q7 U& r* t
- }
复制代码这里要注意两个细节。第一,条件写的是 <,所以光照刚好等于 150 时会走 else 分支;写程序时必须主动说明“等于”归哪一边。第二,阈值可由按键修改,修改后还要限制范围。例如温湿度阈值被限制在 0 到 100;光照下限不能小于 0。只写“加一、减一”却不限制范围,数组下标、计数器和用户输入都可能在真实工程里出错。 if (Threshold.TempMax >= 100) Threshold.TempMax = 100; if (Threshold.TempMax <= 0) Threshold.TempMax = 0; if (Threshold.LightMin <= 0) Threshold.LightMin = 0; 遇到“判断下面代码输出什么”或“补全防越界代码”的题,可以按这个顺序检查:条件是否覆盖相等情况、两个分支是否都有动作、修改后有没有回到合法范围。 五、switch:有限状态的模式切换比连续 if 更直观5 F4 @2 G6 ~$ R* O! \; P9 F
工程用 OperateMode 保存模式编号:0 是自动模式,1 是手动模式,2 是阈值设置,3 是定时设置。按键每触发一次,模式编号加一;超过 3 后回到 0。 - OperateMode++;
. i' D8 N: |) A+ V - if (OperateMode >= 4) OperateMode = 0;$ q+ a' ^" J0 @5 S7 d7 R
- switch (OperateMode)
- }) ^. ^2 l( N4 b7 m; s( A - {
; x! f+ E0 j4 H+ H0 o3 D - case 0: Inform_Show(); AutoContrl(); break; _+ M3 X6 H/ `% z0 y% d6 }) T
- case 1: Manual_Contrl(); break;
& ?7 g: |$ V7 G- J - case 2: ThresholdSet(); break;+ l6 E+ F" d. s
- case 3: TimingSet(); break;+ z% f% Y N! F' F
- }
复制代码这就是典型的“状态变量 + switch”。当分支表示互斥的几种状态时,switch 比多层 if/else if 更容易检查是否漏了情况。break 也很重要:少写它,程序会继续执行下一个 case,出现所谓的“贯穿”。除非题目特意要求利用贯穿,否则每个 case 后都应有 break。 六、类型转换与位运算:两字节数据如何拼成一个光照原始值2 h; \2 l% J+ O+ s0 y
光照模块读取数据时,会先得到高 8 位 H_Data 和低 8 位 L_Data,再拼成 16 位数: - uint16_t Rx_Data;. u0 Z/ ]- e! \8 s8 _& o5 j
- Rx_Data = ((uint16_t)H_Data << 8) + L_Data;
( \# l) Z" R L - return (float)(Rx_Data / 1.2f);
复制代码这段有三个高频点。 · (uint16_t)H_Data 是显式类型转换,先把高字节提升为 16 位,再左移 8 位,避免仅在 8 位范围内运算; · << 8 相当于把高字节放到 16 位数的高八位; · 1.2f 的后缀 f 表明它是 float 常量,最终结果保留小数,适合保存为 float 型的光照强度。 考试里如果把 1.2f 改成整数 1,结果会丢失换算关系;如果把 H_Data << 8 和 L_Data 的位置写反,拼出来的数也会明显不对。 七、期末常见考点与易错点清单) H$ u% a6 G# ]) `+ G% j
下面这张清单适合在刷题前先过一遍。它不是让大家把所有概念重新背一遍,而是提醒自己:题目通常会怎样把考点藏进代码里。 | | | | | | | 看值是否要保留小数;float 参与表达式时留意转换 | | | | | | | | | | | | | | | | | | | 以为函数能直接改掉普通实参;局部变量出了函数仍有效 | | | | | | | | | | | | | | | | | | " O* [, i- I9 |. b* O @ R, C3 X
以本工程为例,((uint16_t)H_Data << 8) + L_Data 同时考查了强制类型转换和位运算;Threshold_Init(&Threshold) 同时考查了结构体、取地址和指针;模式切换部分则适合考 switch、自增和边界回退。看到综合题时,不要被工程名或外设名吓住,先把它拆回这些基础语法点。 此外,有三类题即使不写代码也常丢分:一是读程序题,要按执行顺序模拟,不要只挑某一行看;二是改错题,要同时检查语法、逻辑和边界,能编译不代表结果正确;三是编程题,要先写输入、处理、输出,再补异常情况。写完后至少用“最小值、临界值、最大值”三组数据在纸上走一遍。 八、两道可直接练的例题( v; H% |" } q) A; C: v" S9 r
例题 1:模式循环 已知 OperateMode 的初值为 3,执行下面两行后它的值是多少?程序将进入哪个分支? OperateMode++; if (OperateMode >= 4) OperateMode = 0; 答案:先自增为 4,满足条件后重置为 0,因此进入 case 0 的自动模式。这里的关键不是“4 大不大”,而是看清自增发生在判断之前。 例题 2:阈值边界 如果当前 Threshold.LightMin 为 0,用户继续执行一次“减小阈值”,随后程序执行: Threshold.LightMin -= 50; if (Threshold.LightMin <= 0) Threshold.LightMin = 0; 最终值是多少?为什么不能省略第二行? 答案:最终仍为 0。若省略第二行,阈值会变成负数;后续拿实际光照与负数比较,自动控制会长期落在同一个分支,设置功能看似可用,逻辑却已经失真。 九、考前十分钟的复习清单7 B0 ^; U, C; h/ B* { K' p
1. 看见一组相关字段,先想结构体;区分类型名、变量名和成员访问。 2. 看见函数要修改外部变量,检查调用处有没有 &,函数内是否使用 * 或 ->。 3. 看见 if,把小于、等于、大于三种边界都在脑中走一遍。 4. 看见 switch,逐项找 break,确认默认或循环回退策略。 5. 看见数组或字符串,先写出下标范围,再检查结尾的 \0 是否留出位置。 6. 看见传感器、串口等字节数据,重点检查数据类型、移位方向和强制类型转换。 把这些点放回一个完整工程里理解,比单独死记符号更稳。真正做题时,只要先画出“数据从哪来—在哪判断—改了什么—怎么限制边界”这条线,大多数 C 语言综合题就不会乱。 附件说明:本文为基于已脱敏工程逻辑整理的复习稿,发布时不附原始毕业设计论文、视频或完整工程;如需配图,建议另行制作“主循环流程图”和“结构体/指针关系图”,并确认不含个人信息。 关键词:C语言、期末复习、结构体、指针、switch、STM32、阈值控制
i6 Y7 `4 ~* T' s |