linux下的驱动开发

2019-05-30 14:38:37 mcsbary 阅读数 6966

驱动模型

最近开始开发驱动,现总结通用驱动开发模型如下
驱动整体模型:
在这里插入图片描述
添加一个设备,多数需要用户空间下发指令等操作。那么有两个问题:

  1. kernel如何控制设备
  2. 用户空间如何和kernel中的驱动交互

问题1:
kernel中有各种总线,设备挂载在总线上,驱动通过kernel总线提供的接口初始化控制设备。
问题2:
kernel中提供文件设备驱动,在驱动中增加一个文件设备,如字符设备、proc、sys等文件设备。

基于以上两个问题,驱动包含两部分
在这里插入图片描述

开发设备驱动

系统端驱动开发步骤

1、阅读设备相关的规格书、demo
2、确定设备挂载总线、文件交互设备
3、参照demo,编写驱动代码,代码包含两部分:设备树添加结点、逻辑代码

注意: 设备树结点中的字段可以是标准的(内核已有代码解析),也可以包含自定义的(设备驱动逻辑代码解析)。

设备端基于单片机驱动开发

设备端硬件形态有两种:
1、设备端有一个独立的小系统,通过一个单片机运行
2、设备端仅由电子原件和电路图实现

形态1:
一般情况下,该部分代码,设备厂商已提供,无需系统端负责开发。系统端通过总线和设备进行交互。

形态2:
设备端上电后,驱动实现初始化,控制设备端寄存器,配置设备以满足对设备功能、数据的需求

应用场景

以上描述的驱动开发架构模式,常用于应用级驱动开发。一些控制器类型,如i2c控制器、spi总线控制器等kernel中框架层的,一般不需要文件系统设备的存在,只需要开发设备驱动即可;为debug方便,一般也会搭配文件系统设备,方便命令行查看设备状态。

在开发中,可以根据实际需求决定。

DEMO

需求描述

给一个i2c编写驱动,i2c提供一些接口给userspace使用

添加设备树结点
// SoC上的i2c控制器的地址
i2c@138B0000 {
	#address-cells = <1>;
    #size-cells = <0>;
    samsung,i2c-sda-delay = <100>;
    samsung,i2c-max-bus-freq = <20000>;
    pinctrl-0 =<&i2c5_bus>;
    pinctrl-names="default";
    // 这个一定要okay,其实是对"./arch/arm/boot/dts/exynos4.dtsi +387"处的status = "disabled"的重写,
    // 相同的节点的不同属性信息都会被合并,相同节点的相同的属性会被重写
    status="okay";
    // 设备子节点,/表示板子,它的子节点node1表示SoC上的某个控制器,
    // 控制器中的子节点node2表示挂接在这个控制器上的设备(们)。68即是设备地址。
    // 父结点是一个i2c总线,在此处定义i2c设备,设备i2c客户端自动和总线关联;
    // 否则,多个总线,设备i2c客户端驱动如何和总线关联?(待学习了解)
    mpu6050@68{
    	// 这个属性就是我们和驱动匹配的钥匙,一个字符都不能错
        compatible="invensense,mpu6050";
        // 这个属性是从设备的地址,我们可以通过查阅手册"MPU-6050_DataSheet_V3_4"得到
        reg=<0x68>;
    };
};
驱动实现
//mpu6050_common.h
#define MPU6050_MAGIC 'K'

union mpu6050_data
{
    struct {
        short x;
        short y;
        short z;
    }accel;
    struct {
        short x;
        short y;
        short z;
    }gyro;
    unsigned short temp;
};

#define GET_ACCEL _IOR(MPU6050_MAGIC, 0, union mpu6050_data)
#define GET_GYRO  _IOR(MPU6050_MAGIC, 1, union mpu6050_data) 
#define GET_TEMP  _IOR(MPU6050_MAGIC, 2, union mpu6050_data)
//mpu6050_drv.h

