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

本章实验 · 三个实验

实验一 on/off 解析 · 实验二 虚拟灯命令环 · 实验三 MQTT 远程控灯(真 GPIO)

本章三个实验一览

第五章的主实验要接真 LED、连 Broker、写 mosquitto 回调。为了不一把抓,拆成三个递进实验:实验一、实验二纯软件、无接线、无 Broker,把「精确匹配 on/off」和「改灯后再发 status」这两段先练会;实验三再接到真 GPIO 与真 MQTT。

实验练什么器材源码目录
实验一
on/off 精确解析
写 parse_led_cmd / status_payload;断言卡死大小写与空格变体 无 code/led-cmd-parse/
实验二
虚拟灯命令环
stdin 模拟下行:on/off → 改虚拟灯 → 打印 [STATUS] 无 code/virt-led-repl/
实验三
MQTT 远程控灯
接线 + libmosquitto + libgpiod;把实验一/二的逻辑接到真灯与 Broker 外接 LED、限流电阻、Broker code/mqtt-led/
建议:先在主机或板端把实验一、实验二跑到 ALL PASS / 参考输出一致,再进实验三接线——那时你只剩「mosquitto API + GPIO」没写过。

实验一 · on/off 精确解析(led-cmd-parse)

实验三 on_message 里最容易写松的是载荷匹配:写成忽略大小写、带空格也能过,甚至用 strcmp 把 on\0x 当成合法 on。MQTT 载荷不保证以 '\0' 结尾,所以必须连着长度一起比较。本实验把解析抽出来,用断言卡死。源码 chapters/ch05/code/led-cmd-parse/,只实现 parse_led_cmd 与 status_payload。

步骤

# 主机,或先 scp 到板子再进目录
$ cd chapters/ch05/code/led-cmd-parse      # 板上:cd ~/led-cmd-parse
$ make
$ ./build/led-cmd-parse      # 未实现 → SOME FAIL(预期)

打开 src/led_cmd.c,按头文件契约实现:按 payloadlen 用 memcmp 精确匹配——长度 2 且字节等于 "on"→1;长度 3 且等于 "off"→0;其他(含 "on\0x"、"ON"、"on ")→-1。status_payload 返回静态字符串 "on"/"off"。

/* 契约(include/led_cmd.h):
 *   payloadlen == 2 且 memcmp(payload, "on",  2) == 0 → 1
 *   payloadlen == 3 且 memcmp(payload, "off", 3) == 0 → 0
 *   其他 → -1(含内嵌 '\0' 的伪造载荷)
 */
int parse_led_cmd(const char *payload, int payloadlen);
$ ./build/led-cmd-parse
# 期望:
#   ok   parse 按 payloadlen 精确匹配(含 on\0x 伪造载荷被拒)
#   ok   status_payload 返回 on/off
#   ALL PASS
# 对照:make sol && ./build/led-cmd-parse-sol

本实验成功判据:ALL PASS;能口头说「为什么必须传长度、为什么 strcmp 在这里会放过 on\0x」。

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

实验二 · 虚拟灯命令环(virt-led-repl)

实验三的另一半是「先改灯、成功后再发 status」。本实验用 stdin 模拟 MQTT 下行,没有 Broker、没有 GPIO。源码 chapters/ch05/code/virt-led-repl/,只实现 apply_cmd——签名与实验三回调里的决策顺序一致。

步骤

# 主机,或先 scp 到板子再进目录
$ cd chapters/ch05/code/virt-led-repl      # 板上:cd ~/virt-led-repl
$ make
$ printf 'on\noff\nbad\nquit\n' | ./build/virt-led-repl
# 未实现:全是 [TODO] → 预期

打开 src/virt_led.c:精确 on/off 时改 *led_on,打印 [LED] ON/OFF 与 [STATUS] on/off;其他打印 [ERR] bad cmd。

$ printf 'on\noff\nbad\nquit\n' | ./build/virt-led-repl
# 参考(make sol 一致):
#   [LED] ON
#   [STATUS] on
#   [LED] OFF
#   [STATUS] off
#   [ERR] bad cmd
#   bye led=OFF

本实验成功判据:输出与参考一致;能口头说这套顺序平移到实验三就是 led_set 成功后再 publish_status。

板上 virt-led-repl-sol:on/off/bad 命令环
板端证据(SSH → virt-led-repl):改灯后再打 [STATUS];非法命令报错

实验三 · MQTT 远程控灯(真 GPIO + Broker)

把实验一的 parse_led_cmd / status_payload、实验二的「改灯→发 status」顺序接到真 mosquitto 与真外接 LED。工程 chapters/ch05/code/mqtt-led/ 已通过 Makefile 链上实验一的源文件;你在 on_message 里调用它们,并实现 publish_status。

