Linux 字符设备驱动基础开发详解(2)—— LED 驱动
“书接上回 Linux 字符设备驱动基础开发详解(1)。 在上一小节中我们掌握了字符设备驱动的开发框架。本节我们将编写一个真正的LED 字符设备驱动,并深入讲解 Linux 驱动是如何通过内存映射(MMU)来操作硬件寄存器的。

01 核心理论:MMU 与内存映射
在裸机开发中,我们可以直接操作物理地址。但在 Linux 系统中,开启了 MMU(Memory Management Unit,内存管理单元),CPU 读写的是虚拟地址。
为什么不能直接访问物理地址?
I.MX6ULL(32位处理器)的虚拟地址空间为 4GB ()。物理内存(如 512MB DDR)和外设寄存器都被 MMU 映射到了这 4GB 的虚拟空间中。 如果驱动程序直接访问物理地址(如 0x020C406C),操作系统会拦截并报错(段错误),因为该地址在虚拟空间中可能未被定义或受保护。
解决方案:ioremap
Linux 提供了 ioremap 函数,将物理地址映射为内核空间的虚拟地址。映射完成后,驱动程序通过访问这个虚拟地址即可操作硬件。
| |
|---|
ioremap(phys_addr, size) | 将物理地址 phys_addr 映射为虚拟地址。返回 __iomem * 指针。 |
iounmap(addr) | |
推荐的 I/O 读写函数
虽然可以直接用指针操作映射后的地址,但 Linux 推荐使用专用函数以保证可移植性和原子性:
// 读操作
u32 readl(constvolatilevoid __iomem *addr); // 读 32位
u16 readw(constvolatilevoid __iomem *addr); // 读 16位
u8 readb(constvolatilevoid __iomem *addr); // 读 8位
// 写操作
voidwritel(u32 value, volatilevoid __iomem *addr); // 写 32位
voidwritew(u16 value, volatilevoid __iomem *addr); // 写 16位
voidwriteb(u8 value, volatilevoid __iomem *addr); // 写 8位
02 驱动程序实现 (led.c)
本实验基于 NXP I.MX6ULL 开发板,控制 GPIO1_IO03 上的 LED。
完整代码解析
#include<linux/types.h>
#include<linux/kernel.h>
#include<linux/delay.h>
#include<linux/ide.h>
#include<linux/init.h>
#include<linux/module.h>
#include<linux/errno.h>
#include<linux/gpio.h>
#include<asm/mach/map.h>
#include<asm/uaccess.h>
#include<asm/io.h>
/* ------------------------------------------------------
* 1. 宏定义:设备号与硬件信息
* ------------------------------------------------------ */
#define LED_MAJOR 200 /* 主设备号 (静态注册) */
#define LED_NAME "led"/* 设备节点名 */
#define LEDOFF 0 /* 关灯指令 */
#define LEDON 1 /* 开灯指令 */
/* I.MX6ULL 寄存器物理地址定义 */
#define CCM_CCGR1_BASE (0X020C406C) /* 时钟控制寄存器 */
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068) /* 复用功能选择 */
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) /* 电气属性配置 */
#define GPIO1_DR_BASE (0X0209C000) /* GPIO 数据寄存器 */
#define GPIO1_GDIR_BASE (0X0209C004) /* GPIO 方向寄存器 */
/* 映射后的虚拟地址指针 */
staticvoid __iomem *IMX6U_CCM_CCGR1;
staticvoid __iomem *SW_MUX_GPIO1_IO03;
staticvoid __iomem *SW_PAD_GPIO1_IO03;
staticvoid __iomem *GPIO1_DR;
staticvoid __iomem *GPIO1_GDIR;
/* ------------------------------------------------------
* 2. 硬件控制逻辑
* ------------------------------------------------------ */
/**
* @brief 控制 LED 亮灭
* @param sta: LEDON(1) 打开, LEDOFF(0) 关闭
*/
voidled_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3); /* bit3 清零 (低电平点亮,视硬件原理图而定) */
writel(val, GPIO1_DR);
} elseif(sta == LEDOFF) {
val = readl(GPIO1_DR);
val |= (1 << 3); /* bit3 置一 (高电平熄灭) */
writel(val, GPIO1_DR);
}
}
/* ------------------------------------------------------
* 3. 文件操作函数 (File Operations)
* ------------------------------------------------------ */
staticintled_open(struct inode *inode, struct file *filp)
{
/* 一般在此处处理私有数据,如 filp->private_data = ... */
return0;
}
staticssize_tled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return0;
}
/**
* @brief 向设备写数据 (核心逻辑)
* 用户空间 write -> 内核空间 led_write -> 操作硬件
*/
staticssize_tled_write(struct file *filp, constchar __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsignedchar databuf[1];
unsignedchar ledstat;
/* 接收用户空间数据:copy_from_user */
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态值 0 或 1 */
if(ledstat == LEDON) {
led_switch(LEDON);
} elseif(ledstat == LEDOFF) {
led_switch(LEDOFF);
}
return0;
}
staticintled_release(struct inode *inode, struct file *filp)
{
return0;
}
/* 绑定 file_operations */
staticstructfile_operationsled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/* ------------------------------------------------------
* 4. 驱动入口与出口 (Init & Exit)
* ------------------------------------------------------ */
/**
* @brief 驱动入口函数
* 1. 内存映射 (ioremap)
* 2. 硬件初始化 (时钟、复用、属性、方向)
* 3. 注册字符设备 (register_chrdev)
*/
staticint __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
/* 1. 寄存器地址映射:将物理地址转为虚拟地址 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
/* 2. 使能 GPIO1 时钟 (CCGR1 寄存器) */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清除 bit26-27 */
val |= (3 << 26); /* 设置 bit26-27 为 11,开启时钟 */
writel(val, IMX6U_CCM_CCGR1);
/* 3. 设置 GPIO1_IO03 复用功能 */
writel(5, SW_MUX_GPIO1_IO03); /* 5 = ALT5 (GPIO模式) */
/* 4. 设置 IO 电气属性 (速度、驱动能力、压摆率等) */
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 5. 设置 GPIO1_IO03 为输出模式 (GDIR 寄存器) */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3); /* bit3 置 1 = Output */
writel(val, GPIO1_GDIR);
/* 6. 默认关闭 LED (输出高电平) */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 7. 注册字符设备驱动 */
retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}
printk("led_init success!\r\n");
return0;
}
/**
* @brief 驱动出口函数
* 1. 取消内存映射 (iounmap)
* 2. 注销字符设备 (unregister_chrdev)
*/
staticvoid __exit led_exit(void)
{
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/* 注销字符设备驱动 */
unregister_chrdev(LED_MAJOR, LED_NAME);
printk("led_exit success!\r\n");
}
/* 指定加载/卸载函数 */
module_init(led_init);
module_exit(led_exit);
/* 模块信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxxxx");
03 用户空间测试程序 (ledApp.c)
编写一个简单的 C 程序,接收命令行参数来控制 LED。
#include"stdio.h"
#include"unistd.h"
#include"sys/types.h"
#include"sys/stat.h"
#include"fcntl.h"
#include"stdlib.h"
#include"string.h"
/**
* Usage: ./ledApp <filename> <0/1>
* Example: ./ledApp /dev/led 1 (Turn ON)
*/
intmain(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsignedchar databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return-1;
}
filename = argv[1];
/* 1. 打开 led 驱动文件 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return-1;
}
/* 2. 获取命令行参数 (0 或 1) */
databuf[0] = atoi(argv[2]);
/* 3. 向驱动写入数据 */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return-1;
}
/* 4. 关闭文件 */
retvalue = close(fd);
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return-1;
}
return0;
}
04 编译与测试
1. 编写 Makefile
将驱动编译为 .ko 模块。请确保 KERNELDIR 路径指向你编译过的 Linux 内核源码目录。
KERNELDIR := /home/xxx/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := led.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
2. 编译流程
在终端执行:
# 1. 编译驱动,生成 led.ko
make
# 2. 编译测试 APP
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
3. 上机实测
将 led.ko 和 ledApp 拷贝到开发板中(推荐使用 NFS 挂载)。
💡 重点解析:手动创建设备节点
我们使用了旧版的 register_chrdev 函数,它只负责在内核中“注册”驱动,不会自动生成 /dev/led 设备文件。因此需要使用 mknod 手动创建。
“形象比喻:
- **
register_chrdev (内核注册)**:相当于去工商局办理营业执照,拿到了注册号(主设备号 200)。此时政府(内核)知道你的存在。 - **
mknod (创建节点)**:相当于在路边挂个招牌(/dev/led),招牌后面写着注册号(200)。 - **
open (应用程序)**:顾客(App)看到招牌(/dev/led),系统通过招牌后的号码(200)找到对应的店铺(驱动)为你服务。
测试步骤:
# 1. 加载驱动模块
insmod led.ko
# 2. 确认设备号是否被占用 (应能看到 led 和 200)
cat /proc/devices
# 3. 手动创建设备节点 (c 代表字符设备, 200 主设备号, 0 次设备号)
mknod /dev/led c 200 0
# 4. 运行测试
./ledApp /dev/led 1 # LED 应点亮
./ledApp /dev/led 0 # LED 应熄灭
# 5. 卸载驱动
rmmod led.ko
05 小结
本节通过一个 LED 驱动展示了字符设备开发的完整闭环:
- 内核态:利用
ioremap 映射物理寄存器,通过 writel/readl 操作硬件,并实现 file_operations。 - 用户态:使用标准的文件 I/O (
open, write) 操作 /dev/xxx。 - 连接点:设备号与设备节点(
mknod)将两者关联起来。