目录
(1)因为这里使用了操作系统,我们之前裸机使用的地址都变成了虚拟地址,这里需要进行转换(ioremap函数)
(3)返回EINVAL这样才能使用perror函数来解析这个错误码
[1. 这两个是手动分配设备号的函数](#1. 这两个是手动分配设备号的函数)
4.创建设备节点(绑定名字和设备号)(用自动创建则不需要这一步):
一、设备驱动的三大类型
Linux将设备驱动划分为三类,每一类对应不同的访问模式和硬件特性:
| 类型 | 访问方式 | 典型设备 | 管理方式 |
|---|---|---|---|
| 字符设备驱动 | 字节流、有序访问 | LED、按键、串口(UART) | 文件操作(open/read/write/ioctl/close) |
| 块设备驱动 | 随机访问,以块为单位 | 硬盘、eMMC、SD卡 | 块I/O层 + 请求队列 |
| 网络设备驱动 | 数据包收发 | 以太网卡、WiFi模块 | 套接字接口,靠名字(如eth0)管理,集成协议栈 |
应用程序最常打交道的通常是字符设备,本篇通过控制一个LED为例。
二、从用户态到内核态:一个系统调用的旅程
当你在用户程序(应用层)里写下open("/dev/led", O_RDWR)时,背后发生了这些事:

整个过程中,设备号是内核匹配驱动的关键线索。
我们调用open函数后,内核会产生软中断,然后syscall_open()会找到对应的设备号,进入到设备号的结构体中,这个结构体有open、read、write、ioctl、close函数指针。我们应用层使用的open、write、read函数是封装好了的,调用这个应用层的open\read\write函数它里面会去找到对应的设备号结构体中的对应的open\write\read函数
这里我们主要的工作是led设备号对应的LED_DRV这个结构体。
三、设备驱动程序必须提供的四个要素
要实现一个可用的字符设备驱动,你的代码通常需要完成以下工作:
1.实现操作方法:
填充struct file_operations,至少实现.open、.read、.write、.unlocked_ioctl、.release(即close)等回调,例如:
先定义一个结构体函数指针

然后在代码中实现你的五个函数:
这里在函数中仅仅是实现了打印还没有实现功能,我们后续再实现。
static int open(struct inode* node, struct file* file)
{
printk("led open...\n");
return 0;
}
static ssize_t read(struct file* file, char __user* buf, size_t len, loff_t* offset)
{
printk("led read...\n");
return 0;
}
static ssize_t write(struct file* file, const char __user* buf, size_t len, loff_t* offset)
{
printk("led write...\n");
return 0;
}
static int close(struct inode* node, struct file* file)
{
printk("led close...\n");
return 0;
}
2.分配/注册设备号:
可以静态申请(已知空闲的主设备号)或动态分配
3.向内核注册驱动:
将file_operations与设备号绑定,添加到字符设备缓存中
4.创建设备节点(绑定名字和设备号):
设备节点位于/dev/下,是用户空间访问驱动的入口。节点可以用mknod命令手动创建,也可以在驱动中使用device_create自动创建。
四、led字符设备驱动示例
1.实现操作方法:
从我们裸机的程序里把led.c的代码拿过来修改;
#include <asm/io.h>
#include <asm/string.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/export.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kdev_t.h>
#include <linux/printk.h>
#include <linux/types.h>
#define MAJOR_NUM 240
#define MINOR_NUM 0
#define DEV_NAME "led"
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 0x20e0068U
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 0x20E02F4U
#define GPIO1_DR 0x209C000U
#define GPIO1_GDIR 0x209C004U
static volatile unsigned int* sw_mux;
static volatile unsigned int* sw_pad;
static volatile unsigned int* gpio1_dr;
static volatile unsigned int* gpio1_gdir;
static void led_init(void)
{
*sw_mux = 0x05;
*sw_pad = 0x10b0;
*gpio1_gdir |= (1 << 3);
*gpio1_dr |= (1 << 3);
}
static void led_on(void)
{
*gpio1_dr &= ~(1 << 3);
}
static void led_off(void)
{
*gpio1_dr |= (1 << 3);
}
static int open(struct inode* node, struct file* file)
{
led_init();
printk("led open...\n");
return 0;
}
static ssize_t read(struct file* file, char __user* buf, size_t len, loff_t* offset)
{
// copy_to_user();
printk("led read...\n");
return 0;
}
static ssize_t write(struct file* file, const char __user* buf, size_t len, loff_t* offset)
{
unsigned char data[10] = {0};
size_t len_cp = len < sizeof(data) ? len : sizeof data;
int size_cp = copy_from_user(data, buf, len_cp);
if (size_cp < 0)
return size_cp;
if (!strcmp(buf, "ledon"))
led_on();
else if (!(strcmp(buf, "ledoff")))
led_off();
else
return -EINVAL;
printk("led write...\n");
return size_cp;
}
static int close(struct inode* node, struct file* file)
{
led_off();
printk("led close...\n");
return 0;
}
static dev_t dev;
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = open,
.read = read,
.write = write,
.release = close};
static struct cdev cdev;
///////////////////////////////
sw_mux = ioremap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03, 4);
sw_pad = ioremap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03, 4);
gpio1_dr = ioremap(GPIO1_DR, 4);
gpio1_gdir = ioremap(GPIO1_GDIR, 4);
//这一块代码我们后续放到内核的初始化里面去
/////////////////////////////////////////
三点需要注意:
(1)因为这里使用了操作系统,我们之前裸机使用的地址都变成了虚拟地址,这里需要进行转换(ioremap函数)
为什么需要转换地址?
在启用 MMU(内存管理单元)的 Linux 系统中,CPU 访问内存或外设寄存器时使用的是虚拟地址,而不是物理地址。
外设寄存器的地址在数据手册中给出的是物理地址 (例如
0x020E0068)。如果直接对物理地址进行指针解引用(例如
*(volatile u32 *)0x020E0068),会触发缺页异常,因为内核页表里没有该物理地址的映射。
ioremap的作用就是在内核页表中动态建立一段映射 ,把一段连续的物理地址映射到内核的虚拟地址空间(通常是vmalloc区域或直接映射区域之外),从而使驱动能够安全地访问这些寄存器。
ioremap函数参数的4表示映射的内存区域大小为 4 个字节
(2)写函数中的copy_from_user()用处

