一、从基础框架到硬件控制
昨天写的字符设备驱动框架里,led_open、led_write 只打印日志,不操作硬件。今天要做的是:在框架里加上 GPIO 操作,真正控制 LED 亮灭。
需要操作 4 个寄存器:
| 寄存器 | 地址 | 作用 |
|---|---|---|
| IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 | 0x020E0068 |
复用功能(ALT5 = GPIO) |
| IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 | 0x020E02F4 |
电气属性 |
| GPIO1_GDIR | 0x0209C004 |
GPIO 方向 |
| GPIO1_DR | 0x0209C000 |
GPIO 数据 |
二、GPIO 寄存器操作
1. ioremap 映射物理地址
Linux 开启了 MMU,不能直接访问物理地址,必须先 ioremap 映射为虚拟地址:
c
#include <asm/io.h>
pmux_ctl_addr = ioremap(IOMUXC_CTL_ADDR, 4);
if (NULL == pmux_ctl_addr) {
pr_info("ioremap failed\n");
return -1;
}
-
IOMUXC_CTL_ADDR:物理地址。 -
4:映射 4 字节。 -
返回值:虚拟地址,失败返回
NULL。
2. readl / writel 读写寄存器
c
value = readl(pmux_ctl_addr);
value &= ~0xF;
value |= 0x5; // ALT5 = GPIO
writel(value, pmux_ctl_addr);
为什么要读-改-写:一个寄存器可能控制多个引脚,不能直接覆盖,必须先读出来,改掉要改的位,再写回去。
3. 配置复用功能
c
value = readl(pmux_ctl_addr);
value &= ~0xF; // 低 4 位清零
value |= 0x5; // ALT5 = GPIO
writel(value, pmux_ctl_addr);
ALT5 的含义:查 i.MX6ULL 参考手册的 IOMUXC 章节,GPIO1_IO03 的复用功能表:
| ALT | 功能 |
|---|---|
| ALT0 | I2C1_SDA |
| ALT1 | GPT1_COMPARE3 |
| ALT2 | USB_OTG2_OC |
| ALT4 | USDHC1_CD_B |
| ALT5 | GPIO1_IO03 |
| ALT6 | CCM_DI0_EXT_CLK |
| ALT7 | SRC_TESTER_ACK |
ALT5 对应 GPIO 功能,所以写 0x5。
4. 配置电气属性
c
writel(0x10b0, pmux_pad_addr);
0x10B0 = 0001 0000 1011 0000,配置驱动能力、上下拉、速度等。
5. 配置 GPIO 方向
c
value = readl(pgpio_dir_addr);
value |= (0x1 << 3); // GPIO1_IO03 是第 3 位
writel(value, pgpio_dir_addr);
位号规则 :IO 后面的数字就是位号。GPIO1_IO03 是第 3 位。
6. 配置 GPIO 初始值
c
value = readl(pgpio_dat_addr);
value |= (0x1 << 3); // 高电平,关闭 LED
writel(value, pgpio_dat_addr);
LED 是低电平点亮,所以初始设为高电平(关闭)。
7. 控制 LED 亮灭
c
// 点亮:低电平
value = readl(pgpio_dat_addr);
value &= ~(0x1 << 3);
writel(value, pgpio_dat_addr);
// 熄灭:高电平
value = readl(pgpio_dat_addr);
value |= (0x1 << 3);
writel(value, pgpio_dat_addr);
三、硬件操作和裸机的区别
| 项目 | 裸机 | Linux 驱动 |
|---|---|---|
| 地址访问 | 物理地址直接访问 | ioremap 映射虚拟地址 |
| 寄存器读写 | LDR/STR |
readl/writel |
| 位操作 | BIC/ORR |
&=/` |
| 是否需要 ioremap | ❌ 不需要 | ✅ 必须 |
本质是同一个寄存器、同一个位操作,区别只是 Linux 有 MMU,要用 ioremap 映射虚拟地址。
四、copy_from_user 和 copy_to_user
1. 为什么需要
内核空间和用户空间是隔离的,内核不能直接访问用户空间地址,必须用:
-
copy_from_user:从用户空间拷贝到内核空间。 -
copy_to_user:从内核空间拷贝到用户空间。
2. 原型
c
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
-
第一个参数是
void *,可以接收任意类型的指针。 -
返回值:未成功拷贝的字节数(0 表示成功)。
3. 用法
c
int ledstat = 0;
nret = copy_from_user(&ledstat, puser, n);
if (nret) {
pr_info("copy_from_user failed\n");
return -1;
}
五、自动创建设备节点
手动 mknod 比较麻烦。用 class_create + device_create 可以让内核自动创建设备节点。
1. 代码
c
#include <linux/device.h>
static struct class *pclass = NULL;
static struct device *pdevice = NULL;
// 加载时
pclass = class_create(THIS_MODULE, "myled");
if (NULL == pclass) {
pr_info("class_create failed\n");
return -1;
}
pdevice = device_create(pclass, NULL, devno, NULL, "myled0");
if (NULL == pdevice) {
pr_info("device_create failed\n");
return -1;
}
// 卸载时
device_destroy(pclass, devno);
class_destroy(pclass);
2. 效果
加载驱动后,/dev/myled0 自动出现 ,不用再 mknod。
六、并发控制(互斥锁)
多个应用程序可能同时 操作同一个设备,导致 GPIO 寄存器数据错乱。用**互斥锁(mutex)**保证同一时刻只有一个进程操作。
1. 代码
c
#include <linux/mutex.h>
static struct mutex lock;
// 初始化
mutex_init(&lock);
// 使用
mutex_lock(&lock);
// ... 临界区 ...
mutex_unlock(&lock);
// 销毁
mutex_destroy(&lock);
2. 原理
-
mutex_lock:获取锁,如果被占用就睡眠等待。 -
mutex_unlock:释放锁。 -
中间的代码是临界区,同一时刻只能有一个执行流进入。
七、sysfs 调试接口
通过 /sys 目录下的文件,可以用 echo 和 cat 直接控制驱动,不用写应用程序。
1. 定义属性文件
c
static ssize_t led_show(struct device *dev, struct device_attribute *attr, char *buf)
{
int cnt = 0;
cnt = sprintf(buf, "%s\n", (1 == pled->curledstat) ? "LED_ON" : "LED_OFF");
return cnt;
}
static ssize_t led_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
char tmpbuff[32] = {0};
sscanf(buf, "%s", tmpbuff);
if (0 == strcmp(tmpbuff, "LED_ON")) {
// 点亮
} else if (0 == strcmp(tmpbuff, "LED_OFF")) {
// 熄灭
}
return count;
}
static struct device_attribute attr = __ATTR(attr, 0664, led_show, led_store);
2. 创建和删除
c
// 加载时
device_create_file(pdevice, &attr);
// 卸载时
device_remove_file(pdevice, &attr);
3. 使用
bash
find /sys -name attr 2>/dev/null
cat /sys/devices/virtual/myled/myled0/attr
echo "LED_ON" > /sys/devices/virtual/myled/myled0/attr
八、结构体封装
把分散的全局变量集中到一个结构体,代码更整洁,便于扩展。
c
typedef struct led_drv {
void __iomem *pmux_ctl_addr;
void __iomem *pmux_pad_addr;
void __iomem *pgpio_dir_addr;
void __iomem *pgpio_dat_addr;
struct class *pclass;
struct device *pdevice;
int curledstat;
struct mutex lock;
dev_t devno;
struct cdev *pcdev;
} led_drv_t;
static led_drv_t *pled = NULL;
// 加载时
pled = kmalloc(sizeof(led_drv_t), GFP_KERNEL);
// 卸载时
kfree(pled);
九、设备树(Device Tree)
1. 为什么需要设备树
之前寄存器地址写死在驱动里,换一个开发板就要改代码。设备树把硬件信息从驱动中独立出来 ,实现设备与驱动分离。
2. 设备树节点
dts
myled0 {
compatible = "myled";
reg = <0x020E0068 0x4
0x020E02F4 0x4
0x0209C004 0x4
0x0209C000 0x4>;
status = "okay";
};
-
compatible:驱动匹配的关键。 -
reg:寄存器地址和长度,顺序和驱动里of_iomap的索引对应。 -
status:okay表示启用。
3. 驱动里读设备树
c
#include <linux/of.h>
#include <linux/of_address.h>
struct device_node *plednode = NULL;
plednode = of_find_node_by_path("/myled0");
if (NULL == plednode) {
pr_info("of_find_node_by_path failed\n");
return -1;
}
pmux_ctl_addr = of_iomap(plednode, 0);
pmux_pad_addr = of_iomap(plednode, 1);
pgpio_dir_addr = of_iomap(plednode, 2);
pgpio_dat_addr = of_iomap(plednode, 3);
-
of_find_node_by_path:通过路径查找节点。 -
of_iomap:从设备树节点的reg里读地址,直接映射为虚拟地址(替代ioremap)。
4. 改了设备树要做什么
bash
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /home/linux/tftpboot/
# 开发板重启
reboot
# 验证
find /proc/device-tree -name "*myled*"
十、misc 设备
1. 什么是 misc 设备
misc 是简化版的字符设备驱动:
-
主设备号固定为 10。
-
不需要
alloc_chrdev_region、cdev_alloc、cdev_add。 -
不需要
class_create、device_create。 -
用
misc_register一步完成注册。
2. 代码框架
c
#include <linux/miscdevice.h>
static struct file_operations fops = {
.owner = THIS_MODULE,
.write = led_write,
};
static struct miscdevice led_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "led_misc",
.fops = &fops,
};
static int __init led_misc_init(void)
{
misc_register(&led_misc);
// 配置寄存器...
return 0;
}
static void __exit led_misc_exit(void)
{
misc_deregister(&led_misc);
}
module_init(led_misc_init);
module_exit(led_misc_exit);
3. 和普通字符设备的对比
| 项目 | 普通字符设备 | misc 设备 |
|---|---|---|
| 主设备号 | 动态申请或静态指定 | 固定 10 |
| 设备号申请 | alloc_chrdev_region |
❌ 不需要 |
| cdev 注册 | cdev_alloc + cdev_add |
❌ 不需要 |
| 设备节点 | mknod 或 class_create |
misc_register 自动创建 |
| 代码量 | 多 | 少 |
4. 适用场景
misc 适合 LED、按键、看门狗等简单 GPIO 设备。
十一、蜂鸣器驱动
蜂鸣器和 LED 的驱动框架完全一样,只是寄存器地址和位号不同。
| 项目 | LED | 蜂鸣器 |
|---|---|---|
| 引脚 | GPIO1_IO03 | GPIO5_IO01(SNVS_TAMPER1) |
| 复用寄存器 | 0x020E0068 |
0x0229000C |
| 电气属性 | 0x020E02F4 |
0x02290050 |
| GPIO 方向 | 0x0209C004 |
0x020AC004 |
| GPIO 数据 | 0x0209C000 |
0x020AC000 |
| 位号 | 0x1 << 3 |
0x1 << 1 |
| 有效电平 | 低电平点亮 | 低电平响 |
关键:SNVS_TAMPER1 复用为 GPIO5_IO01,是 GPIO5 的第 1 位,所以用 0x1 << 1。
十二、今日总结
| 实验 | 核心内容 |
|---|---|
| GPIO 操作 | ioremap + readl/writel |
| 数据拷贝 | copy_from_user / copy_to_user |
| 自动创建设备节点 | class_create + device_create |
| 并发控制 | mutex |
| sysfs 调试 | device_create_file + __ATTR |
| 结构体封装 | kmalloc + 结构体 |
| 设备树 | of_find_node_by_path + of_iomap |
| misc 设备 | misc_register 一步注册 |
| 蜂鸣器 | 和 LED 框架一样,换 GPIO |
整体思路:同一套字符设备框架,逐步加功能(GPIO、自动创建、锁、sysfs、设备树、misc)。掌握这个框架,后面做按键、I2C、SPI、ADC 驱动都是在这个基础上加对应的硬件操作。