#define SMPLRT_DIV      0x19    //陀螺仪采样率,典型值:0x07(125Hz)
#define CONFIG          0x1A    //低通滤波频率,典型值:0x06(5Hz)
#define GYRO_CONFIG     0x1B    //陀螺仪自检及测量范围,典型值:0x18(不自检,2000deg/s)
#define ACCEL_CONFIG        0x1C    //加速计自检、测量范围及高通滤波,典型值:0x18(不自检,2G,5Hz)
#define ACCEL_XOUT_H        0x3B
#define ACCEL_XOUT_L        0x3C
#define ACCEL_YOUT_H        0x3D
#define ACCEL_YOUT_L        0x3E
#define ACCEL_ZOUT_H        0x3F
#define ACCEL_ZOUT_L        0x40
#define TEMP_OUT_H      0x41
#define TEMP_OUT_L      0x42
#define GYRO_XOUT_H     0x43
#define GYRO_XOUT_L     0x44
#define GYRO_YOUT_H     0x45
#define GYRO_YOUT_L     0x46
#define GYRO_ZOUT_H     0x47    //陀螺仪z轴角速度数据寄存器(高位)
#define GYRO_ZOUT_L     0x48    //陀螺仪z轴角速度数据寄存器(低位)
#define PWR_MGMT_1      0x6B    //电源管理,典型值:0x00(正常启用)
#define WHO_AM_I        0x75    //IIC地址寄存器(默认数值0x68,只读)
#define SlaveAddress        0x68    //MPU6050-I2C地址寄存器
#define W_FLG           0
#define R_FLG           1
//mpu6050.c
struct mpu6050_pri {
    struct cdev dev;
    struct i2c_client *client;
};
struct mpu6050_pri dev;
static void mpu6050_write_byte(struct i2c_client *client,const unsigned char reg,const unsigned char val)
{ 
    char txbuf[2] = {reg,val};
    struct i2c_msg msg[2] = {
        [0] = {
            .addr = client->addr,
            .flags= W_FLG,
            .len = sizeof(txbuf),
            .buf = txbuf,
        },
    };
    i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
}
static char mpu6050_read_byte(struct i2c_client *client,const unsigned char reg)
{
    char txbuf[1] = {reg};
    char rxbuf[1] = {0};
    struct i2c_msg msg[2] = {
        [0] = {
            .addr = client->addr,
            .flags = W_FLG,
            .len = sizeof(txbuf),
            .buf = txbuf,
        },
        [1] = {
            .addr = client->addr,
            .flags = I2C_M_RD,
            .len = sizeof(rxbuf),
            .buf = rxbuf,
        },
    };

    i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
    return rxbuf[0];
}
static int dev_open(struct inode *ip, struct file *fp)
{
    return 0;
}
static int dev_release(struct inode *ip, struct file *fp)
{
    return 0;
}
static long dev_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
{
    int res = 0;
    union mpu6050_data data = {{0}};
    switch(cmd){
    case GET_ACCEL:
        data.accel.x = mpu6050_read_byte(dev.client,ACCEL_XOUT_L);
        data.accel.x|= mpu6050_read_byte(dev.client,ACCEL_XOUT_H)<<8;
        data.accel.y = mpu6050_read_byte(dev.client,ACCEL_YOUT_L);
        data.accel.y|= mpu6050_read_byte(dev.client,ACCEL_YOUT_H)<<8;
        data.accel.z = mpu6050_read_byte(dev.client,ACCEL_ZOUT_L);
        data.accel.z|= mpu6050_read_byte(dev.client,ACCEL_ZOUT_H)<<8;
        break;
    case GET_GYRO:
        data.gyro.x = mpu6050_read_byte(dev.client,GYRO_XOUT_L);
        data.gyro.x|= mpu6050_read_byte(dev.client,GYRO_XOUT_H)<<8;
        data.gyro.y = mpu6050_read_byte(dev.client,GYRO_YOUT_L);
        data.gyro.y|= mpu6050_read_byte(dev.client,GYRO_YOUT_H)<<8;
        data.gyro.z = mpu6050_read_byte(dev.client,GYRO_ZOUT_L);
        data.gyro.z|= mpu6050_read_byte(dev.client,GYRO_ZOUT_H)<<8;
        printk("gyro:x %d, y:%d, z:%d\n",data.gyro.x,data.gyro.y,data.gyro.z);
        break;
    case GET_TEMP:
        data.temp = mpu6050_read_byte(dev.client,TEMP_OUT_L);
        data.temp|= mpu6050_read_byte(dev.client,TEMP_OUT_H)<<8;
        printk("temp: %d\n",data.temp);
        break;
    default:
        printk(KERN_INFO "invalid cmd");
        break;
    }
    printk("acc:x %d, y:%d, z:%d\n",data.accel.x,data.accel.y,data.accel.z);
    res = copy_to_user((void *)arg,&data,sizeof(data));
    return sizeof(data);
}

// 初始化文件系统设备操作接口
struct file_operations fops = {
    .open = dev_open,
    .release = dev_release,
    .unlocked_ioctl = dev_ioctl, 
};

#define DEV_CNT 1
#define DEV_MI 0
#define DEV_MAME "mpu6050"

struct class *cls;
dev_t dev_no ;

static void mpu6050_init(struct i2c_client *client)
{
    mpu6050_write_byte(client, PWR_MGMT_1, 0x00);
    mpu6050_write_byte(client, SMPLRT_DIV, 0x07);
    mpu6050_write_byte(client, CONFIG, 0x06);
    mpu6050_write_byte(client, GYRO_CONFIG, 0x18);
    mpu6050_write_byte(client, ACCEL_CONFIG, 0x0);
}
static int mpu6050_probe(struct i2c_client * client, const struct i2c_device_id * id)
{
    dev.client = client;
    printk(KERN_INFO "xj_match ok\n");
    // 初始化设备文件系统
    cdev_init(&dev.dev,&fops);    
    alloc_chrdev_region(&dev_no,DEV_MI,DEV_CNT,DEV_MAME);    
    cdev_add(&dev.dev,dev_no,DEV_CNT);
    
    // 设备初始化
    mpu6050_init(client);

    /*自动创建设备文件*/
    cls = class_create(THIS_MODULE,DEV_MAME);
    device_create(cls,NULL,dev_no,NULL,"%s%d",DEV_MAME,DEV_MI);
    
    printk(KERN_INFO "probe\n");
    
    return 0;
}

static int mpu6050_remove(struct i2c_client * client)
{
    device_destroy(cls,dev_no);
    class_destroy(cls);
    unregister_chrdev_region(dev_no,DEV_CNT);
    return 0;
}

struct of_device_id mpu6050_dt_match[] = {
    {.compatible = "invensense,mpu6050"},
    {},
};

