ARM嵌入式学习笔记——Linux字符设备驱动程序设计

Linux设备驱动开发

Linux系统调用实现原理

  • 作用:实现用户应用程序和内核程序的交互。
  • 原理:基于软终端实现。
  • 结论:应用程序调用和内核函数之间的调用关系。
    • 应用open->C库open->软中断->内核sys_open->应用open返回。
    • 应用cloase->C库close->软中断->内核sys_close->应用close返回。
    • 应用read->C库read->软中断->内核sys_read->应用read返回。
    • 应用write->C库write->软中断->内核sys_write->应用write返回。

Linux内核设备驱动开发相关内容

何为设备驱动?

  • 切记设备驱动两个核心内容:
    • 驱动一定要操作硬件。
    • 驱动一定要能给用户提供访问操作硬件的接口(函数)。将来应用程序能给利用这些接口访问硬件。

Linux内核设备驱动的分类:三大类

  • 字符设备驱动
    • 特点:字符设备硬件操作的数据按照字节流形式访问。
    • 例如:LED、按键、蜂鸣器、LCD显示屏(RGB)、触摸屏(绝对坐标X,Y)、鼠标(相对坐标)、摄像头、声卡、GPS(高度,时间,经纬度)、GPRS(AT指令:字符串)、蓝牙、
    • 只要是UART接口的外设都是字符设备,各种硬件传感器都是字符设备。
  • 块设备驱动
    • 特点:块设备操作的数据按照数据块进行访 问,一般数据块为512字节,一次性访问512字节。
    • 例如:硬盘、U盘、TF卡、SD卡、EMMC、Nonflash、nandflash。
    • 注意:此驱动Linux内核完美支持,通常不需要进行修改。
  • 网络设备驱动
    • 特点:数据按照网络协议进行。网络设备驱动属于数据链路层。
    • 例如:有线网卡驱动和无限网卡驱动。
    • 注意:这些驱动芯片厂家提供源码,无需开发。

Linux系统的理念(信仰):一切皆文件

  • “一切”:就是指任何硬件外设。
  • “一切皆文件”:计算机中任何硬件在Linux系统中都是以文件的形式访问操作。
  • 核心:Linux应用程序要想访问某个硬件,必须找到这个硬件对应的文件,将来访问这个文件本质就是在访问硬件。
Linux系统设备对应的文件分两类:字符设备文件和块设备文件。
  • 网络设备无设备文件,通过socket套接字访问。

字符设备文件属性

  • 字符设备文件本身就是字符设备硬件。

    • 访问字符设备文件本身就是访问硬件。
  • 字符设备文件只能存在于根文件系统必要目录dev目录下。

    • 例如:下位机操作执行:ls /dev/ttySAC* -lh。
    • crw-rw---- 204, 64 /dev/ttySAC0
    • “c”:表示此文件为字符设备文件
    • “204”:表示此字符设备文件包含的主设备号。
    • “64”:表示第一个串口的字符设备文件的次设备号。
    • “ttySAC0”:表示第一个串口的字符设备文件名。
  • 将来访问字符设备硬件只需要访问对应的字符设备文件即可。

    • 问:怎么访问?
    • 答:利用系统调用函数。
int fd = open("/dev/ttySAC0", O_RDWR);
char buf[1024] = {0};

//读数据
read(fd, buf, sizeof(buf));

//写数据
write(fd, "hello", 5);

//关闭设备
close(fd);

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

  • 设备号:同时包含了主设备号和次设备号
  • 设备号的数据类型:dev_t(unsigned int)
  • 设备号的高12个bit位表示主设备号。
  • 设备号的低20个bit位表示次设备号
  • Linux内核提供的三者转换的宏:
    • (已知主、次设备号来合并设备号),设备号=MKDEV(已知的主设备号,已知的次设备号)
    • (从设备号中提取主设备号)主设备号=MAJOR(已知的设备号)
    • (从设备号中提取次设备号)次设备号=MINOR(已知的设备号)
主设备号作用:
  • 应用程序设备文件的主设备号在茫茫的Linux内核驱动中找到对应的唯一驱动程序。
  • 简称:应用根据主设备号找驱动。
  • 结论:一个驱动仅有唯一的主设备号。
此设备号作用:
  • 如果一个驱动管理多个同类型的硬件设备驱动将来根据次设备号来找到要访问的具体某个硬件设备
  • 简称:驱动根据次设备号找硬件。
  • 结论:一个硬件个体仅有唯一的次设备。
由于主、次设备号对于Linux内核来说是一种宝贵的资源,所以驱动程序或者硬件要关联某个主设备号和次设备号必须向内核操作系统申请设备号资源
申请和释放设备号的函数分别是:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
  • 函数功能:向Linux内核申请设备号。
  • 形参:
    • dev:保存将来内核给你分配的设备号信息。
    • baseminor:内核希望起始的次设备号。一般给0也就是次设备号从0开始分配。例如:现在有四个硬件,对应的设备号分配:0,1,2,3
    • count:次设备号的个数。
    • name:指定设备的名称,名字随便取,作为调试手段,将来通过执行cat /proc/devices命令来获取名称。
void unregister_chrdev_region(dev_t from, unsigned count)
  • 函数功能:释放申请到的设备号。
  • 参数:
    • from:传递申请到的设备号,from中包含唯一的主设备号和起始设备号。
    • count:传递次设备号的个数。

如何编写一个字符设备驱动呢?

  • 再次明确:驱动程序属于Linux内核的一部分。也就是内核的其他代码是可以调用自己写的驱动程序的函数或者变量,也就是内核代码可以互相直接调用。

  • 再次明确:不管是什么驱动。都必须有两个核心内容:

    • 必须操作硬件。
    • 必须给应用程序提供操作硬件的接口。(函数)
    • Linux字符设备驱动都要关联唯一的主设备号,如果驱动管理多个硬件,每个硬件还要关联多个次设备号。
  • 结论:通过以上信息,发现字符设备驱动本身就是一个“事物”,这个事物本身还包含一了一些属性(设备号)和方法(硬件操作接口函数)。所以驱动必然有对应的数据结构(类)。

