正点原子IMX6ULL驱动开发

Linux驱动开发分类

    1. linux驱动分为三类:
      • 字符设备驱动,最多的
      • 块设备驱动(存储设备)
      • 网络设备驱动

应用程序和驱动的交互原理

  1. 驱动就是获取外设,或者传感器数据,控制外设。数据会提交给应用程序。
  2. Linux驱动编译既要编写一个驱动,还要我们编写一个简单的测试应用程序,APP。
  3. Linux下驱动和应用是完全分开的。
  4.  用户空间(用户态)和内核空间(内核态)
  5. Linux操作系统内核和驱动程序运行在内核空间,应用程序运行在用户空间。

字符设备驱动开发流程

  1. 驱动设备表现就是一个/dev/下的一个文件,应用程序调用open函数打开一个设备的时候,不如LED。应用程序通过write函数向/dev/led写数据。不如写1表示打开,写0表示关闭。如果要关闭这个设备那么就是close函数。
  2. 编写驱动的时候,也需要编写驱动对应的open,close,write函数。字符设备驱动file_operations结构体

字符设备驱动框架

  1. 字符设备驱动的编写主要就是驱动对应的open,close,read。其实就是file_operations结构体的成员变量的实现。

驱动模块的加载与卸载

  1. Linux驱动程序可以编译到kernel里面,也就是zImage,也可以编译为模块,.ko。测试的时候值需要加载.ko模块就可以。
  2. 编写驱动的时候注意事项:
    1. 编译驱动的时候需要用到Linux内核源码,因此要解压缩Linux内核源码。编译Linux内核源码。得到zImage和.dtb。需要使用编译后得到的zImage和.dtb启动系统
  3. 将编译出来的.ko文件放到根文件系统里面。加载驱动会用到加载命令insmod,modprobe。
  4. 移除驱动使用命令rmmod。
  5. 对于一个新的模块使用modprobe加载的时候需要先调用一下depmod命令。
  6. 驱动模块加载成功以后可以使用lsmod查看一下
  7. 卸载模块使用rmmod命令

字符设备的注册与注销

  1. 我们需要向系统注册一个字符设备,使用函数register_chrdev。
  2. 卸载驱动的时候需要注销掉前面注册的字符设备,使用函数unregister_chrdev,注销字符设备。

设备号

  1. Linux内核里使用 :typedef             __kernel_dev_t                dev_t;
  2. typedef __u32 __kernel_dev_t;
  3. typedef unsigned int         __u32;
  4. Linux内核将设备好分为两部分,主设备号和次设备号
  5. 主设备号占用前12位,次设备号占用低20位
  6. 设备号的操作函数,或宏
    1. 从dev_t获取主设备号和次设备号,MAJOR(dev_t)或MINOR(dev_t)。
    2. 也可以使用主设备号和次设备号构成dev_t,通过MKDEV(major,minor)

应用程序编写

  1. 首先要open
  2. 驱动给应用传递数据的时候需要用到copy_to_user函数

地址映射

  1. 在裸机LED灯实验就是操作寄存器。
  2. Linux驱动开发也可以操作寄存器,Linux下不能直接对寄存器的物理地址进行读写操作,比如寄存器A物理地址为0x01010101。裸机的时候可以直接对0x01010101这个物理地址进行操作,但是在Linux不行。因为Linux会使能MMU。
  3. 在Linux里面操作的都是虚拟地址,所以需要得到0x01010101这个物理地址对应的虚拟地址
  4. 获得物理地址对应的虚拟地址使用ioremap函数,第一个参数就是物理地址起始大小,第二个参数就是要转化的字节数量。
  5. 当我们卸载驱动的时候iounmap(va),去除映射。

驱动程序编写

  1. 初始化时钟,IO,GPIO等。
  2. 初始化完成以后进行测试。

新字符设备驱动原理

  1. 以前的缺点:
    1. 注册字符设备,浪费了很多次设备号,而且需要我们手动指定主设备号。
  2. 字符设备注册
    1. cdev结构体表示字符设备,然后使用cdev_init函数来初始化cdev
    2. cdev_init初始化完成以后,使用cdev_add添加到Linux内核。

自动创建设备节点

  1. 在以前的实验中,都需要手动调用mknod创建设备节点。
  2. 为此6内核引入了udev机制,替换了devfs。udev机制提供提供了热插拔管理,可以在加载驱动的时候,自动创建/dev/xxx设备文件。busybox提供了udev的简化版本mdev。

流程:

  1. 注册字符设备:
    1. 给定主设备号使用函数 register_chrdev_region()
    2. 没有给定主设备号使用函数 alloc_chrdev_region ()
  2. cdev_init()函数进行初始化
  3. cdev_add()函数进行添加设备相当于 modprob xxx.ko
  4. 自动创建设备节点先创建一个类:class_create 将设备添加到类里面,然后创建设备:device_create相当于mknod /dev/xxxx c 200 0
  5. 要对函数的返回值进行错误处理,使用goto语句进行跳转,先发生的错误,放到最后执行,后发生的错误先进行处理。
  6. 创建设备的结构体,来对这个设备进行描述,在open函数里面将这个结构体filp->private_data = &newchrled;将这个结构体添加进去,这个操作成为文件的私有数据。要访问和数据要进行struct newchrled_dev *dev = (struct newchrled_dev*)filp->private_data;这样的操作。

