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

串口对话与温控

SSH 里打字对话 · 第一次接 DHT22 · select 同时等采样与命令
— 4.1 / 4.2 / 4.3 —

跟做:心智模型 实验:接线 + 自己写 C 工程:chapters/ch04/code/

开始之前

本章要干什么:在第三章模拟温度控制之上,第一次接真温湿度传感器(DHT22),并在 SSH 终端里用打字查状态、改阈值;采样与打字同时及时。
你能学到什么:字节进出;1.8 V 与 3.3 V 为何要 TXS;命令表与派发;用 select 同时等「该采样了」和「你打了一行字」;温度或湿度过高都会开风扇。
前置:已完成第三章——能用 GPIO 经继电器控风扇、写过 gpiod 与滞回函数。

你需要准备什么

主机 + SSH

能登录荔枝派 4A;板端 gcc 或交叉工具链。

板卡 + 套件

DHT22;TXS0108E(排针需焊);AMS1117-1.8V;继电器 + 风扇(第三章的)。

代码

chapters/ch04/code/:脚手架已有采样与风扇;命令与 select 主循环由你实现。

三节各自打透什么

4.1 串口与字节

敲的字如何变成程序里的一行;回复如何打回 SSH 窗口。

4.2 传感器与命令

DHT22 + TXS 的因果;命令表与 status / set 语义。

4.3 两边同时

两种错误写法;select「有事叫我」;温湿度联合开/关。

术语表

本章术语:先说清,再往下

下面这些词会在本章反复出现。先扫一遍,知道「它是什么、为什么需要它」;读到再见到就不慌,也可以随时翻回这一页。它们不是堆砌——每引入一个词,都是因为我们到了必须说清那一步的关口。