自行设计Linux内核字符设备驱动的数据结构
struct char_device {
    char *name;//驱动的名称
    dev_t dev;//驱动申请的设备号
    int count;//驱动申请的次设备号的个数
    int (*open)(...);//提供打开硬件接口
    int (*close)(...);//提供关闭硬件的接口
    int (*read)(...);//提供读硬件数据接口
    int (*write)(...);//提供向硬件写入数据接口
};

*· 缺点:将来根据用户的需求经常要改动操作接口,一会加了lseek,一会加个mmap等,不便于维护,提取出来单独管理。

  • 优化:
//声明描述字符设备驱动的数据结构
strucet file_operations{
    int (*open)(..);
    int (*close)(...);
    int (*read)(...);
    int (*write)(...);
};  
//声明描述字符设备驱动的数据结构

struct char_device
{
    char *name;//驱动的名称
    dev_t dev;//驱动申请到的设备号。
    int count;//驱动申请的次设备号的个数。
    struct file_operations *ops;//给字符设备驱动定义的结构体。
};
}
利用自行设计的数据结构来实现一个字符设备驱动
  • 1、定义初始化硬件操作接口对象(实例化硬件操作接口对象)
struct file_operations led_fops = {
    .open = led_open, //打开设备
    .close = led_close,//关闭设备
    .read = led_read,//读设备
    .write = led_write//写设备
};
  • 定义初始化一个字符设备驱动对象(实例化对象)
struct char_device  led_dev = {
    .name = "led",
    .dev = (申请好的设备号)(alloc_chrdev_region)
    .count = 次设备号的个数、
    .ops = &led_fops //添加硬件操作接口
};
  • 2、吹毛求疵:初始化有点不爽,进行优化——提供一个函数来进行初始化,将两个对象关联起来。
  • 定义初始化函数:
void char_device_init(struct char_device *dev, struct file_operations *fops)
{
    dev->ops = fops; //两者结合,提供操作接口。
}

//优化之后的程序使用:
char_device_init(&led_dev, &led_fops);
  • 3、目前内核还不认这个字符设备驱动,还需要向内核注册登记这个字符设备驱动对象,并且这个对象里面有操作接口:
    • 自行设计一个注册函数:
void char_device_add(struct char_device *dev, dev_t dev, int count, char *name)
{
    //1、先将其余没有初始化的字段初始化
    dev->name = name;
    dev->dev = dev;
    dev->count = count;
    
    //2、向内核注册登记
    //思路:事先在内核中准备一个大数组,数组的下标就是主设备号,数组元素就是字符设备对象的地址。
    //结论:以主设备号为下标,将要注册的字符设备对象的地址放到这个大数组中即可。
    //联想:应用如何调用底层驱动提供的操作接口函数。应用open->C库open ->软中断->内核sys_open->应用open,这里要明确:内核的sys_open只有一个。
    //问:内核唯一的函数sys_open如何能找到众多驱动中对应的open接口呢?
    //应用open->软中断->内核sys_open->驱动的beep_open(如何找到beep_open呢?)
    //答:由于字符设备文件本身包含了主设备号,当应用open时也可以获取主设备号,最终跑到内核的sys_open,进程在内核的sys_open中找到内核事前准备好的大数组,然后以主设备号为下标找到应用的字符设备驱动对象,例如led_dev,一旦内核的sys_open找到字符设备驱动对象led_dev然后直接调用代码即可:int 内核:sys_open(...){
    //1、在大数组中以主设备号为下标找字符设备对象地址,
    //2、直接调用字符设备对象中的操作函数接口。
    &led_dev->ops->open();//最终调用驱动的led_dev的函数
    }
    //以此类推:
    
}
*   结论:一旦注册成功,将来这个驱动静静在内存中等待着应用程序利用系统调用函数来访问驱动注册的接口函数
  • 4、踏踏实实的根据用户需求编写各个操作接口函数:

    • int led_open();
    • int led_close();
    • int led_read();
    • int led_write();
  • 5、设计一个函数将字符设备对象从内核卸载,也就是从大数组中删除

void char_device_del(struct char_device *dev)
{
    //1、以dev->dev的主设备号为下标,以大数组中删除字符设备对象即可。
}
一旦删除,除非重启,否则不可恢复。

Linux内核字符设备驱动的实现过程

Linux内核描述字符设备驱动的硬件操作接口数据结构
struct file_operations{
    open,
    close,
    read,
    write,
};
Linux内核描述字符设备驱动的数据结构
struct cdev{
    const struct file_operations *ops;//硬件操作接口结构对象
    dev_t dev;//保存申请的设备号
    unsigned int count;//次设备号的个数。
    ...
};
配套函数
void cdev_init(struct cdev *cdev, struct file_operations *fops);
  • 功能:给字符设备驱动对象添加硬件操作接口
cdev_add(struct cdev *p, dev_t dev, unsigned count);
  • 功能:向内核的大数组注册一个字符设备驱动对象。
cdev_del(struct cdev *p);
  • 功能:从内核大数组中删除字符设备对象。

总结:编写一个字符设备驱动的编程步骤

根据用户需求先定义初始化硬件操作接口对象
struct file_operations A={
    .opern = xxx_open,
    .close = xxx_close,
    ......
}
然后定义初始化字符设备驱动对象
struct cdev B;//定义
cdev_init(&B, &A);//初始化
向内核注册字符设备对象
cdev_add(&B, 申请号的设备号,次设备号的个数);
  • 至此,内核就有了一个真实的字符设备驱动存在于内存中,静静等待着应用程序利用系统调用函数来访问驱动中的各个接口函数。