文件私有数据

  1. 在open函数里面设置filp->private_data为设备变量。
  2. 在read,write里面访问设备的时候,直接读取私有数据。

设备树

  1. uboot启动内核用到zImage,imx6ull-alientek-emmc.dtb。使用bootz命令进行启动 bootz 80800000 - 83000000启动内核
  2. 在单片机驱动里面比如W25QXX,SPI,速度都是在.c文件里面写死。板级信息都写到.c里面,导致Linux内核臃肿。因此将板级信息做成独立格式,文件扩展名为.dts的文件。一个平台或着一个机器对应一个.dts文件

DTS,DTB和DTC的关系

  1. .dts相当于.c文件,就是DTS源码文件。
  2. DTC工具相当于gcc编译器,将.dts编译成.dtb文件
  3. .dtb相当于bin文件,或可执行文件。
  4. 通过make dtbs编译所有的.dts文件

DTS基本语法

  1. DTS也是’/’开始
  2. 设备树也有头文件,扩展名为.dtsi。可以将一款SOC他的其他所有设备/平台的共有的信息提出来,作为一个通用的.dtsi文件。
  3. 从/根节点开始描述设备信息
  4. 在/根节点外有一些&cpu0这样的语句是“追加”。
  5. 节点名字,完整的要求:node-name@unit-address, unit-address一般都是外设寄存器的起始地址,有时候是I2C的设备地址,或者其他含义,具体节点具体分析。

设备树在系统中的体现

  1. 系统启动以后可以在根文件系统里面看到设备树的节点信息。在/proc/device-tree这个目录下存放这设备树信息。
  2. 内核启动的时候会解析设备树,然后在/proc/device-tree目录下呈现出来。

特殊节点:

  1. aliases
  2. chosen节点,主要目的就是将uboot里面的bootargs环境变量值,传递给Linux内核作为命令行参数cmdline,在uboot中使用函数fdt_chosen函数,创建一个”bootargs”的文件,里面存放的内容是uboot中bootargs的内容。这样就把uboot中bootargs的信息传递给了内核。

特殊属性:

  1. compatible(兼容的)属性:
    1. 值为字符串
    2. 根节点/下的compatible属性,用于内核查找这块板子内核是否支持。内核启动的时候,会检查是否支持此平台,或者这个机器。不使用设备树的时候通过检查machine id来判断内核是否支持此机器。
    3. 使用设备树的时候,不使用机器ID,而是使用根节点/下的compatible的值。

#define DT_MACHINE_START(_name, _namestr) \

static const struct machine_desc __mach_desc_##_name \

__used \

