Linux驱动开发——(Linux内核系统调用实现原理)gpio(2)

简介: Linux驱动开发——(Linux内核系统调用实现原理)gpio(2)

文章目录

Linux内核系统调用实现原理

Linux设备驱动相关概念

Linux内核设备驱动分类

字符设备文件特点及属性

字符设备文件创建的方法

主设备号、次设备号、设备号

Linux相关库函数

字符设备相关数据结构

配套相关函数

编写Linux字符设备驱动步骤

定义初始化硬件操作接口对象:

定义初始化字符设备对象:

最终向内核注册字符设备对象

从内核卸载字符设备对象

最后编写之前定义的设备接口具体内容

示例

具体代码:

测试执行:

总结


Linux内核系统调用实现原理

  • 当应用程序(进程)调用write系统调用函数,首先会调用C库的write函数
  • 接下来C库的write函数会做以下的事情:
  • 首先保存write函数的系统调用号到R7寄存器。
  • “系统调用号”就是Linux内核给每一个系统调用函数分配的唯一的软件编号(类似函数ID,write为4)定义内核源码的arch/arm/include/asm/unistd.h
  • 然后调用swi指令触发一个软中断异常(新版本触发异常指令为svc,老版本触发软中断指令为swi,新的编译器也同样支持swi指令)。
  • 一旦触发软中断异常,CPU核立刻处理软中断异常。
  • 硬件自动执行:1、备份CPSR到SPSR_SVC,设置CPSR:MODE=SVC_MODE(切换SVC管理模式),T=0:切换到ARM状态,IF=11:禁止FIQ/IRQ异常中断。保存返回地址:LR_SVC=PC-4,设置PC=0x08,至此CPU跑到0x08软中断处理的入口地址开启了软件处理中断异常流畅,软中断的处理入口地址由相关的Linux内核来实现,也就是说当前进程到此由用户空间陷入了内核空间执行。
  • Linux内核软中断处理的入口地址相关代码将做以下工作:
  • 从R7寄存器中取出之前保存的write函数的系统调用号(write : 4)。
  • 以write系统调用号4作为下标在内核的系统调用表(数组)中找到对应的内核函数sys_write,找到对应的内核函数继续调用此函数,调用完毕后原路返回用户空间,至此write函数返回!
  • “系统调用表”:本质就是一个数组,数组元素就是函数指针,数组元素的小标就是系统调用号,定义在内核源码的arch/arm/kernel/calls.S
  • 20191223210915512.png

Linux设备驱动相关概念

Linux内核设备驱动分类

  • 字符设备驱动:对字符设备的访问按照字节流形式访问。
  • 例如:
  • LED,按键,蜂鸣器,GPS(UART),GPRS(UART),BT(UART)
  • 触摸屏(XY绝对坐标),LCD显示屏(像素点)
  • 声卡,摄像头,各种硬件传感器(三轴,重力,光线,距离,温度等)
  • EEPROM存储器(I2C接口)
  • 块设备驱动:对块设备的访问按照数据块进行,比如一次操作512字节
  • 例如:
  • 硬盘,U盘,TF卡,SD卡
  • Nandflash,Norflash,EMMC
  • 网络设备驱动:对网络设备驱动一般按照网络协议进行
  • 例如:
  • 有线网卡和无线网卡

注意


  • 对应Linux系统来说,“一切皆文件”,即任何硬件外设都是以文件的形式存放,用户访问文件本质就是在访问对应的硬件外设。
  • 字符设备对应的文件称之为字符设备文件。
  • 块设备对应的文件称之为块设备文件。
  • 网络设备无设备文件,通过socket套接字进行访问。

