本项目包含一个基于内存映射 I/O(MMIO)的 LED 字符设备驱动(led_driver.c)以及一个用户态测试程序(led_app.c)。驱动直接对寄存器进行读改写(使用 readl/writel 以两次 32 位访问拼装 64 位),用户态通过 read/write/ioctl 控制 LED 的开关与状态查询。
作为南京大学数字系统实验二的实验课程报告——实验三:字符型设备驱动开发
led_driver.c:LED 字符设备驱动(内核模块,生成led_driver.ko)led_app.c:用户态测试程序,演示ioctl/闪烁Makefile:同时构建内核模块与用户程序(交叉编译,LoongArch)README.md:说明文档(本文件)
- 字符设备名称:
leddev_mmio - 动态分配主设备号(major),需手工
mknod建立设备节点 - 可配置参数(模块参数)决定寄存器物理地址、偏移、掩码、方向语义和电平极性
- 支持操作方式:
read(2):读取所有 LED 当前状态,返回由'0'/'1'组成的字节串write(2):写入'0'/'1'字符串批量设置 LED 状态(按led_mask位序)ioctl(2):LED_IOCTL_ON:点亮指定索引的 LEDLED_IOCTL_OFF:熄灭指定索引的 LEDLED_IOCTL_TOGGLE:翻转指定索引的 LEDLED_IOCTL_GET:读取指定索引的 LED 状态
- 已安装 LoongArch 交叉工具链:
loongarch64-linux-gnu-gcc - 可用的目标内核源码或内核构建目录(与目标板内核版本/配置一致)
- 根据硬件手册确定以下信息(对于Loongson开发板,这些数据已经在代码内加载为默认数据,不需要修改和额外在insmod时候导入):
- LED 控制寄存器物理基地址(
gpio_phys_base) - 区域大小(
region_size),如 0x40/0x100 - 数据/输出寄存器偏移(
out_offset) - 方向寄存器偏移(
dir_offset,若无填 -1) - 方向语义(
dir_output_clear:1 表示清零为输出;0 表示置位为输出) - LED 位掩码(
led_mask),如 0x7 表示使用 bit0~bit2 - 有效电平(
active_high,1 表示高电平点亮,0 表示低电平点亮)
- LED 控制寄存器物理基地址(
注:上述参数也可在加载模块时传入,详见下文“加载模块与参数”。
请先在 Makefile 中检查并按需修改:
KDIR:指向你的目标内核源码/构建目录(包含build文件)ARCH、CROSS_COMPILE:与目标环境一致
编译内核模块与用户程序:
make清理:
make clean将驱动的.ko文件与用户测试文件复制到 NFS 根目录(可选,按需在 Makefile 中修改 NFS_DIR):
make install生成的文件:
led_driver.ko:内核模块led_app:用户态程序(LoongArch 目标)
常用模块参数(当前代码默认值以源码为准)
gpio_phys_base:LED 寄存器物理基地址,默认0x1FE00500region_size:MMIO 区域大小,默认0x40out_offset:数据/输出寄存器偏移,默认0x10dir_offset:方向寄存器偏移,默认0x00(若无方向寄存器,请传-1)dir_output_clear:方向位语义,默认1(清零=输出;置位=输入)。若你的硬件相反,请传0。led_mask:LED 位掩码,默认0x7(bit0~bit2)active_high:有效电平,默认1(高电平点亮)。若硬件低电平点亮,请传0。
示例(请按你的硬件实际值替换参数):
insmod led_driver.ko \
gpio_phys_base=0x1fe00500 region_size=0x40 \
out_offset=0x10 dir_offset=0x00 dir_output_clear=1 \
led_mask=0x7 active_high=1加载成功后,查看日志获取主设备号(major)及确认参数:
dmesg | tail -n 20
# 期望看到类似:
# leddev_mmio: initialized (major=248) phys=0x1fe00500 out_off=0x10 dir_off=0x0 dir_output_clear=1 mask=0x7 active_high=1 leds=3驱动不会自动创建 /dev 节点,请用上一步日志中的 major 手动创建(或从 /proc/devices 获取):
grep leddev_mmio /proc/devices # 可从这里取到主设备号,我自己实验时候是248
mknod /dev/leddev_mmio c <major> 0
chmod 666 /dev/leddev_mmio如果使用 udev 规则,也可自动化创建设备节点(可选)。
假设 led_mask=0x70(3 个 LED),可直接通过字符设备测试:
- 读取当前状态(返回 N 字节,如
010;N=由led_mask位数决定):
cat /dev/leddev_mmio- 批量设置状态(如:点亮第 1 和第 3 个):
echo -n 101 > /dev/leddev_mmioled_app 使用 ioctl 顺序点亮/熄灭 LED 并测试 TOGGLE,默认操作 3 个 LED:
./led_app程序会输出时间戳日志;如需更改循环次数/延时,可修改源码中的 loops/delay_ms 后重新编译。
如果你的板上仅有一个 LED 接在某个 GPIO 位(例如 bit2),可以将
led_mask=0x4,这样用户程序只会操作一个 LED,避免其它位没有硬件连接时造成“看似成功但无亮灯”的困扰。
以下结合源码说明关键实现(参见 led_driver.c):
-
模块参数与地址映射
- 通过
gpio_phys_base/region_size/out_offset/dir_offset等参数确定寄存器区域与偏移。 - 使用
ioremap()将物理地址映射为内核虚拟地址指针mmio_base。
- 通过
-
64 位数据寄存器访问(避免使用 writeq/readq)
- 读:
u32 lo = readl(mmio_base + out_offset);u32 hi = readl(mmio_base + out_offset + 4);- 组装为
((u64)hi << 32) | lo。
- 写:
- 先
writel(低32)再writel(高32),最后wmb()保证写入次序。
- 先
- 同样的策略也用于方向寄存器(若存在)。
- 读:
-
方向寄存器配置
- 若
dir_offset >= 0,则根据dir_output_clear配置:dir_output_clear=1:清零led_mask对应位表示输出。dir_output_clear=0:置位led_mask对应位表示输出。
- 若
-
位映射与索引
led_mask指定哪些 bit 是 LED;驱动会计算 LED 数量与逻辑索引。- 通过
led_index_to_bit(idx, &bitpos)将逻辑索引(0..N-1)映射到实际 bit 号。
-
读写与 ioctl 的核心路径
read(2):读取数据寄存器,按led_mask位序返回'0'/'1'串;若active_high=0则翻转语义。write(2):读-改-写数据寄存器,按输入串逐位设置/清除目标位;忽略多余字节。ioctl(2):LED_IOCTL_ON/OFF/TOGGLE/GET:按bitpos精确操作对应位,不再硬编码具体位号。
-
卸载时的收尾
- 将所有 LED 置为“关闭”状态(依据
active_high),随后iounmap()。
- 将所有 LED 置为“关闭”状态(依据
说明:驱动中
request_mem_region()被注释跳过,方便在教学或共享区域的实验环境中加载(若区域被其他模块占用也能映射)。 我自己实验时候发现,loongson开发板启动时候自带有GPIO驱动,会声明对这一部分的寄存器的使用,所以我们自己的驱动代码中不可以加入这一部分
- 加载时报错
request_mem_region failed:- 物理地址区间已被其他驱动占用;确认
phys_base/region_size是否正确,或卸载冲突驱动。 - 按照上面说的,注释
request_mem_region()可以解决
- 物理地址区间已被其他驱动占用;确认
- 加载时报错
ioremap failed:- 检查地址与大小是否可映射,确认内核内存映射限制与平台 IOMMU/安全设置。
- 打开设备返回
-ENODEV:- 模块未正确初始化或地址设置有误;检查
dmesg日志。
- 模块未正确初始化或地址设置有误;检查
ioctl返回-ENOTTY:- 命令号不匹配;确认用户态宏与内核一致(
LED_MAGIC/序号)。
- 命令号不匹配;确认用户态宏与内核一致(
- 读到的位顺序与实际 LED 不一致:
- 位序由
led_mask从低位到高位依次映射,确认掩码与硬件连线一致。
- 位序由
- LED 不亮但寄存器值已变:
- 检查
active_high(尝试 0/1)。 - 检查
dir_output_clear(尝试 0/1)。 - 确认
led_mask是否覆盖真实连接的位(例如只接了 GPIO2 就设0x4)。 - 确认
out_offset/dir_offset与芯片手册一致。
- 检查
- 检测驱动已加载
lsmod | grep led_driver # 查看是否在列表中(或使用 busybox lsmod)
grep leddev_mmio /proc/devices # 查看主设备号
dmesg | tail -n 50 # 查看初始化日志(包含 base/offset/mask/active_high 等)- 卸载驱动
fuser -v /dev/leddev_mmio 2>/dev/null || true # 确认无进程占用(可选)
rmmod led_driver- 清理构建产物(开发主机上执行)
make clean