ARM嵌入式学习(十九)--- 字符设备驱动的注册与调用流程

目录

一、设备驱动的三大类型

二、从用户态到内核态:一个系统调用的旅程

三、设备驱动程序必须提供的四个要素

1.实现操作方法:

2.分配/注册设备号:

3.向内核注册驱动:

4.创建设备节点(绑定名字和设备号):

四、led字符设备驱动示例

1.实现操作方法:

三点需要注意:

(1)因为这里使用了操作系统,我们之前裸机使用的地址都变成了虚拟地址,这里需要进行转换(ioremap函数)

(2)写函数中的copy_from_user()用处

(3)返回EINVAL这样才能使用perror函数来解析这个错误码

​编辑

2.分配/注册设备号/3.向内核注册驱动:

这里用到的函数有:

[1. 这两个是手动分配设备号的函数](#1. 这两个是手动分配设备号的函数)

2.自动分配设备号的函数

3.将操作函数和设备号关联到字符设备结构体

4.自动创建字符设备驱动

5.转换虚拟地址

6.各个销毁函数

7.__init和__eixt解释

8.加载到内核初始化(启动内核会自动执行)(较难理解)

整个过程可以解释为:

4.创建设备节点(绑定名字和设备号)(用自动创建则不需要这一步):

五、总结与补充

1.vim的ctags使用方法:

2.我们的侧重点:


一、设备驱动的三大类型

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函数来验证

相关推荐
Engineer邓祥浩2 小时前
JVM学习笔记(8) 第三部分 虚拟机执行子系统 第7章 虚拟机类加载机制
jvm·笔记·学习
深蓝海拓2 小时前
基于QtPy (PySide6) 的PLC-HMI工程项目(七)上位机通信部分的初步建设:socket客户端
网络·笔记·python·学习·plc
送外卖的CV工程师2 小时前
STM32 CubeMX Makefile 工程编译 入门指南
stm32·单片机·嵌入式硬件·学习·makefile·stm32cubemx
喜欢吃燃面2 小时前
Linux 进程间通信:命名管道与 System V 共享内存深度解析
linux·运维·服务器·学习
ai产品老杨2 小时前
异构计算时代的安防底座:基于 Docker 的 X86/ARM 双模部署与 NPU 资源池化实战
arm开发·docker·容器
承渊政道2 小时前
【递归、搜索与回溯算法】(递归问题拆解与经典模型实战大秘笈)
数据结构·c++·学习·算法·macos·dfs·bfs
xuhaoyu_cpp_java2 小时前
MySql学习(一)
经验分享·学习·mysql
爱上好庆祝2 小时前
clip-path裁剪,css的滤镜,动画时间线,css的变量和函数
前端·css·学习·html·css3
somi72 小时前
ARM-驱动-06-中断底半部 + ioctl + 原子操作与锁
arm开发·单片机·嵌入式硬件