开始之前
你能学到什么:字节进出;1.8 V 与 3.3 V 为何要 TXS;命令表与派发;用
select 同时等「该采样了」和「你打了一行字」;温度或湿度过高都会开风扇。
你需要准备什么
主机 + 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 位 |
| 电平转换 / TXS | 1.8 V ↔ 3.3 V 双向转换 | 板子信号与传感器电平对齐 |
| 命令表 | 「名字 → 处理函数」的表 | 一行字按名字派到对应函数 |
| 切词 | 把一行拆成命令名 + 参数 | "set temp 30" → set + temp 30 |
| 派发 dispatch | 查表、名字对上就调用 | 加命令只多一行表项 |
| select | 让内核同时盯多个来源 | 同时等「该采样了」和「你打字了」 |
| 超时 | select 最多等多久 | 超时 = 该采样了 |
| 阈值 bar | 开/关的界限 | 温度、湿度各一条,可 set 改 |
| 滞回 | 开/关用两个阈值,中间保持 | 临界附近风扇不狂开狂关 |
| 模拟模式 | 程序自己变温度(SIMULATE_SENSOR=1) | 没有真传感器也能先跑通软件流程 |
串口:字节怎么进出
字节怎么进出
串口是按约定速率、逐字节收发的双向通道。硬件上常叫 UART。对本课程序来说:你敲的字变成字节流进入进程;程序 printf 回去的字出现在同一窗口。
第一章用串口登录系统;本章用同一类通道做应用层对话:程序读你的命令行,再打印状态。验收一律走 SSH 的 stdin/stdout——本课不要求再接一根物理串口线。
用 SSH 终端练读写
在 SSH 里跑程序:从 stdin 读一行、往 stdout 写回复。确认「打字 → 程序 → 回复」闭环。脚手架默认即此路径。
/* 读一行(到 '\\n' 或缓冲满) */ char line[128]; if (fgets(line, sizeof(line), stdin) == NULL) /* EOF 或错误 */ ; printf("[INFO] got: %s", line);
接传感器与命令表
DHT22:第一次接传感器
DHT22 是温湿度探头,模块上三根针:正(VCC)、负(GND)、OUT(数据线)。它内部一直在测,但你不问它不说——程序发开始信号后,它回传 40 位(湿度、温度、校验)。对程序来说,这是输入侧「读一个数」。
为何数据线要过 TXS
DHT22 数据线是 3.3 V 电平,板子 GPIO 是 1.8 V:
- 板子叫不醒它:GPIO 高只有 1.8 V,传感器认「高」要更高。
- 它回话会超压:传感器把线拉到 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.1 ms → 拉高 → 改输入 → 读 40 位 → 校验。用户态要稳,靠三件事:忙轮询分辨位宽、忽略过短毛刺、校验不过时换一种对齐再试。失败打 [ERR] 并重试;脚手架已写好,实验侧接对线。
命令表:字符串对应函数
命令表是「名字 → 处理函数」。收到一行后先拆出命令名,再查表调用。比一长串 if/else 清晰;后面加命令只多一行。
struct cmd_entry { const char *name; int (*handler)(char *args); };
把「读一行 → 切词 → 查表 → 派发」拆成几块各自讲清,再合成一个完整程序。这里用 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
| 命令 | 例子 | 行为 |
|---|---|---|
status | status | 打印最近温度、湿度、风扇、两条 bar |
set temp <数> | set temp 30 | 改温度 bar(高于开、低于关) |
set humidity <数> | set humidity 80 | 改湿度 bar(同上) |
命令只改内存里的阈值,下一轮采样决策立刻用上,不必重启进程。
荔枝派上跑 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
两边同时工作
一句话:有事叫我(你敲了字,或采样时间到了),没事就睡觉——别一直阻塞在打字上顾不上采样,也别用固定 sleep 让命令迟迟不回。
为何「先读命令再采样」串成一条线不行
程序要同时做两件事:约每 2 秒采样并按阈值开/关风扇;随时响应你敲的 status / set。
错误一:阻塞在读命令上。没人打字时进程卡在 fgets,采样永不执行,风扇停在上次状态。
错误二:固定 sleep(2) 再瞥一眼命令。风扇节奏对了,但敲完字最多要等满整个 sleep 才有回复。
/* 阻塞版:卡在 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(); /* 有字:读命令 */ }
正确机制: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 秒。超时要用「距下次采样还剩多久」,否则风扇节奏会被打字打乱。时间轴示意:
温湿度怎么一起控风扇
DHT22 同时给出温度和湿度。脚手架里是两条单阈值 bar:
- 开:温度高于温度 bar,或 湿度高于湿度 bar
- 关:温度低于温度 bar,并且 湿度低于湿度 bar
模拟模式(默认)里湿度大致恒定,主要靠模拟温度演示;接真传感器后可哈气观察湿度支路。命令 set temp / set humidity 分别改两条 bar。