字符设备文件特点及属性

  • 字符设备文件本身代表的就是字符设备硬件本身

  • 字符设备文件存在于根文件系统必要目录的dev目录下 ,当然块设备文件也存于dev目录下

  • 例如:查看板子上UART设备的字符设备文件:
  • ls /dev/ttySAC* -lh 得到以下信息:
  • crw-rw---- 204, 64 /dev/ttySAC0
  • crw-rw---- 204, 65 /dev/ttySAC1
  • crw-rw---- 204, 66 /dev/ttySAC2
  • crw-rw---- 204, 67 /dev/ttySAC3

  • 说明:
  • c:表示此设备文件对应的设备为字符设备
  • 204:表示串口的主设备号
  • 64/65/66/67:分别表示第一个,第二个,第三个,第四个串口的次设备号
  • ttySAC0:表示第一个串口的设备文件名
  • ttySAC1:表示第二个串口的设备文件名
  • ttySAC2:表示第三个串口的设备文件名
  • ttySAC3:表示第四个串口的设备文件名
  • 注意:一个硬件外设个体有唯一的一个设备文件名

字符设备文件创建的方法

  • 手动创建,只需要mknod命令:
mknod /dev/(字符设备文件名) c 主设备号  次设备号
  • 自动创建,该方式后续补上。

主设备号、次设备号、设备号

  • 主设备号作用:应用程序根据字符设备文件的主设备号,在茫茫的内核驱动中找到对应的唯一的设备驱动,一个设备驱动仅有唯一的主设备号
  • 次设备号作用:设备驱动根据次设备号能够找到应用程序要访问的硬件外设个体
  • 总结:一个驱动仅有一个主设备号,一个硬件个体仅有一个次设备号应用根据主设备号找驱动,驱动根据次设备号找硬件个体,所以,主,次设备号对于linux内核是一个宝贵的资源,某个 设备驱动必须要想linux内核申请主,次设备号
  • 设备号:设备号包含主,次设备号,linux内核用dev_t(unsigned int)数据类型描述设备号信息,
  • 设备号的高12位用来描述主设备号,
  • 设备号的低20位用来描述次设备号,
  • 设备号和主,次设备号之间的转换操作宏:
  • 设备号=MKDEV(已知的主设备号,已知的次设备号);
  • 主设备号=MAJOR(已知的设备号);
  • 次设备号=MINOR(已知的设备号);

Linux相关库函数

  • 向内核申请设备号函数
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
  • 参数:

  • dev:保存申请的设备号,包括主设备号和起始的次设备号
  • baseminor:希望申请的起始次设备号,一般给0。
  • count:申请的次设备号的个数 , 如果baseminor=0,count=2,那么申请的次设备号分别是0和1
  • name:申请设备号指定的名称,随便取 ,将来通过执行cat /proc/devices查看。
  • 设备驱动一旦不再使用设备号,记得要将设备号资源归还给linux内核:
释放申请的设备号资源函数
void unregister_chrdev_region(dev_t dev, unsigned count);
  • 参数:
  • dev:申请的设备号
  • count:申请的次设备号的个数

字符设备相关数据结构

  • Linux内核描述给用户提供操作接口的数据结构

//描述字符设备驱动给用户提供的操作接口
struct file_operations {
  int (*open) (struct inode *, struct file *); //给用户提供打开设备的接口
  int (*release) (struct inode *, struct file *); //给用户提供关闭设备的接口
  ...
};
//描述字符设备驱动
struct cdev {
  dev_t dev; //描述申请的设备号
  int count; //描述申请的次设备号的个数
  struct file_operations *ops;//给用户提供的操作接口
    ...
  };


  • 字符设备驱动和应用程序调用关系:
  • 应用程序open->软中断->内核的sys_open->驱动的open接口
  • 应用程序close->软中断->内核的sys_close->驱动的release接口

配套相关函数

cdev_init(strcut cdev *cdev, 
          struct file_operations *fops)


  • 函数功能:初始化字符设备驱动对象,就是给字符设备驱动对象添加一个硬件操作接口
  • cdev:要初始化的字符设备对象
  • fops:给用户提供的硬件操作接口

 

cdev_add(struct cdev *cdev, dev_t dev, int count)
  • 函数功能:向内核注册添加一个字符设备对象,一旦添加完毕内核就有一个真实的字符设备驱动
  • cdev:要注册的字符设备对象
  • dev:申请的设备号
  • count:申请的次设备号的个数
