Linux驱动开发分类
-
- linux驱动分为三类:
- 字符设备驱动,最多的
- 块设备驱动(存储设备)
- 网络设备驱动
- linux驱动分为三类:
应用程序和驱动的交互原理
- 驱动就是获取外设,或者传感器数据,控制外设。数据会提交给应用程序。
- Linux驱动编译既要编写一个驱动,还要我们编写一个简单的测试应用程序,APP。
- Linux下驱动和应用是完全分开的。
- 用户空间(用户态)和内核空间(内核态)
- Linux操作系统内核和驱动程序运行在内核空间,应用程序运行在用户空间。
字符设备驱动开发流程
- 驱动设备表现就是一个/dev/下的一个文件,应用程序调用open函数打开一个设备的时候,不如LED。应用程序通过write函数向/dev/led写数据。不如写1表示打开,写0表示关闭。如果要关闭这个设备那么就是close函数。
- 编写驱动的时候,也需要编写驱动对应的open,close,write函数。字符设备驱动file_operations结构体
字符设备驱动框架
- 字符设备驱动的编写主要就是驱动对应的open,close,read。其实就是file_operations结构体的成员变量的实现。
驱动模块的加载与卸载
- Linux驱动程序可以编译到kernel里面,也就是zImage,也可以编译为模块,.ko。测试的时候值需要加载.ko模块就可以。
- 编写驱动的时候注意事项:
- 编译驱动的时候需要用到Linux内核源码,因此要解压缩Linux内核源码。编译Linux内核源码。得到zImage和.dtb。需要使用编译后得到的zImage和.dtb启动系统
- 将编译出来的.ko文件放到根文件系统里面。加载驱动会用到加载命令insmod,modprobe。
- 移除驱动使用命令rmmod。
- 对于一个新的模块使用modprobe加载的时候需要先调用一下depmod命令。
- 驱动模块加载成功以后可以使用lsmod查看一下
- 卸载模块使用rmmod命令
字符设备的注册与注销
- 我们需要向系统注册一个字符设备,使用函数register_chrdev。
- 卸载驱动的时候需要注销掉前面注册的字符设备,使用函数unregister_chrdev,注销字符设备。
设备号
- Linux内核里使用 :typedef __kernel_dev_t dev_t;
- typedef __u32 __kernel_dev_t;
- typedef unsigned int __u32;
- Linux内核将设备好分为两部分,主设备号和次设备号
- 主设备号占用前12位,次设备号占用低20位
- 设备号的操作函数,或宏
- 从dev_t获取主设备号和次设备号,MAJOR(dev_t)或MINOR(dev_t)。
- 也可以使用主设备号和次设备号构成dev_t,通过MKDEV(major,minor)
应用程序编写
- 首先要open
- 驱动给应用传递数据的时候需要用到copy_to_user函数
地址映射
- 在裸机LED灯实验就是操作寄存器。
- Linux驱动开发也可以操作寄存器,Linux下不能直接对寄存器的物理地址进行读写操作,比如寄存器A物理地址为0x01010101。裸机的时候可以直接对0x01010101这个物理地址进行操作,但是在Linux不行。因为Linux会使能MMU。
- 在Linux里面操作的都是虚拟地址,所以需要得到0x01010101这个物理地址对应的虚拟地址
- 获得物理地址对应的虚拟地址使用ioremap函数,第一个参数就是物理地址起始大小,第二个参数就是要转化的字节数量。
- 当我们卸载驱动的时候iounmap(va),去除映射。
驱动程序编写
- 初始化时钟,IO,GPIO等。
- 初始化完成以后进行测试。
新字符设备驱动原理
- 以前的缺点:
- 注册字符设备,浪费了很多次设备号,而且需要我们手动指定主设备号。
- 字符设备注册
- cdev结构体表示字符设备,然后使用cdev_init函数来初始化cdev
- cdev_init初始化完成以后,使用cdev_add添加到Linux内核。
自动创建设备节点
- 在以前的实验中,都需要手动调用mknod创建设备节点。
- 为此6内核引入了udev机制,替换了devfs。udev机制提供提供了热插拔管理,可以在加载驱动的时候,自动创建/dev/xxx设备文件。busybox提供了udev的简化版本mdev。
流程:
- 注册字符设备:
- 给定主设备号使用函数 register_chrdev_region()
- 没有给定主设备号使用函数 alloc_chrdev_region ()
- cdev_init()函数进行初始化
- cdev_add()函数进行添加设备相当于 modprob xxx.ko
- 自动创建设备节点先创建一个类:class_create 将设备添加到类里面,然后创建设备:device_create相当于mknod /dev/xxxx c 200 0
- 要对函数的返回值进行错误处理,使用goto语句进行跳转,先发生的错误,放到最后执行,后发生的错误先进行处理。
- 创建设备的结构体,来对这个设备进行描述,在open函数里面将这个结构体filp->private_data = &newchrled;将这个结构体添加进去,这个操作成为文件的私有数据。要访问和数据要进行struct newchrled_dev *dev = (struct newchrled_dev*)filp->private_data;这样的操作。
文件私有数据
- 在open函数里面设置filp->private_data为设备变量。
- 在read,write里面访问设备的时候,直接读取私有数据。
设备树
- uboot启动内核用到zImage,imx6ull-alientek-emmc.dtb。使用bootz命令进行启动 bootz 80800000 - 83000000启动内核
- 在单片机驱动里面比如W25QXX,SPI,速度都是在.c文件里面写死。板级信息都写到.c里面,导致Linux内核臃肿。因此将板级信息做成独立格式,文件扩展名为.dts的文件。一个平台或着一个机器对应一个.dts文件
DTS,DTB和DTC的关系
- .dts相当于.c文件,就是DTS源码文件。
- DTC工具相当于gcc编译器,将.dts编译成.dtb文件
- .dtb相当于bin文件,或可执行文件。
- 通过make dtbs编译所有的.dts文件
DTS基本语法
- DTS也是’/’开始
- 设备树也有头文件,扩展名为.dtsi。可以将一款SOC他的其他所有设备/平台的共有的信息提出来,作为一个通用的.dtsi文件。
- 从/根节点开始描述设备信息
- 在/根节点外有一些&cpu0这样的语句是“追加”。
- 节点名字,完整的要求:node-name@unit-address, unit-address一般都是外设寄存器的起始地址,有时候是I2C的设备地址,或者其他含义,具体节点具体分析。
设备树在系统中的体现
- 系统启动以后可以在根文件系统里面看到设备树的节点信息。在/proc/device-tree这个目录下存放这设备树信息。
- 内核启动的时候会解析设备树,然后在/proc/device-tree目录下呈现出来。
特殊节点:
- aliases
- chosen节点,主要目的就是将uboot里面的bootargs环境变量值,传递给Linux内核作为命令行参数cmdline,在uboot中使用函数fdt_chosen函数,创建一个”bootargs”的文件,里面存放的内容是uboot中bootargs的内容。这样就把uboot中bootargs的信息传递给了内核。
特殊属性:
- compatible(兼容的)属性:
- 值为字符串
- 根节点/下的compatible属性,用于内核查找这块板子内核是否支持。内核启动的时候,会检查是否支持此平台,或者这个机器。不使用设备树的时候通过检查machine id来判断内核是否支持此机器。
- 使用设备树的时候,不使用机器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操作函数
- 驱动如何获取到设备树中节点信息:
- 在驱动使用OF函数获取设备树属性内容。
- 驱动要想获取到设备树节点的内容,首先要找到节点。
1.6ULL的GPIO的使用
- 设置PIN的复用和电气属性
- 配置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 = <®_sd1_vmmc>;
status = "okay";
};
- 定义了一个cd-gpios属性
- 首先换取到GPIO所处的设备节点,比如of_find_node_by_path。
- 获取GPIO编号通过of_get_named_gpio函数,返回值就是GPIO编号
- 请求此编号的GPIO,通过函数gpio_request函数
- 设置GPIO,输入或者输出,gpio_direction_input或gpio_direction_output
- 如果是输入,那么通过gpio_get_value函数读取GPIO值,如果是输出,通过gpio_set_value函数设置GPIO的值
- 连接层向应用层提供API函数,而底层向连接层注册(使用函数gpiochip_add)。
- mxc_gpio_probe-->mxc_gpio_get_hw(获取6ULL的GPIO寄存器组)-->bgpio_init重点初始化gpio_chip结构体-->gpiochip_add向系统添加gpio_chip
总结:
- 添加pinctrl信息
- 检查当前设备树中要使用的IO有没有被其他设备使用,如果有的话要处理
- 添加设备节点,在设备节点中传建一个属性,此属性描述所使用的gpio
- 编写驱动,获取对应的gpio编号,并申请IO,成功以后即可使用此IO
并发与竞争解决方法
- 原子操作:
- 原子变量和原子位
- 自旋锁:
- 用于多核SMP
- 使用自旋锁,要注意死锁现象的发生
- 线程与线程
- 线程与中断
Linux内核定时器原理
-
- 内核时间管理
- Cortex-M内核使用systick作为系统定时器。
- 硬件定时器,软件定时器,原理是依靠系统定时器来驱动。
内核定时器
- 软件定时器不像硬件定时器一样,直接给周期值。设置期满以后的时间点。
- 定时处理函数
- 内核定时器不是周期性的,一次定时时间到了以后就会关闭,除非重新打开。
编写实验驱动
- 定义一个定时器,结构体 timer_list
ioctl函数-->long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
- ioctl命令是自己定义的,但是要符合linux规则。
Linux内核中断处理简介
linux中断
- 先知道你要使用的中断对应的中断号。
- 先申请request_irq,此函数会激活中断。
- 如果不用中断了,那就释放掉,使用free_irq。
- 中断处理函数irqreturn_t (*irq_handler_t)(int,void*)
- 使能和禁止中断enable_irq , disable_irq
上半部和下半部
- 中断一定要处理的越快越好
- 要使用软中断,要先注册,使用函数open_softirq。注册以后使用raise_softirq触发
- tasklet
- 也需要用到上半步,只是上半部的中断处理函数重点是调用tasklet_schedule。
- 定义一个tasklet函数
- 初始化,重点是设置对应的处理函数
- 设备树中断节点信息
- #interrupt-cells指定interrupt的cells数量,也就是属性interrupts
- fxls8471@1e {
- compatible = "fsl,fxls8471";
- reg = <0x1e>;
- position = <0>;
- interrupt-parent = <&gpio5>;
- interrupts = <0 8>;
- };
- interrupt-parent指定父中断。interrupts第一个cells就是gpio编号,因此上面就是用的是gpio5_io00。
- 通过函数irq_of_parse_and_map从interrupt属性获取中断号。
编写实验驱动
- 编写设备树
- 按键消抖
Linux阻塞和非阻塞IO
- 阻塞与非阻塞简介
- 阻塞:当资源不可用的时候,应用程序就会刮起。当资源可用的时候,唤醒任务。应用程序使用open打开驱动文件,默认是阻塞方式打开。
- 非阻塞:当资源不可用的时候,应用程序轮询查看,或放弃。会有超时处理机制。应用程序在使用open打开驱动的时候,使用O_NOBLOCK。
- 等待队列
- 等待队列头:
- wait_queue_head_t需要定义一个。定义以后使用init_waitqueue_head函数进行初始化。或者使用宏DECLARE_WAIT_QUEUE_HEAD。
- 等待队列项
- wait_queue_t表示等待队列项,或者使用宏DECLARE_WAITQUEUE(name,task);
- 添加队列项到等待队列头
- add_wait_queue函数
- 移除等待队列项
- 资源可用的时候使用remove_wait_queue函数
- 唤醒
- wake_up函数唤醒等待队列头中所有的等待队列项
- 等待队列头:
驱动里面的poll函数
- unsigned int (*poll) (struct file *, struct poll_table_struct *);
- signal_pending函数用于判读当前进程是否有信号处理,返回值不为0的话表示有信号需要处理
异步通知
- 信号:
- 软件层次上的“中断”,也叫做软中断信号,软件层次上对中断机制的一种模拟
- kill -9 xxx //关闭某个应用
- 驱动中对异步通知的处理
- 要使用fasync_struct 定义一个指针结构体变量
- 实现file_operations里面的fasync函数。
- 驱动里面调用kill_fasync函数来向应用程序发送信号。
- 关闭驱动的时候要删除信号
Linux驱动分离与分层
- 以前的驱动实验都很简单,就是对IO的操作。
- 驱动的分离与分隔
- 单片机驱动开发,IIC设备MPU6050。
- 将驱动分离:主机控制器驱动和设备驱动,主机控制器一般是半导体厂商写的。
- 在linux驱动框架下编写具体的设备驱动
- 中间的联系就是核心层。
总线-驱动-设备
- 驱动-总线-设备
- 驱动:就是具体的设备驱动
- 设备:设备属性,包括地址范围,如果是IIC的话还有IIC器件地址,速度。
- 总线:
- 总线数据类型为:bus_type。向内核注册总线使用bus_register。
- 总线主要工作就是完成总线下的设备和驱动之间的匹配。
- 向linux内核注册总线:
- 使用函数bus_register
- 卸载总线 bus_unregister
- 驱动:
- 驱动和设备匹配以后驱动里面的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函数。
- 设备:
- 设备数据类型为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平台驱动模型
- 方便开发,linux提出了驱动的分离与分层。
- 进一步引出了驱动-总线-设备驱动模型,或者框架
- 对于SOC内部的RTC,timer等等,不好归结为具体的总线,为此linux提出了虚拟总线:platform总线,platform设备和platform驱动
- platform总线注册:
- platform_bus_init -- > bus_register
- 注册内容:
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
-
- 对于platform平台而言,platform_match函数负责驱动和设备的匹配。
- 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 -->
设置driver的probe位platform_drv_probe, //如果platform_driver的probe有效的话
--> driver_register
--> 执行device_drive --> probe,对于platform总线,也就是platform_drv_probe函数。而函数platform_drv_probe会执行platform_driver下的probe函数
结论;
向内核注册platform驱动的时候,如果驱动和设备匹配成功,最终会执行platform_driver的probe函数
-
- 设备:
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函数注册设备。
- 有设备树的时候,修改设备树的设备节点即可。
- 当设备与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属性值
-
实验程序编写
- 无设备树
- 两部分:platform_driver , platform_device。
- 编写向platform总线注册设备。编写驱动需要寄存器地址信息,地址信息使用设备信息,定义在platform_device里面,因此需要在驱动里面获取设备中的信息,或者叫做资源。使用函数platform_get_resource()。
- 有设备树
- 有设备树的时候设备是由设备树描述的,因此不需要向总线注册设备,而是直接修改设备树。只需要修改设备树,然后编写驱动。
- 驱动和设备匹配成功以后,设备信息就会从设备树节点转为platform_device结构体。
- platform提供了很多API函数去获取设备信息的。
内核自带LED驱动使能
- 内核自带的驱动,都是通过图形化配置,选择使能或者不使用。
- 输入 make menuconfig,使能驱动以后.config里面就会存在:CONFIG_LEDS_GPIO = y
- 在linux内核源码里面一般驱动文件夹下Makefile会使用CONFIG_XXX来决定要编译那个文件
- obj-$(CONFIG_LEDS_GPIO) += leds-gpio.o 也就是 obj - y +=leds-gpio.o ---> leds-gpio.c
内核自带LED驱动使用
- 首先将驱动编译进内核里面
- 根据绑定文档在设备树里面添加对应的设备节点信息。
如果无设备树,那么就要使用platform_device_register
内核自带MISC驱动简介
- MISC设备的主设备号为10。
- MISC设备会自动创建cdev,不需要手动创建
- MISC驱动编写的核心就是初始化miscdevice结构体变量。然后使用misc_register向内核注册,卸载驱动的时候使用misc_deregister.
- MISC驱动是基于platform平台
- 如果设备miscdevice里面minor为255的话,表示由内核自动分配一个次设备号。
INPUT子系统简介
- input子系统也是字符设备,input核心层会帮我们注册input字符设备驱动。
- input_dev:
- 申请并初始化并注册input_dev,使用input_allocate_device申请,evbit表示输入事件,比如按键对应的事件就是EV_KEY,那么还要加EV_REP。
- 设置按键对应的键值,也就是keybit。
- 初始化完成以后input_dev以后,需要向内核注册。使用input_register_device函数注册
- 按键按下以后上报事件,比如对于按键而言就是在按键中断服务函数,或者消抖定时器函数里面获取按键按下情况,并且上报:可以使用input_event函数。
- 对于按键而言,也可以使用:
- input_report_key
- 使用上面两个函数上报完成输入事件以后,还需要使用input_sync做同步
- 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;
-
- 编写应用程序:
- 按键驱动对应的文件就是 /dev/input/eventX (X = 0,1,2,3),应用程序读取/dev/input/event1来得到按键信息,也就是按键有没有被按下。
- 我们通过/dev/input/event1读到的信息就是input_event结构体形式的。
- Linux内核自带按键驱动程序使用:
- 配置内核,选中内核自带的KEY驱动程序,然后在 .config里面产生CONFIG_KEYBOARD_GPIO = y。经过查找内核自带的驱动程序为gpio_keys.c
- 这是一个标准的platform驱动
- 编写应用程序:
Linux LCD驱动实验
- Frambebuffer设备:
- RGB LCD屏幕,framebuffer是一种机制,应用程序操作驱动里面LCD显存的一种机制,因为应用程序需要通过操作显存来在LCD上显示字符,图片信息。
- 通过framebufer机制将底层的LCD抽象为 /dev/fbX(X = 0,1,2,3),应用程序通过操作fbX来操作屏幕
- framebuffer在内核中的表现就是 fb_info结构体,屏幕驱动重点就是初始化fb_info里面的各个成员变量。初始化完成fb_info以后,要通过register_framebuffer函数向内核注册刚刚初始化以后的的fb_info
- 卸载驱动的时候调用unregister_framerbuffer来卸载前面注册的fb_info。
- 内核LCD驱动简介:
- 驱动文件为mxsfb.c,为platform驱动框架,驱动和设备匹配以后,mxsfb_probe函数执行
- 给mxsfb_info申请内存,申请fb_info,然后将这两个联系起来
- host->base就是内存映射以后的LCDIF外设基地址。
- mxsfb_probe函数会调用 mxsfb_init_fbinfo函数初始化fb_info
- mxsfb_probe函数重点工作:
- 初始化fb_info并且向内核注册
- 初始化LCDIF控制器
- mxsfb_init_fbinfo_dt函数会从设备树中读取相关属性信息
- 驱动编写
- 屏幕引脚设置:
- 将屏幕引脚电气属性改为0x49,修改LCD引脚驱动能力
- 背光:
- 一般屏幕背光用PWM控制亮度,一般测试屏幕的时候直接将背光引脚拉高或拉低
- 屏幕引脚设置:
RTC驱动实验
- rtc_device结构体
- RTC也是一个标准字符设备驱动
rtc_device是RTC设备在内核中的具体实现,找到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>;
};
-
- 根据compatible找到驱动文件:rtc_snvs.c,驱动文件里面就是初始化rtc_device并注册
- rtc_device结构体里面重点是rtc_class_ops操作集
- IMX6U RTC驱动解析
- RTC驱动过程 在应用层调用ioctl函数,因为RTC还是一个字符设备,所以给应用层回应的函数是file_operations结构体里面的ioctl函数来实现的,但是要驱动RTC设备还要有一层,在file_operations中实现的ioctl函数,会调用rtc_class_ops中的函数如read_time,read_alarm等等。
- 驱动和设备匹配以后,snvs_rtc_probe函数会执行,NXP为6ULL的RTC外设创建了一个结构体snvs_rtc_data,此结构体里面包含了rtc_device成员变量
- 首先从设备树里面获取SNVS RTC外设寄存器,初始化RTC,申请中断处理闹钟,最后通过devm_rtc_device_register函数向内核注册rtc_device,重点是注册的时候设置了snvs_rtc_ops。
- 当应用通过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。
- RTC_ALM_SET就是设置闹钟 执行函数rtc_set_alarm --> rtc_timer_enqueue ---> __rtc_set_alarm ---> rtc->ops->set_alarm
I2C驱动框架
- 裸机下的I2C驱动框架
- 首先编写IIC控制器驱动,bsp_i2c.c和bsp_i2c.h为IIC外设驱动,向外提供i2c_master_transfer函数。
- I2C设备驱动
- I2C适配器在内核里面使用i2c_adapter结构体,IIC适配器驱动(控制器)核心就是申请i2c_adapter结构体,然后初始化,最后注册。
- 初始化完成i2c_adapter以后,使用i2c_add_adapter或者i2c_add_numbered_adapter来向内核注册I2C控制器驱动
- 在i2c_adapter里面有一个非常重要的成员变量:i2c_algorithm,此变量包含了IIC控制器访问IIC设备的接口函数,需要IIC适配器的编写人员实现。
- i2c_adpater -> i2c_algorithm ->master_xfer 此函数就是I2C控制器最终进行数据收发的函数
- 驱动与设备匹配以后i2c_imx_probe函数就会执行,NXP创建了一个结构体imx_i2c_struct包含IMX6ULL的I2C相关属性,
- 设置i2c_adapter下的i2c_algorithm为i2c_imx_algo实现算法,通过IMX6ULL的I2C控制器读取I2C或者向I2C设备写入数据的时候最终是通过i2c_imx_xfer实现的
- I2C设备驱动
- i2c_client表示i2c设备,不需要我们自己创建i2c_client,我们一般在设备树里面添加具体的I2C芯片,比如fsl,fxls8471,系统在解析设备树的时候就会知道有这个I2C设备,然后会创建对应的i2c_client。
- i2c设备驱动框架,i2c_driver初始化与注册,需要IIC设备驱动人员编写的。IIC驱动程序就是初始化i2c_driver,然后向系统注册使用函数i2c_register_driver或者i2c_add_driver。注销IIC使用i2c_del_driver。
- 在设备树中添加,I2C设备挂到那个I2C控制器下,就在那个控制器下添加对应的节点.
- 驱动编写
- 修改设备树,IO,添加AP3216C节点信息。UART4_RXD作为I2C1_SDA,UART4_TXD作为I2C1_SCL。
- 编写驱动框架,I2C设备驱动框架,字符设备驱动框架。
- 初始化AP3216C,实现ap3216c_read函数。
- 通过IIC控制器,来向AP3216C里面发送数据或者读取,这里使用i2c_transfer这个api函数来实现IIC数据的传输。
- 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驱动框架
- 裸机下的SPI驱动框架:
- spi具体芯片驱动:ICM20608
- Linux下的SPI驱动框架
- 设备驱动:具体的SPI芯片驱动
- 当spi控制器的设备和驱动匹配以后,spi_imx_probe函数就会执行
- SPI控制器驱动核心就是spi_master的构建,spi_master里面就有如何通过SPI控制器与SPI外设进行通信的函数,此函数是原厂编写
- spi_master -> transfer 函数和 spi_master -> transfer_one_message 函数 6ULL主机控制器使用transfer_one_message函数。
- spi_imx_data -> bitbang -> master -> setup_transfer ->txrx_bufs
- spi_master, 一般需要申请spi_alloc_master,释放的话就是spi_master_put。,初始化,最终使用函数spi_register_master注册。
- 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
- SPI接收
- SPI通过中断接受,中断处理函数为spi_imx_isr 此函数会调用spi_imx->rx(spi_imx)函数来完成具体的接受过程。
- SPI设备驱动
- SPI设备驱动就是具体的SPI芯片驱动,比如ICM2060.
- spi_device : 每个spi_device下都有一个spi_master。每个SPI设备,肯定挂载了一个SPI控制器,比如ICM20608挂载到了6ULL的ECSPI3接口上。
- spi_driver: 非常重要!申请或者定义一个spi_driver,然后初始化spi_driver中的各个成员变量,当SPI设备和驱动匹配以后,spi_driver下的probe函数就会执行。
- spi_driver初始化成功以后需要内核注册,函数为spi_register_driver,当注销驱动的时候需要spi_unregister_driver。
- 驱动编写:
- 修改设备树,添加IO相关信息
- 片选信号不作为硬件片选,而是作为普通的GPIO,我们在程序里面自行控制片选引脚
- 在ecspi3节点下传建icm20608子结点。
- 需要初始化icm20608芯片,然后从里面读取原始数据,这个过程就要用到如何使用linux内的SPI驱动的API函数来读取ICM20608
- 用到两个重要的结构体: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来发送信息
- NXP官方cs-gpios属性是软件片选!!!
串口驱动框架
- 两个重要的结构体uart_port和uart_driver
- uart_driver:需要驱动编写人员编写和注册,使用uart_register_driver注册到内核,卸载驱动的时候使用uart_unregister_driver卸载。
- uart_port:用于描述一个具体的串口端口,驱动编写人员需要实现uart_port,然后添加到内核里面去,使用uart_add_one_port函数向内核添加一个uart端口,卸载的时候使用uart_remove_one_port函数。里面有一个非常重要的成员变量uart_ops,此结构体包含了针对uart端口进行的所有操作,需要驱动编写人员实现。
- 串口驱动是和tty结合起来的
- NXP官方串口驱动入口函数为imx_serial_init。此函数会调用uart_register_driver 先向内核注册uart_driver,为imx_reg
- 为什么IMX6ULL的串口为 /dev/ttymxc0 和 /dev/ttymxc1,因为uart_device的dev_name来确定的。
- 接下来就是uart_port的处理,NXP自定义了一个imx_port,里面包含了uart_port。uart_ops为imx_pops。
- 串口接受中断处理函数 imx_rxint获取到串口接收到的数据,然后使用 tty_insert_flip_char 将其放到tty里面
minicom移植
- Linux下的软件移植,基本都是自己编译源码,步骤都是配置,然后编译,安装.
- 注意:
- 配置的时候会设置--prefix参数,也就是我们最终安装的位置,如果不设置的话就是默认安装位置。将编译出来的库放到开发板里面去
多点电容触摸屏驱动框架
- 电容触摸屏,上报多点触摸信息,通过触摸芯片,比如FT5426,这是一个IIC接口,多点电容触摸屏本质是IIC驱动
- 触摸IC一般都是有中断引脚,当检测到触摸信息以后就会触发中断,那么就要在中断处理函数里面读取触摸点信息
- 得到的触摸点信息,linux系统如何使用,input设备。linux系统下有触摸屏萨上报的流程,涉及到input子系统下触摸信息的上报。
- 触摸协议分为:TypeA 和 TypeB
- 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 //所有点发送完成以后发送这个事件
-
- 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
-
- 上报触摸信息是通过不同的事件来上报的:ABS_MT_XXX
- 驱动编写与测试
- 设备驱动主框架为IIC,会用到中断,在中断处理函数里面上报触摸点信息,要用到input子系统框架,
- 设备树IO修改,IIC节点添加
- 在I2C2节点下添加ft5426
- 主题I2C框架准备好
- 复位引脚和中断引脚,包括中断
- input子系统框架
- 初始化ft5426芯片
- 在中断服务函数里读取触摸坐标值,然后上报给系统