在适当的地方卸载字符设备驱动对象
cdev_del(&B);
根据用户需求编写各个接口函数
int xxx_open()
{
    //打开设备
}
...

案例:编写LED字符设备驱动,实现打开设备开灯,关闭设备关灯。

操作流程:
  • 上位机执行:
mkdir /opt/drivers/day03/1.0 -p
cd /opt/drivers/day03/1.0
vim led_drv.c //驱动程序
vim led_test.c //应用程序
vim Makefile
make
arm... gcc -o led_test led_test.c
cp led_drv.ko led_test /opt/rootfs/home/drivers
  • 下位机测试:
cd /home/drivers
insmod led_drv.ko //调用入口函数
cat /proc/devices //查看申请到的主设备号
mknod /dev/myled c 主设备号 0 //创建设备文件,代表LED0
./led_test
//open device fail 测试失败
cd /home/drivers
insmod led_drv.ko //调用入口函数
cat /proc/devices //查看申请到的主设备号
    character devices://当前系统支持的字符设备,主设备号和设备名称
    1   mem
    5   /dev/tty
    5   /dev/console
    5   /dev/ptmx
    ...
    244 myled  //LED驱动申请到的主设备号就是244,设备名称为myled
    ...
mknod /dev/myled c 主设备号 0 //创建设备文件,它代表LED设备。
./led_test //再次执行
代码编写:
  • led_drv.c
#include 
#include 
#include 
#inlcude 
#include  // struct cdev
#include  //struct file_operations

//声明描述LED硬件信息的数据结构
struct led_resource{
    char *name; //名称
    int gpio; //gpio编号
};

//定义初始化四个LED灯的硬件信息对象
static struct led_resource led_info[] = {
    {
        .name = "LED1",
        .gpio = PAD_GPIO_C+12
    },
    {
        .name = "LED2",
        .gpio = PAD_GPIO_C+17
    },
    {
        .name = "LED3",
        .gpio = PAD_GPIO_C+11
    },
    {
        .name = "LED4",
        .gpio = PAD_GPIO_B+26
    }
};

