开始之前
你能学到什么:分层因果(程序 → libgpiod → 内核 → 引脚 → 继电器)、chip/line 寻址、申请与写入、用观察手段核对位翻转、滞回如何驱动执行链。
gcc 或主机交叉编译都行)、SSH/SCP 可用。
你需要准备什么
编译
板端原生 gcc(最简单),或主机交叉工具链。见第一章。
荔枝派 4A + 套件
继电器模块(1 路 5V)、5V 风扇、面包板、杜邦线。继电器和风扇由课程提供。
代码
讲义跟做命令行 gpioset。代码在 chapters/ch03/code/。
四节各自打透什么
3.1 外设与 I/O
物理量如何变成程序里的数;寄存器 / MMIO;用户态经内核接口访问硬件。
3.2 GPIO:认识与控制
chip/line、本板 1.8 V、gpiod 申请与写电平、只读核对、独占。
3.3 继电器、接线与跟做
信号端接线、gpioset 跟做、故障排查。
3.4 决策 → 执行
模拟温度与滞回如何驱动执行链。真传感器在第四章。
gpioset 控一次继电器。实验接上风扇负载,由你写出 line_request / fan_set / fan_decide,跑通模拟温度控制并改阈值。
本章术语:先说清,再往下
下面这些词会在本章反复出现。先扫一遍,知道「它是什么、为什么需要它」;读到再见到就不慌,也可以随时翻回这一页。它们不是堆砌——每引入一个词,都是因为我们到了必须说清那一步的关口。
| 术语 | 是什么 | 为什么需要它 |
|---|---|---|
| 外设 | CPU 之外的硬件模块(传感器、风扇、继电器) | 程序要控制的「外部世界」就是它 |
| I/O | 程序与硬件的输入/输出 | 把外界变成数、把数变成动作 |
| GPIO | 可配置方向的引脚(输出高/低或读输入) | 用一根线的高低控制外部 |
| 寄存器 | 芯片内部可读写的控制位 | 芯片控制外设的唯一途径,一位一个开关 |
| MMIO | 把寄存器映射到内存地址空间 | 软件用「读写内存」的方式控制硬件 |
| 位(bit) | 二进制的一位 0/1 | 寄存器里每个开关就是一 位 |
| gpiochip | 一组 GPIO 控制器的设备节点(/dev/gpiochipN) | Linux 给一组脚统一编号、仲裁占用 |
| line | 某个 chip 里的脚号(偏移) | 精确定位「哪一组、第几根脚」 |
| 电平 | 引脚电压的高低(本板信号脚 1.8 V) | 「写 1/0」最终变成引脚上的电压 |
| 继电器 | 小信号控制大电流通断的器件 | 让 1.8 V 的 GPIO 能控制 5V 风扇 |
| 信号端 / 触点 | 继电器输入侧 / 输出侧 | GPIO 只碰信号端,大电流走触点 |
| NO / NC | 常开 / 常闭触点 | 决定「无驱动时」风扇默认是关还是开 |
| gpiod | 用户态 GPIO 库与命令行工具(libgpiod) | 走内核接口控制脚,不直接碰寄存器 |
| consumer | 占用者的名字 | gpioinfo 里认出「谁占着我的脚」 |
| 独占 | 一根 line 同一时刻只能一个申请者 | 防止两个程序各写各的、互相踩 |
| 悬空 | 引脚没有被任何程序驱动 | 电平不定(常读到高),继电器可能乱吸合 |
| 滞回 | 开/关用两个不同阈值,中间保持 | 温度在临界附近时风扇不频繁开合 |
外设与 I/O
I/O:物理世界怎么进程序
外设是 CPU 之外、用来感知或驱动外部世界的硬件模块——传感器、LED、风扇驱动等都算。I/O(Input/Output)是程序与这些硬件打交道的总称:输入把外界变成程序里的数,输出把程序里的决定变成外界动作。
芯片不直接理解「风扇开」。它只有地址和位:外设的每个功能都是一组寄存器,寄存器里的每一位是一个开关。要控制外部世界,程序最终做的是——往某个地址写一个位。
第二章的位运算(& | <<)在这里落到硬件上:改寄存器的一位,就是控制物理世界的一根线。后面出现的 gpioset、gpiod、devmem 全在做同一件事——只是入口不同、离硬件远近不同。
同一条 GPIO 脚,配置成输出就能驱动执行器(如继电器信号端);配置成输入就能读按键。对用户态程序来说,最终仍是「经内核接口写出一个命令」。下一节把这根脚在 Linux 里的名字(chip / line)和本板的 1.8 V 电平讲清楚——否则「写了 1 却不见动」会很难排查。
寄存器、MMIO 与用户态边界
寄存器是芯片内部一组可读写的控制位,用来配置外设、读状态、写输出。MMIO(Memory-Mapped I/O,内存映射 I/O)把这些寄存器映射到 CPU 可见的地址空间:驱动对某个地址的读写,实际落到外设控制逻辑,而不是普通 RAM。
| 说法 | 含义 | 本课怎么用 |
|---|---|---|
| MMIO | 用「访存」方式访问设备寄存器 | 建立因果链;业务程序经 gpiod / 内核访问 |
| 设备节点 | 如 /dev/gpiochip5 | 应用通过 open / 库 API 访问 |
| 内核驱动 | 独占硬件、做权限与时序管理 | 多进程仲裁、屏蔽芯片细节 |
set_value 之后,会用 devmem 只读核对某一位是否翻转——用来观察驱动确实写了寄存器,不当日常控外设手段。
GPIO:认识与控制
这一节把「外设」落到一根具体的脚上。读完应能回答:GPIO 是什么、Linux 怎么给脚编号、高低电平在本板上意味着什么、为什么要先申请再改电平,以及 set_value 之后如何用只读手段看见寄存器位翻转。
一根脚能做什么
GPIO(General-Purpose Input/Output,通用输入输出)是芯片上可以由软件配置方向的引脚。配置成输出时,程序决定它是高还是低,引脚电平驱动外部模块(继电器吸合、风扇转)。配置成输入时,程序读外部此刻是高还是低(按键有没有按下)。同一根物理脚,同一时刻通常只做一件事:要么输出,要么输入。
风扇、传感器模块内部可能很复杂;对 GPIO 这一层,常常仍然是「某一根脚输出高/低」或「某一根脚被拉高/拉低」。你的程序首先要管好自己的那根脚。
Linux 怎么称呼一根脚:chip 与 line
一块板子上往往有多组 GPIO 控制器。Linux 把每一组暴露成一个字符设备,名字像 /dev/gpiochip0、/dev/gpiochip1……每一个叫一个 gpiochip(芯片控制器节点,不一定等于「整颗 SoC」)。
在某一个 chip 里面,脚按编号排列,这个编号叫 line(线号 / offset)。本课默认控继电器信号脚用的是:
#define GPIO_CHIP_PATH "/dev/gpiochip5" #define FAN_LINE 5 /* 对应排针丝印 IO1_5;换脚只改数字 */
chip 指出哪一组控制器,line 指出这一组里的第几根。只说「GPIO3」而不说 chip,在多控制器的板上会说不清到底是哪一根。
| 说法 | 指什么 | 本课例子 |
|---|---|---|
| 排针丝印 | 硬件手册上的名字,接线时认这个 | IO1_5、IO1_6 |
/dev/gpiochipN | 内核暴露的某一组 GPIO 控制器 | gpiochip5 |
| line 编号 | 该 chip 内从 0 起的偏移 | line 5 → IO1_5 |
| 内核 debug 全局号 | /sys/kernel/debug/gpio 里的 gpio-651 一类 | 基址 + line;编程宏里只写 line,全局号留给排查 |
/dev/gpiochip5 line 5。日常编程用 chip + line。/sys/kernel/debug/gpio 用来看占用与方向。gpiochipN 的 N 随内核探测顺序变化:本课 RevyOS 上排针在 gpiochip5;旧材料里的 gpiochip1 在本板上是另一组内部控制器。换镜像后,用运行时核对再写宏。
在本课 RevyOS 板上用 sudo cat /sys/kernel/debug/gpio 现查:gpiochip5 覆盖全局号 646–677(基址 646),所以 line 5 = gpio-651;它的父寄存器物理地址正是 0xffec006000——和讲义下面的寄存器地址对上了。编程只用 gpiochip5 + line 5,全局号 gpio-651 留给排查时对号。
「高」「低」在本板上是什么意思
程序里写 1 / 0(或 active / inactive),最终变成引脚上的电压:
- 高(high):接近该脚所在电源域的电压。
- 低(low):接近地(GND)。
许多教材默认「GPIO = 3.3 V」。荔枝派 4A 的 SoC 原生 IO 为 1.8 V。排针上的 3.3 V / 5 V 电源脚是给外设供电的;信号脚仍是 1.8 V 域。
| 你接什么 | 1.8 V GPIO 与模块的关系 | 连接要求 |
|---|---|---|
| 本课继电器(5V 光耦隔离模块) | 输入侧是光耦 LED,1.8 V 足够触发 | 直连 GPIO(IO1_5) |
| 需要 3.3 V 电平的传感器(如第四章的 DHT22) | 信号电平要对齐 3.3 V | 第四章再讲电平转换 |
本课继电器能直连,因为输入侧是光耦隔离:1.8 V 足以触发光耦;模块 5 V 线圈与输入信号在电气上不相通。第四章 DHT22 的数据线要 3.3 V 电平,那时再接电平转换。
经内核改电平:gpiod / libgpiod
在 Linux 用户态,日常业务经内核改芯片寄存器。你向内核请求:「使用 gpiochip5 的 line 5,配成输出,初始为低。」内核同意后,再请求设为高或低。这套用户态库和命令行工具统称 gpiod(库名常见 libgpiod,链接 -lgpiod)。
同一根脚可能被别的进程、某个内核驱动、或上次没退干净的命令占用。内核统一仲裁,才不会两个人各写各的。日常控继电器信号端走 gpiod;下面用 devmem 只读核对寄存器位,是旁路观察。
板端常见命令(本课板子多为 libgpiod 2.x):
gpioinfo:看某个 chip 上每根 line 的方向、占用者。gpioset:申请并保持某根脚为指定电平(进程活着时通常一直占着)。gpioget:读电平——若该脚正被别人以输出方式占用,往往会失败。
许多板上没有名叫 gpio 的万能命令。需要权限时用 sudo,或把用户加入 gpio 组(以课程环境为准)。
/* libgpiod v2 调用轮廓 */ chip = gpiod_chip_open("/dev/gpiochip5"); /* 申请 line 为输出并给出初值 → 得到 line_request */ gpiod_line_request_set_value(req, line, 1); /* 高:继电器吸合 */ gpiod_line_request_set_value(req, line, 0); /* 低:继电器释放 */ gpiod_line_request_release(req);
把上面的调用轮廓合成一个完整可运行的程序。头文件 + main,关键步骤都有注释。这是演示 API 生命周期的平铺程序。
/* relay-toggle.c — 完整演示 gpiod 生命周期 * 打开 chip → 申请 line 5 为输出(初值 0) → 拉高 → 拉低 → 释放 */ #include <gpiod.h> #include <stdio.h> #include <unistd.h> int main(void) { /* 1. 打开 chip:IO1_5 所在控制器 */ struct gpiod_chip *chip = gpiod_chip_open("/dev/gpiochip5"); /* 2. 配置 line 5:输出,初值低。 * 初值=低 很关键:不驱动 = 悬空 = 电平不定,继电器可能乱吸合 */ struct gpiod_line_settings *st = gpiod_line_settings_new(); gpiod_line_settings_set_direction(st, GPIOD_LINE_DIRECTION_OUTPUT); gpiod_line_settings_set_output_value(st, GPIOD_LINE_VALUE_INACTIVE); struct gpiod_line_config *cfg = gpiod_line_config_new(); gpiod_line_config_add_line_settings(cfg, (unsigned int[]){5}, 1, st); /* 3. 申请(consumer="relay-toggle",gpioinfo 里认得出) */ struct gpiod_request_config *rcfg = gpiod_request_config_new(); gpiod_request_config_set_consumer(rcfg, "relay-toggle"); struct gpiod_line_request *req = gpiod_chip_request_lines(chip, rcfg, cfg); printf("已申请 line 5,初值低 → 风扇确定关\n"); sleep(1); /* 4. 拉高:继电器吸合,风扇开 */ gpiod_line_request_set_value(req, 5, GPIOD_LINE_VALUE_ACTIVE); printf("line 5 = 高 → 继电器吸合,风扇开\n"); sleep(2); /* 5. 拉低 → 6. 释放(把 line 还给内核) */ gpiod_line_request_set_value(req, 5, GPIOD_LINE_VALUE_INACTIVE); printf("line 5 = 低 → 继电器释放,风扇关\n"); gpiod_line_request_release(req); printf("已释放 line 5\n"); return 0; }
程序没运行时,没人申请 line 5——它悬空,电平由周围电路决定(常读到高)。如果继电器按 NO 接线,悬空高电平就把继电器吸合,风扇自己转起来。这正是「申请时给初值 0」要解决的问题:程序一启动就先把脚拉成确定的低,风扇才确定关着。
$ sudo /tmp/relay-toggle
已申请 line 5,初值低 → 风扇确定关
line 5 = 高 → 继电器吸合,风扇开
line 5 = 低 → 继电器释放,风扇关
已释放 line 5
set_value 之后:只读核对寄存器位
调用 gpiod_line_request_set_value(..., 1) 之后,真正改硬件的是内核 GPIO 驱动:经系统调用接到请求,往某个 MMIO 地址写一笔,把对应引脚那一位从 0 翻成 1(或反过来)。引脚电平变了,继电器才吸合。
本课板子(RevyOS)排针 GPIO 在 gpiochip5,数据寄存器物理地址 0xffec006000;IO1_5 是 gpiochip5 的 line 5(内核全局号 gpio-651,基址 646 + 5)。寄存器地址和位是心智模型:改 0xffec006000 的第 5 位(值 0x20),就是改 IO1_5 引脚。
CONFIG_STRICT_DEVMEM,用户态 devmem 对这个地址 mmap 会被拒(实测报 Operation not permitted)。这是内核刻意隔离硬件——正好印证 3.1 的边界:用户态不该绕过内核碰寄存器。核对电平,用内核自己的视图 debug/gpio。
在荔枝派上实测(gpioset 控 IO1_5,看内核视角):
$ sudo gpioset -c gpiochip5 5=1 $ sudo cat /sys/kernel/debug/gpio | grep gpio-651 gpio-651 (GPIO13 |gpioset ) out hi # ↑ 名字 ↑ 占用者 ↑ out hi = 引脚高 $ sudo pkill -x gpioset $ sudo cat /sys/kernel/debug/gpio | grep gpio-651 gpio-651 (GPIO13 )
改电平前后,out hi 出现 / 消失——「程序 → 内核 → 引脚」这条链就验证过了。对照上表:占用者列出现 gpioset,就是它申请并占住了这根 line。
devmem 读。业务控外设走 gpiod;读用来证明「程序 → 内核 → 寄存器位」这条链发生过。
独占与「Device or resource busy」
GPIO line 通常独占申请:同一时刻一个成功的请求占住这根脚,别人再申请或用另一种方式去读,就可能得到 Device or resource busy。
课堂上常见情况:用 gpioset 试验后命令一直占着;随后跑程序或 gpioget 失败。先弄清谁占着,再结束那个进程或换另一根 line。同一根脚上保持一个占用者。
在荔枝派上实测——占着 line 5 时,gpioget 读它直接失败:
$ sudo gpioget -c gpiochip5 5
gpioget: unable to request lines: Device or resource busy
$ ps aux | grep gpioset $ sudo pkill -x gpioset # -x:按进程名精确匹配 $ sudo cat /sys/kernel/debug/gpio
继电器、接线与跟做
这一节把 GPIO 接到真实的继电器与风扇上:信号端怎么接、用 gpioset 跟做一次、以及继电器不动时怎么排查。动手前先弄清面包板怎么连通,以及本章首次可能用到的电烙铁安全要求;完整步骤与插图见本章实验。
面包板与电烙铁(首次)
面包板:用语与商品详情一致——电源轨(「+」「−」横向连通)、连接孔(同一列纵向连通)、隔离凹槽(上下互不通)。摘录与结构图见 chapters/ch03/manuals/面包板_MB102摘录.md。把排针 5V / GND 接到电源轨后,再从轨上分线到继电器,可减少在排针上反复插拔。完整步骤与插图见本章实验。
电烙铁:本章以杜邦线插接为主;需要补焊端子或固定焊环时才使用。通电后烙铁放在架子上,勿用手试温,结束后断电冷却再收纳。安全细则与外形图见实验「电烙铁与安全」。
继电器:信号端与负载端
继电器把小信号变成大电流通路的通断:GPIO 驱动信号端(丝印 IN / X1,高电平吸合);风扇等大电流走触点(NO / COM)。程序只写高/低,电机电流不进 SoC 脚。
| 对象 | GPIO 在做什么 | 要建立的因果 |
|---|---|---|
| 继电器信号端 | 输出高/低 → IN 有效 → 线圈侧动作 | 讲义跟做打通这一段 |
| 风扇电源(触点) | 触点通断 5V | 实验接上 NO–COM 负载 |
跟做前:信号端三线
| 从 | 到 |
|---|---|
| 排针 IO1_5 | 继电器 IN / X1 |
| 排针 5V | 继电器 V+ / VCC |
| 排针 GND | 继电器 V- / GND |
跳线帽拨到 H。此时即可听吸合或看工作灯;风扇电源回路放到实验再接。
跟做:用 gpioset 控一次继电器
目的:证明「chip + line → 引脚电平 → 继电器信号端」。
- 1确认脚
本课默认:gpiochip5line 5 = 丝印 IO1_5。若镜像不同,用sudo cat /sys/kernel/debug/gpio与 pinmux 现查。 - 2清占用
sudo pkill -x gpioset(若有残留)。 - 3拉高 / 拉低
sudo gpioset -c gpiochip5 5=1,观察继电器;再5=0释放。 - 4只读核对(建议)
改电平前后各跑一次sudo devmem 0xffec006000 32,对照第 5 位。
$ sudo gpioset -c gpiochip5 5=1 $ sudo cat /sys/kernel/debug/gpio | head -n 80 # 找到 gpiochip5 / line 5:应为 out hi $ sudo pkill -x gpioset # 释放(gpioset 是持线命令,进程活着就一直占着;用 Ctrl+C 或 pkill 释放)
gpioset ... 5=1 会一直占住 line 5 直到进程结束(Ctrl+C / pkill)。要"改状态 + 看状态",开两个终端:一个发 gpioset,另一个 cat /sys/kernel/debug/gpio 看变化。
/dev/gpiochip* 默认归 root,普通用户打不开(Permission denied)。
故障排查(信号链)
继电器未动时,按软件状态 → 编号是否对应排针 → 信号端接线取证。
| 先查 | 证据 | 常见结论 |
|---|---|---|
| 进程与占用 | ps / gpioinfo / debug/gpio | 残留 gpioset;或写的是 0 却以为写了 1 |
| chip / line 是否排针 | pinmux、debug/gpio | 旧材料写的 gpiochip1 在本课 RevyOS 上是内部控制器;排针在 gpiochip5 |
| 内核状态 vs 继电器 | line 为 out hi 且继电器未动 | 查共地、5V、跳帽 H、IN 是否接到 IO1_5 |
某脚恒 out lo | 换 IO1_4/5/6 对比 | 板级脚失效或硬下拉:换脚并记下 |
gpiochip5 line 5。丝印是硬件事实;(chip, line) 随镜像变化——认脚认丝印,编号查运行时。
$ sudo cat /sys/kernel/debug/pinctrl/*/pinmux-pins $ sudo cat /sys/kernel/debug/gpio
决策 → 执行:模拟温度怎么驱动风扇
数据流:决策 → 执行
第三章的程序用程序内部自变的模拟温度(约 24°C → 30°C 再降回)演示「决策 → 执行」:模拟温度 → 阈值判断 → 继电器 → 风扇。真传感器(DHT22)到第四章再接。
执行侧是开/关:GPIO 高/低 → 继电器 → 风扇电源通断。两档阈值形成滞回(第二章已定义:中间区间保持上一状态),温度在临界附近时风扇保持上一状态。
/* 伪代码:方向示意 */ if (模拟温度 > T_HIGH) 风扇应开; else if (模拟温度 < T_LOW) 风扇应关; /* 中间区间:保持上一状态 */
两个阈值的作用:高开、低关,中间区间保持——真实温控普遍如此。默认 T_HIGH=29 / T_LOW=25(与实验学生版、图 3 一致)。