// 设备驱动注册到总线
struct i2c_device_id mpu6050_dev_match[] = {};
struct i2c_driver mpu6050_driver = {
    .probe = mpu6050_probe,
    .remove = mpu6050_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "mpu6050drv",
        .of_match_table = of_match_ptr(mpu6050_dt_match), 
    },
    .id_table = mpu6050_dev_match,
};
module_i2c_driver(mpu6050_driver);
MODULE_LICENSE("GPL");

在代码实现中把文件系统设备实现和设备驱动实现混合在一起,个人加以分开实现,利于代码重用和设备驱动替换和多方案切换。

验证

通过上面的驱动, 我们可以在应用层操作设备文件从mpu6050寄存器中读取原始数据, 应用层如下

int main(int argc, char * const argv[])
{
    int fd = open(argv[1],O_RDWR);
    if(-1== fd){
        perror("open");
        return -1;
    }
    union mpu6050_data data = {{0}};
    while(1){
        ioctl(fd,GET_ACCEL,&data);
        printf("acc:x %d, y:%d, z:%d\n",data.accel.x,data.accel.y,data.accel.z);
        ioctl(fd,GET_GYRO,&data);
        printf("gyro:x %d, y:%d, z:%d\n",data.gyro.x,data.gyro.y,data.gyro.z);
        ioctl(fd,GET_TEMP,&data);
        printf("temp: %d\n",data.temp);
        sleep(1);
    }
    return 0;
}

最终可以获取传感器的原始数据如下
在这里插入图片描述
说明: 以上demo是借鉴 https://www.cnblogs.com/xiaojiang1025/p/6500540.html 的,在实际开发中个人也有实现,代码不在写该篇博客的电脑中,就借用了大神代码。

2014-06-30 20:22:54 21cnbao 阅读数 49434

本博实时更新《Linux设备驱动开发详解(第3版)》的最新进展。 目前已经完成稿件。

2015年8月9日,china-pub开始上线预售:

http://product.china-pub.com/4733972

2015年8月20日,各路朋友报喜说已经拿到了书。

本书已经rebase到开发中的Linux 4.0内核,案例多数基于多核CORTEX-A9平台

本书微信公众号二维码, 如需联系请扫描下方二维码

[F]是修正或升级;[N]是新增知识点;[D]是删除的内容

 

第1章 《Linux设备驱动概述及开发环境构建》
[D]删除关于LDD6410开发板的介绍
[F]更新新的Ubuntu虚拟机
[N]添加关于QEMU模拟vexpress板的描述

 

第2章 《驱动设计的硬件基础》

[N]增加关于SoC的介绍;
[N]增加关于eFuse的内容;
[D]删除ISA总线的内容了;
[N]增加关于SPI总线的介绍;
[N]增加USB 3.0的介绍;
[F]修正USB同步传输方式英文名;
[D]删除关于cPCI介绍;
[N]增加关于PCI Express介绍;
[N]增加关于Xilinx ZYNQ的介绍;
[N]增加SD/SDIO/eMMC的章节;
[D]删除“原理图分析的内容”一节;
[N]增加通过逻辑分析仪看I2C总线的例子;

 

第3章 《Linux内核及内核编程》

[N]新增关于3.X内核版本和2015年2月23日 Linux 4.0-rc1
[N]新增关于内核版本升级流程以及Linux社区开发模式讲解
[N]新增关于Linux内核调度时间的图式讲解
[N]新增关于Linux 3.0后ARM架构的变更的讲解
[N]新增关于TASK_KILLABLE状态的简介
[N]新增Linux内存管理图式讲解

[F]修正Kconfig和Makefile中的一些表述
[D]删除关于x86启动过程讲解
[N]新增ARM Linux启动过程讲解
[N]新增关于likely()和unlikely()讲解
[N]新增toolchain的讲解,以及toolchain的几种浮点模式


第4章 《Linux内核模块》
[F]改正关于模块使用非GPL license的问题;
[F]修正关于__exit修饰函数的内存管理

第5章 《Linux文件系统与设备文件》
[F]修正关于文件系统与块设备驱动关系图;
[N]增加应用到驱动的file操作调用图;
[N]增加通过netlink接受内核uevent的范例;
[N]增加遍历sysfs的范例;

[N]增加为kingston U盘编写udev规则的范例;
[F]更新udev规则,以符合新版本;
[N]增加udevadm的讲解;
[N]高亮Android vold
 
第6章 《字符设备驱动》
[F]更新file_operations的定义,升级ioctl()原型;
[N]增加关于Linux access_ok()的讲解以及Linux内核安全漏洞的说明;
[F]修正globalmem的编码风格;
[F]在globalmem支持2个以上实例的时候,从直接2个实例,升级为支持N个实例;

第7章 《Linux设备驱动中的并发控制》
[N]绘图深入讲解单核和多核下的各种竞态;
[N]增加关于编译乱序,执行乱序,编译屏障和内存屏障的讲解;
[N]增加关于ARM LDREX/STREX指令的讲解;
[N]对spin_lock单核和多核的使用场景进行深入分析;

[F]重新整理RCU的讲解方法和实例;
[F]明确指明信号量已过时;
[F]将globalmem中使用的信号量换为mutex。


