字符设备硬件控制:应用ioctl到 Jetson Nano GPIO 点灯
课程位置:驱动初级 → 字符设备 → 实现定制文件操作 → 定制操作命令、硬件控制、顺序申请逆序释放。
依据:2026-10-05 19:11《硬件控制》录音转写;学习系统中的“实现定制文件操作/应用程序/验证测试/硬件控制”笔记;老师资料10.内核驱动/led/step3.实现文件操作ioctl/{led.c,app.c,Makefile};Tegra X1 芯片手册与 Jetson Nano J41 载板规格。转写里的同音字按老师源码、寄存器截图和手册校正。
适用范围:本例针对Jetson Nano / Tegra X1、J41 物理 12 脚(GPIO3_PJ.07),内核示例为板端4.9.253-tegra。其他 Jetson 型号不能直接复用这些物理寄存器地址。课堂截图是老师的验证结果;文末代码尚未在你的板上实测。
一、先把整条链路串起来
上一节已经做到应用open("/dev/led"),内核能找到驱动的.open。这节新增一个定制命令入口.unlocked_ioctl,再把命令变成 GPIO 输出电平。老师先只打印收到的命令,证明“应用 → 驱动”通路成立,再按板卡引脚、载板原理图、Tegra X1 手册找到 GPIO 寄存器,完成“驱动 → 硬件”通路。(录音约 00:03—21:29、21:29—01:02:17)
A[app.c: open /dev/led] --> B[/dev/led: 字符设备 500:0] B --> C[led.c: cdev + file_operations] C --> D[app.c: ioctl LED_ON / LED_OFF] D --> E[led_ioctl: 判断命令] E --> F[led_on / led_off: readl + writel] F --> G[GPIO3_PJ.07: J41 物理 12 脚高/低电平] G --> H[外接 LED 亮/灭]应用每隔100000微秒发一次开灯、关灯命令,约亮 0.1 秒、灭 0.1 秒,每秒 5 次完整闪烁。这里的闪烁节奏由用户态程序控制,内核驱动只接收命令并改电平;不是在内核里写无限循环。
二、先纠正接线:39 脚是 GND,40 脚不是
课堂文字笔记写“地线接 40 脚”,但同节的 J41 引脚截图和载板规格图都标明:物理 39 脚是 GND,物理 40 脚是 I²S 数据输出/GPIO 信号。本实验应以 39 脚接地;接线前断电并按板上 J41 的 1 脚标记数脚位。本地 NVIDIA《Jetson Nano Developer Kit Carrier Board Specification》I:\Linux内核驱动\嵌入式AI班资料\4.官方文档\jetson nano的官方资料\SP-09732-001_v1.1.pdf第 21 页的引脚表已核对;Jetson Nano 用户指南 介绍了 J41 接口。
| J41 物理脚位 | 本例用途 | 代码中的对应关系 |
|---|---|---|
| 12 | GPIO 输出信号,接 LED 电路控制端。 | GPIO3_PJ.07;位 7;Pinmux 寄存器0x70003150。 |
| 39 | 地线 GND。 | 外接电路的公共地。 |
| 40 | 不是地线,不接本例的 GND。 | 是另一路信号,不能拿来代替 39。 |
老师的接线思路是“12 脚输出高电平,串限流的 LED 接向 GND”,所以代码约定高电平亮、低电平灭。课堂直接接法的方向是J41-12 → 限流电阻 → LED 正极(长脚) → LED 负极(短脚) → J41-39。若使用裸 LED,电阻要按该 LED 和载板的输出能力选;Jetson Nano J41 信号为 3.3V,载板上有电平转换器,驱动能力有限。先用低电流负载;若直连 LED 很暗或电平被拉低,应改用高阻输入的缓冲/驱动级,不要靠减小电阻强行取电。NVIDIA Jetson Nano J41 说明
若手头有逻辑电平 N 沟道 MOSFET,可让 12 脚只驱动栅极:J41-12 → 栅极,源极到J41-39,漏极接 LED 负极,J41-1(3.3V) → 约 1kΩ 限流电阻 → LED 正极,栅极再用约100kΩ下拉到 39 脚。这样 LED 电流由 3.3V 电源脚提供,代码仍保持“高亮低灭”。选用元件前仍应核对实际 LED、MOSFET 和载板资料。
三、命令如何从应用传到驱动
老师在应用和驱动两侧都定义同一组值:
#defineLED_MAGIC'L'#defineLED_ON_IOW(LED_MAGIC,1,int)#defineLED_OFF_IOW(LED_MAGIC,2,int)'L'是命令编码中的类型字段,1/2区分点亮与熄灭;两侧必须逐字一致。课程文字笔记中也出现过0/1编号,但老师原始step3源码实际用1/2,配套代码按原始源码保留。_IOW是按类型、序号、方向和参数大小编码命令值,不是加密,也不是安全认证;类型相同的其他驱动仍可能发生编码冲突。Linux ioctl 编码文档
本节驱动只看cmd,没有使用arg;老师的_IOW(..., int)因而有语义不一致的地方。为少改老师示例,配套代码保留_IOW命令值,应用调用时提供一个有效的int地址作第三参数,驱动暂时忽略它。若以后正式设计“无参数开/关”接口,两侧应同时换成_IO('L', 1/2);只改一侧会得到“不认识命令”。Linux ioctl 接口文档
在驱动的file_operations中,.unlocked_ioctl = led_ioctl让应用的ioctl(fd, LED_ON, ...)进入led_ioctl()。switch (cmd)命中LED_ON调led_on();命中LED_OFF调led_off();其他命令返回-ENOTTY。老师原始源码的LED_OFF分支少了break,会在灭灯后继续落到default返回-1;本例改为每个合法分支直接return 0,同时在应用侧检查ioctl()是否失败。
四、从 12 脚找到寄存器
老师采用的查找顺序是:J41 物理 12 脚 → 载板规格/原理图 → Tegra X1 的 DAP4_SCLK → GPIO3_PJ.07 → 寄存器基地址和偏移量。NVIDIA 开发者论坛中也明确核对了“Jetson Nano 12 脚 = GPIO3_PJ.07”;J 属于 GPIO3 控制器的第二个 port,因此其寄存器相对GPIO3_BASE=0x6000D200有0x04等偏移。NVIDIA GPIO 映射讨论
| 代码常量 | 地址/偏移 | 本例要改的位 | 效果 |
|---|---|---|---|
PINMUX_AUX_DAP4_SCLK_0 | 0x70003150 | PM[1:0]选非 I²S 功能;TRISTATE[4]=0。 | 管脚复用允许作 GPIO 输出。 |
GPIO3_BASE + CNF | 0x6000D200 + 0x04 | 位 7 = 1。 | GPIO3_PJ.07 选 GPIO 模式。 |
GPIO3_BASE + OE | 0x6000D200 + 0x14 | 位 7 = 1。 | GPIO 输出使能。 |
GPIO3_BASE + OUT | 0x6000D200 + 0x24 | 位 7 = 1 或 0。 | 高电平亮、低电平灭,前提是外部 LED 按本例接法。 |
ioremap(物理地址, 大小)得到内核可用于 MMIO 的__iomem地址;再用readl()读 32 位寄存器,改目标位,用writel()写回,不能把物理地址当普通 C 指针直接解引用。卸载时iounmap()与两次映射配对。Linux MMIO 文档
/* 老师的核心方法:只改变目标位 7,保留同组其他 GPIO 的位。 */writel(readl(gpio_base+OUT)|(1U<<7),gpio_base+OUT);/* 亮 */writel(readl(gpio_base+OUT)&~(1U<<7),gpio_base+OUT);/* 灭 */老师还在初始化末尾写了MSK_CNF、MSK_OE,并把它们解释为“取消屏蔽”。芯片手册截图说明这类掩码写寄存器的高 8 位控制哪些位允许被写,低 8 位是要写入的值。老师的| (1 << 7)只涉及低位,不能按注释理解为“解除屏蔽”;本例既然已经用普通CNF/OE/OUT做读改写,就省去这两句,不改变课程的主流程。
五、完整代码:沿用老师的led.c和app.c结构
下载或复制同目录的三个文件到 Ubuntu 虚拟机的一个独立工程目录:
- led.c:注册设备号、映射寄存器、准备 GPIO、添加
cdev,并用ioctl控制高低电平。 - app.c:打开
/dev/led,循环发送LED_ON/LED_OFF;按Ctrl+C后先灭灯再关闭文件。 - Makefile:把模块编译目录指向你此前使用的
/home/yhai/bsp/Linux_for_Tegra/source/public/kernel/kernel-4.9,目标架构是 arm64。
代码保留了老师的固定主次号500:0、'L'、编号1/2、DAP4_SCLK/GPIO3 物理寄存器和100000微秒间隔。为使示例可重复加载、可检查错误,做了以下有限修正:
- 先完成两处
ioremap()和 GPIO 准备,最后才cdev_add();因为cdev_add()一成功,旧的/dev/led节点可能立即打开并调用回调,不能让回调看到未映射的 GPIO 地址。Linux 字符设备 API - 映射失败明确返回
-ENOMEM,并按“成功取得什么就释放什么”的顺序清理。老师的部分错误路径未设置负错误码,可能让模块初始化错误地返回成功。 LED_OFF正常返回0,未知命令返回-ENOTTY;应用检查每次ioctl()的返回值,并提供与_IOW(..., int)匹配的第三参数。- 初始化先把 OUT 位 7 置低,再使能输出;卸载先删除
cdev、把 LED 关掉,再释放映射和设备号。这样执行insmod时灯保持灭,运行app后才开始闪烁。
**课程示例的边界:**这是为了学习寄存器、ioremap()和字符设备调用链的直接寄存器实验,没有接入 Linux GPIO/pinctrl 的资源管理。运行前确保 12 脚未被 I²S 或其他驱动占用;卸载后示例会拉低输出,但不会恢复加载前的全部 pinmux/CNF/OE 配置。其他板卡、不同载板或产品驱动应使用与硬件描述和 GPIO 子系统匹配的实现。
六、你这台板子的复现步骤
0. 板端确认对象与接线
先在板子执行:
cat/proc/device-tree/modeluname-runame-m只有确认是Jetson Nano / Tegra X1,并与此前的4.9.253-tegra、aarch64相符,才使用本例固定地址。接线按上面表格核对:12 为控制信号、39 为 GND,不要按文字笔记把地线接 40。若你的实际 LED 电路为“低电平亮”,需反转led_on()/led_off()的 OUT 位逻辑;本文以老师“高亮低灭”接法为准。
1. Ubuntu 虚拟机编译
在复制好led.c、app.c、Makefile的工程目录执行:
makecleanmakeaarch64-linux-gnu-gcc-Wall-Wextra-O2app.c-oappfileled.ko app modinfo-Fvermagic led.ko预期led.ko为ARM aarch64 relocatable,app为AArch64 用户程序,模块vermagic以4.9.253-tegra开头,并与板上运行内核匹配。若从 VMware 共享目录编译出现“文件修改时间在未来”,可把源码复制到 Ubuntu 自身文件系统的目录再make clean && make。不要把之前 x86_64 的led.ko或旧版应用当成新文件传板。
2. 传到板子
老师使用cp led.ko a.out /nfs/rootfs,前提是板上已经挂载并能访问同一个 NFS 根目录。你的 Ubuntu/Jetson 已用过文件传输时,也可用 SSH:
# 仅在板端地址仍为此前的 10.20.3.91 时,才照抄这条scpled.ko app aidb@10.20.3.91:/home/aidb/地址若变动,先在板子执行ip -br addr查当前 IP,再替换scp目标。这里的app是新交叉编译的应用;老师默认输出名a.out,功能对应但文件名不同。
3. 板端加载、创建节点、闪烁、卸载
在板子执行,先停掉仍运行的旧版app,并确认旧led模块已卸载:
cd/home/aidb lsmod|grep'^led '# 若上一行确实显示旧 led 模块,再执行:sudo rmmod ledsudoinsmod ./led.kogrep'yhai_led'/proc/devicesif[!-e/dev/led];thensudomknod/dev/led c5000fils-l/dev/ledsudo./appls -l /dev/led应显示它是字符设备,主号500、次号0。若已有/dev/led却不是c 500, 0,先查清原节点来源,不能把错误节点当成通过。运行后观察外接 LED 闪烁;按Ctrl+C,程序发最后一个关灯命令并关闭文件。随后再查日志和卸载:
sudodmesg|tail-n30sudormmod ledgrep'yhai_led'/proc/devicessudodmesg|tail-n104. 验收判据
| 观察 | 应得结论 |
|---|---|
insmod成功,/proc/devices出现500 yhai_led。 | 设备号取得成功,模块已加载。 |
/dev/led是c 500, 0;app无open或ioctl错误。 | 应用找到正确节点,命令与驱动定义匹配。 |
dmesg反复出现led on、led off,LED 实际明暗交替。 | “应用 → 驱动 → GPIO → 外接灯”整条链路验证成功。仅有日志不能替代实际闪烁。 |
Ctrl+C 后灭灯,rmmod后/proc/devices不再有yhai_led。 | 用户程序、字符设备、映射和设备号按示例退出。 |
七、遇到故障先按现象定位
| 现象 | 先查什么 |
|---|---|
insmod: File exists | `lsmod |
Invalid module format | file led.ko、modinfo -F vermagic led.ko与板子的uname -r、uname -m。 |
open /dev/led报错 | /dev/led是否存在、是否为c 500, 0、模块是否加载、设备节点权限。 |
ioctl ... Inappropriate ioctl for device | 两侧的'L'和命令序号是否同为1/2,板上是否还是旧led.ko。 |
日志有led on/off,LED 不闪 | 先核对12/39 接线、LED 极性、限流电路;再用万用表测 J41-12 相对 J41-39 的高低变化。若 GPIO 电平未变化,再检查板型、管脚复用和是否被其他驱动占用。 |
| 卸载提示模块仍在使用 | 先让应用正常退出并关闭/dev/led,再rmmod led。 |
八、这节课要记住的四句话
ioctl让应用发送课程自定义的LED_ON/LED_OFF命令;file_operations.unlocked_ioctl让命令进入驱动。ioremap负责把已确认的寄存器物理地址映射为内核可访问的 I/O 地址;readl/writel负责读改写,目标是 GPIO3_PJ.07 的第 7 位。- GPIO 12 脚只是物理接线点,主设备号 500 是
/dev/led的软件编号;两个“编号 12/500”不是一回事。 - 老师的验证路径是“应用操作 → 内核日志 → 引脚电平 → 灯实际闪烁”;你此前的
open led ok只完成到应用能打开设备文件,运行本节代码并看到真实闪烁才算完成硬件控制。