本章实验 · 三个实验
本章三个实验一览
第二章打的是「够用的 C」。为了不在一页里囫囵吞枣,拆成三个纯软件实验:先练「位运算与字节拼装」(第四章读 DHT22 的底子),再练「切词与数值解析」(第四章 set temp 28.5 的底子),最后做本来的闯关实验——写滞回决策、改读失败分支。三个实验都只用 C 与标准输入,没有硬件器材,在主机或板端(riscv64 gcc)都能跑。
| 实验 | 练什么 | 源码目录 |
|---|---|---|
| 实验一 位运算与字节拼装 |
移位、或、按字节求和校验(DHT22 式字节 → 温湿度数值) | chapters/ch02/code/bits-check/ |
| 实验二 切词与数值解析 |
把一行命令按空白切词,并用 strtod 认数字 |
chapters/ch02/code/cmd-parse/ |
| 实验三 写滞回 · 改读数闯关 |
从空写出 thermo_decide;修 thermo_read 的错误分支 |
chapters/ch02/code/project-template/ 与 fake-thermostat/ |
实验一 · 位运算与字节拼装
第四章的 DHT22 一次给 40 个 bit,拼起来是 5 个字节:湿度高/低字节、温度高/低字节、校验和。本实验先练这三件小事:把两个字节合成 uint16、把 4 个字节求和取低 8 位当校验和、把 0.1°C 单位的原码换算成摄氏度。源码目录 chapters/ch02/code/bits-check/。
步骤
# 主机,或先 scp 到板子(板端 gcc)再进目录 $ scp -r chapters/ch02/code/bits-check user@board-ip:~/ $ cd chapters/ch02/code/bits-check # 板上:cd ~/bits-check $ make $ ./build/bits-check # 三个函数还是空返回,会打印 FAIL —— 预期现象
打开 src/bits.c,只实现标了 STUDENT TODO 的三个函数:
| 函数 | 契约(怎么算对) | 关卡 |
|---|---|---|
dht_pack_u16(hi, lo) | hi 移到高 8 位再 | lo;注意 hi 要先提升成 uint16 再 <<8 | [B1] |
u8_sum(d, n) | 前 n 字节求和,最后 & 0xFF 只留低 8 位(溢出是特性不是 bug) | [B2] |
tenths_to_c(raw) | raw 每 1 单位 = 0.1°C:(float)raw / 10.0f | [B3] |
实现后重新 make 再跑,直到测试驱动全绿:
$ make $ ./build/bits-check # 期望结尾:ALL PASS(中间逐条 ok) # 对照参考:make sol && ./build/bits-check-sol 应同样 ALL PASS
本实验成功判据:三函数实现正确,./build/bits-check 打印 ALL PASS;能口头解释「为什么要 & 0xFF」「为什么要先提升成 16 位再移位」。
ALL PASS实验二 · 切词与数值解析
第四章要在命令行敲 status、set temp 28.5。这一步要先能把一行字符串按空白切成一串词,并认出哪个词是数字。源码目录 chapters/ch02/code/cmd-parse/,只实现 src/parse.c 里的 split_tokens。
步骤
# 主机,或先 scp 到板子再进目录 $ cd chapters/ch02/code/cmd-parse # 板上:cd ~/cmd-parse $ make $ ./build/cmd-parse # split_tokens 还是空,样例词数全错 —— 预期现象
契约(看 include/parse.h):每个 Tok 有 word[24]、is_num、val;split_tokens 把 line 按空白切进 toks,返回词数。建议逐字符写:跳过空白 → 复制到下一空白 → 用 strtod 判断是否「干净浮点数」(endptr 动了且 *endptr=='\0')。
# 实现正确后,参考输出(可对照 make sol): $ ./build/cmd-parse # == "set temp 28.5" -> 3 词 # 'set' # 'temp' # '28.5' = 28.50 # ok # == "set temp abc" -> 3 词 (abc 不是数字) # 'set' # 'temp' # 'abc' # ok # ... # ALL PASS
本实验成功判据:./build/cmd-parse 打印 ALL PASS;能口头说出「什么样的词才算数字」「strtod 的 endptr 为什么能用来判断干净与否」。
ALL PASS实验三 · 写滞回 · 改读数闯关(fake-thermostat)
实验一、实验二分别练了「字节 → 数值」「字符串 → 命令」,实验三把它们收拢成这一章真正的主线:写出滞回决策 thermo_decide,并修好读温度失败的错误分支。这块逻辑第三、四章还会以 GPIO 形式再出现一次。
说明
讲义已跟做 hello / project-template / cmd-table,并打透:类型与控制流、函数与多文件、编译 → 链接 → _start → main、指针与结构体,以及切词和函数指针命令表。
本实验在此之上递进三步:核对编译链接产物;对着契约从空写出 thermo_decide(这就是第三、四章 TODO 的手感);再修好 thermo_read 里故意留下的指针与错误分支。温度来自内置数组或标准输入,无真 DHT。
- 核对工程:
chapters/ch02/code/project-template/(讲义已跑通;此处只取产物证据) - 闯关工程:
chapters/ch02/code/fake-thermostat/(动手文件src/thermo.c) - 输入:默认内置温度序列;也可用
--stdin读浮点数(负数表示本轮「读失败」) - 输出:每步打印温度与风扇 ON/OFF;读失败打印
READ_FAIL并保持上一风扇状态
cmd-table 跟做),且 project-template 已能 make 出 ./build/app。实验一、实验二已在板端原生 gcc 验证过;实验三在本机编译即可,板端复跑输出应一致。
thermo_decide 现在是空函数,不要去抄半成品里的「反了的 if」——那里已经没了。先按下方契约自己写;thermo_read 里的 L3/L4 一次只改一类。对照答案:make sol。脚手架 README.md。
步骤 1:编译链接核对(project-template)
目的:把讲义 2.2 里的机制落到可复查输出。若讲义跟做时已做过且产物仍在,可快速复核后进入闯关。
$ cd chapters/ch02/code/project-template $ make $ ls build/ $ file build/*.o build/app # .o 与可执行文件角色不同;app 应能执行 $ nm build/app | grep -E '_start|main' || true # 通常能同时看到 main 与启动相关符号(有 nm 时) $ ./build/app
可选:临时改错 greet.c 里的函数名再 make,应出现 undefined reference——改回后重新 make 通过。这证明「编译过了、链接对不上」与「对上了」的差别。
本步成功判据:build/ 中有对应 .o 与 app;file 能区分二者;./build/app 能跑。然后进入闯关,不再停留在本模板。
步骤 2:写出 thermo_decide
进入闯关目录,先编译运行。半成品能编过,但 thermo_decide 是空的:风扇会一直 OFF。这和第三章先 make 因函数未声明而失败是同一类起点——main 已经在等你补上决策。
$ cd chapters/ch02/code/fake-thermostat $ make $ printf '25\n27\n29\n27\n24\n-1\n' | ./build/fake-thermo --stdin # 未写 decide 时:温度在变,风扇几乎总是 OFF
打开 src/thermo.c,只实现 thermo_decide(标了 STUDENT TODO [W1])。原型已在 thermo.h。
/* 根据 cfg 与当前温度更新 *fan_on(0=OFF, 1=ON)。 * cfg 或 fan_on 为 NULL → 直接返回。 * 只用 cfg->t_high / cfg->t_low,不要用 period_ms。 * temp_c > t_high → *fan_on = 1 * temp_c < t_low → *fan_on = 0 * 中间区间 → 保持 *fan_on */ void thermo_decide(const ThermoConfig *cfg, float temp_c, int *fan_on);
默认阈值:t_high = 28.0,t_low = 26.0,初始风扇 OFF。写完 make 再跑;thermo_read 的坑还在,这一步先把前五步的 ON/OFF 跑对即可(第 6 步 READ_FAIL 下一步再修)。
本步成功判据:25 OFF、27 OFF、29 ON、27 ON、24 OFF;空指针调用不崩溃。对照:make sol。
步骤 3:改 thermo_read
1. 看坏现象
thermo_read 里仍有两处故意错误。先跑通半成品,对照关卡表,一次只改一类。
$ printf '25\n27\n29\n27\n24\n-1\n' | ./build/fake-thermo --stdin # 负数表示本轮「读失败」;修好前最后一步行为不对
2. 按关卡改代码(一次一类)
只改 src/thermo.c 里的 thermo_read。每修完一关,重新 make 再跑。
| 关卡 | 修好前你会看到什么 | 你要改成什么(成功判据) | 主要坑类型 |
|---|---|---|---|
| L3 | 传入空输出指针时崩溃 | thermo_read 在 out_c 为 NULL 时返回非 0、不写内存 |
指针使用 |
| L4 | 读失败时仍用垃圾温度做决策,或失败路径却返回「成功」 | 失败时返回非 0 且不写 *out_c;主循环打印 READ_FAIL 并跳过本轮决策,保持上一风扇状态 |
缺少错误分支 |
修 L3 时,可对 thermo_read(NULL) 调用一次确认不崩溃。修 L4 时,内置序列最后一步会注入读失败。
3. 对照内置序列做最终验收
W1 与 L3/L4 都完成后,运行 ./build/fake-thermo(默认内置序列)。期望(初始风扇 OFF):
| 步 | 温度 °C | 期望风扇 | 说明 |
|---|---|---|---|
| 1 | 25.0 | OFF | 低于下限,保持关 |
| 2 | 27.0 | OFF | 滞回带中间,保持上一状态 |
| 3 | 29.0 | ON | 高于上限,打开 |
| 4 | 27.0 | ON | 仍在中间带,保持开 |
| 5 | 24.0 | OFF | 低于下限,关闭 |
| 6 | 读失败(哨兵) | 保持 OFF | 打印 READ_FAIL,不根据垃圾温度改风扇 |
本步成功判据:六步输出与上表一致;能口头说明滞回是自己按契约写的,读失败路径是改出来的。
gcc 复跑同一条 SOL 命令,输出一致验收表
| 验收项 | 证据 | □ |
|---|---|---|
| 编译链接核对完成 | project-template 的 build/*.o 与 app;file 输出可区分 | |
| W1 从空写出滞回 | thermo_decide 使用 t_high/t_low;升温开、降温关、中间保持;空指针不崩溃 | |
| L3 空指针安全返回 | thermo_read(NULL) 不崩溃 | |
| L4 读失败跳过决策 | 有 READ_FAIL(或等价)日志,且风扇状态保持 | |
| 内置序列跑通 | ./build/fake-thermo 输出与期望表六步一致 | |
| 能区分「写函数」和「改 bug」 | 口头说明 decide 是按契约写的,read 的失败路径是改出来的 |