外接 LED 怎么接

默认脚位:丝印 IO1_4(gpiochip5 line 4)。限流电阻串在 GPIO 与 LED 正极之间;LED 短脚回电源轨「−」(GND)。风扇与 DHT 仍占 IO1_5 / IO1_6。

串联:电阻与 LED 在面包板上的接法示意
图 1 商品详情:串联在面包板上的接法示意(建立电阻限流与正负极直觉)
串联链接图:电池、电阻与 LED
图 2 商品详情「串联链接图」:电阻与 LED 在同一回路
并联链接图:多路 LED
图 3 商品详情「并联链接图」:多路 LED 各有回负极的支路
本课回路(对照上图,开关换成 GPIO): IO1_4 → 限流电阻 → LED 长脚(正极) 电源轨「−」/ GND → LED 短脚(负极)
荔枝派 4A 与面包板:外接红 LED、继电器、风扇与 DHT 同板联调(灯灭)
图 4 本课实机:荔枝派 4A + 面包板;外接红 LED 接在 IO1_4 一路;风扇/继电器与 DHT 仍沿用前几章
同上接线,外接红 LED 点亮
图 5 同一套接线:主机 pub on 后外接 LED 亮
说明书摘录:chapters/ch05/manuals/LED_UR5202D摘录.md(极性 / VF)、电阻_100欧姆摘录.md、LED_串联并联示意.md。

步骤

1. 查 Broker IP 并配置

实验三要求板子能连上局域网里的 Mosquitto。先确认主机 Broker 已按讲义 5.1配好:listener 1883(监听局域网,不是只绑 127.0.0.1)、本课默认 allow_anonymous true,并已 systemctl restart / 重启容器使配置生效。课堂若统一提供 Broker,记下板书上的 IP、端口与是否匿名即可,跳过自装。

在跑 Mosquitto 的主机上查本机局域网 IP(与板子同一网段的那个):

$ hostname -I
# 记下该地址,下文记为 BROKER_IP
# 可选自检:本机 ss -lntp | grep 1883 应见 0.0.0.0:1883

把地址交给板端程序(环境变量优先于源码默认值):

  • 推荐:板端启动时写 BROKER_HOST=BROKER_IP ./mqtt-led。
  • 或编辑 main.c 里的 DEFAULT_BROKER_HOST 后重新 make。

其余宏按需核对:

  • BROKER_PORT:默认 1883(MQTT 明文常用端口)。
  • TOPIC_CMD / TOPIC_STATUS:默认 course/led/cmd 与 course/led/status;若课堂统一改了主题,以板书为准并保持发布端与订阅端一致。
  • GPIO_CHIP_PATH、LED_LINE:与外接 LED 接线一致。默认 LED_LINE=4(丝印 IO1_4);IO1_5/IO1_6 留给风扇与 DHT。Broker 填主机局域网 IP;板上的 127.0.0.1 只指向板自己。

本步成功判据:你能说出本机查到的 Broker IP、端口 1883、是否匿名;知道用环境变量或默认宏二选一配置;板端能连上(日志出现连接成功)。

2. 补全下行控灯与上行状态发布

术语提醒:下行指从电脑终端发命令到板子控硬件;上行指板子把当前状态发回 Broker,供别人订阅。

  1. 在 on_message 中处理命令载荷
    调用实验一的 parse_led_cmd(msg->payload, msg->payloadlen)(要把长度一起传进去,别只按字符串比):返回 1/0 时 led_set,成功后再 publish_status(status_payload(...));返回 -1 打 [ERR],不改灯、不发 status。
    成功样子:主机 pub on 后板端日志有收包记录,外接 LED 亮;pub off 后灯灭;pub ON 应报错(精确匹配)。
  2. 实现 publish_status
    用 mosquitto_publish 向 TOPIC_STATUS 发布当前灯态字符串。成功打 [INFO],失败打 [ERR](可参考 mosquitto 返回值与 mosquitto_strerror)。
    成功样子:另一台机器上 mosquitto_sub -t course/led/status -v 能看到与灯态一致的 payload。

3. 编译并部署

两条路径任选其一(产物都须是 RISC-V ELF):

# 路径 A:在荔枝派上原生编译
$ cd chapters/ch05/code/mqtt-led
$ make clean && make
$ file mqtt-led
# 应为 RISC-V ELF
$ BROKER_HOST=BROKER_IP ./mqtt-led

# 路径 B:在 x86 主机交叉编译后再拷到板
$ . ~/venv-gnu-ruyisdk/bin/ruyi-activate
$ cd chapters/ch05/code/mqtt-led
$ make clean && make CROSS_COMPILE=riscv64-ruyisdk-linux-gnu-
$ file mqtt-led
# 应为 RISC-V ELF
$ scp mqtt-led user@board-ip:~/
$ ssh user@board-ip 'BROKER_HOST=BROKER_IP ./mqtt-led'

