【学基1】C语言期末复习别只背语法:用一个 STM32 光控窗帘项目串起高频考点

4 0
EDGE 发表于 2026-8-29 15:10:29 | 查看全部 阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
期末复习 C 语言时,很多同学会遇到一个很典型的情况:iffor、结构体单独看都会,放到一份单片机工程里就不知道程序从哪里开始读。做题时也容易只记住语法长什么样,却说不清变量为什么这样定义、边界为什么要判断。
我这次整理了一份脱敏的 STM32 光控窗帘工程资料。工程里有光照采集、阈值设置、OLED 显示、舵机开合和手动/自动模式切换。它不是为期末考试专门写的例题,但刚好把 C 语言最容易考、也最容易用错的知识点连在了一起。下面的代码片段均按原工程的逻辑重新整理,适合拿来复习,不依赖具体开发板也能看懂。
一、先用主循环看懂一个 C 程序的骨架" N+ ]" R3 f* j# z7 [/ c0 q
嵌入式程序通常不是运行一次就结束,而是在 while(1) 中不断采集、判断和执行。光控窗帘工程的核心链路可以概括为:
初始化 读取光照/温湿度 判断当前模式 按阈值控制舵机或继电器 回到采集
初始化中能看到串口、OLEDBH1750 光照传感器、继电器、SG90 舵机、按键和定时器。主循环里先把光照值写入 SensorData.LightVal,再调用 Mode_Decide()。这个顺序值得记住:先更新输入,再做判断。如果把采集放到判断后面,控制逻辑读到的可能就是上一轮的旧数据。
  1. while (1)$ K# R3 D& H' `# b, B. U4 x4 L
  2. {
    , l) G/ L  {* P, a2 g
  3.     SensorData.LightVal = Light_Intensity();
    ; L; ?, z3 H0 O: \( P8 B% Q: U6 S
  4.     DHT11_Read_TempAndHumidity(&dht11Data);
    + J7 C4 ?7 g) t
  5.     Mode_Decide();
    ! h4 V) d& T% f8 l" `2 _
  6. }