cdev_del(struct cdev *cdev)


  • 函数功能:从内核中卸载字符设备对象,一旦卸载完毕,内核就不会有一个真实的字符设备驱动

编写Linux字符设备驱动步骤

定义初始化硬件操作接口对象:

  • 例如:
struct file_operations led_fops = {
    .open = led_open, //打开设备接口
    .release = led_close, //关闭设备接口
  };

定义初始化字符设备对象:

struct cdev led_cdev; //定义字符设备对象
//led_cdev.ops = &led_fops
cdev_init(&led_cdev, &led_fops);//给字符设备对象添加硬件操作接口

最终向内核注册字符设备对象

  • cdev_add(&led_dev, 申请的设备号,次设备号的个数);
  • 一旦注册成功,内核就有一个真实的字符设备驱动,并且给用户提供硬件操作接口(open/release)。

从内核卸载字符设备对象

cdev_del(&led_cdev);


最后编写之前定义的设备接口具体内容

  • 具体内容就是操作字符设备的操作内容了。

示例

  • 编写LED字符设备驱动,实现打开设备开灯,关闭设备关灯

具体代码:

  • led_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/cdev.h> //strcut cdev
#include <linux/fs.h> //struct file_operations
//声明描述LED硬件信息相关的数据结构
struct led_resource {
    int gpio; //GPIO软件编号
    char *name;//LED的名称
};
//定义初始化四个LED灯的硬件信息对象
static struct led_resource led_info[] = {
    {
        .name = "LED1",
        .gpio = PAD_GPIO_C+12
    },
  {
        .name = "LED2",
        .gpio = PAD_GPIO_C+7
    },
  {
        .name = "LED3",
        .gpio = PAD_GPIO_C+11
    },
  {
        .name = "LED4",
        .gpio = PAD_GPIO_B+26
    }
};
//定义设备号对象
static dev_t dev;
//定义字符设备对象
static struct cdev led_cdev;
//打开设备操作接口
//调用关系:应用open->软中断->内核的sys_open->驱动的led_open
int led_open(struct inode *inode, struct file *file)
{
    //1.开灯
    int i;
    for(i = 0; i < ARRAY_SIZE(led_info); i++) {
        gpio_set_value(led_info[i].gpio, 0);
    }
    printk("%s\n", __func__);
    return 0; //执行成功返回0,执行失败返回负值
}
//关闭设备操作接口
//调用关系:应用close->软中断->内核的sys_close->驱动的led_close
int led_close(struct inode *inode, struct file *file)
{
    //1.关灯
    int i;
    for(i = 0; i < ARRAY_SIZE(led_info); i++) {
        gpio_set_value(led_info[i].gpio, 1);
    }
    printk("%s\n", __func__);
    return 0; //执行成功返回0,执行失败返回负值
}
//定义初始化LED的硬件操作接口对象
static struct file_operations led_fops = {
    .open = led_open, //打开设备接口
    .release = led_close //关闭设备接口
};
static int led_init(void)
{
    int i;
    //1.申请GPIO资源,配置GPIO为输出,输出1(省电)
    for (i = 0; i < ARRAY_SIZE(led_info); i++) {
        gpio_request(led_info[i].gpio,
                        led_info[i].name);
        gpio_direction_output(led_info[i].gpio, 1);
    }
    //2.申请设备号
    alloc_chrdev_region(&dev, 0, 1, "tarena");
    //3.初始化字符设备对象,给字符设备对象添加操作接口
    cdev_init(&led_cdev, &led_fops);
    //4.最终向内核注册字符设备对象,一旦注册成功,
    //内核就有一个真实的字符设备对象并且提供了
    //硬件操作接口
    cdev_add(&led_cdev, dev, 1);
    return 0;
}
static void led_exit(void)
{
    int i;
    //1.输出1,释放GPIO资源
    for(i = 0; i < ARRAY_SIZE(led_info); i++) {
        gpio_set_value(led_info[i].gpio, 1);
        gpio_free(led_info[i].gpio);
    }
    //2.释放设备号
    unregister_chrdev_region(dev, 1);
    //3.卸载字符设备对象
    cdev_del(&led_cdev);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");


  • Makefile
kernel_dir=/home/ww/ARM/kernel
obj-m += led_drv.o
all:
        make -C ${kernel_dir} SUBDIRS=$(PWD) modules
clean:
        make -C ${kernel_dir} SUBDIRS=$(PWD) clean


  • led_test.c(用户空间测试程序)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
    int fd;
    //应用open->软中断->内核sys_open->驱动led_open
    fd = open("/dev/myled", O_RDWR);
    if (fd < 0) {
        printf("打开设备失败~!\n");
        return -1;
    }
    sleep(3);
    //应用close->软中断->内核sys_close->驱动led_close
    close(fd);
    return 0;
}

