嵌入式学习 day64:字符设备驱动

一、从基础框架到硬件控制

昨天写的字符设备驱动框架里,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 驱动都是在这个基础上加对应的硬件操作。

相关推荐
个 人 练 习 生1 小时前
C++ string 类模拟实现:从底层理解字符串(上)
开发语言·c++·经验分享·学习·程序人生
坤坤子吖1 小时前
C++智能指针:RAII、shared_ptr与内存泄漏
开发语言·c++·笔记·学习
词却3 小时前
OpenCV学习:MediaPipe 人脸网格检测
opencv·学习
李日华大战鸡红3 小时前
FOC SVPWM过调制(学习记录)
stm32·单片机·学习
AI职业加油站3 小时前
AI 校招现状:大模型应用工程师证书,助力简历能力证明
大数据·运维·人工智能·学习·职场发展
陈年老古董4 小时前
Hive 函数学习笔记(上):单行函数与炸裂函数
hive·笔记·学习
喜欢打篮球的普通人5 小时前
MiniMind 学习笔记(十八):速通强化学习——从 MDP 到 PPO 的概念与算法地图
笔记·深度学习·学习
老王爱玩车6 小时前
动态内存管理
c语言·开发语言·学习·面试
lvqinglou7 小时前
学习Sql-part1 for婷姐
sql·学习·性能优化