第8章 《Linux设备驱动中的阻塞与非阻塞I/O》
[N]新增阻塞和非组塞的时序图
[F]修正关于等待队列头部、等待队列元素的一些中文说法
[N]添加等待队列的图形描述
[F]修正globalfifo的编码风格
[F]修正globalfifo可读可写的if判断为while判断
[N]新增select的时序图
[N]新增EPOLL的章节


第9章 《Linux设备驱动中的异步通知与异步I/O》
[F]修正关于glibc AIO支持
[F]修正关于内核AIO支持
[F]修正驱动AIO接口
[D]删除关于驱动AIO支持的错误实例
[N]高亮C10问题

第10章 《中断与时钟》
[N]增加关于ARM GIC的讲解
[N]增加关于irq_set_affinity() API的讲解
[N]增加关于devm_request_irq() API的讲解
[N]增加关于request_any_context_irq() API的讲解

[F]修正interrupt handler原型
[F]修正work queue原型
[N]新增关于Concurrency-managed workqueues讲解
[N]增加关于ksoftirqd讲解
[N]增加关于request_threaded_irq()讲解
[D]删除s3c6410 rtc驱动中断实例
[N]新增GPIO按键驱动中断实例
[N]新增hrtimer讲解和实例
[F]修正second设备编码风格

第11章 《内存与I/O访问》
[F]修正关于页表级数的描述,添加PUD
[F]修正page table walk的案例,使用ARM Linux pin_page_for_write
[N]新增关于ARM Linux内核空间虚拟地址分布
[F]修正关于内核空间与用户空间界限
[N]新增关于DMA、NORMAL和HIGHMEM ZONE的几种可能分布
[N]新增关于buddy的介绍
[F]修正关于用户空间malloc的讲解
[N]增加mallopt()的案例
[N]增加关于devm_ioremap、devm_request_region()和devm_request_mem_region()的讲解
[N]增加关于readl()与readl_relaxed()的区别,writel()与writel_relaxed()的区别

[F]更新vm_area_struct的定义
[F]修正nopage() callback为fault() callback
[N]增加io_remap_pfn_range()、vm_iomap_memory()讲解
[F]强调iotable_init()静态映射目前已不太推荐
[N]增加关于coherent_dma_mask的讲解
[N]讲解dma_alloc_coherent()与DMA ZONE关系
[N]提及了一致性DMA缓冲区与CMA的关系
[N]增加关于dmaengine驱动和API的讲解

 

第12章 《工程中的Linux设备驱动》
[F]更名为《Linux设备驱动的软件架构思想》;
[N]本章新增多幅图片讲解Linux设备驱动模型;
[N]新增内容详细剖析为什么要进行设备与驱动的分离,platform的意义;
[N]新增内容详细剖析为什么Linux设备驱动要分层,枚举多个分层实例;
[N]新增内容详细剖析Linux驱动框架如何解耦,为什么主机侧驱动要和外设侧驱动分离;
[N]DM9000实例新增关于在dts中填充平台信息的内容;
[N]新增内容讲解驱动核心层的3大功能;
[N]新增内容以面向对象类泛化对比Linux驱动;
[N]SPI案例部分新增通过dts填充外设信息;

[F]从tty, LCD章节移出架构部分到本章

 

第13章 《Linux块设备驱动》
[N]介绍关于block_device_operations的check_events成员函数
[N]添加关于磁盘文件系统,I/O调度关系的图形
[F]更新关于request_queue、request、bio、segment关系的图形
[F]淘汰elv_next_request
[F]淘汰blkdev_dequeue_request
[N]添加关于blk_start_request描述
[F]淘汰Anticipatory I/O scheduler
[N]添加关于ZRAM块设备驱动实例
[F]更新针对内核4.0-rc1的vmem_disk
[N]添加关于vmem_disk处理I/O过程的图形
[N]增加关于Linux MMC子系统的描述

 

第14章 《Linux终端设备驱动》
[D]整章全部删除,部分架构内容前移到第12章作为驱动分层实例

 

第15章 《Linux I2C核心、总线与设备驱动》

[F]修正i2c_adpater驱动的案例
[N]增加关于在device tree中增加i2c设备的方法的描述

 

第16章 《Linux网络设备驱动》

[F]本章顺序从第16章前移到第14章
[N]澄清sk_buff head、data、tail、end指针关系
[F]更新sk_buff定义
[F]澄清skb_put、skb_push、skb_reserve
[N]增加netdev_priv的讲解,加入实例
[N]增加关于get_stats()可以从硬件读统计信息的描述

[F]修正关于net_device_stats结构体的定义位置
[F]修正关于统计信息的更新方法

 

第17章 《Linux音频设备驱动》

[D] 本章直接删除

 

第18章 《LCD设备驱动》

 

[D] 本章直接删除,部分架构内容前移到第12章

 

第19章 《Flash设备驱动》

[D] 本章直接删除

 

第20章 《USB主机与设备驱动》
[F]前移到第16章;
[F]更名为《USB主机、设备与Gadget驱动》;
[N]增加关于xHCI的介绍;
[F]修正usb gadget驱动为function驱动;
[D]删除OHCI实例;
[N]添加EHCI讲解和Chipidea EHCI实例;
[F]修正iso传输实例;
[F]修正usb devices信息到/sys/kernel/debug/usb/devices
[N]介绍module_usb_driver;
[N]介绍usb_function;
[N]介绍usb_ep_autoconfig;
[N]介绍usb_otg;