//打开设备接口:
//执行成功返回0,失败返回负值
static int led_open(struct inode *, struct file *)
{
    //开灯
    int i;
    for(i = 0; i 
  • led_test.c
#include 
#include 
#include 
#include 

int main()
{
    int fd;
    //应用open->软中断->内核sys_open->驱动led_open
    fd = open("/dev/myled", O_RDWR);
    if(fd < 0){
            printf("open led device failed.\n");
            return -1;
    }
    sleep(3);
    
    //应用close->软中断->内核sys_close->驱动led_close
    close(fd);
    return 0;    
}

  • Makefile
obj-m += led_drv.o

all:
    make -C /opt/kernel SUBDIRS=$(PWD) modules

clean:
    make -C /opt/kernel SUBDIRS=$(PWD) clean

总结编写字符设备驱动的详细步骤

  • 先搭建驱动框架:

    • 头文件
    • 入口函数
    • 出口函数
    • 此时先不要写入口和出口
  • 各种该:

    • 该声明的声明
    • 该定义的定义
    • 该初始化的初始化
    • 先搞硬件后搞软件【变量】
  • 填充入口和出口

    • 先写注释
    • 后塞代码【体力活】
  • 最后编写各个接口函数

Linux字符设备驱动硬件操作接口之write接口

  • 明确:Linux系统缓冲区其实就是内存,分两类:
    • 用户缓冲区:分配的内存在用户3G虚拟内存上。
    • 内核缓冲区:分配的内存在内核1G虚拟内存上。

回顾write系统调用函数

ssize_t write(int fd, const void *buf, size_t count);

  • 功能:向硬件设备写入数据
  • fd:设备文件描述符,fd代表的就是硬件,fd代表的就是
  • buf:传递要写入的数据所在数据所在用户缓冲区的首地址。
  • count:传递要写入的数据大小。
  • 返回值:返回实际写入的字节数。
  • write(fd, &cmd, sizeof(cmd)); //向硬件写入数据1

对应的底层驱动的write接口

struct file_operations {
    int (*open)(struct inode *, struct file *);
    int (*release)(struct inode *, struct file *);
    ssize_t (*write) (struct file *, const char __user *buf, size_t count, loff_t *ppos);
}

注意:

  • 底层驱动的open,release两个接口可以不用初始化(.open = led_open…),应用程序调用open,close永远返回成功,扩展内核的sys_open代码:
int sys_open(...)
{
    if(驱动的open接口是否为NULL)
        return fd > 0 // 永远成功
    else
        xxx->ops->open();//调用驱动的open接口
}

如果用户对open/close没有要求,底层驱动open/close可以不用初始化!

write接口和应用write函数的调用关系:
  • 应用write->C库的write->软中断->内核的sys_write->驱动write接口->应用write返回。

  • write接口的功能:向硬件设备写入数据

    • 本质就是一个桥梁:连接用户和硬件,也就是用户数据->底层驱动write->硬件

    ssize_t (*write) (struct file *, const char __user *buf, size_t count, loff_t *ppos);

  • 参数说明:

    • file:文件指针,暂时用不着。
    • buf:此指针变量用“__user”修饰,说明此指针变量保存的地址一定是用户缓冲区的首地址,例如buf = &cmd.
    • 所以底层驱动的write接口可以通过buf指针来获取用户缓冲区的数据,底层驱动write接口获取数据的代码无脑的写。
    • count:传递要写入的字节数,等于write的第三个参数。
    • ppos:保存上一次的写位置,开始值为0。
      • 如果要记录位置,编程步骤:
      • 1、先获取上一次的写位置:unsigned long pos = *ppos;
      • 2、假设这次write又写了100字节,底层驱动write返回之前记得要更新写位置:*ppos = pos +100;
      • 3、注意:应用于连续多次write操作,如果一次性write完,无需关注此参数。

注意:此种写法极其危险,两种危险情况

  • 如果应用write这么写:
    • write(fd, NULL, 0); //直接空指针的非法访问
  • 如果&cmd用户虚拟地址和物理地址的映射没有建立,会造成地址非法访问。

int copy_from_user(void *to, const void __user *from, int n);

  • 所以:底层驱动要想通过buf指针来获取用户缓存区要写入的数据,必须利用内核来提供的内存拷贝函数来实现用户缓存区和内核缓冲区的数据拷贝此函数会帮你检查地址是否有效:
  • 功能:拷贝用户缓冲区的数据到内核缓存区。
  • 参数:
    • to:内核缓冲区的首地址,目的地址。
    • from:用户缓冲区的首地址,源地址。
    • n:要拷贝的字节数。

案例:编写LED字符设备驱动,实现向设备写1开灯,写0关灯。

  • led_test.c
#include 
#include 
#include 
#include 

int main()
{
    int fd;
    int cmd = 0;
    if(argc != 2){
        printf("usage : %s \n", argv[0]);
        return -1;
    }
    fd = open("/dev/myled", O_RDWR);
    if(fd < 0){
        printf("open led device failed.\n);
        return -1;
    }
    
    if(!strcmp(argv[1], "on"))
    {
        cmd = 1;
    }
    else if(!strcmp(argv[1], "off"))
    {
        cmd = 0;
    }
    //向设备驱动文件写cmd
    write(fd, &cmd, sizeof(cmd));
    
    close(fd);
    
    return 0;
}

  • led_drv.c
#include 
#include 
#include 
#include 
#include 
#include 

//声明描述LED硬件信息数据结构
struct led_resource{
    char *name;
    int gpio;
};

//初始化LED硬件信息对象
static struct led_resource led_info[] = {
    {
        .name = "LED1",
        .gpio = PAD_GPIO_C + 12
    },
    {
        .name = "LED2",
        .gpio = PAD_GPIO_C + 17
    },
    {
        .name = "LED3",
        .gpio = PAD_GPIO_C + 11
    },
    {
        .name = "LED4",
        .gpio = PAD_GPIO_B + 26
    }
};

//向硬件设备写入数据接口
//参数对应关系
//write的fd  <->led_write file;  write的buf  <-> led_write  buf;
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    int i;
    //分配内核缓冲区,暂存从用户缓冲区获取的数据
    int kcmd;
    //拷贝用户缓冲区的数据到内核缓冲区中
    //kcmd = *(int *)buf; <---危险操作,使用操作函数
    copy_from_user(&cmd, buf, sizeof(kcmd));
    
    //操作硬件
    for(i = 0; i< ARRAY_SIZE(led_info); i++)
    {
        gpio_set_value(led_info[i].gpio, !kcmd);
        printk("%s: %s 第 %d 个灯", __func, kcmd ? "开":"关", i+1);
    }
    
    return count
}

//定义初始化LED硬件操作接口对象
static struct file_operations led_fops = {
    .write = led_write//向硬件写入数据
}

//定义设备号对象
static dev_t dev;
//定义字符设备对象
static struct cdev led_cdev;

static int led_init(void)
{
    int i;
    //申请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);
    }
    //申请设备号.
    alloc_chrdev_region(&dev, 0, 1, "myled");
    //初始化字符设备对象,添加操作接口
    cdev_init(&led_cdev, &led_fops);
    //向内核注册字符设备对象。
    cdev_add(&led_cdev, dev, 1);
    return 0;
}

static void led_exit(void)
{
    //从内核卸载字符设备对象
    cdev_del(&led_cdev);
    //释放设备号
    unregister_chrdev_region(dev, 1);
    //释放GPIO资源,输出1
    for(i = 0; i < ARRAY_SIZE(led_info); i++)
    {
        gpio_set_value(led_info[i].gpio, 1);
        gpio_free(led_info[i].gpio);
    }
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

  • led_test.c
#include 

//声明描述LED操作信息的数据结构
struct led_event {
    int cmd;//开关灯命令,1-开灯,0-关灯。
    int index;//灯编号。1、2、3、4
};

int main()
{
    int fd;
    struct led_event led;
    if(argc != 3){
        printf("usage: %s  <1|2|3|4> \n", argv[0]);
        return -1;
    }
    
    fd = open("/dev/myled", O_RDWR);
    if(fd < 0)
        return -1;
    
    if(!strcmp(argv[1],  "on"))
        led.cmd = 1;
    else if(!strcmp(argv[1],  "off"))
        led.cmd = 0;
        
    led.index = strtoul(argv[2], NULL, 0);
    
    write(fd, &led, sizeof(led));
    
    close(fd);
    return 0;
}

Linux字符设备驱动硬件操作接口之read接口

回顾应用程序read函数

ssize_t read(int fd, void *buf, size_t count);

  • 功能:从硬件读取数据放到用户缓冲区
  • 参数:
    • fd:设备文件描述符,它是字符设备文件代理
    • buf:传递用户缓冲区的首地址。
    • count:传递要读取的字节数。
    • 返回值:返回实际读取的字节数。

对于底层驱动的read函数接口

ssize_t (*read)(struct file *file, char __user *buf, size_t count, loff_t *ppos);

  • 调用关系:应用read->C库read->软中断->内核sys_read->驱动read。
  • 接口功能:从硬件读取数据给应用程序起到桥梁的作用,连接应用和硬件。
  • 参数:
    • file:文件指针,跟应用read的第一个参数有关。
    • buf:此指针变量用“__user”修饰,所以必须保存的是用户缓存区首地址。所以将来底层驱动的read接口可以从硬件读取的数据通过buf放到用户缓冲区,
    • (*buf = 250,此操作极其不安全),两种情况:
      • 1、应用程序故意传递非法地址,例如read(fd, NULL,0);
      • 2、用户缓冲区对应的虚拟内存映射无效,造成地址非法访问。
    • 驱动read接口要想通过buf来操作用户缓冲区必须利用内核提供的内存拷贝函数:
      • int copy_to_user(void __user *to, void *from, int n);
      • 功能:将内核缓冲区的数据拷贝到用户缓冲区。
      • to:目的地址,用户缓冲区首地址。
      • from:源地址,内核缓冲区首地址。
      • n:要拷贝的字节数。
    • count:传递要读取的数据字节数,等于应用read的第三个参数。
    • ppos:记录上一次的读位置,如果驱动read采用连续多次读取。
      • 1、先获取上一次read的读位置,unsigned long pos = *pos;
      • 2、假设这次read又读取了100字节,底层驱动read返回之前记得更新读位置,*ppos = pos + 100;
      • 注意:如果读一次搞定无需关注ppos。

案例:编写LED字符设备驱动,不仅仅实现开关某个灯,还能获取灯的开关状态。

  • 实验步骤同上。
  • led_test.c
#include 
#include 
#include 
#include 

//声明描述led操作信息的数据结构
struct led_event {
    int cmd;//开灯命令,1-开灯,0-关灯。
    int index; //灯编号
};
//声明描述led等状态的数据结构
struct led_state{
    int index;//灯编号。
    int state;//灯状态,1-关灯,0-开灯。
};


int main()
{
    int fd;
    struct led_event led;//分配用户缓冲区,保存灯的操作信息。
    struct led_state ledst;//分配用户缓冲区,保存灯的状态。
    if(argc != 3){
        printf("usage: %s  <1|2|3|4> \n", argv[0]);
        return -1;
    }
    
    fd = open("/dev/myled", O_RDWR);
    if(fd < 0)
        return -1;
    
    if(!strcmp(argv[1],  "on"))
        led.cmd = 1;
    else if(!strcmp(argv[1],  "off"))
        led.cmd = 0;
        
    led.index = strtoul(argv[2], NULL, 0);
    //将LED操作信息发生给驱动,开关灯。
    write(fd, &led, sizeof(led));
    
    //获取灯的状态
    //为ledst 赋值。
    ledst.index = strtol(argv[2], NULL, 0);
    
    read(fd, &ledst, sizeof(ledst));
    //打印状态
    printf("%d led state is %s \n", ledst.index, ledst.state? "close":"open");
    
    close(fd);
    return 0;
}

  • led_drv.c
#include 
#include 
#include 
#include 
#include 
#include 


//声明描述led操作信息的数据结构
struct led_event {
    int cmd;//开灯命令,1-开灯,0-关灯。
    int index; //灯编号
};
//声明描述led等状态的数据结构
struct led_state{
    int index;//灯编号。
    int state;//灯状态,1-关灯,0-开灯。
};

//声明描述LED硬件信息数据结构
struct led_resource{
    char *name;
    int gpio;
};

//初始化LED硬件信息对象
static struct led_resource led_info[] = {
    {
        .name = "LED1",
        .gpio = PAD_GPIO_C + 12
    },
    {
        .name = "LED2",
        .gpio = PAD_GPIO_C + 17
    },
    {
        .name = "LED3",
        .gpio = PAD_GPIO_C + 11
    },
    {
        .name = "LED4",
        .gpio = PAD_GPIO_B + 26
    }
};

//向硬件设备写入数据接口
//参数对应关系
//write的fd  <->led_write file;  write的buf  <-> led_write  buf;
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    int i;
    //分配内核缓冲区,暂存从用户缓冲区获取的数据
    int kcmd;
    //拷贝用户缓冲区的数据到内核缓冲区中
    //kcmd = *(int *)buf; <---危险操作,使用操作函数
    copy_from_user(&cmd, buf, sizeof(kcmd));
    
    //操作硬件
    for(i = 0; i< ARRAY_SIZE(led_info); i++)
    {
        gpio_set_value(led_info[i].gpio, !kcmd);
        printk("%s: %s 第 %d 个灯", __func, kcmd ? "开":"关", i+1);
    }
    
    return count
}

//读硬件接口
static ssize_t led_read(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    //1、分配内核缓冲区,暂存数据
    struct led_state kledst;
    //2.先从用户缓存区拷贝数据到内核缓冲区
    copy_from_user(&kledst, buf, sizeof(kledst));
    
    kledst.state = gpio_get_value(led_info[kledst.index-1].gpio);
    //将内核缓存区中的有效数据拷贝到用户缓冲区
    copy_to_user(buf, &kledst, sizeof(kledst));
    
    return count;
}

//定义初始化LED硬件操作接口对象
static struct file_operations led_fops = {
    .write = led_write//向硬件写入数据
    .read = led_read;//从硬件读取数据
}

//定义设备号对象
static dev_t dev;
//定义字符设备对象
static struct cdev led_cdev;

static int led_init(void)
{
    int i;
    //申请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);
    }
    //申请设备号.
    alloc_chrdev_region(&dev, 0, 1, "myled");
    //初始化字符设备对象,添加操作接口
    cdev_init(&led_cdev, &led_fops);
    //向内核注册字符设备对象。
    cdev_add(&led_cdev, dev, 1);
    return 0;
}