术语是什么为什么需要它
串口 / UART逐字节收发的双向通道登录系统、应用层对话都走它
标准输入 stdin程序读输入的流你敲的字从这里进程序
标准输出 stdout程序打印的流回复回到终端窗口
fgets读一行到缓冲区把「一行命令」拿进程序
传感器感知物理量的器件DHT22 读温度/湿度
单总线一根线收发数据的协议DHT22 靠一根数据线传 40 位
电平转换 / TXS1.8 V ↔ 3.3 V 双向转换板子信号与传感器电平对齐
命令表「名字 → 处理函数」的表一行字按名字派到对应函数
切词把一行拆成命令名 + 参数"set temp 30"set + temp 30
派发 dispatch查表、名字对上就调用加命令只多一行表项
select让内核同时盯多个来源同时等「该采样了」和「你打字了」
超时select 最多等多久超时 = 该采样了
阈值 bar开/关的界限温度、湿度各一条,可 set
滞回开/关用两个阈值,中间保持临界附近风扇不狂开狂关
模拟模式程序自己变温度(SIMULATE_SENSOR=1没有真传感器也能先跑通软件流程
4.1

串口:字节怎么进出

字节怎么进出

串口是按约定速率、逐字节收发的双向通道。硬件上常叫 UART。对本课程序来说:你敲的字变成字节流进入进程;程序 printf 回去的字出现在同一窗口。

你(键盘) │ 敲 "status\n" ▼ SSH 终端 ──► 标准输入 stdin │ fgets() / read() ▼ 你的 C 程序(解析命令、做事) │ printf / write ▼ SSH 终端窗口里出现回复

第一章用串口登录系统;本章用同一类通道做应用层对话:程序读你的命令行,再打印状态。验收一律走 SSH 的 stdin/stdout——本课不要求再接一根物理串口线。

用 SSH 终端练读写

在 SSH 里跑程序:从 stdin 读一行、往 stdout 写回复。确认「打字 → 程序 → 回复」闭环。脚手架默认即此路径。

/* 读一行(到 '\\n' 或缓冲满) */
char line[128];
if (fgets(line, sizeof(line), stdin) == NULL)
    /* EOF 或错误 */ ;

printf("[INFO] got: %s", line);
本节成果:能画出「键盘 → stdin → 程序 → 终端回复」;知道本章命令入口是 SSH 窗口。
4.2

接传感器与命令表

DHT22:第一次接传感器

DHT22 是温湿度探头,模块上三根针:正(VCC)、负(GND)、OUT(数据线)。它内部一直在测,但你不问它不说——程序发开始信号后,它回传 40 位(湿度、温度、校验)。对程序来说,这是输入侧「读一个数」。

为何数据线要过 TXS

DHT22 数据线是 3.3 V 电平,板子 GPIO 是 1.8 V:

  1. 板子叫不醒它:GPIO 高只有 1.8 V,传感器认「高」要更高。
  2. 它回话会超压:传感器把线拉到 3.3 V,直接进 1.8 V 脚不安全。

TXS0108E 做双向电平转换:A 侧接板子 1.8 V,B 侧接传感器 3.3 V;同号 A1↔B1 是一对。AMS1117-1.8V 小板给 TXS 的 VA(低压侧电源)供电;VB 接 3.3 V;OE 拉高使能。

排针 IO1_6 (1.8V) ── A1 | TXS | B1 ── DHT22 OUT (3.3V) VA ← AMS 1.8V VB ← 3.3V 继电器 / 风扇:仍用第三章 IO1_5 直连

读取:单总线位带(脚手架已实现)

流程:IO1_6 配输出 → 拉低约 1.1 ms → 拉高 → 改输入 → 读 40 位 → 校验。用户态要稳,靠三件事:忙轮询分辨位宽、忽略过短毛刺、校验不过时换一种对齐再试。失败打 [ERR] 并重试;脚手架已写好,实验侧接对线。

命令表:字符串对应函数

命令表是「名字 → 处理函数」。收到一行后先拆出命令名,再查表调用。比一长串 if/else 清晰;后面加命令只多一行。

输入行:"set temp 30" │ ▼ 拆词:cmd="set", args="temp 30" │ ▼ 查表 → cmd_set(...) │ ▼ 改温度 bar,打印确认
struct cmd_entry {
    const char *name;
    int (*handler)(char *args);
};
先分再合:一个完整的命令读取器(cmd-demo)

把「读一行 → 切词 → 查表 → 派发」拆成几块各自讲清,再合成一个完整程序。这里用 help / echo / quit 三个命令做骨架。

/* 分①:处理函数——每个命令做一件事 */
static int cmd_help(char *args) { printf("commands: help echo quit\n"); return 0; }
static int cmd_echo(char *args) { printf("%s\n", args); return 0; }
static int cmd_quit(char *args) { return 1; }   /* 返回 1 = 让主循环退出 */

/* 分②:命令表——名字 → 函数指针(加命令只多一行) */
struct cmd_entry { const char *name; int (*handler)(char *); };
static const struct cmd_entry cmds[] = {
    { "help", cmd_help },
    { "echo", cmd_echo },
    { "quit", cmd_quit },
    { NULL, NULL },
};

/* 分③:切词 + 查表派发——名字对上就调用 */
static int dispatch(char *line)
{
    char *args = strchr(line, ' ');   /* 第一个空格后是参数 */
    if (args) { *args = '\0'; args++; }
    for (const struct cmd_entry *e = cmds; e->name; e++)
        if (strcmp(line, e->name) == 0)
            return e->handler(args);
    printf("unknown command: %s\n", line);
    return 0;
}

/* 合:读一行 → 派发 → 直到 quit */
int main(void)
{
    char line[128];
    while (fgets(line, sizeof(line), stdin)) {
        line[strcspn(line, "\n")] = '\0';   /* 去掉换行 */
        if (dispatch(line)) break;
    }
    printf("bye\n");
    return 0;
}
板上实测输出
$ printf 'help\necho hello\nfoo\nquit\n' | ./cmd-demo
commands: help echo quit
hello
unknown command: foo
bye

status 与 set

命令例子行为
statusstatus打印最近温度、湿度、风扇、两条 bar
set temp <数>set temp 30改温度 bar(高于开、低于关)
set humidity <数>set humidity 80改湿度 bar(同上)

命令只改内存里的阈值,下一轮采样决策立刻用上,不必重启进程。

一行命令怎么走:set temp 30 fgets 读一行 "set temp 30\n" │ 去掉换行 ▼ 切词 cmd="set", args="temp 30" │ ▼ 查表 name "set" → cmd_set │ ▼ cmd_set 解析 sscanf(args, ...) → 改温度 bar │ ▼ 下一轮采样决策 按新 bar 判断 → 风扇跟着变
板上实测:模拟模式下的 status / set 对话

荔枝派上跑 cmd_thermo-sol(模拟模式 SIMULATE_SENSOR=1)。注意先出现「尚无采样」——DHT22 要等第一个采样周期才有读数;随后 set 改的 bar 会立刻体现在「规则」行里。

$ printf 'status\nset temp 30\nset humidity 80\n' | sudo ./cmd_thermo-sol
[规则] 温度高于 30.0°C,或湿度高于 95.0% → 开风扇
[规则] 温度低于 30.0°C,且湿度低于 95.0% → 关风扇
[STATUS] 尚无采样,正在等待 DHT22
[INFO] 温度 bar 已设为 30.0°C(高于则开,低于则关)
[规则] 温度高于 30.0°C,或湿度高于 95.0% → 开风扇
[规则] 温度低于 30.0°C,且湿度低于 95.0% → 关风扇
[STATUS] 尚无采样,正在等待 DHT22
[INFO] 湿度 bar 已设为 80.0%(高于则开,低于则关)
[规则] 温度高于 30.0°C,或湿度高于 80.0% → 开风扇
[规则] 温度低于 30.0°C,且湿度低于 80.0% → 关风扇
[STATUS] 尚无采样,正在等待 DHT22
本节成果:能说明为何要 TXS;能说出命令表与 status/set 语义。
4.3

两边同时工作

一句话:有事叫我(你敲了字,或采样时间到了),没事就睡觉——别一直阻塞在打字上顾不上采样,也别用固定 sleep 让命令迟迟不回。

为何「先读命令再采样」串成一条线不行

程序要同时做两件事:约每 2 秒采样并按阈值开/关风扇;随时响应你敲的 status / set

错误一:阻塞在读命令上。没人打字时进程卡在 fgets,采样永不执行,风扇停在上次状态。

错误二:固定 sleep(2) 再瞥一眼命令。风扇节奏对了,但敲完字最多要等满整个 sleep 才有回复。

对照:阻塞 vs select,差别只在「等什么」
/* 阻塞版:卡在 fgets,没人打字就永远轮不到采样(错误一) */
while (1) {
    fgets(line, sizeof(line), stdin);  /* 无字时一直卡在这 */
    sample_and_control();               /* 采样轮不到执行,风扇停在上次状态 */
}

/* select 版:内核同时盯「stdin 有字」和「超时」,谁先到谁叫醒(正确) */
for (;;) {
    /* cmd_fd 放进读集合;超时 = 距下次采样还剩多久 */
    n = select(cmd_fd + 1, &rfds, NULL, NULL, &timeout);
    if (n == 0)       sample_and_control();            /* 超时:该采样 */
    else if (FD_ISSET(cmd_fd, &rfds)) read_command_line();  /* 有字:读命令 */
}
目标: 风扇仍按温湿度自动开/关 同时 敲 status / set 立刻有回复

正确机制:select

select 就是把上面那句话交给内核:盯着 stdin;再设一个超时。谁先到谁叫醒你。

select 返回含义本课该做什么
< 0出错(含被信号打断)perror;按策略退出或重试
0超时(该采样了)调用 sample_and_control
> 0有字可读读一行并派发命令
/* 伪代码:方向示意 */
for (;;) {
    准备 rfds(cmd_fd)与超时;
    n = select(...);
    if (n == 0) sample_and_control();
    else if (FD_ISSET(cmd_fd, &rfds)) read_command_line();
}

写主循环时注意:处理完命令不要把采样时钟重新拨成满 2 秒。超时要用「距下次采样还剩多久」,否则风扇节奏会被打字打乱。时间轴示意:

t=0.0 采样,下次该在 t=2.0 t=0.0 select(还剩 ≈2s) t=0.7 敲 status → 立刻回复 再 select(还剩 ≈1.3s) ← 不重置成 2s t=2.0 超时 → 再采样

温湿度怎么一起控风扇

DHT22 同时给出温度和湿度。脚手架里是两条单阈值 bar

模拟模式(默认)里湿度大致恒定,主要靠模拟温度演示;接真传感器后可哈气观察湿度支路。命令 set temp / set humidity 分别改两条 bar。

下一步:打开 lab.html 开始实验。