[D]删除otg_transceiver;

 

第21章 《PCI设备驱动》
[D]整章删除

 

第22章 《Linux设备驱动的调试》

[F]变为第21章;

[D]把实验室环境建设相关的节移到第3章;

[F]修正关于gdb的set step-mode的含义讲解;

[F]增加关于gdb的set命令的讲解;

[F]增加gdb call命令的案例

[D/N]删除手动编译工具链的代码,使用crosstool-ng;
[N]更新toolchain的下载地址(codesourcery -> memtor),加入linaro下载地址;
[N]增加pr_fmt的讲解;
[N]增加关于ignore_loglevel bootargs的讲解;
[N]增加EARLY_PRINTK和DEBUG_LL的讲解;

[F]调整proc的范例,创建/proc/test_dir/test_rw;
[N]修正关于3.10后内核proc实现框架和API的变更;
[N]增加关于BUG_ON和WARN_ON的讲解
[F]不再以BDI-2000为例,改为ARM DS-5;
[N]增加关于ARM streamline性能分析器的介绍;
[N]增加使用qemu调试linux内核的案例;
[F]调整Oops的例子,使用globalmem,平台改为ARM;
[F]更新LTT为LTTng。

 

第23章 《Linux设备驱动的移植》
[D]整章删除

 

全新的章节

 

第17章  《I2C、SPI、USB架构类比》

 

本章导读

本章类比I2C、SPI、USB的结构,从而更进一步帮助读者理解本书第12章的内容,也进一步实证Linux驱动万变不离其宗的道理。

 

第18章  《Linux设备树(Device Tree)》

 

本章导读

本章将介绍Linux设备树(Device Tree)的起源、结构和因为设备树而引起的驱动和BSP变更。

18.1节阐明了ARM Linux为什么要采用设备树。

18.2节详细剖析了设备树的结构、结点和属性,设备树的编译方法以及如何用设备树来描述板上的设备、设备的地址、设备的中断号、时钟等信息。

18.3节讲解了采用设备树后,驱动和BSP的代码需要怎么改,哪些地方变了。

18.4节补充了一些设备树相关的API定义以及用法。

本章是相对《Linux设备驱动开发详解(第2版)》全新的一章内容,也是步入内核3.x时代后,嵌入式Linux工程师必备的知识体系。

 

 

 

第19章  《Linux的电源管理》

 

本章导读

Linux在消费电子领域的应用已经铺天盖地,而对于消费电子产品而言,省电是一个重要的议题。

本章将介绍Linux设备树(Device Tree)的起源、结构和因为设备树而引起的驱动和BSP变更。

19.1节阐述了Linux电源管理的总体架构。

19.2~19.8节分别论述了CPUFreq、CPUIdle、CPU热插拔以及底层的基础设施Regulator、OPP以及电源管理的调试工具PowerTop。

19.9节讲解了系统Suspend to RAM的过程以及设备驱动如何提供对Suspend to RAM的支持。

19.10节讲解了设备驱动的Runtimesuspend。

本章是相对《Linux设备驱动开发详解(第2版)》全新的一章内容,也是Linux设备驱动工程师必备的知识体系。

 

 

 

第20章 《Linux芯片级移植与底层驱动》

 

本章导读

本章主要讲解,在一个新的ARM SoC上,如何移植Linux。当然,本章的内容也适合MIPS、PowerPC等其他的体系架构。

第20.1节先总体上介绍了Linux 3.x之后的内核在底层BSP上进行了哪些优化。

第20.2节讲解了如何提供操作系统的运行节拍。

第20.3节讲解了中断控制器驱动,以及它是如何为驱动提供标准接口的。

第20.4节讲解多核SMP芯片的启动。

第20.6~20.9节分别讲解了作为Linux运行底层基础设施的GPIO、pinctrl、clock和dmaengine驱动。

本章相对《Linux设备驱动开发详解(第2版)》几乎是全新的一章内容,有助于工程师理解驱动调用的底层API的来源,以及直接有助于进行Linux的平台移植。

 

2014-11-30 12:23:00 hwunion 阅读数 1604



IMX6技术交流群:195829497

物联网实验室:345957209

Python编程俱乐部:516307649





linux设备驱动开发,看起来是一份很高大上的职业,毕竟从事上层应用开发人员太多,而且门槛又不是特别高,而内核级开发从业人员要少得多,而且资料又较少。有许多刚刚接触到linux设备驱动开发的同仁会感觉非常困惑,面对复杂的linux内核有一种无从下手的感觉。根据自己之前积累的一些经验,今天就和大家分享一下,让刚刚步入驱动开发的同仁少走一些弯路。

1.要知道将来要做什么

学习,都是有目的性的,要么是兴趣使然,要么就是刚性需求,为了找一份好的工作。在这里先和大家聊聊做设备驱动将来可以做哪些方面。我把linux设备驱动开发工作分为两大类,一类是做BSP级的开发,另外一类是做外设驱动的开发。