(3)返回EINVAL这样才能使用perror函数来解析这个错误码
2.分配/注册设备号/3. 向内核注册驱动:
static dev_t dev;
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = open,
.read = read,
.write = write,
.release = close};
static struct cdev cdev;
static int __init led1_init(void)
{
int ret = 0;
struct class* led_class;
struct device* led_dev;
// dev = MKDEV(MAJOR_NUM, MINOR_NUM); //(MAJOR_NUM << 20) | MINOR_NUM;
// ret = register_chrdev_region(dev, 1, DEV_NAME);
ret = alloc_chrdev_region(&dev, 0,1, DEV_NAME);
if (ret < 0)
goto err_alloc_chrdev;
cdev_init(&cdev, &fops);
ret = cdev_add(&cdev, dev, 1);
if (ret < 0)
goto err_cdev;
led_class = class_create(THIS_MODULE, "led");
led_dev = device_create(led_class, NULL, dev, NULL, "led");
if (ret < 0)
goto err_device;
sw_mux = ioremap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03, 4);
sw_pad = ioremap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03, 4);
gpio1_dr = ioremap(GPIO1_DR, 4);
gpio1_gdir = ioremap(GPIO1_GDIR, 4);
printk("led_init ##############\n");
return 0;
// 销毁设备节点
err_device:
device_destroy(led_class, dev);
class_destroy(led_class);
err_alloc_chrdev:
unregister_chrdev_region(dev, 1);
err_cdev:
cdev_del(&cdev);
printk("led_init failed ############## ret = %d\n", ret);
return ret;
}
static void __exit led1_exit(void)
{
iounmap(gpio1_gdir);
iounmap(gpio1_dr);
iounmap(sw_pad);
iounmap(sw_mux);
device_destroy(led_class, dev);
class_destroy(led_class);
unregister_chrdev_region(dev, 1);
cdev_del(&cdev);
printk("led_exit ##############\n");
}
module_init(led1_init);
module_exit(led1_exit);
这里用到的函数有:
1. 这两个是手动分配设备号的函数
dev = MKDEV(MAJOR_NUM, MINOR_NUM); //(MAJOR_NUM << 20) | MINOR_NUM;
ret = register_chrdev_region(dev, 1, DEV_NAME);
2.自动分配设备号的函数
ret = alloc_chrdev_region(&dev, 0,1, DEV_NAME);
3.将操作函数和设备号关联到字符设备结构体
cdev_init(&cdev, &fops); //关联操作函数
cdev_add(&cdev, dev, 1); //添加设备号,后面的1是设备个数
4.自动创建字符设备驱动
led_class = class_create(THIS_MODULE, "led");
led_dev = device_create(led_class, NULL, dev, NULL, "led");
5.转换虚拟地址
sw_mux = ioremap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03, 4);
sw_pad = ioremap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03, 4);
gpio1_dr = ioremap(GPIO1_DR, 4);
gpio1_gdir = ioremap(GPIO1_GDIR, 4);
6.各个销毁函数
err_device:
device_destroy(led_class, dev);
class_destroy(led_class);
err_alloc_chrdev:
unregister_chrdev_region(dev, 1);
err_cdev:
cdev_del(&cdev);
7.__init和__eixt解释
这个是代码段的标识
被 __init 标记的函数中不能调用被 __exit 标记的函数(因为卸载时初始化内存可能已释放)
__init标记该函数仅在模块初始化阶段使用
内核在完成模块加载(调用 module_init 指定的函数)后,会释放该函数占用的内存(因为后续不会再执行它)。
通常用于模块的入口函数(如 demo1_init),其内部做设备注册、资源申请等一次性工作。
__eixt标记该函数仅在模块卸载阶段使用。
如果模块被编译进内核(而非作为可加载模块),该函数会被直接丢弃,不会链接进最终内核映像,从而节省内存。
如果模块以动态加载方式使用,卸载时内核会调用这个函数(通常由 module_exit 指定),用于清理、释放资源。
8.加载到内核初始化(启动内核会自动执行)(较难理解)
module_init(led1_init);
module_exit(led1_exit);
Module_init相当于把我们的函数放到初始化的内存里面,放进去后,启动PC会自动访问内存初始化这些函数
我们看这个函数内部:

x是函数指针,继续看initcall函数内部:


#id是转字符串的意思
Attribute是属性的意思,section是段的意思
__used和__init一样是标识符,它表示这个函数必须被执行
##是连接符 123##45 = 12345
\续行符
LTO_REFERENCE_INITCALL这个宏是定义函数指针并返回它
id:0-7顺序执行
所以宏定义可以精简为:

整个过程可以解释为:

4.创建设备节点(绑定名字和设备号)(用自动创建则不需要这一步):
(1)进入开发板使用cat proc/devcies 查看设备号是否正确初始化
(2)使用mknod /dev/led c 249 0
c是字符设备驱动的意思
249 0 前面是主设备号后面是次设备号
五、总结与补充
1.vim的ctags使用方法:

2.我们的侧重点:

这一步只要知道有这个过程就可以了,我们主要是实现

这些字符设备驱动的,通过应用层调用open/read/write函数来验证
