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

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

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 <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#inlcude <mach/platform.h>
#include <linux/cdev.h> // struct cdev
#include <linux/fs.h> //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 <ARRAY_SIZE(led_info); i++)
    {
        gpio_set_value(led_info[i].gpio, 0);
        printk("%s:打开第%d个灯.\n", __func__, i+1);
    }
    return 0;//执行成功返回0
}
//关闭设备接口:
static int led_close(struct inode *, struct file *)
{
    //关灯
    int i;
    for(i = 0; i <ARRAY_SIZE(led_info); i++)
    {
        gpio_set_value(led_info[i].gpio, 1);
        printk("%s:关闭第%d个灯.\n", __func__, i+1);
    }
    return 0;//执行成功返回0
}
//定义初始化硬件操作接口对象
static struct file_operations led_fops = {
    .open = led_open, //打开设备
    .release = led_close //关闭设备
};
//定义字符设备对象
static struct cdev led_cdev;
//定义设备号对象
static dev_t dev;
static int led_init(void)
{
    int i;
    //1、申请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, "myled");
    printk("major: %d, minor: %d\n", MAJOR(dev), MINOR(dev));
    //3、初始化字符设备对象,添加硬件操作接口
    cdev_init(&led_cdev, &led_fops);
    //4、向内核的大数组中注册字符设备对象
    //至此,内核就有了一个真实的字符设备驱动,等待应用调用。
    cdev_add(&led_cdev, dev, 1);
    return 0;
}
static void led_exit(void)
{
    //1、从内核大数组中卸载字符设备对象
    cdev_del(&led_cdev);
    //2、释放设备号
    unregister_chrdev_region(dev, 1);
    //3、输出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);
    }
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");


  • led_test.c


#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
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 <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    int fd;
    int cmd = 0;
    if(argc != 2){
        printf("usage : %s <on|off>\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 <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <mach/platform.h>
//声明描述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 <fcntl.h>
//声明描述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 <on|off> <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;
}
相关文章
|
6天前
|
消息中间件 存储 缓存
【嵌入式软件工程师面经】Linux系统编程(线程进程)
【嵌入式软件工程师面经】Linux系统编程(线程进程)
20 1
|
6天前
|
网络协议 算法 Linux
【嵌入式软件工程师面经】Linux网络编程Socket
【嵌入式软件工程师面经】Linux网络编程Socket
22 1
|
6天前
|
消息中间件 安全 Java
【嵌入式软件工程师面经】Linux多进程与多线程
【嵌入式软件工程师面经】Linux多进程与多线程
8 1
|
6天前
|
存储 缓存 Unix
【嵌入式软件工程师面经】Linux文件IO
【嵌入式软件工程师面经】Linux文件IO
12 1
|
29天前
|
数据处理 编译器 数据库
x64 和 arm64 处理器架构的区别
x64 和 arm64 处理器架构的区别
639 0
【各种问题处理】X86架构和ARM架构的区别
【1月更文挑战第13天】【各种问题处理】X86架构和ARM架构的区别
|
29天前
|
缓存 API Android开发
一起学点ARM的微架构二?
一起学点ARM的微架构二?
94 1
|
8天前
|
Ubuntu Windows
ubuntu 安装vnc_vnc4server arm架构
ubuntu 安装vnc_vnc4server arm架构
|
20天前
|
弹性计算 编解码 运维
飞天技术沙龙回顾:业务创新新选择,倚天Arm架构深入探讨
基于「倚天710自研芯片+CIPU云原生基础设施处理器」组合的倚天ECS实例为解决算力挑战提供新思路。
|
21天前
|
弹性计算 编解码 运维
飞天技术沙龙回顾:业务创新新选择,倚天Arm架构深入探讨
阿里云、平头哥与Arm联合举办的飞天技术沙龙在上海举行,聚焦Arm Neoverse核心优势和倚天710计算实例在大数据、视频领域的应用。活动中,专家解读了倚天710的性能提升和成本效益,强调了CIPU云原生基础设施处理器的角色,以及如何通过软件优化实现资源池化和稳定性平衡。实例展示在视频编码和大数据处理上的性能提升分别达到80%和70%的性价比优化。沙龙吸引众多企业代表参与,促进技术交流与实践解决方案的探讨。
飞天技术沙龙回顾:业务创新新选择,倚天Arm架构深入探讨