BSP的开发指的是板级代码的开发,和CPU是密切相关的,例如I2C/SPI Adapter的驱动.如果使用通用的芯片,比如三星的Exynos,飞思卡尔的I.MX系列,TI的OMAP或者DaVinci系列,基本都会有现成的BSP包,这部分代码通常是芯片厂商提供和大型公司贡献。大家可以看看linux内核源码中/arch/arm/mach-omap,内部很多代码都是诺基亚贡献。做BSP级的开发需要有较深的功底,首先要十分了解CPU特性,另外要使代码有良好的扩展性和复用性,方便后续移植。有这样需求的往往是芯片商或使用专用芯片的设备商。

外设驱动开发就相对简单一些,都是和特定的外设硬件打交到。通过利用BSP级代码提供的API或者linux提供的更高层的抽象接口来操作硬件。实际上和应用层的开发大同小异。例如操作I2C总线上的EEPROM,实际上的读和写操作都有已经封装好的API来完成.而开发者需要做的是了解外设的特性,通过封装好的API对外设进行操作。新入门的开发者建议从外设驱动开发入手,循序渐进。当然,一个优秀的开发者是即可以做BSP级代码的开发,也能做外设驱动的开发的。

2.用面向对象的思想去思考

面向对象,即OO思想,大家应该非常熟悉。linux的内核虽然用面向过程的C语言实现,但是仍然是通过面向对象的思想去设计的。如果从单片机转行做linux设备驱动,会发现和单片机的或者裸机的驱动设计有很大区别。设计linux设备驱动不单单是对硬件设备的操作,更多需要考虑的是扩展性和代码的复用。所以就出现了platform device/driver,i2c device/driver,spi device/driver,抽象出了设备和驱动两部分,使设备细节和驱动分离。另外还出现了一些框架,提供了底层接口的封装,做开发时要习惯用OO思想去设计。当然要记住条条大路通罗马,不使用这些device/driver也可以实现设备驱动,只是不太推荐这样做。

3.从各驱动框架入手

linux提供了各种框架(子系统),对底层进行封装,抽象出相同操作的接口,可样可以更好的实现复用。想入门linux驱动开发,可以先从框架入手,掌握API的使用,再逐渐深入研究,从上到下去学习。不要把驱动开发想象的太复杂,实际和英语的完型填空差不多,框架有了,只需要自己去填写操作具体硬件的细节代码而已。

几个比较重要和常用的框架有:

GPIO:这个就不用多说了,刚开始接触驱动的基本会练习通过GPIO点亮LED的操作,linux封装了相关的gpio操作接口。

SPI:学会spi device/driver的用法,以及收发消息API,可以参考一些代码,基本都是相同的套路。

I2C:学会i2c device/driver的用法,和学习SPI的套路一样。

PINCTRL:非常重要的一个框架,负责CPU引脚复用,由于现在的CPU都很复杂,一个引脚支持多种复用。

V4L2:一个非常复杂的视频采集框架,具体可以参考相关的文档。驱动里面有很多例子可供参考,同时提供了模板vivi.c

Framebuffer:显示相关的框架,熟悉其中API,而且有模板skeletonfb.c。

DMA Engine: 把DMA操作进行封装,目前驱动代码中关于DMA的操作很多是使用私有的BSP包中的DMA接口,如果支持DMA Engine的话,建议使用DMA Engine。

中断:比较常用的了,接口不多,很少掌握。

USB框架:USB框架比较复杂,API较多,可以通过读已有的代码进行学习。

MTD框架:存储相关比较重要的框架,网上相关的文档很多。

设备树:设备树是在新的内核里面引进来的,可以把板级代码中的各种device通过设备树文件去描述,动态创建,这样更灵活。其实不要把设备树想象的太复杂,实际和解析JSON,XML一样,各个节点中记录设备相关的信息,提供给驱动使用。

4.选好参考资料

推荐大家参考《Essential_Linux_Device_Drivers>>,《linux device driver》,还有linux代码目录中的Documentation也非常重要,另外要学会参考现有的驱动代码。既然是参考资料,只是在用的时候去读,这样效果会更好。

2018-04-12 14:49:18 feixiaoxing 阅读数 9808

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com

 

01、linux驱动编写(入门)

02、linux驱动编写(虚拟字符设备编写)

03、linux驱动编写(字符设备编写框架)

04、linux驱动编写(Kconfig文件和Makefile文件)

05、linux驱动编写(块设备驱动代码)

06、linux驱动编写(platform总线和网卡驱动)

07、linux驱动编写(usb host驱动入门)

08、linux驱动编写(声卡驱动之asoc移植)

09、linux驱动编写(sd卡驱动)

10、linux驱动编写(摄像头驱动)

11、linux驱动编写(nandflash驱动)

12、linux驱动编写(dma驱动)

13、linux驱动编写(电源管理驱动)

14、linux驱动编写(看门狗)

15、linux驱动编写(lcd驱动)

16、linux驱动编写(触摸屏驱动)

17、linux驱动编写(pwm驱动)

18、linux驱动编写(其他的驱动代码)

 

ps:

a, drivers目录下面协议和功能交叉在一起,sound目录独立在外,这些都需要分开一下。

b,选择开发板的时候可以选择一些大牌子的开发板,比如zlg或者友善电子的板子。

c,如果是学习,那么开发的板子以性能够用为主,比如arm7、arm9。如果是性能调优,还是尽量arm性能高一些为好,比如a53、a56,甚至是a72、a73。

d,linux下面的驱动会屏蔽掉很多的硬件细节,建议可以先学习一下stm32下面各个外设的一般处理方法,再回来处理linux驱动就会达到很好的效果。

 

 

 