static void led_exit(void)
{
    //从内核卸载字符设备对象
    cdev_del(&led_cdev);
    //释放设备号
    unregister_chrdev_region(dev, 1);
    //释放GPIO资源,输出1
    for(i = 0; i < ARRAY_SIZE(led_info); i++)
    {
        gpio_set_value(led_info[i].gpio, 1);
        gpio_free(led_info[i].gpio);
    }
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

字符设备驱动硬件操作接口之ioctl

学习掌握ioctl系统调用函数

int ioctl(int fd, int request, …);

  • 函数功能:
    • 应用程序利用此函数可以向硬件设备发生控制命令略带写write意味。
    • 应用程序利用此函数可以和硬件进行读或者写操作,简直侵略read/write的地位。
    • 参数:
      • fd:设备文件描述符。
      • cmd:向硬件设备发送的控制命令扩展成任何命令都可以,命令又驱动工程师自行定义。
      • 将来驱动程序可以利用第三个参数可以读写用户缓冲区。
      • 返回值:成功返回0,失败返回-1.

对应底层驱动的ioctl接口:

struct file_operations {
    long (*unlocked_ioctl)(struct file *file , unsigned int cmd, unsigned long buf);
}
  • 调用关系:应用程序ioctl->C库的ioctl->软中断->内核sys_ioctl->驱动ioctl接口。
  • 接口功能:
    • 驱动可以向硬件设备发送控制命令,略带写write意味。
    • 驱动还可以和硬件进行读写操作。
  • 参数:
    • file:文件指针,与fd有关。
    • cmd:保存应用程序传递的控制命令
    • buf:如果应用要和硬件进行读写操作,buf一般保存用户缓冲区的首地址(当然也可以保存一些普通的变量),所以将来底层驱动的ioctl接口可以通过buf对用户缓冲区进行读写访问,但是使用时注意数据类型的强转换。
    • 具体使用buf时使用copy_from_user(&kdata, (int *)buf, 4);
    • 例如:ioctl(fd, LED_ON);//仅仅向设备发送开灯命令。
    • ioctl(fd, MMA8653_WRITE, &data);//除了发送命令还要写入数据。
    • 返回值:成功返回0,失败返回-1。

案例:编写LED字符设备驱动,实现开关灯、开关某个灯。

下位机测试:
cd /home/drivers
insmod led_drv.ko
cat /proc/devices
mknod /dev/myled c 244 0
./led_test on
./led_test off
./led_test on 1
./led_test off 1
代码实现:实现开关灯
  • led_test.c
#include 
#include 
#include 
#include 

//自定义两个命令
#define LED_ON  0x100001
#define LED_OFF 0x100002

//声明描述led操作信息的数据结构
struct led_event {
    int cmd;//开灯命令,1-开灯,0-关灯。
    int index; //灯编号
};
//声明描述led等状态的数据结构
struct led_state{
    int index;//灯编号。
    int state;//灯状态,1-关灯,0-开灯。
};


int main()
{
    int fd;
    struct led_event led;//分配用户缓冲区,保存灯的操作信息。
    struct led_state ledst;//分配用户缓冲区,保存灯的状态。
    if(argc != 3){
        printf("usage: %s  <1|2|3|4> \n", argv[0]);
        return -1;
    }
    
    fd = open("/dev/myled", O_RDWR);
    if(fd < 0)
        return -1;
    
    if(!strcmp(argv[1],  "on"))
        ioctl(fd, LED_ON);
    else if(!strcmp(argv[1],  "off"))
        ioctl(fd, LED_OFF);
            
    close(fd);
    return 0;
}

  • led_drv.c
//自定义两个命令
#define LED_ON  0x100001
#define LED_OFF 0x100002

//控制操作接口
static long led_ioctl(struct file *file, unsigned long cmd, unsigned long buf)
{
    int i;
    switch(cmd){
        case LED_ON:
            for(i = 0; i < ARRAY_SIZE(led_info); i++){
                gpio_set_value(led_info[i].gpio, 0);
                printk("%s : turn on %d led. \n", __func__, i+1);
            }
            break;
        case LED_OFF:
            for(i = 0; i < ARRAY_SIZE(led_info); i++){
                gpio_set_value(led_info[i].gpio, 1);
                printk("%s : turn off %d led. \n", __func__, i+1);
            }
            break;
        default:
            printk("command is invalid\n");
            return -1;
    }
    retrun 0;
}

//部分代码:
static struct file_operations led_fops = {
    .unlocked_ioctl = led_ioctl
};
代码实现:实现开关某个灯
  • led_test.c
#include 
#include 
#include 
#include 

//自定义两个命令
#define LED_ON  0x100001
#define LED_OFF 0x100002

//声明描述led操作信息的数据结构
struct led_event {
    int cmd;//开灯命令,1-开灯,0-关灯。
    int index; //灯编号
};
//声明描述led等状态的数据结构
struct led_state{
    int index;//灯编号。
    int state;//灯状态,1-关灯,0-开灯。
};


int main()
{
    int fd;
    int index;
    struct led_event led;//分配用户缓冲区,保存灯的操作信息。
    struct led_state ledst;//分配用户缓冲区,保存灯的状态。
    if(argc != 3){
        printf("usage: %s  <1|2|3|4> \n", argv[0]);
        return -1;
    }
        
    fd = open("/dev/myled", O_RDWR);
    if(fd < 0)
        return -1;
    
    //获取灯编号。
    index = strtoul(argv[2], NULL, 0);
    
    if(!strcmp(argv[1],  "on"))
        ioctl(fd, LED_ON, &index);//三个参数
    else if(!strcmp(argv[1],  "off"))
        ioctl(fd, LED_OFF, &index);
            
    close(fd);
    return 0;
}

  • led_drv.c
//自定义两个命令
#define LED_ON  0x100001
#define LED_OFF 0x100002

//控制操作接口
static long led_ioctl(struct file *file, unsigned long cmd, unsigned long buf)
{
    int i;
    //分配内核缓冲区
    int kindex;
    copy_from_user(&kindex, (int *)buf, sizeof(kindex));
    switch(cmd){
        case LED_ON:
                gpio_set_value(led_info[kindex - 1].gpio, 0);
                printk("%s : turn on %d led. \n", __func__, kindex);
            break;
        case LED_OFF:
                gpio_set_value(led_info[kindex - 1].gpio, 1);
                printk("%s : turn off %d led. \n", __func__, kindex);
            break;
        default:
            printk("command is invalid\n");
            return -1;
    }
    retrun 0;
}

//部分代码:
static struct file_operations led_fops = {
    .unlocked_ioctl = led_ioctl
};

了解两个数据结构:struct inode和struct file

struct inode{
    umode_t i_mode;
    unsigned short i_opflags;
    uid_t i_uid;
    gid_t i_gid;
    unsigned int i_flags;
    struct posix_acl *i_acl;
    struct posix_acl *i_default_acl;
    const struct inode_operations *i_op;
    struct super_block *i_sb;
    struct address_spacc *i_mapping;
    void *i_security;
    ....
    dev_t i_rdev;//如果此文件是设备文件,i_rdev保存对应的设备号。
    struct cdev *i_cdev;//指向设备文件对应的字符设备驱动对象。
    ....
};
  • 功能:描述一个文件的物理信息(权限,用户和组、大小、日期)
  • 生命周期:每当创建一个文件时(touch,mknod,echo,vim等),Linux内核就会为此文件定义初始化一个inode对象来描述新文件的物理属性信息,每当删除文件(rm),内核就会删除之前创建的inode对象。
    • 一个文件仅有唯一一个inode对象。
  • 结论:硬件操作接口open/release中的第一个形参inode指针指向内核创建的Inode对象,将来驱动程序可以利用inode来获取文件的物理信息,例如:设备号
int led_open(struct inode *inode, ...)
{
    printk("主设备号  %d, 次设备号  %d\n", MAJOR(inode->i_rdev, MINOR(indoe->i_rdev));
    return 0;
}

结构体file

struct file{
    struct file_operations *f_op;
}
  • 功能:描述一个文件被成功打开open之后的属性。
  • 生命周期:打开文件open成功,内核就会创建一个file对象来描述文件打开后的属性,当close文件后,内核就会销毁对应的file对象。
  • 注意:一个文件可以有多个file对象。
  • 成员:
    • f_op:指向字符设备对象操作结构体。
    • 拔高:了解内核实现原理:应用通过系统调用号找到内核函数sys_open,sys_open创建一个file对象,然后将之前通过inode找到的字符设备对象中的操作接口对象的地址给了file.f_op,最终sys_open返回一个fd,并且内核将fd和创建file建立亲戚关系。
    • 将来其余系统调用函数:read/write/ioctl/close等访问都是通过fd找到相应的执行函数。找到驱动的流程:
      • read(fd)->找到file->可以访问file.f_op->访问驱动函数->xxx_read

案例:编写LED字符设备驱动,利用ioctl开关灯,此时将四个LED作为四个硬件个体。

分析思路:
  • 四个LED灯物理特性一致,所以只需要一个驱动。
  • 一个驱动主设备号一个
  • 四个硬件四个次设备号:0/1/2/3
  • 四个硬件设备文件四个:myled0,myled1,myled2,myled3.
  • 四个设备文件四个inode:inode0, inode1, inode2, inode3
  • 如果应用程序将四个灯全部打开:fd设备文件描述符:file0, file1, file2, file3
  • 终极对应关系:
    • fd0->file0->inode0->i_rdev0->主设备号A,次设备号0。
    • fd1->file1->inode1->i_rdev1->主设备号A,次设备号1。
    • fd2->file2->inode2->i_rdev2->主设备号A,次设备号2。
    • fd3->file3->inode3->i_rdev3->主设备号A,次设备号3。
  • cdev对象:一个led_cdev。
  • 操作接口对象:一个led_fops,操作接口共用。
    • 问:共用的操作接口函数如区分某个硬件设备呢?
    • 答:通过文件描述符下的设备文件。
long led_ioctl(struct file *file, cmd, buf)
{
    struct inode *inode = file->f_path.dentry->d_inode;
    
    int minor = MINOR(inode->i_rdev);
    led_info(minor);
}
代码实现:
  • led_test.c
#include 
#include 
#include 
#include 

//自定义两个命令
#define LED_ON  0x100001
#define LED_OFF 0x100002

//声明描述led操作信息的数据结构
struct led_event {
    int cmd;//开灯命令,1-开灯,0-关灯。
    int index; //灯编号
};
//声明描述led等状态的数据结构
struct led_state{
    int index;//灯编号。
    int state;//灯状态,1-关灯,0-开灯。
};


int main()
{
    int fd0, fd1, fd2, fd3;
    int index;
    struct led_event led;//分配用户缓冲区,保存灯的操作信息。
    struct led_state ledst;//分配用户缓冲区,保存灯的状态。
    if(argc != 3){
        printf("usage: %s  <1|2|3|4> \n", argv[0]);
        return -1;
    }
        
    fd0 = open("/dev/myled0", O_RDWR);
    if(fd0 < 0)
        return -1;
    
    fd1 = open("/dev/myled1", O_RDWR);
    if(fd1 < 0)
        return -1;
    fd2 = open("/dev/myled2", O_RDWR);
    if(fd2 < 0)
        return -1;
    fd3 = open("/dev/myled3", O_RDWR);
    if(fd3 < 0)
        return -1;
    
    while(1){
        ioctl(fd0, LED_ON);
        sleep(1);
        ioctl(fd1, LED_ON);
        sleep(1);
        ioctl(fd2, LED_ON);
        sleep(1);
        ioctl(fd3, LED_ON);
        sleep(1);
        
        ioctl(fd0, LED_OFF);
        sleep(1);
        ioctl(fd1, LED_OFF);
        sleep(1);
        ioctl(fd2, LED_OFF);
        sleep(1);
        ioctl(fd3, LED_OFF);
        sleep(1);
    }
            
    close(fd0);
    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}

  • led_drv.c

//自定义两个命令
#define LED_ON  0x100001
#define LED_OFF 0x100002

//控制操作接口
static long led_ioctl(struct file *file, unsigned long cmd, unsigned long buf)
{
    int i;
    //通过file找到对应inode
    struct inode *inode = file->f_path.dentry->d_inode;
    
    int minor = MINOR(inode->i_rdev);
    //分配内核缓冲区
    
    switch(cmd){
        case LED_ON:
                gpio_set_value(led_info[minor].gpio, 0);
                printk("%s : turn on %d led. \n", __func__, minor + 1);
            break;
        case LED_OFF:
                gpio_set_value(led_info[minor].gpio, 1);
                printk("%s : turn off %d led. \n", __func__, minor + 1);
            break;
        default:
            printk("command is invalid\n");
            return -1;
    }
    retrun 0;
}


//定义初始化LED硬件操作接口对象
//共用
static struct file_operations led_fops = {
    .unlocked_ioctl = led_ioctl;
};

//定义设备号对象
static dev_t dev;

//定义字符设备对象
static struct cdev led_cdev;

static int led_init(void)
{
    int i;
    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);
    }
    //申请设备号
    //次设备号0,1,2,3
    alloc_chrdev_region(&dev, 0, 4, "myled");
    cdev_init(&led_cdev, &led_fops);
    cdev_add(&led_cdev, dev, 4);
    
    return 0;
}

