版权信息
warning
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本篇文章主要讲解创建字符驱动设备的一个代码框架,配置一个GPIO的方式依然是最原始的配置寄存器方式(配合讲解地址映射)。
1. 地址映射介绍
1.1. 地址映射究竟在干什么?
Linux 世界里,任何外设的寄存器都不允许你“像裸机一样”随便拿地址就写。物理地址只是硬件的地盘,而用户空间、内核空间都活在自己的“虚拟地址”里。
虚拟地址这个东西,只是对 CPU 说的一套语言;真实的电路是物理地址。所以得靠 MMU(内存管理单元)做翻译,把虚拟地址 → 物理地址挂上关系。这一步就叫 地址映射(memory mapping)。
简单描述一下:
虚拟地址(进程眼里看到的) → 通过页表(内核维护的一堆数据结构) → 映射到物理地址(板子上的真实寄存器)
为什么要这么麻烦?
因为 Linux 要保证:
- 每个进程互不干扰,不会互相写爆对方。
- 不让用户程序直接摸外设,保证系统稳定。
- 内核可以把物理地址重新分布,不影响程序。
1.2. 那如何访问外设寄存器?
访问寄存器通常需要两步:
- 获得物理地址(比如 datasheet 会写某个 GPIO 控制寄存器的地址)
- 将物理地址映射成虚拟地址 → 才能读写
在 Linux 内核中,这个动作由 ioremap() 完成。
1.3. 常用函数
1.3.1. ioremap()
把“物理地址段”映射到“内核虚拟地址空间” 你传入基地址和大小,它返回一段能访问的虚拟地址。
void __iomem *virt = ioremap(phys_addr, size);
1.3.2. iounmap()
释放映射。
1.3.3. readl()/writel()
读写 32bit 寄存器的常用函数。
value = readl(virt_addr + offset);
writel(0x1, virt_addr + offset);
比直接解引用指针更安全。
2. 创建一个字符设备
2.1. step1.定义操作集合
就是大名鼎鼎的 file_operations。让内核知道当用户空间调用open、close等文件操作函数时实际应该怎么操作。
示例:
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,// 对应用户空间的 open()
.write = led_write,// 对应用户空间的 write()
.release = led_release}; // 对应用户空间的 close()
需要理清楚这些操作函数与对应用户空间的函数的关系。
2.2. step2.编写实际操作函数
我们确定好要编写哪些操作函数后就可以开始编写了,函数名字要和file_operations里的一样。
我们编写实际操作函数时,要填形参,可以进入到“file_operations”的定义里面看操作函数对应的形参。
2.3. step3.四个关键结构体
我们首先定义四个关键结构体:
dev_t devno; // 用于存储设备号
struct cdev cdev; //申请一个cdev对象
static struct class *class = NULL; // 类
struct device *device //设备节点
我们将其统一包装在一个结构体,作为该设备的私有数据,例如:
struct led_dev{
dev_t devno; // 用于存储设备号
struct cdev cdev; // 申请一个cdev对象
struct class *class; // 类
struct device *device; //设备节点
};
static struct led_dev* led_dev;
2.4. step4.确定设备号
有两种方式:静态和动态。用到step3的第一个结构体。
此步只是 在内核中预留了数字,没有创建任何对象;这是字符设备的“身份证号码”。
2.4.1. alloc_chrdev_region():
动态分配一个未被占用的主设备号,并注册设备号区域。建议使用此方法,避免与现有设备冲突。
用法:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
- *dev:用于返回设备号
- baseminor:指定次设备号起始值
- count:申请的次设备号个数
- name: 设备名称
2.4.2. register_chrdev_region():
静态注册一个已知的主设备号。如果知道分配给设备的设备号,可以使用此方法.
用法:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
- from:设备号,一般在调用之前就得通过一个主设备号和次设备号得到一个设备号
- count: 次设备个数 以major为主设备号,以0~0+count的次设备号
- name: 设备名称
2.5. step5.注册字符设备
此步包含 初始化cdev结构体 和 将设备添加到内核。
2.5.1. cdev_init()
用于初始化我们step3提供的第二个结构体,将设备与对应的操作集合(step1、2)绑定。
void cdev_init(struct cdev *, const struct file_operations *);
- cdev*:字符设备结构体地址
- struct file_operations* :操作集合地址
2.5.2. cdev_add()
此步正式将设备添加到内核。
int cdev_add(struct cdev *, dev_t, unsigned count);
- cdev*:字符设备结构体地址
- dev_t:设备号
- count:注册的次设备数量
2.6. step5.创建设备节点
向内核添加字符设备后,在用户空间使用仍然不方便,因为它没有暴露接口,也就是/dev/目录下没有它的身影。
2.6.1. class_create()
创建设备节点前要求必须有对应的类。它会在在 sysfs 中创建一个类目录。class 本质上只是一个目录 + 一些描述信息。此处用到step3提供的第三个结构体。
用法:
struct class *class_create(const char *name);
- name:类名
- 返回:类结构体指针
同一类的设备 = 功能相似、统一管理、统一呈现、自动创建节点
- sysfs 中统一目录
- 设备节点命名统一
- 用户态可自动识别
- 驱动文件不同也能加入同一类
- 便于 udev/systemd/应用程序管理设备
tips:
想要将不同设备添加到同一类,可以在申请类的那个初始化文件里提供一个接口函数然后 extern,例如:
a.c:
struct class *my_class;
struct class *get_my_class(void)
{
return my_class;
}
static int __init main_dev_init(void)
{
my_class = class_create(THIS_MODULE, "myclass");
if (IS_ERR(my_class))
return PTR_ERR(my_class);
return 0;
}
b.c:
extern struct class *get_my_class(void);
device_create(get_my_class(), NULL, devno, NULL, "otherdev");
2.6.2. device_create()
正式创建设备节点。用到step3提供的第四个结构体
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
cls: 类结构体指针
parent:父设备,一般为 NULL
devt:设备号
drvdata:驱动私有数据,可为 NULL
fmt:设备名格式化字符串,即在dev目录下显示的设备名。
3. 销毁一个字符设备
举一反三,不再过多赘述
销毁需要按创建时的顺序 反顺序销毁。
//删除设备节点
void device_destroy(const struct class *cls, dev_t devt);
//删除类
void class_destroy(const struct class *cls)
//删除字符设备
void cdev_del(struct cdev *);
//释放设备号
void unregister_chrdev_region(dev_t, unsigned);
4. 完整示例程序,包含地址映射
注意的细节:goto的错误处理,私有数据。
说明一下使用私有数据后,怎么在open函数中获取这些私有数据:
container_of关键宏:已知某个结构体成员的指针,反推出整个结构体的指针。它定义在 include/linux/kernel.h 中。
在led_open()中,我们通过container_of 反推出 装有cdev成员的 struct led_dev 结构体指针(因为这里的cdev正好是struct led_dev的成员,为什么cdev不是其他结构体的成员呢?因为我们在cdev_add中指定的就是struct led_dev中的cdev的地址),也就是我们在step3中定义的结构体。即私有数据,并将这个指针指向fp的privatedata成员变量。这样read、write就可以通过fp->privatedata访问这些数据。
使用container_of在几个设备共用一个驱动时更具有通用性。
还有这个常用的函数:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
- to:目标地址(内核空间)
- from:源地址(用户空间)
- n:要拷贝的字节数
- 返回值:成功返回0,失败返回未拷贝的字节数
表示从用户空间拷贝到内核空间,防止用户程序直接访问或修改内核数据结构和硬件资源。
相应的也有
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
表示从内核空间复制到用户空间。当目标地址无效时,copy_to_user会将目标内核缓冲区清零,防止信息泄漏。
除了上述两个主要函数外,Linux还提供了一系列简化函数:
get_user:获取简单变量(char/int/long等)
put_user:写入简单变量
get_user/put_user:不进行地址检查的版本
这些函数适用于简单数据类型的传输,效率更高。
—终于他妈写完了,草— 写得真是依托。。。。。不管了
led.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/io.h> //io映射相关
#include <linux/uaccess.h>
#include <asm-generic/int-ll64.h>
#include <linux/string.h>
#define DEV_NAME "led"
// 定义开关状态
#define LED_OFF 0
#define LED_ON 1
// 定义寄存器物理地址
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E006C)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F8)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
// 定义映射后的虚拟地址
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
struct led_dev{
dev_t devno; // 用于存储设备号
struct cdev cdev; // 申请一个cdev对象
struct class *class; // 类
struct device *device; //设备节点
};
static struct led_dev* led_dev;
static void led_switch(u8 sta)
{
u32 temp = 0;
if (sta == LED_ON)
{
temp = readl(GPIO1_DR);
temp &= ~(1 << 4);
writel(temp, GPIO1_DR);
}
else if (sta == LED_OFF)
{
temp = readl(GPIO1_DR);
temp |= (1 << 4);
writel(temp, GPIO1_DR);
}
}
static int led_open(struct inode *inode, struct file *fp)
{
struct led_dev* this_devp;
// container_of:已知某个结构体成员的指针,反推出整个结构体的指针。
this_devp = container_of(inode->i_cdev, struct led_dev, cdev);
// 指定私有数据,后续read/write函数均可以直接读取
fp->private_data = this_devp;
printk(KERN_INFO "led dev open!");
return 0;
}
static ssize_t led_write(struct file *fp, const char __user *buf, size_t cnt, loff_t *offt)
{
int ret;
char databuf[5];
char* ledstat;
ret = copy_from_user(databuf, buf, cnt);
if (ret < 0)
{
printk(KERN_ERR "kernel write failed!\n");
return -EFAULT;
}
ledstat = databuf;
if (strcmp(ledstat, "on") == 0)
led_switch(LED_ON);
else if (strcmp(ledstat, "off") == 0)
led_switch(LED_OFF);
else
{
printk(KERN_ERR "E: only can write \"off\" or \"on\"\n");
return -1;
}
return 0;
}
static ssize_t led_read(struct file *fp, char __user *buf, size_t nt, loff_t *offt)
{
printk(KERN_INFO "it's unnecessary to read led!");
return 0;
}
static int led_release(struct inode *, struct file *)
{
printk(KERN_INFO "led dev release!");
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release};
static int __init led_init(void)
{
u32 temp = 0;
int ret = 0;
/* 映射 */
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);
/* 设置寄存器值 */
/* 1. 使能时钟 */
// 这是通用寄存器,最好不要直接写入,要保证其他值不变,否则可能会影响其他外设,以下同理
temp = readl(IMX6U_CCM_CCGR1);
temp &= ~(3 << 26); // 清除设置
temp |= (3 << 26); // 设定26-27为1,使能时钟
writel(temp, IMX6U_CCM_CCGR1);
temp = 0;
/* 2. 引脚复用为GPIO */
writel(5, SW_MUX_GPIO1_IO03);
/* 3. 设置电气属性 */
writel(0x10b0, SW_PAD_GPIO1_IO03);
/* 4. 设置为输出 */
temp = readl(GPIO1_GDIR);
temp &= ~(1 << 4); // 清除设定
temp |= (1 << 4);
writel(temp, GPIO1_GDIR);
temp = 0;
/* 5. 设置为输出低电平(开启LED) */
temp = readl(GPIO1_DR);
temp &= ~(1 << 4);
writel(temp, GPIO1_DR);
temp = 0;
/* 向系统添加字符设备 */
printk(KERN_INFO "led load!");
/* 获取设备号 */
ret = alloc_chrdev_region(&led_dev->devno, 1, 1, DEV_NAME); // 成功后,主设备号存储在MAJOR(devno),次设备号为1
if (ret < 0)
{
printk(KERN_ERR "Failed to alloc dev number!\n");
goto err_alloc;
}
/* 注册字符设备 */
cdev_init(&led_dev->cdev, &led_fops); // 初始化cdev结构体
led_dev->cdev.owner = THIS_MODULE;
ret = cdev_add(&led_dev->cdev, led_dev->devno, 1); // 此步正式将设备添加到内核
if (ret < 0)
{
printk(KERN_ERR "Failed to add cdev!\n");
goto err_cdev;
}
/* 创建设备节点 */
led_dev->class = class_create("led"); // 创建设备节点前要求必须有对应的类
if (IS_ERR(led_dev->class))
{
printk(KERN_ERR "Failed to create class \n");
ret = PTR_ERR(led_dev->class); // 设置返回值,否则返回0
goto err_class;
}
led_dev->device = device_create(led_dev->class, NULL, led_dev->devno, NULL, DEV_NAME);
if (IS_ERR(led_dev->device))
{
printk(KERN_ERR "Failed to create device\n");
ret = PTR_ERR(led_dev->device);
goto err_device;
}
return 0;
err_device:
class_destroy(led_dev->class);
err_class:
cdev_del(&led_dev->cdev);
err_cdev:
unregister_chrdev_region(led_dev->devno, 1); // 释放设备号
err_alloc:
return ret;
}
static void __exit led_exit(void)
{ /* 关灯 */
led_switch(LED_OFF);
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/* 释放资源 */
device_destroy(led_dev->class, led_dev->devno);
class_destroy(led_dev->class);
cdev_del(&led_dev->cdev);
unregister_chrdev_region(led_dev->devno, 1); // 释放设备号
printk(KERN_INFO "led dev closed!\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("gdm");
测试程序chrdevbaseAPP,用于写入字符串
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int test(const char *argv[])
{
int fd, ret;
char readbuf[30], writebuf[30];
const char *filename = argv[1];
/* 打开 */
fd = open(filename, O_RDWR); // 可读可写打开
if (fd < 0)
{
printf("E: Can not open %s\n", filename);
return -1;
}
/* 读取选项 */
if (strcmp(argv[2], "r") == 0)
{
memset(readbuf, 0, sizeof(readbuf));
ret = read(fd, readbuf, strlen(writebuf) + 1);
if (ret < 0)
printf("E: read %s filed!\n", filename);
else
printf("read data with %d byte: %s \n", strlen(readbuf), readbuf);
}
/* 写入选项 */
else if (strcmp(argv[2], "w") == 0)
{
memcpy(writebuf, argv[3], strlen(argv[3]) + 1);
ret = write(fd, writebuf, strlen(writebuf) + 1);
if (ret < 0)
printf("E: write %s filed!\n", filename);
else
printf("write data with %d byte: %s \n", strlen(writebuf), writebuf);
}
/* 关闭 */
ret = close(fd);
if (ret < 0)
{
printf("E: Can't close file %s\n", filename);
return -1;
}
return 0;
}
int main(int argc, const char *argv[])
{
printf("===%s:now running test APP for chrdevbase===\n", argv[0]);
/* 参数判断 */
if (!((argc == 3 && strcmp(argv[2], "r") == 0) || (argc == 4 && strcmp(argv[2], "w") == 0)))
{
printf("E: wrong usage! now argc = %d! argv[2] = %s\n usage:\n\t read:%s %s r \n\t write:%s %s w [str_data]\n",
argc, argv[2], argv[0], argv[1], argv[0], argv[1]);
return -1;
}
test(argv);
}
Makefile
# =========================================================
# Makefile for building a Linux Kernel Module
# Module Name: led
# =========================================================
# KERNEL_SOURCE 指向你的内核源码或头文件路径
# 如果你使用系统默认内核头,直接用 /lib/modules/$(shell uname -r)/build
KERNEL_SOURCE := /home/gdm/prjts/linux/driver_learning/kernel/linux-imx-lf-6.6.52-2.2.1
OUTPUT_DIR := $(PWD)/../output
# 模块源文件
obj-m := led.o
# obj-m 用于告诉内核模块系统需要编译 chrdevbase.c 成为模块
# 默认目标
# 使用内核的 Makefile 来构建模块
# -C 指定内核源码路径
# M=$(PWD) 告诉内核在当前目录下查找源文件
all:
arm-none-linux-gnueabihf-gcc -static chrdevbaseAPP.c -o chrdevbaseAPP
$(MAKE) -C $(KERNEL_SOURCE) M=$(PWD) modules
mv .*.cmd *.mod *mod.c *.o *.symvers *.order $(OUTPUT_DIR)
# 清理生成文件
clean:
# 使用内核 Makefile 清理模块相关文件
$(MAKE) -C $(KERNEL_SOURCE) M=$(PWD) clean
rm chrdevbaseAPP
rm $(OUTPUT_DIR)/* $(OUTPUT_DIR)/.*.cmd
app:
arm-none-linux-gnueabihf-gcc -static chrdevbaseAPP.c -o chrdevbaseAPP
cleanapp:
rm chrdevbaseAPP