RISC-V Linux 嵌入式编程技术 · 第二章

本章实验 · 三个实验

实验一 位运算与字节拼装 · 实验二 切词与数值解析 · 实验三 写滞回 · 改读数闯关

本章三个实验一览

第二章打的是「够用的 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/
器材:无。实验一、实验二在主机或板上(板端原生 gcc)跑通即可;实验三沿用本来的工程。

实验一 · 位运算与字节拼装

第四章的 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 位再移位」。

板上 bits-check-sol 输出 ALL PASS
板端证据(SSH → bits-check):ALL PASS

实验二 · 切词与数值解析

第四章要在命令行敲 statusset 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):每个 Tokword[24]is_numvalsplit_tokensline 按空白切进 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;能口头说出「什么样的词才算数字」「strtodendptr 为什么能用来判断干净与否」。

板上 cmd-parse-sol 输出 ALL PASS
板端证据(SSH → cmd-parse):ALL PASS

实验三 · 写滞回 · 改读数闯关(fake-thermostat)

实验一、实验二分别练了「字节 → 数值」「字符串 → 命令」,实验三把它们收拢成这一章真正的主线:写出滞回决策 thermo_decide,并修好读温度失败的错误分支。这块逻辑第三、四章还会以 GPIO 形式再出现一次。

说明

讲义已跟做 hello / project-template / cmd-table,并打透:类型与控制流、函数与多文件、编译 → 链接 → _startmain、指针与结构体,以及切词和函数指针命令表。

本实验在此之上递进三步:核对编译链接产物;对着契约从空写出 thermo_decide(这就是第三、四章 TODO 的手感);再修好 thermo_read 里故意留下的指针与错误分支。温度来自内置数组或标准输入,无真 DHT。

前置:已读完讲义 2.1–2.4(含 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/ 中有对应 .oappfile 能区分二者;./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.0t_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_readout_cNULL 时返回非 0、不写内存 指针使用
L4 读失败时仍用垃圾温度做决策,或失败路径却返回「成功」 失败时返回非 0 且不写 *out_c;主循环打印 READ_FAIL跳过本轮决策,保持上一风扇状态 缺少错误分支

修 L3 时,可对 thermo_read(NULL) 调用一次确认不崩溃。修 L4 时,内置序列最后一步会注入读失败。

3. 对照内置序列做最终验收

W1 与 L3/L4 都完成后,运行 ./build/fake-thermo(默认内置序列)。期望(初始风扇 OFF):

温度 °C期望风扇说明
125.0OFF低于下限,保持关
227.0OFF滞回带中间,保持上一状态
329.0ON高于上限,打开
427.0ON仍在中间带,保持开
524.0OFF低于下限,关闭
6读失败(哨兵)保持 OFF打印 READ_FAIL,不根据垃圾温度改风扇

本步成功判据:六步输出与上表一致;能口头说明滞回是自己按契约写的,读失败路径是改出来的。

板上 fake-thermo-sol:滞回 OFF/ON 与 READ_FAIL
图 1 板端证据(SSH → fake-thermo-sol):25 OFF → 29 ON → 24 OFF → READ_FAIL
荔枝派上同样跑通 fake-thermo-sol 六步
图 2 荔枝派上用板载 gcc 复跑同一条 SOL 命令,输出一致

验收表

验收项证据
编译链接核对完成project-templatebuild/*.oappfile 输出可区分
W1 从空写出滞回thermo_decide 使用 t_high/t_low;升温开、降温关、中间保持;空指针不崩溃
L3 空指针安全返回thermo_read(NULL) 不崩溃
L4 读失败跳过决策READ_FAIL(或等价)日志,且风扇状态保持
内置序列跑通./build/fake-thermo 输出与期望表六步一致
能区分「写函数」和「改 bug」口头说明 decide 是按契约写的,read 的失败路径是改出来的
完成标志:验收表全部勾选。无需真 DHT;实验三在本机即可验收(板端复跑输出一致更佳)。