把命令里的 BROKER_IP 换成你在第 1 步 hostname -I 记下的地址。可选:板端设 MQTT_CLIENT_ID 自定义客户端名(默认 mqtt-led-<主机名>)。启动后应看到正在连接 Broker、连接成功、以及已订阅命令主题的日志。

板上 SSH 跑 mqtt-led:订阅、on/off、拒绝 ON
图 6 板端证据(SSH → mqtt-led):连 Broker → 订阅 → on/off 发布 status;非法 ON 被拒
主机侧用仓库 MQTT 工具发 led on/off 并读回状态
图 7 主机侧收发:主机用仓库的 MQTT 工具发 led on / led off,读回状态 on / off;非法 ON 被拒(同时验证 setLed 参数校验)
板端 mosquitto_sub 抓到的实际报文
图 8 应用层视角:板端 mosquitto_sub -t 'course/led/#' -v 看到经过 Broker 的 course/led/cmd 与 course/led/status 报文
板端 tcpdump 抓到的 MQTT TCP 报文
图 9 链路层视角:板端 tcpdump -i wlan0 -n 'tcp port 1883' 抓到与 Broker 192.168.31.206:1883 的实际 TCP 报文(若板端未装 tcpdump:sudo apt-get install -y tcpdump,源不通可换镜像)
mqtt-led 收到 on 后 published status=on
同一次联调快照:published course/led/status = on

4. 联调(固定顺序,全部要做)

  1. 在主机开一个订阅窗口,盯住状态主题(BROKER_IP 仍用第 1 步查到的值):
    $ mosquitto_sub -h BROKER_IP -t 'course/led/status' -v

    mosquitto_sub 的意思:连上 Broker,一直听某个主题;有新消息就打印出来。-v 会把主题名和载荷一起打出来。

    主机 mosquitto_sub 收到 course/led/status on 与 off
    图 7 主机订阅状态主题:先后收到 course/led/status on 与 off(上行验收)
  2. 再开一个窗口发下行命令:
    $ mosquitto_pub -h BROKER_IP -t 'course/led/cmd' -m on
    期望:外接 LED 亮;订阅窗口出现状态 payload(如 on)。
  3. 再发 -m off:灯灭,状态同步为 off。
    主机 mosquitto_pub 向 course/led/cmd 发送 on 与 off
    图 8 主机下行:向 course/led/cmd 发 on / off
  4. 另开一个电脑终端,用 mosquitto_pub 连同一 Broker,对同一命令主题再发一次 on/off,确认灯有动作。
  5. 抓包(必做):在主机或板端(以讲义 5.3 与课堂约定为准,通常在能看到板↔Broker 流量的那一侧)先开抓包,再触发一次 publish:
    $ sudo tcpdump -i any -nn -A port 1883 -c 20
    tcpdump 在 port 1883 上 listening
    图 9 先开 tcpdump,过滤 port 1883,再去 pub 一条命令

    成功时,摘要行里应能看到板子 IP 与 Broker IP、目的端口 1883,以及 TCP 标志。示例(本课联调实录,IP 按你的局域网会不同):

    19:12:32.626647 ens33 In  IP 192.168.31.179.47778 > 192.168.31.206.1883: Flags [P.], ... length 2
    19:12:32.627119 ens33 Out IP 192.168.31.206.1883 > 192.168.31.179.47778: Flags [.], ack 2, ...
    19:12:32.627601 ens33 Out IP 192.168.31.206.1883 > 192.168.31.179.47778: Flags [P.], ... length 2
    你应当在 tcpdump 输出里看到什么:
    • 过滤条件 port 1883 命中的包(MQTT 默认明文端口);
    • 用 -nn 看到的 IP 地址与端口,对应「这是 TCP 连接上的流量」;
    • 用 -A 打印的 ASCII 区,有时能瞥到主题名或 on/off 一类载荷片段。
    分层心智(口述即可):应用层是 MQTT 的 publish 内容 → 装在传输层 TCP(端口 1883)里 → 再装在网络层 IP 里发出去。把含 1883 的片段保存下来作为验收证据。

验收

实验项通过条件□
一解析led-cmd-parse 打印 ALL PASS
二虚拟灯喂 on/off/bad/quit 输出与参考一致
三编译板端或交叉产物为 RISC-V ELF
三下行pub on/off 外接 LED 亮灭正确
三上行状态主题订阅端能看到对应 payload
三抓包保存含 1883 的 tcpdump 片段;能口述 MQTT→TCP→IP 分层
完成标志:上表全部勾选;可进入第六章把 MQTT 放进独立通信线程。