static void led_exit(void)
{
    int i;
    cdev_del(&led_cdev);
    unregister_chrdev_region(dev, 4);
    for(i = 0; i < ARRAY_SIZE(led_info); i++){
        gpio_direction_output(led_info[i].gpio, 1);
        gpio_free(led_info[i].gpio);
    }
}

module_init(led_init);
module_exit(led_exit);
MODULE_LINCESE("GPL");

字符设备文件自动创建只需要三个保证+四个函数即可完成

  • 保证根文件系统rootfs必要脚本文件rcS中添加以下两句话:

  • mount -a:就是为了执行fstab文件。

  • echo /sbin/mdev > /proc/sys/kernel/hotplug

    • 说明:表面看是向文件hotplug写入字符串“/sbin/mdev”,本质是将来驱动要创建设备文件时,驱动自动解析hotplug文件,找到/sbin/mdev,并且执行此命令mdev,让mdev命令来帮驱动创建设备文件。
  • 保证根文件系统rootfs必须有mdev命令,执行在下位机执行:which is mdev查看是否存在即可。

  • 保证根文件系统rootfs必要配置文件fstab中必须有以下两句话:

    • proc /proc proc defaults 0 0
    • sysfs /sys sysfs defaults 0 0
    • 结论:将proc虚拟文件系统挂接到/proc目录下,将sysfs虚拟文件系统挂接到/sys目录下。