测试执行:

20191223220419666.png

从中能看到设备号244是我们的设备myled,使用mknod命令注册设备文件:

mknode /dev/myled c 244 0

2019122322063019.png

总结

  • 字符设备驱动,最终达到的效果就是用户程序调用Linux系统调用函数open、close、read、write等从而最终能够调用到内核驱动空间内部写好的操作接口来达到操作硬件的效果。
  • 这样需要注意到设备号、主设备号、次设备号的含义及使用方式。
相关文章
|
24天前
|
网络协议 Linux 调度
深入探索Linux操作系统的心脏:内核与系统调用####
本文旨在揭开Linux操作系统中最为核心的部分——内核与系统调用的神秘面纱,通过生动形象的语言和比喻,让读者仿佛踏上了一段奇妙的旅程,从宏观到微观,逐步深入了解这两个关键组件如何协同工作,支撑起整个操作系统的运行。不同于传统的技术解析,本文将以故事化的方式,带领读者领略Linux内核的精妙设计与系统调用的魅力所在,即便是对技术细节不甚了解的读者也能轻松享受这次知识之旅。 ####
|
20天前
|
缓存 算法 安全
深入理解Linux操作系统的心脏:内核与系统调用####
【10月更文挑战第20天】 本文将带你探索Linux操作系统的核心——其强大的内核和高效的系统调用机制。通过深入浅出的解释,我们将揭示这些技术是如何协同工作以支撑起整个系统的运行,同时也会触及一些常见的误解和背后的哲学思想。无论你是开发者、系统管理员还是普通用户,了解这些基础知识都将有助于你更好地利用Linux的强大功能。 ####
31 1
|
27天前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
75 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
2月前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
2月前
|
存储 Linux 程序员
Linux中的主要系统调用
【9月更文挑战第11天】在Linux操作系统中,通过系统调用`fork`创建新进程,子进程继承父进程的数据结构与代码,但可通过`execve`执行不同程序。`fork`返回值区分父子进程,`waitpid`让父进程等待子进程结束。
|
2月前
|
Linux 开发者 Python
从Windows到Linux,Python系统调用如何让代码飞翔🚀
【9月更文挑战第10天】在编程领域,跨越不同操作系统的障碍是常见挑战。Python凭借其“编写一次,到处运行”的理念,显著简化了这一过程。通过os、subprocess、shutil等标准库模块,Python提供了统一的接口,自动处理底层差异,使代码在Windows和Linux上无缝运行。例如,`open`函数在不同系统中以相同方式操作文件,而`subprocess`模块则能一致地执行系统命令。此外,第三方库如psutil进一步增强了跨平台能力,使开发者能够轻松编写高效且易维护的代码。借助Python的强大系统调用功能,跨平台编程变得简单高效。
39 0
|
2月前
|
Linux 测试技术 芯片
在Linux中使用GPIO线【ChatGPT】
在Linux中使用GPIO线【ChatGPT】
|
4天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
19 9
|
1天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
14 4
|
4天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。