__attribute__((__section__(".arch.info.init"))) = { \

.nr = ~0, \

.name = _namestr,

DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")

        .map_io           = imx6ul_map_io,

        .init_irq    = imx6ul_init_irq,

        .init_machine  = imx6ul_init_machine,

        .init_late  = imx6ul_init_late,

        .dt_compat      = imx6ul_dt_compat,

MACHINE_END

展开以后:

static const struct machine_desc __mach_desc_IMX6UL__used

__attribute__((__section__(".arch.info.init"))) = { \

.nr = ~0, \

.name = "Freescale i.MX6 Ultralite (Device Tree)"

        .map_io           = imx6ul_map_io,

        .init_irq    = imx6ul_init_irq,

        .init_machine  = imx6ul_init_machine,

        .init_late  = imx6ul_init_late,

        .dt_compat      = imx6ul_dt_compat,

};

Linux内核的OF操作函数

  1. 驱动如何获取到设备树中节点信息:
    1. 在驱动使用OF函数获取设备树属性内容。
    2. 驱动要想获取到设备树节点的内容,首先要找到节点。

1.6ULL的GPIO的使用

  1. 设置PIN的复用和电气属性
  2. 配置GPIO的属性

2.pinctrl子系统

1. 借助pinctrl来设置一个PIN的复用和电气属性。

2. 打开imx6ull.dtsi:

a. IOMUX SNVS控制器

                         iomuxc_snvs: iomuxc-snvs@02290000 {

                                  compatible = "fsl,imx6ull-iomuxc-snvs";

                                  reg = <0x02290000 0x10000>;

                         };

b.IOMUXC控制器

                         iomuxc: iomuxc@020e0000 {

                                  compatible = "fsl,imx6ul-iomuxc";

                                  reg = <0x020e0000 0x4000>;

                         };

&iomuxc {

        pinctrl-names = "default";

        pinctrl-0 = <&pinctrl_hog_1>;

        imx6ul-evk {

                 pinctrl_hog_1: hoggrp-1 {

                         fsl,pins = <

                                  MX6UL_PAD_UART1_RTS_B__GPIO1_IO19    0x17059 /* SD1 CD */

                                  MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT        0x17059 /* SD1 VSELECT */

                                  MX6UL_PAD_GPIO1_IO09__GPIO1_IO09        0x17059 /* SD1 RESET */

                         >;

                 };

};

根据设备的类型,创建对应的子结点,然后设备所用PIN都放到此节点。

c.IOMUXC GPR控制器

                         gpr: iomuxc-gpr@020e4000 {

                                  compatible = "fsl,imx6ul-iomuxc-gpr",

                                          "fsl,imx6q-iomuxc-gpr", "syscon";

                                  reg = <0x020e4000 0x4000>;

                         };

d.如何添加一个PIN的信息

                 pinctrl_hog_1: hoggrp-1 {

                         fsl,pins = <

                                  MX6UL_PAD_UART1_RTS_B__GPIO1_IO19        0x17059 /* SD1 CD */

                         >;

                 };

3.gpio子系统

1. 使用gpio子系统来使用gpio。

4.pinctrl驱动

1. 通过compatible属性来确定,此属性是一个字符串列表。驱动文件里面有一个描述驱动兼容性的东西,当设备树节点的compatible属性和驱动里面的兼容性字符串匹配,也就是一模一样的时候就表示设备和驱动匹配了。

2. 在全局搜索,设备节点里面的compatible属性的值,在那个.c文件里面有,那么此.c文件就是驱动文件。

3. 当驱动和设备匹配以后执行,probe函数

imx6ul_pinctrl_probe-->imx_pinctrl_probe(初始化imx_pinctrl_desc结构体)-->imx_pinctrl_probe_dt-->imx_pinctrl_parse_functions-->imx_pinctrl_parse_groups

imx_pinconf_set函数设置PIN的电气属性

imx_pmx_set函数设置PIN的复用

GPIO子系统

&usdhc1 {

pinctrl-names = "default", "state_100mhz", "state_200mhz";

pinctrl-0 = <&pinctrl_usdhc1>;

pinctrl-1 = <&pinctrl_usdhc1_100mhz>;

pinctrl-2 = <&pinctrl_usdhc1_200mhz>;

cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;

keep-power-in-suspend;

enable-sdio-wakeup;

vmmc-supply = <&reg_sd1_vmmc>;

status = "okay";

};

  1. 定义了一个cd-gpios属性
  2. 首先换取到GPIO所处的设备节点,比如of_find_node_by_path。
  3. 获取GPIO编号通过of_get_named_gpio函数,返回值就是GPIO编号
  4. 请求此编号的GPIO,通过函数gpio_request函数
  5. 设置GPIO,输入或者输出,gpio_direction_input或gpio_direction_output
  6. 如果是输入,那么通过gpio_get_value函数读取GPIO值,如果是输出,通过gpio_set_value函数设置GPIO的值
  7. 连接层向应用层提供API函数,而底层向连接层注册(使用函数gpiochip_add)。
  8. mxc_gpio_probe-->mxc_gpio_get_hw(获取6ULL的GPIO寄存器组)-->bgpio_init重点初始化gpio_chip结构体-->gpiochip_add向系统添加gpio_chip

总结:

  1. 添加pinctrl信息
  2. 检查当前设备树中要使用的IO有没有被其他设备使用,如果有的话要处理
  3. 添加设备节点,在设备节点中传建一个属性,此属性描述所使用的gpio
  4. 编写驱动,获取对应的gpio编号,并申请IO,成功以后即可使用此IO

并发与竞争解决方法

  1. 原子操作:
    1. 原子变量和原子位
  2. 自旋锁:
    1. 用于多核SMP
    2. 使用自旋锁,要注意死锁现象的发生
    3. 线程与线程
    4. 线程与中断

Linux内核定时器原理

    1. 内核时间管理
  1. Cortex-M内核使用systick作为系统定时器。
  2. 硬件定时器,软件定时器,原理是依靠系统定时器来驱动。

内核定时器

  • 软件定时器不像硬件定时器一样,直接给周期值。设置期满以后的时间点。
  • 定时处理函数
  • 内核定时器不是周期性的,一次定时时间到了以后就会关闭,除非重新打开。

编写实验驱动

  1. 定义一个定时器,结构体 timer_list

ioctl函数-->long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

  1. ioctl命令是自己定义的,但是要符合linux规则。

Linux内核中断处理简介

linux中断

  1. 先知道你要使用的中断对应的中断号。
  2. 先申请request_irq,此函数会激活中断。
  3. 如果不用中断了,那就释放掉,使用free_irq。
  4. 中断处理函数irqreturn_t (*irq_handler_t)(int,void*)
  5. 使能和禁止中断enable_irq , disable_irq

上半部和下半部

  1. 中断一定要处理的越快越好
  2. 要使用软中断,要先注册,使用函数open_softirq。注册以后使用raise_softirq触发
  3. tasklet
    1. 也需要用到上半步,只是上半部的中断处理函数重点是调用tasklet_schedule。
    2. 定义一个tasklet函数
    3. 初始化,重点是设置对应的处理函数
  4. 设备树中断节点信息
    1. #interrupt-cells指定interrupt的cells数量,也就是属性interrupts
    2.         fxls8471@1e {
    3.                  compatible = "fsl,fxls8471";
    4.                  reg = <0x1e>;
    5.                  position = <0>;
    6.                  interrupt-parent = <&gpio5>;
    7.                  interrupts = <0 8>;
    8.         };
    9. interrupt-parent指定父中断。interrupts第一个cells就是gpio编号,因此上面就是用的是gpio5_io00。
    10. 通过函数irq_of_parse_and_map从interrupt属性获取中断号。

编写实验驱动

  1. 编写设备树
  2. 按键消抖

Linux阻塞和非阻塞IO

  1. 阻塞与非阻塞简介
    1. 阻塞:当资源不可用的时候,应用程序就会刮起。当资源可用的时候,唤醒任务。应用程序使用open打开驱动文件,默认是阻塞方式打开。
    2. 非阻塞:当资源不可用的时候,应用程序轮询查看,或放弃。会有超时处理机制。应用程序在使用open打开驱动的时候,使用O_NOBLOCK。
  2. 等待队列
    1. 等待队列头:
      • wait_queue_head_t需要定义一个。定义以后使用init_waitqueue_head函数进行初始化。或者使用宏DECLARE_WAIT_QUEUE_HEAD。
    2. 等待队列项
      • wait_queue_t表示等待队列项,或者使用宏DECLARE_WAITQUEUE(name,task);
    3. 添加队列项到等待队列头
      • add_wait_queue函数
    4. 移除等待队列项
      • 资源可用的时候使用remove_wait_queue函数
    5. 唤醒
      • wake_up函数唤醒等待队列头中所有的等待队列项

驱动里面的poll函数

  1. unsigned int (*poll) (struct file *, struct poll_table_struct *);
  2. signal_pending函数用于判读当前进程是否有信号处理,返回值不为0的话表示有信号需要处理

异步通知

  1. 信号:
    1. 软件层次上的“中断”,也叫做软中断信号,软件层次上对中断机制的一种模拟
    2. kill -9 xxx          //关闭某个应用
  2. 驱动中对异步通知的处理
    1. 要使用fasync_struct 定义一个指针结构体变量
    2. 实现file_operations里面的fasync函数。
    3. 驱动里面调用kill_fasync函数来向应用程序发送信号。
    4. 关闭驱动的时候要删除信号

Linux驱动分离与分层

  1. 以前的驱动实验都很简单,就是对IO的操作。
  2. 驱动的分离与分隔
    1. 单片机驱动开发,IIC设备MPU6050。
    2. 将驱动分离:主机控制器驱动和设备驱动,主机控制器一般是半导体厂商写的。
    3. 在linux驱动框架下编写具体的设备驱动
    4. 中间的联系就是核心层。

总线-驱动-设备

  1. 驱动-总线-设备
    1. 驱动:就是具体的设备驱动
    2. 设备:设备属性,包括地址范围,如果是IIC的话还有IIC器件地址,速度。
    3. 总线:
      • 总线数据类型为:bus_type。向内核注册总线使用bus_register。
      • 总线主要工作就是完成总线下的设备和驱动之间的匹配。
      • 向linux内核注册总线:
        1. 使用函数bus_register
        2. 卸载总线 bus_unregister
    4. 驱动:
      • 驱动和设备匹配以后驱动里面的probe函数就会执行
      • 使用driver_register函数注册驱动。
      • driver_register -- > bus_add_driver -- > driver_attch(查找bus下的所有设备,找到与其匹配的) --> bus_for_each_dev --> __driver_attach   //每个设备都调用此函数,查看每个设备是否与驱动匹配 -- > driver_match_device    //检查是否匹配 -- > driver_probe_device  -- > really_probe -->drv->probe(dev)                 //执行driver的probe函数
      • 向总线注册驱动的时候会检查当前总线下的所有设备,有没有与此驱动匹配的设备,如果有的话就执行驱动里面的probe函数。
    5. 设备:
      • 设备数据类型为device,通过device_register向内核注册设备
      • 向总线注册设备的时候使用device_register函数
      • device_register --> device_add --> bus_add_device -->bus_probe_device -->device_attach --> bus_for_each_drv --> __device_attach --> driver_match_device --> driver_probe_device
      • 驱动与设备匹配以后驱动的probe函数就会执行。probe函数就是驱动编写人员编写的。

platform平台驱动模型

  1. 方便开发,linux提出了驱动的分离与分层。
  2. 进一步引出了驱动-总线-设备驱动模型,或者框架
  3. 对于SOC内部的RTC,timer等等,不好归结为具体的总线,为此linux提出了虚拟总线:platform总线,platform设备和platform驱动
  4. platform总线注册:
    1. platform_bus_init -- > bus_register
    2. 注册内容:

struct bus_type platform_bus_type = {

.name = "platform",

.dev_groups = platform_dev_groups,

.match = platform_match,

.uevent = platform_uevent,

.pm = &platform_dev_pm_ops,

};

    1. 对于platform平台而言,platform_match函数负责驱动和设备的匹配。
    2. platform驱动:

结构体为:

struct platform_driver {

int (*probe)(struct platform_device *);

int (*remove)(struct platform_device *);

void (*shutdown)(struct platform_device *);

int (*suspend)(struct platform_device *, pm_message_t state);

int (*resume)(struct platform_device *);

struct device_driver driver;

const struct platform_device_id *id_table;

bool prevent_deferred_probe;

};

使用platform_driver_register向内核注册platform驱动

platform_driver_register -->

__platform_driver_register -->

设置driverprobeplatform_drv_probe,      //如果platform_driverprobe有效的话

--> driver_register

--> 执行device_drive --> probe,对于platform总线,也就是platform_drv_probe函数。而函数platform_drv_probe会执行platform_driver下的probe函数

结论;

向内核注册platform驱动的时候,如果驱动和设备匹配成功,最终会执行platform_driverprobe函数

    1. 设备:

struct platform_device {

const char *name;

int id;

bool id_auto;

struct device dev;

u32 num_resources;

struct resource *resource;

const struct platform_device_id *id_entry;

char *driver_override; /* Driver name to force a match */

/* MFD cell pointer */

struct mfd_cell *mfd_cell;

/* arch specific additions */

struct pdev_archdata archdata;

};

      • 无设备树的时候,此时需要驱动开发人员编写设备注册文件,使用platform_device_register函数注册设备。
      • 有设备树的时候,修改设备树的设备节点即可。
        1. 当设备与platform的驱动匹配以后,就会执行platform_driver ->probe函数
    • 匹配过程(无设备树);
      • 根据前面的分析,驱动和设备匹配是通过bus ->match函数。platform总线下的match函数就是:platform_match.
      • of_driver_match_device          (设备树)
      • acpi_driver_match_device      (ACPI类型)
      • platform_match_id根据platform_driver下的id_table来匹配
      • strcmp(pdev->name, drv->name) //最终通过比较字符串就是platform_device -> name 和 platform_driver  -> driver -> name。无设备树情况下使用
    • 匹配过程(有设备树);
      • of_driver_mach_device
      • of_match_device(drv->of_match_table, dev) of_match_table 类型为of_device_id  compatible属性
      • of_match_node
      • __of_match_node
      • __of_device_is_compatible   
      • __of_find_property(device, "compatible", NULL);         //取出 compatible属性值

实验程序编写

  1. 无设备树
    1. 两部分:platform_driver , platform_device。
    2. 编写向platform总线注册设备。编写驱动需要寄存器地址信息,地址信息使用设备信息,定义在platform_device里面,因此需要在驱动里面获取设备中的信息,或者叫做资源。使用函数platform_get_resource()。
  2. 有设备树
    1. 有设备树的时候设备是由设备树描述的,因此不需要向总线注册设备,而是直接修改设备树。只需要修改设备树,然后编写驱动。
    2. 驱动和设备匹配成功以后,设备信息就会从设备树节点转为platform_device结构体。
    3. platform提供了很多API函数去获取设备信息的。

内核自带LED驱动使能

  1. 内核自带的驱动,都是通过图形化配置,选择使能或者不使用。
  2. 输入 make menuconfig,使能驱动以后.config里面就会存在:CONFIG_LEDS_GPIO = y
  3. 在linux内核源码里面一般驱动文件夹下Makefile会使用CONFIG_XXX来决定要编译那个文件
  4. obj-$(CONFIG_LEDS_GPIO)                 += leds-gpio.o 也就是 obj - y +=leds-gpio.o ---> leds-gpio.c

内核自带LED驱动使用

  1. 首先将驱动编译进内核里面
  2. 根据绑定文档在设备树里面添加对应的设备节点信息。

如果无设备树,那么就要使用platform_device_register

内核自带MISC驱动简介

  1. MISC设备的主设备号为10。
  2. MISC设备会自动创建cdev,不需要手动创建
  3. MISC驱动编写的核心就是初始化miscdevice结构体变量。然后使用misc_register向内核注册,卸载驱动的时候使用misc_deregister.
  4. MISC驱动是基于platform平台
  5. 如果设备miscdevice里面minor为255的话,表示由内核自动分配一个次设备号。

INPUT子系统简介

  1. input子系统也是字符设备,input核心层会帮我们注册input字符设备驱动。
  2. input_dev:
    1. 申请并初始化并注册input_dev,使用input_allocate_device申请,evbit表示输入事件,比如按键对应的事件就是EV_KEY,那么还要加EV_REP。
    2. 设置按键对应的键值,也就是keybit。
    3. 初始化完成以后input_dev以后,需要向内核注册。使用input_register_device函数注册
    4. 按键按下以后上报事件,比如对于按键而言就是在按键中断服务函数,或者消抖定时器函数里面获取按键按下情况,并且上报:可以使用input_event函数。
    5. 对于按键而言,也可以使用:
      • input_report_key
    6. 使用上面两个函数上报完成输入事件以后,还需要使用input_sync做同步
    7. input_event:

应用程序可以通过input_event来获取输入事件数据,比如按键值,input_event是一个结构体struct input_event {

struct timeval time;

__u16 type;                 事件类型

__u16 code;                事件码,对于按键而言就是键码

__s32 value;               对于按键就是按下或抬起

};

struct timeval {

__kernel_time_t tv_sec; /* seconds */

__kernel_suseconds_t tv_usec; /* microseconds */

};

typedef        __kernel_long_t         __kernel_time_t;

typedef        long      __kernel_long_t;

typedef        __kernel_long_t        __kernel_suseconds_t;

typedef long     __kernel_long_t;

    1. 编写应用程序:
      • 按键驱动对应的文件就是 /dev/input/eventX (X = 0,1,2,3),应用程序读取/dev/input/event1来得到按键信息,也就是按键有没有被按下。
      • 我们通过/dev/input/event1读到的信息就是input_event结构体形式的。
    2. Linux内核自带按键驱动程序使用:
      • 配置内核,选中内核自带的KEY驱动程序,然后在 .config里面产生CONFIG_KEYBOARD_GPIO = y。经过查找内核自带的驱动程序为gpio_keys.c
      • 这是一个标准的platform驱动

Linux LCD驱动实验

  1. Frambebuffer设备:
    1. RGB LCD屏幕,framebuffer是一种机制,应用程序操作驱动里面LCD显存的一种机制,因为应用程序需要通过操作显存来在LCD上显示字符,图片信息。
    2. 通过framebufer机制将底层的LCD抽象为 /dev/fbX(X = 0,1,2,3),应用程序通过操作fbX来操作屏幕
    3. framebuffer在内核中的表现就是 fb_info结构体,屏幕驱动重点就是初始化fb_info里面的各个成员变量。初始化完成fb_info以后,要通过register_framebuffer函数向内核注册刚刚初始化以后的的fb_info
    4. 卸载驱动的时候调用unregister_framerbuffer来卸载前面注册的fb_info。
  2. 内核LCD驱动简介:
    1. 驱动文件为mxsfb.c,为platform驱动框架,驱动和设备匹配以后,mxsfb_probe函数执行
    2. 给mxsfb_info申请内存,申请fb_info,然后将这两个联系起来
    3. host->base就是内存映射以后的LCDIF外设基地址。
    4. mxsfb_probe函数会调用 mxsfb_init_fbinfo函数初始化fb_info
    5. mxsfb_probe函数重点工作:
      • 初始化fb_info并且向内核注册
      • 初始化LCDIF控制器
      • mxsfb_init_fbinfo_dt函数会从设备树中读取相关属性信息
  3. 驱动编写
    1. 屏幕引脚设置:
      • 将屏幕引脚电气属性改为0x49,修改LCD引脚驱动能力
    2. 背光:
      • 一般屏幕背光用PWM控制亮度,一般测试屏幕的时候直接将背光引脚拉高或拉低

RTC驱动实验

  1. rtc_device结构体
    1. RTC也是一个标准字符设备驱动

rtc_deviceRTC设备在内核中的具体实现,找到RTC相关节点snvs_rtc: snvs-rtc-lp {

compatible = "fsl,sec-v4.0-mon-rtc-lp";

regmap = <&snvs>;

offset = <0x34>;

interrupts = <GIC_SPI 19 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>;

};

    1. 根据compatible找到驱动文件:rtc_snvs.c,驱动文件里面就是初始化rtc_device并注册
    2. rtc_device结构体里面重点是rtc_class_ops操作集
  1. IMX6U RTC驱动解析
    1. RTC驱动过程 在应用层调用ioctl函数,因为RTC还是一个字符设备,所以给应用层回应的函数是file_operations结构体里面的ioctl函数来实现的,但是要驱动RTC设备还要有一层,在file_operations中实现的ioctl函数,会调用rtc_class_ops中的函数如read_time,read_alarm等等。
    2. 驱动和设备匹配以后,snvs_rtc_probe函数会执行,NXP为6ULL的RTC外设创建了一个结构体snvs_rtc_data,此结构体里面包含了rtc_device成员变量
    3. 首先从设备树里面获取SNVS RTC外设寄存器,初始化RTC,申请中断处理闹钟,最后通过devm_rtc_device_register函数向内核注册rtc_device,重点是注册的时候设置了snvs_rtc_ops。
    4. 当应用通过ioctl读取RTC事件的时候,RTC核心层的rtc_dev_ioctl会执行,通过CMD来执行具体操作,比如RTC_ALM_READ就是读取闹钟,此时rtc_read_alarm就会执行,rtc_read_alarm函数就会找到具体的rtc_device,运行其下的const struct rtc_class_ops *ops里面的read_alarm。
    5. RTC_ALM_SET就是设置闹钟 执行函数rtc_set_alarm --> rtc_timer_enqueue ---> __rtc_set_alarm ---> rtc->ops->set_alarm

I2C驱动框架

  1. 裸机下的I2C驱动框架
    1. 首先编写IIC控制器驱动,bsp_i2c.c和bsp_i2c.h为IIC外设驱动,向外提供i2c_master_transfer函数。
  2. I2C设备驱动
    1. I2C适配器在内核里面使用i2c_adapter结构体,IIC适配器驱动(控制器)核心就是申请i2c_adapter结构体,然后初始化,最后注册。
    2. 初始化完成i2c_adapter以后,使用i2c_add_adapter或者i2c_add_numbered_adapter来向内核注册I2C控制器驱动
    3. 在i2c_adapter里面有一个非常重要的成员变量:i2c_algorithm,此变量包含了IIC控制器访问IIC设备的接口函数,需要IIC适配器的编写人员实现。
    4. i2c_adpater -> i2c_algorithm ->master_xfer 此函数就是I2C控制器最终进行数据收发的函数
    5. 驱动与设备匹配以后i2c_imx_probe函数就会执行,NXP创建了一个结构体imx_i2c_struct包含IMX6ULL的I2C相关属性,
    6. 设置i2c_adapter下的i2c_algorithm为i2c_imx_algo实现算法,通过IMX6ULL的I2C控制器读取I2C或者向I2C设备写入数据的时候最终是通过i2c_imx_xfer实现的
  3. I2C设备驱动
    1. i2c_client表示i2c设备,不需要我们自己创建i2c_client,我们一般在设备树里面添加具体的I2C芯片,比如fsl,fxls8471,系统在解析设备树的时候就会知道有这个I2C设备,然后会创建对应的i2c_client。
    2. i2c设备驱动框架,i2c_driver初始化与注册,需要IIC设备驱动人员编写的。IIC驱动程序就是初始化i2c_driver,然后向系统注册使用函数i2c_register_driver或者i2c_add_driver。注销IIC使用i2c_del_driver。
    3. 在设备树中添加,I2C设备挂到那个I2C控制器下,就在那个控制器下添加对应的节点.
  4. 驱动编写
    1. 修改设备树,IO,添加AP3216C节点信息。UART4_RXD作为I2C1_SDA,UART4_TXD作为I2C1_SCL。
    2. 编写驱动框架,I2C设备驱动框架,字符设备驱动框架。
    3. 初始化AP3216C,实现ap3216c_read函数。
    4. 通过IIC控制器,来向AP3216C里面发送数据或者读取,这里使用i2c_transfer这个api函数来实现IIC数据的传输。
    5. int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs,int num); adap:IIC设备对应的适配器,也就是IIC接口。当IIC设备和驱动匹配以后,pobe函数就会执行,probe函数传递进来的第一个参数就是i2c_client。在i2c_client里面保存了此IIC设备所对应的i2c_adapter。msgs:就是构成的I2C传输数据

SPI驱动框架

  1. 裸机下的SPI驱动框架:
    1. spi具体芯片驱动:ICM20608
  2. Linux下的SPI驱动框架
    1. 设备驱动:具体的SPI芯片驱动
    2. 当spi控制器的设备和驱动匹配以后,spi_imx_probe函数就会执行
    3. SPI控制器驱动核心就是spi_master的构建,spi_master里面就有如何通过SPI控制器与SPI外设进行通信的函数,此函数是原厂编写
    4. spi_master -> transfer 函数和 spi_master -> transfer_one_message 函数 6ULL主机控制器使用transfer_one_message函数。
    5. spi_imx_data -> bitbang -> master -> setup_transfer ->txrx_bufs
    6. spi_master, 一般需要申请spi_alloc_master,释放的话就是spi_master_put。,初始化,最终使用函数spi_register_master注册。
    7. bitbang下的spi_imx_setupxfer函数
      • spi_imx_setupxfer -> spi_imx -> rx = spi_imx_buf_rx_u8       //最终的SPI接受函数
      • spi_imx_setupxfer -> spi_imx -> tx = spi_imx_buf_tx_u8       //最终的SPI发送函数
      • spi_imx_setupxfer -> spi_imx ->devtype_data -> config(spi_imx, &config); -> mx51_ecspi_config              //配置6ULL的SPI控制器寄存器
      • bitbang下的spi_imx_transfer函数:spi_imx_transfer -> spi_imx_pio_transfer -> spi_imx_push -> spi_imx->tx(spi_imx);
      • 最终调用spi_bitbang_start  -> master->transfer_one_message = spi_bitbang_transfer_one最后注册 spi_register_master 向系统注册spi_master。
      • spi_bitbang_transfer_one -> bitbang -> txrx_bufs(spi,t) = spi_imx_transfer -> spi_imx->tx(spi_imx) =  spi_imx_buf_tx_u8
      • 所以,经过这么复杂的操作,最终目的就是,设置spi_master的transfer_one_message函数为spi_imx_buf_tx_u8
    8. SPI接收
      • SPI通过中断接受,中断处理函数为spi_imx_isr 此函数会调用spi_imx->rx(spi_imx)函数来完成具体的接受过程。
  3. SPI设备驱动
    1. SPI设备驱动就是具体的SPI芯片驱动,比如ICM2060.
    2. spi_device : 每个spi_device下都有一个spi_master。每个SPI设备,肯定挂载了一个SPI控制器,比如ICM20608挂载到了6ULL的ECSPI3接口上。
    3. spi_driver: 非常重要!申请或者定义一个spi_driver,然后初始化spi_driver中的各个成员变量,当SPI设备和驱动匹配以后,spi_driver下的probe函数就会执行。
    4. spi_driver初始化成功以后需要内核注册,函数为spi_register_driver,当注销驱动的时候需要spi_unregister_driver。
  4. 驱动编写:
    1. 修改设备树,添加IO相关信息
    2. 片选信号不作为硬件片选,而是作为普通的GPIO,我们在程序里面自行控制片选引脚
    3. 在ecspi3节点下传建icm20608子结点。
    4. 需要初始化icm20608芯片,然后从里面读取原始数据,这个过程就要用到如何使用linux内的SPI驱动的API函数来读取ICM20608
    5. 用到两个重要的结构体:spi_transfer , spi_message
      • spi_transfer用来构建收发数据内容。
      • 构建spi_transfer,然后将其打包到spi_message里面,需要使用spi_message_init初始化spi_message,然后再使用spi_message_add_tail将spi_transfer添加到spi_message里面,最终使用spi_sync或者spi_async来发送信息
    6. NXP官方cs-gpios属性是软件片选!!!

串口驱动框架

  1. 两个重要的结构体uart_port和uart_driver
    1. uart_driver:需要驱动编写人员编写和注册,使用uart_register_driver注册到内核,卸载驱动的时候使用uart_unregister_driver卸载。
    2. uart_port:用于描述一个具体的串口端口,驱动编写人员需要实现uart_port,然后添加到内核里面去,使用uart_add_one_port函数向内核添加一个uart端口,卸载的时候使用uart_remove_one_port函数。里面有一个非常重要的成员变量uart_ops,此结构体包含了针对uart端口进行的所有操作,需要驱动编写人员实现。
    3. 串口驱动是和tty结合起来的
    4. NXP官方串口驱动入口函数为imx_serial_init。此函数会调用uart_register_driver 先向内核注册uart_driver,为imx_reg
    5. 为什么IMX6ULL的串口为 /dev/ttymxc0 和 /dev/ttymxc1,因为uart_device的dev_name来确定的。
    6. 接下来就是uart_port的处理,NXP自定义了一个imx_port,里面包含了uart_port。uart_ops为imx_pops。
    7. 串口接受中断处理函数 imx_rxint获取到串口接收到的数据,然后使用 tty_insert_flip_char 将其放到tty里面

minicom移植

  1. Linux下的软件移植,基本都是自己编译源码,步骤都是配置,然后编译,安装.
  2. 注意:
    1. 配置的时候会设置--prefix参数,也就是我们最终安装的位置,如果不设置的话就是默认安装位置。将编译出来的库放到开发板里面去

多点电容触摸屏驱动框架

  1. 电容触摸屏,上报多点触摸信息,通过触摸芯片,比如FT5426,这是一个IIC接口,多点电容触摸屏本质是IIC驱动
  2. 触摸IC一般都是有中断引脚,当检测到触摸信息以后就会触发中断,那么就要在中断处理函数里面读取触摸点信息
  3. 得到的触摸点信息,linux系统如何使用,input设备。linux系统下有触摸屏萨上报的流程,涉及到input子系统下触摸信息的上报。
  4. 触摸协议分为:TypeA 和 TypeB
    1. TypeA:一股脑全部上报所有触摸点信息,系统去甄别这些信息属于那个触摸点的

   ABS_MT_POSITION_X x[0]                //第一个点x轴坐标  ABS_MT_POSITION_X     使用函数input_report_abs

   ABS_MT_POSITION_Y y[0]        //第一个点Y轴坐标  ABS_MT_POSITION_Y

   SYN_MT_REPORT                               // 点与点之间使用  SYN_MT_REPORT隔离 使用 input_mt_sync上报这个事件     

   ABS_MT_POSITION_X x[1]        //第二个点x轴坐标  ABS_MT_POSITION_X

   ABS_MT_POSITION_Y y[1]        //第二个点Y轴坐标  ABS_MT_POSITION_Y

   SYN_MT_REPORT

   SYN_REPORT                                       //所有点发送完成以后发送这个事件

    1. TypeB:适用于触摸芯片有硬件追踪能力的,TypeB使用slot来区分触摸点,slot使用 ABS_MT_TRACKING_ID来增加,删除,替换一个触摸点信息。

   ABS_MT_SLOT 0                //表示上报第一个触摸点信息 通过input_mt_solt来完成

   ABS_MT_TRACKING_ID 45       //通过调用input_mt_report_slot_state

   ABS_MT_POSITION_X x[0]

   ABS_MT_POSITION_Y y[0]

   ABS_MT_SLOT 1                //第二个触摸点

   ABS_MT_TRACKING_ID 46       //通过调用input_mt_report_slot_state

   ABS_MT_POSITION_X x[1]

   ABS_MT_POSITION_Y y[1]

   SYN_REPORT                      //所有点发送完成以后,input_sync

    1. 上报触摸信息是通过不同的事件来上报的:ABS_MT_XXX
  1. 驱动编写与测试
    1. 设备驱动主框架为IIC,会用到中断,在中断处理函数里面上报触摸点信息,要用到input子系统框架,
    2. 设备树IO修改,IIC节点添加
    3. 在I2C2节点下添加ft5426
    4. 主题I2C框架准备好
    5. 复位引脚和中断引脚,包括中断
    6. input子系统框架
    7. 初始化ft5426芯片
    8. 在中断服务函数里读取触摸坐标值,然后上报给系统
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值