四个函数:给mdev提供参数数据。
  • class_create
  • device_create
  • device_destroy
  • class_destroy
struct class *cls; //创建一个设备类指针,类似苹果上长一个嫩芽。
//cls指向创建的设备对象。
cls = class_create(THIS_MODULE, "tarena1");

//正式创建设备文件,本质是将来调用mdev来创建设备文件。
//dev:设备文件的设备号,myled:设备文件名
device_create(cls, NULL, dev, NULL, "myled");

//删除设备文件
device_destroy(cls, dev);

//删除设备对象。
class_destroy(cls);

案例:
  • led_drv.c

//自定义两个命令
#define LED_ON  0x100001
#define LED_OFF 0x100002

//控制操作接口
static long led_ioctl(struct file *file, unsigned long cmd, unsigned long buf)
{
    int i;
    //通过file找到对应inode
    struct inode *inode = file->f_path.dentry->d_inode;
    
    int minor = MINOR(inode->i_rdev);
    //分配内核缓冲区
    
    switch(cmd){
        case LED_ON:
                gpio_set_value(led_info[minor].gpio, 0);
                printk("%s : turn on %d led. \n", __func__, minor + 1);
            break;
        case LED_OFF:
                gpio_set_value(led_info[minor].gpio, 1);
                printk("%s : turn off %d led. \n", __func__, minor + 1);
            break;
        default:
            printk("command is invalid\n");
            return -1;
    }
    retrun 0;
}