2018-10-09 10:09:13 qq_33611327 阅读数 2575

  本文首先描述了一个可以实际测试运行的驱动实例,然后由此去讨论Linux下驱动模板的要素,以及Linux上应用程序到驱动的执行过程。相信这样由浅入深、由具体实例到抽象理论的描述更容易初学者入手Linux驱动的大门。

一、一个简单的驱动程序实例

驱动文件hello.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>

#define    HELLO_MAJOR     231
#define    DEVICE_NAME     "HelloModule"

static int hello_open(struct inode *inode, struct file *file){
    printk(KERN_EMERG "hello open.\n");
    return 0;
}

static int hello_write(struct file *file, const char __user * buf, size_t count, loff_t *ppos){
    printk(KERN_EMERG "hello write.\n");
    return 0;
}

static struct file_operations hello_flops = {
    .owner  =   THIS_MODULE,
    .open   =   hello_open,     
    .write  =   hello_write,
};

static int __init hello_init(void){
    int ret;
    
    ret = register_chrdev(HELLO_MAJOR,DEVICE_NAME, &hello_flops);
    if (ret < 0) {
      printk(KERN_EMERG DEVICE_NAME " can't register major number.\n");
      return ret;
    }
    printk(KERN_EMERG DEVICE_NAME " initialized.\n");
    return 0;
}