复制代码
这段很适合考函数调用顺序循环体的执行过程。不要把 while(1) 理解成死循环错误;在单片机里,它就是持续响应外部环境的主循环。
二、结构体:把一组有关的数据装进一个名字里
* o0 C; q2 R/ v- v; Y3 B7 R+ D
项目没有把温度、湿度、光照阈值、模式开关零散地定义成一长串全局变量,而是用 SENSORTHRESHOLDSYSTEMTIMING 这几类结构体归类。复习结构体时,重点不是背定义格式,而是理解它解决了什么问题。
  1. typedef struct {5 X0 Q7 X7 n) d
  2.     float LightVal;
    / D9 A7 A% g! H" D" H9 o2 Y
  3. } SENSOR;
    # M8 v0 ?7 z9 x2 c+ L
  4. typedef struct {
    - {* M0 r3 q* z3 O  a0 g# a
  5.     int TempMax;
    % W$ ~9 g, V% ]" D
  6.     int HumiMax;
    - K0 J' K& y2 z. j
  7.     int LightMin;9 e8 ~5 P$ v+ Y
  8. } THRESHOLD;0 \1 J$ E9 N9 ]* k
  9. SENSOR SensorData;
    / G, V! C6 n  ^+ I4 }
  10. 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)。这里的 & 不是取值,而是取变量的地址;函数接收到地址后,才能直接修改原来的结构体成员。
  1. void Threshold_Init(THRESHOLD *p)
    9 J9 [  T4 [* ~+ W1 A1 k; A2 h- P
  2. {
    1 D+ m" Y% \) ^, k. Y! M- ^
  3.     p->TempMax = 35;
    1 t% G( {. d( j  R5 L
  4.     p->HumiMax = 55;
    ! ^+ z; r7 t/ K* Z+ @
  5.     p->LightMin = 150;
    , P4 }& x8 d: V) Z9 F4 I
  6. }1 Q# a# [/ P) h7 Y, I0 p; o3 r
  7. 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 语言逻辑是明确的。
  1. if (SensorData.LightVal < Threshold.LightMin). K* \4 U2 k0 K/ N) \5 E; k6 s
  2. {! d8 o4 e* f6 d  E6 D
  3.     System.Switch1 = 0;
    2 |& w$ T) Q$ L8 G( V- R
  4.     Control_SG90(500);
    : z& L9 \5 [- x! e
  5. }
    9 f2 Z# S+ X3 n' x1 S
  6. else' E. {; v. x" O+ s3 k. s& n- Q4 \9 z
  7. {
    6 S3 [& O, m3 S  c; V. {# ?
  8.     System.Switch1 = 1;
    7 @  d& Q, Z% K; e& O
  9.     Control_SG90(2000);; F4 x5 Q7 U& r* t
  10. }
复制代码
这里要注意两个细节。第一,条件写的是 <,所以光照刚好等于 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
  1. OperateMode++;
    . i' D8 N: |) A+ V
  2. if (OperateMode >= 4) OperateMode = 0;$ q+ a' ^" J0 @5 S7 d7 R
  3. switch (OperateMode)
    - }) ^. ^2 l( N4 b7 m; s( A
  4. {
    ; x! f+ E0 j4 H+ H0 o3 D
  5.     case 0: Inform_Show(); AutoContrl();   break;  _+ M3 X6 H/ `% z0 y% d6 }) T
  6.     case 1: Manual_Contrl();               break;
    & ?7 g: |$ V7 G- J
  7.     case 2: ThresholdSet();                break;+ l6 E+ F" d. s
  8.     case 3: TimingSet();                   break;+ z% f% Y  N! F' F
  9. }
复制代码
这就是典型的状态变量 + switch。当分支表示互斥的几种状态时,switch 比多层 if/else if 更容易检查是否漏了情况。break 也很重要:少写它,程序会继续执行下一个 case,出现所谓的贯穿。除非题目特意要求利用贯穿,否则每个 case 后都应有 break
六、类型转换与位运算:两字节数据如何拼成一个光照原始值2 h; \2 l% J+ O+ s0 y
光照模块读取数据时,会先得到高 8 H_Data 和低 8 L_Data,再拼成 16 位数:
  1. uint16_t Rx_Data;. u0 Z/ ]- e! \8 s8 _& o5 j
  2. Rx_Data = ((uint16_t)H_Data << 8) + L_Data;
    ( \# l) Z" R  L
  3. 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
下面这张清单适合在刷题前先过一遍。它不是让大家把所有概念重新背一遍,而是提醒自己:题目通常会怎样把考点藏进代码里。
常见考点
题目常见问法
最容易错在哪里
复习抓手
基本数据类型与格式输出
判断变量能存什么、补全 printf 格式
intfloatchar 的用途混在一起
看值是否要保留小数;float 参与表达式时留意转换
运算符与优先级
求表达式结果、判断自增后变量值
分不清 i++ ++i,漏看括号
复杂表达式先拆成多步,不靠直觉算
分支语句
判断 if/else 会走哪条路
= 写成比较、漏掉等于边界、悬空 else
每个条件代入临界值:小于、等于、大于
循环语句
数循环次数、找死循环、补全 for
分号多写在 for 后、循环变量没更新
逐轮写出循环变量和条件真假
数组与字符串
求下标、找越界、判断字符串长度
数组下标从 0 开始;'\0' 忘记占一个位置
先写出有效下标范围 0 n-1
函数与作用域
判断返回值、形参与实参变化
以为函数能直接改掉普通实参;局部变量出了函数仍有效
画出调用前、调用中、返回后三份变量值
指针
补全 *p&ap->member
把地址当数值,把 . -> 混用
先问现在手里是变量本身还是它的地址
结构体
定义类型、访问成员、传结构体参数
类型名和变量名混淆,成员访问符写错
变量.成员指针->成员 各写一例
位运算与类型转换
合并字节、掩码、强转结果
左移前位宽不够,整数除法意外丢小数
先标清每一位的位置和运算后的类型
编译预处理与宏
判断宏展开、区分 #define 与变量
宏参数缺少括号,产生优先级问题
宏里的每个参数都加括号再推导
" 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语言、期末复习、结构体、指针、switchSTM32、阈值控制

  i6 Y7 `4 ~* T' s
回复 转播

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

学研领航向全体高校师生打造的一站式综合交流与资源服务平台,集知识学习、经验分享、资源下载、互动问答、职场成长、兼职实践于一体,覆盖校园生活、专业学习、求职就业、兴趣发展等全场景需求。

快捷导航

小黑屋
Copyright © 2026 学研领航 版权所有 陕ICP备2025077879号-1
关灯 在本版发帖 返回顶部
快速回复 返回顶部 返回列表