//定义初始化LED硬件操作接口对象
//共用
static struct file_operations led_fops = {
    .unlocked_ioctl = led_ioctl;
};

//定义设备号对象
static dev_t dev;

//定义字符设备对象
static struct cdev led_cdev;
struct class *cls; //创建一个设备类指针,类似苹果上长一个

static int led_init(void)
{
    int i;
    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);
    }
    //申请设备号
    //次设备号0,1,2,3
    alloc_chrdev_region(&dev, 0, 4, "myled");
    cdev_init(&led_cdev, &led_fops);
    cdev_add(&led_cdev, dev, 4);
    
    //5.创建设备类对象
    cls = class_create(THIS_MODULE, "tarena1");
    
    //6、自动创建设备文件
    device_create(cls, NULL,  \
                MKDEV(MAJOR(dev), 0), NULL, "myled0");
    device_create(cls, NULL,  \
                MKDEV(MAJOR(dev), 1), NULL, "myled1");
    device_create(cls, NULL,  \
                MKDEV(MAJOR(dev), 2), NULL, "myled2");
    device_create(cls, NULL,  \
                MKDEV(MAJOR(dev), 3), NULL, "myled3");
    return 0;
}

static void led_exit(void)
{
    int i;
    cdev_del(&led_cdev);
    unregister_chrdev_region(dev, 4);
    for(i = 0; i < ARRAY_SIZE(led_info); i++){
        gpio_direction_output(led_info[i].gpio, 1);
        gpio_free(led_info[i].gpio);
    }
    
    //删除设备文件,
    device_destroy(cls, \
                MKDEV(MAJOR(dev), 0), 0);
    device_destroy(cls, \
                MKDEV(MAJOR(dev), 1), 1);
    device_destroy(cls, \
                MKDEV(MAJOR(dev), 2), 2);
    device_destroy(cls, \
                MKDEV(MAJOR(dev), 3), 3);
                
    //删除设备类对象
    class_destroy(cls);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LINCESE("GPL");

你可能感兴趣的:(嵌入式相关基础知识)