static void __exit hello_exit(void){
    unregister_chrdev(HELLO_MAJOR, DEVICE_NAME);
    printk(KERN_EMERG DEVICE_NAME " removed.\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

  驱动文件主要包括函数hello_open、hello_write、hello_init、hello_exit,测试案例中并没有赋予驱动模块具有实际意义的功能,只是通过打印日志的方式告知控制台一些调试信息,这样我们就可以把握驱动程序的执行过程。

  在使用printk打印的时候,在参数中加上了“KERN_EMERG”可以确保待打印信息输出到控制台上。由于printk打印分8个等级,等级高的被打印到控制台上,而等级低的却输出到日志文件中。

编译驱动所需的Makefile

ifneq ($(KERNELRELEASE),)
MODULE_NAME = hellomodule
$(MODULE_NAME)-objs := hello.o
obj-m := $(MODULE_NAME).o
else
KERNEL_DIR = /lib/modules/`uname -r`/build
MODULEDIR := $(shell pwd)

.PHONY: modules
default: modules

modules:
    make -C $(KERNEL_DIR) M=$(MODULEDIR) modules

clean distclean:
    rm -f *.o *.mod.c .*.*.cmd *.ko
    rm -rf .tmp_versions
endif

  编译驱动文件需要一个合适的makefile,因为编译驱动的时候需要知道内核头文件,编译规则等。

测试驱动的上层应用代码hellotest.c

#include <fcntl.h>
#include <stdio.h>

int main(void)
{
    int fd;
    int val = 1;
    fd = open("/dev/hellodev", O_RDWR);
    if(fd < 0){
        printf("can't open!\n");
    }
    write(fd, &val, 4);
    return 0;
}

  上层测试案例中,首先打开设备文件,然后向设备中写入数据。如此,则会调用驱动中对应的xxx_open和xxx_write函数,通过驱动程序的打印信息可以判断是否真的如愿执行了对应的函数。

二、驱动实例测试

  测试的方法整体来说就是,编译驱动和上层测试应用;加载驱动,通过上层应用调用驱动;最后,卸载驱动。

1、编译驱动

#make

  make命令,直接调用Makefile编译hello.c,最后会生成“hellomodule.ko”。

2、编译上层应用

#gcc hellotest.c -o hellotest

  通过这条命令,就能编译出一个上层应用hellotest。

3、加载驱动

#insmod hellomodule.ko

  insmod加载驱动的时候,会调用函数hello_init(),打印的调试信息如下。

  此外,在"/proc/devices"中可以看到已经加载的模块。

4、创建节点

  虽然已经加载了驱动hellomodule.ko,而且在/proc/devices文件中也看到了已经加载的模块HelloModule,但是这个模块仍然不能被使用,因为在设备目录/dev目录下还没有它对应的设备文件。所以,需要创建一个设备节点。

#mknod /dev/hellodev c 231 0

  在/proc/devices中看到HelloModule模块的主设备号为231,创建节点的时候就是将设备文件/dev/hellodev与主设备号建立连接。这样在应用程序操作文件/dev/hellodev的时候,就会定位到模块HelloModule。

/proc/devices 与 /dev的区别

  • /proc/devices中的设备是驱动程序生成的,它可产生一个major供mknod作为参数。这个文件中的内容显示的是当前挂载在系统的模块。当加载驱动HelloModule的时候,并没有生成一个对应的设备文件来对这个设备进行抽象封装,以供上层应用访问。
  • /dev下的设备是通过mknod加上去的,用户通过此设备名来访问驱动。我以为可以将/dev下的文件看做是硬件模块的一个抽象封装,Linux下所有的设备都以文件的形式进行封装。

5、上层应用调用驱动

#./hellotest

  hellotest应用程序先打开文件“/dev/hellodev”,然后向此文件中写入一个变量val。期间会调用底层驱动中的hello_open和hello_write函数,hellotest的运行结果如下所示。

6、卸载驱动

#rmmod hellomodule

  insmod卸载驱动的时候,会调用函数hello_exit(),打印的调试信息如下。

总结一个模块的操作流程:

  (1)通过insmod命令注册module

  (2)通过mknod命令在/dev目录下建立一个设备文件"xxx",并通过主设备号与module建立连接

  (3)应用程序层通过设备文件/dev/xxx对底层module进行操作

三、驱动模板

  从宏观上把握了驱动程序的框架,然后再从细节上完善驱动的功能,这是开发驱动程序的一般步骤。驱动模板必备要素有头文件、初始化函数、退出函数、版权信息,常用的扩展要素是增加一些功能函数完善底层驱动的功能。

1、头文件

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>

init.h     定义了驱动的初始化和退出相关的函数
kernel.h     定义了经常用到的函数原型及宏定义
module.h   定义了内核模块相关的函数、变量及宏

2、初始化函数

static int __init hello_init(void){
    int ret;
    ret = register_chrdev(HELLO_MAJOR,DEVICE_NAME,&hello_flops);
    if (ret < 0) {
          printk(KERN_EMERG DEVICE_NAME " can't register major number.\n");
          return ret;
    }
    printk(KERN_EMERG DEVICE_NAME " initialized.\n");
    return 0;
}
module_init(hello_init);

 

  当加载驱动到内核的时候,这个初始化函数就会被自动执行。

  初始化函数顾名思义是用来初始化模块的,常用的功能是通过register_chrdev来注册函数。内核分配了一块内存(数组)专门用来存放字符设备的函数集,register_chrdev函数会在这个数组的HELLO_MAJOR位置将hello_flops中的内容进行填充,也就是将HelloModule的功能函数地址注册到设备管理内存集中。

  形象的比喻好像是操作系统提供了很多的衣服架,注册设备就好像是把一个衣服挂到某一个衣服架上。衣服上有许多口袋,就好像每一个模块有许多功能程序接口。显然,如果想使用设备的某个功能,就可以先找到对应的衣服架,然后找到相应的口袋,去调用对应的函数,执行动作。

3、退出函数

static void __exit hello_exit(void){
    unregister_chrdev(HELLO_MAJOR, DEVICE_NAME);
    printk(KERN_EMERG DEVICE_NAME " removed.\n");
}
module_exit(hello_exit);

  当卸载驱动的时候,退出函数便会自动执行,完成一些列清楚工作。

  在加载驱动的时候,我们向设备管理内存集中注册了该模块的相关功能函数。当卸载驱动的时候,就有必要将这个模块占用的内存空间清空。这样当其他的设备注册的时候便有更多的空间可以选择。

  形象的比喻是, 当卸载驱动的时候,就是把衣服从衣服架上取下来,这样衣服架就腾空了。

4、版权信息

MODULE_LICENSE("GPL");

  Linux内核是按照GPL发布的,同样Linux的驱动程序也要提供版权信息,否则当加载到内核中系统会给出警告信息。

5、功能函数

static int hello_open(struct inode *inode, struct file *file){
    printk(KERN_EMERG "hello open.\n");
    return 0;
}

static int hello_write(struct file *file, const char __user * buf, size_t count, loff_t *ppos){
    printk(KERN_EMERG "hello write.\n");
    return 0;
}

static struct file_operations hello_flops = {
    .owner  =   THIS_MODULE,
    .open   =   hello_open,     
    .write  =   hello_write,
};

 

  功能函数虽然不是一个驱动模板所必须的,但是一个有实际意义的驱动程序一定包含功能函数。功能函数实际上定义了这个驱动程序为用户提供了哪些功能,也就是用户可以对一个硬件设备可以进行哪些操作。
  常见的功能函数有xxx_open()、xxx_write()、xxx_read()、xxx_ioctl()、xxx_llseek()等。当上层应用调用open()、write()、read()、ioctl()、llseek()等这些函数的时候,经过层层调用最后到达底层,调用相应的功能函数。结构体file_operations中的成员定义了很多函数,实际应用可以只对其部分成员赋值,其定义如下。

 View Code

四、从上层应用到底层驱动的执行过程

 1、Linux系统的分层结构

  Linux系统的分层结构为:应用层 ----> 库 ----> 内核 ----> 驱动程序 ----> 硬件设备。

2、从上层应用到底层驱动的执行过程

  以“open("/dev/hellodev", O_RDWR)”函数的执行过程为例来说明。

(1)应用程序使用库提供的open函数打开代表hellodev的设备文件。

(2)库根据open函数传入的参数执行swi指令,这条指令会引起CPU异常,从而进入内核。

(3)内核的异常处理函数根据这些参数找到相应的驱动程序。

(4)执行相应的驱动程序。

(5)返回一个文件句柄给库,进而返回给应用程序。

 3、驱动程序的执行特点

  与应用程序不同,驱动程序从不主动运行,它是被动的:根据应用程序的要求进行初始化,根据应用程序的要求进行读写。驱动程序加载进内核,只是告诉内核“我在这里,我能做这些工作”,至于这些工作何时开始,则取决于应用程序。

  驱动程序运行于“内核空间”,它是系统“信任”的一部分,驱动程序的错误有可能导致整个系统的崩溃。

 

参考资料:

             linux驱动开发框架

     《嵌入式Linux应用开发完全手册》