【Linux】imx6ull学习笔记

笔记

板子: 正点原子imx6ull

目录


Linux驱动开发篇

字符设备驱动

字符设备驱动的编写就是编写驱动对应的open,close,read,其实就是file_operations结构体的实现

linux驱动程序可以编译到内核,也可以编译到模块里,测试的时候只需要加载模块

编译出来zImage和dtb,得对应上
一定要确定zImage,设备树,uboot的对应

将.ko放到根文件系统里,加载用insmod,modprobe,移除用rmmod

对于一个新的模块,第一次需要调用depmod
驱动模块加载成功以后,可以用lsmod查看

我们需要向系统注册一个设备号,使用register_chrdev
卸载设备就需要unregister_chrdev
设备号用u32标识
Linux内核将设备号分为两个部分,主设备号,次设备号
从MAJOR或者MINOR获取主次设备号,不能冲突

如果用register_chrdev,对应主设备号下面的次设备号会全部占用,建议不用

Linux内核可以申请没有用的设备号
mknod生成设备节点文件

Linux下不能直接操作物理地址,因为有MMU

如果希望操作物理地址,一定要转换

ioremap[获取物理地址对应的虚拟地址]和ipunmap[卸载驱动的时候释放对应的虚拟地址]函数

不要直接操作内存,有专门的函数操作

cat /proc/devices //查看设备号
mknod /dev/xxx c yy zz 创建设备节点

以前的方法必须指定设备号,而且不能指定次设备号,次设备号会全部被用掉

新的方式相对于以前的方法来说不需要再手动指定设备号了,采用申请的方式,而且不需要再手动创建节点

alloc_chrdev_region让系统动态申请设备号
unregister_chrdev_region释放设备号
register_chrdev_region指定申请主设备号的设备号

字符设备注册

cdev结构体表示字符设备

cdev_init初始化
*cdev_init(struct cdev *cdev, const struct file_operations fops)
cdev_add添加设备
*cdev_add(struct cdev p, dev_t dev, unsigned count)

如何自动创建设备节点—mdev机制

简单说就是它提供了热插拔机制,自动创建设备节点 busybox会创建一个简化版本的mdev-udev

创建,删除类
文件私有数据,在open的时候设置

设备树

将板子信息写成独立的格式,扩展名dts,编译出来叫dtb
有dtc可以编译dts成dtb,uboot下载dtb到板子
dtsi是一款SOC的共有信息提取出来形成一个类似头文件的文件

dts是/开始的
/dts-v1/是dts文件标识

效果是叠加的
节点名字完整的要求
node-name@unit-address

unit-address一般是外设起始地址

系统启动以后可以看到根文件系统里节点的信息

cd /proc/device-tree

存放的就是根节点下的一级子节点
青色的标识文件夹(二级子节点),是可以进去看的,如果是白色的便是属性,是可以通过cat查看的
里面显示的都是名称,不是别名
和设备树文件结构是一致的,通用dtsi+你自己写的设备树的结构合并在一起
里面都是描述外设寄存器的信息

内核启动之后会解析设备树,然后再呈现在**/proc/device-tree**

aliases是别名的意思

chosen主要是将uboot里面的bootargs环境变量值传递给内核作为命令行参数
其中包含bootargs属性值,它和uboot中的一样

uboot通过bootz加载dtb,uboot拥有bootargs环境变量和dtb
在uboot里面fdt函数中会查找节点,再里面添加bootargs环境变量的值

特殊的属性

compatible属性

兼容性 值是字符串,用来描述兼容性 "厂家号,设备名"这样写 驱动要维护一个驱动兼容性列表

model描述模块信息

status描述设备的状态

#address-cells和#size_cells

#address-cells决定了字节点reg属性中地址信息所占用的字长

#size_cells决定了字节点reg属性中长度信息所占用的字长

在通用设备树文件里

aips2: aips-bus@02100000 {
	compatible = "fsl,aips-bus", "simple-bus";
	#address-cells = <1>;
	#size-cells = <1>;
	reg = <0x02100000 0x100000>;

一个节点里面的reg是由父节点里的#address-cells和#size_cells定义的

i2c1: i2c@021a0000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
	reg = <0x021a0000 0x4000>;
	interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_I2C1>;
	status = "disabled";
};

i2c1的父节点是aips2,所以其实i2c1这样写reg是没有写错的

我们再看到具体到板子的设备树

&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";

ap3216c@1e {
	compatible = "alientek,ap3216c";
	reg = <0x1e>;
};

这里看i2c1下接的设备ap3216c,reg的值(1e和@1e)就是正确的了

一个节点里面的reg是由父节点里的#address-cells和#size_cells定义的

spi4 {

	#address-cells = <1>;
	#size-cells = <1>;
	gpio_spi: gpio_spi@0x0111 {
		reg = <0x0111,0x100>;

	};

};

spi4 {

	#address-cells = <1>;
	#size-cells = <0>;
	gpio_spi: gpio_spi@0 {
		reg = <0>;

	};

};

reg属性

一般是描述内存地址和长度,也有不是的

举例

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";

	ap3216c@1e {
		compatible = "alientek,ap3216c";
		reg = <0x1e>;
	};
};

此时这个reg就是描述i2c地址的信息

range属性

一般是用于内存映射

aips1: aips-bus@02000000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
ranges;
}

但在ARM架构下似乎都是空的
就是说不存在地址映射

device_type属性

一般用于cpu或者memory

这个是i2c的一个例子,i2c4: i2c@021f8000 这种冒号前面是标签,后面才是名字
i2c4标签
i2c@021f8000是完整的名字

通过&i2c4来访问这个节点

i2c4: i2c@021f8000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-i2c","fsl,imx21-i2c";
	reg = <0x021f8000 0x4000>;
	interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_I2C4>;
	status = "disabled";
};

这个是挂在i2c总线上的一个设备的例子,1a是i2c的设备地址

i2c2这个是标签,定义在dtsi文件里

&i2c2 {
	clock_frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c2>;
	status = "okay";

	codec: wm8960@1a {
		compatible = "wlf,wm8960";
		reg = <0x1a>;
		clocks = <&clks IMX6UL_CLK_SAI2>;
		clock-names = "mclk";
		wlf,shared-lrclk;
	};
	....
};
compatible属性的特殊用法

根节点下面的compatible是用来让内核查找设备是不是支持的
没有设备树的话要找machine id,内核通过machine id找

例如

MACHINE_START(MX35_3DS, "Freescale MX35PDK")
	/* Maintainer: Freescale Semiconductor, Inc */
	.atag_offset = 0x100,
	.map_io = mx35_map_io,
	.init_early = imx35_init_early,
	.init_irq = mx35_init_irq,
	.init_time	= mx35pdk_timer_init,
	.init_machine = mx35_3ds_init,
	.reserve = mx35_3ds_reserve,
	.restart	= mxc_restart,
MACHINE_END


/*
 * Set of macros to define architecture features.  This is built into
 * a table by the linker.
 */
#define MACHINE_START(_type,_name)			\
static const struct machine_desc __mach_desc_##_type	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE_##_type,		\
	.name		= _name,

#define MACHINE_END				\
};

展开了以后实际上是一个完成初始化了的结构体,.nr属性就是machine id
放在一个内存段里

内核初始化后会去查表,看这个id再自己的表里面有没有,决定支持与否

使用设备树的话
mach-imx6ul.c里面定义了这样的东西

static const char *imx6ul_dt_compat[] __initconst = {
	"fsl,imx6ul",
	"fsl,imx6ull",
	NULL,
};

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


#define DT_MACHINE_START(_name, _namestr)		\
static const struct machine_desc __mach_desc_##_name	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= ~0,				\
	.name		= _namestr,

不管用没有用设备树,设备信息都是放到.arch.info.init段里

这种定义还是兼容的之前的machine id的
.nr=~0表示设备树不支持读machine id
imx6ul_dt_compat描述兼容属性

内核启动的时候会去这个段里,读然后循环查找能不能匹配到设备

绑定信息文档

Documentation/devicetree/bindings/ 可以看到各个厂家对设备树设备描述的说明

OF操作函数

驱动如何获取设备树中的节点信息 ->在驱动中用OF函数获取设备信息给驱动用

驱动要想获取到设备树内容,首先要找到节点

extern struct device_node *of_find_node_by_name(
struct device_node *from,
const char *name
);

of_find_node_by_name
static inline struct device_node *of_find_node_by_path(const char *path)

from从哪里开始查找,/就是头开始查找
name就是设备的名字

举例一个设备树路径
soc/aips-bus@02100000/i2c@021a0000/xxxxx

子节点,父节点关系查找

struct device_node *of_get_next_parent(struct device_node *node)

/***************************************************
 *函数参数及返回值的含义:
 *node:父节点。
 *prev:前一个子节点,表示从哪一个子节点开始查找下一个子节点;可以设置为NULL,表示从第一个子节点开始查找。
 *返回值:找到的下一个子节点。
 ***************************************************/
 static inline struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)

这个函数适合找那种父节点和子节点关系的设备树,那种父子关系的节点似乎不能直接用路径找,要先找到父节点,在用of_get_next_child找对应的字节点

找属性

const void *of_get_property(const struct device_node *np, const char *name,int *lenp)

name属性名字
lenp属性的字节数

获取指定标号的u32的值

int of_property_read_u32_index(const struct device_node *np,
				       const char *propname,
				       u32 index, u32 *out_value)
int of_property_read_u8_array(const struct device_node *np,
			const char *propname, u8 *out_values, size_t sz)
一个设备树文件的阅读
#include <>
#include "xxx.dtsi"
/
{
	//skeleton.dtsi
	#address-cells = <1>;
	#size-cells = <1>;
	chosen { };
	aliases {
		can0 = &flexcan1;
		can1 = &flexcan2;
		....
 };

	/*属性,属性名*/
	model = "Freescale i.MX6 ULL 14x14 EVK Board";
	compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
	/*节点*/
	chosen {
		stdout-path = &uart1;
	};
	/*开始地址0x80000000 长度0x20000000*/
	memory {
		device_type = "memory";//skeleton.dtsi
		reg = <0x80000000 0x20000000>;
	};
	/*节点下又有子节点*/
	reserved-memory {

		linux,cma {

		};
	};

	backlight {

	};

	pxp_v4l2 {

	};
	regulators {

	};
};

pinctrl子系统

借助pinctrl来设置pin的复用属性

  • 获取设备树的pin信息
  • 根据获取到的信息初始化pin功能
iomuxc_snvs: iomuxc-snvs@02290000 {
    compatible = "fsl,imx6ull-iomuxc-snvs";
    reg = <0x02290000 0x10000>;
};

iomuxc: iomuxc@020e0000 {
    compatible = "fsl,imx6ul-iomuxc";
    reg = <0x020e0000 0x4000>;
};
gpr: iomuxc-gpr@020e4000 {
    compatible = "fsl,imx6ul-iomuxc-gpr",
        "fsl,imx6q-iomuxc-gpr", "syscon";
    reg = <0x020e4000 0x4000>;
};

根据设备的类型创建对应的子节点,进行描述,然后设备所用的pin都放在此节点

我们以其中一个来举例说明

pinctrl_hog_1: hoggrp-1 {
    fsl,pins = <
        MX6UL_PAD_UART1_RTS_B__GPIO1_IO19	0x17059 /* SD1 CD */
    >;
};

我们在imx6ul-pinfunc.h找到对应的引脚的复用功能定义

#define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19                         0x0090 0x031C 0x0000 0x5 0x0

<mux_reg conf_reg input_reg mux_mode input_val>
<0x0090  0x031C    0x0000    0x5      0x0>

该节点的父节点是iomuxc@020e0000
对应到这个引脚的复用功能寄存器就是从0x020e0000开始,偏移0x0090,复用模式是5
电气属性配置寄存器的偏移是0x031C,对应的配置值0x17059
input_reg为0,说明这个io没有这个功能
input_val(写给input_reg的值)为0,说明这个io没有这个功能

以下是6ull板子的iomux的描述


&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 */
				MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID	0x13058	/* USB_OTG1_ID */
			>;
		};

		/* zuozhongkai BEEP */
		pinctrl_beep: beepgrp {
			fsl,pins = <
				MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 	0x10B0 /* beep */	
			>;
		};

		/* zuozhongkai KEY */
		pinctrl_key: keygrp {
			fsl,pins = <
				MX6UL_PAD_UART1_CTS_B__GPIO1_IO18		0xF080	/* KEY0 */
			>;
		};		

		/* zuozhongkai ECSPI */
		pinctrl_ecspi3: icm20608 {
			fsl,pins = < 
				MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20		0x10b0	/* CS */
				MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK	0x10b1	/* SCLK */
				MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO		0x10b1	/* MISO */
				MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI		0x10b1	/* MOSI */
			>;
		};
        ....
};

pinctrl驱动

通过compatible属性,是一串字符串列表
驱动文件里有一个描述驱动兼容性的结构体。当设备树节点里的compatible属性和驱动里的驱动兼容性列表一致的时候就说明找到了。
所以只需要内核全局搜索哪一个驱动里有符合要求的,就可以。
匹配以后执行probe函数(举例说明)

static struct of_device_id imx6ul_pinctrl_of_match[] = {
	{ .compatible = "fsl,imx6ul-iomuxc", .data = &imx6ul_pinctrl_info, },
	{ .compatible = "fsl,imx6ull-iomuxc-snvs", .data = &imx6ull_snvs_pinctrl_info, },
	{ /* sentinel */ }
};

static int imx6ul_pinctrl_probe(struct platform_device *pdev)
{
	const struct of_device_id *match;
	struct imx_pinctrl_soc_info *pinctrl_info;

	match = of_match_device(imx6ul_pinctrl_of_match, &pdev->dev);

	if (!match)
		return -ENODEV;

	pinctrl_info = (struct imx_pinctrl_soc_info *) match->data;

	return imx_pinctrl_probe(pdev, pinctrl_info);
}

static struct platform_driver imx6ul_pinctrl_driver = {
	.driver = {
		.name = "imx6ul-pinctrl",
		.owner = THIS_MODULE,
		.of_match_table = of_match_ptr(imx6ul_pinctrl_of_match),
	},
	.probe = imx6ul_pinctrl_probe,
	.remove = imx_pinctrl_remove,
};
pinctrl的调用路径

imxul_pinctrl_probe
-> imx_pinctrl_probe(初始化imx_pinctrl_desc结构体)
-> 向系统注册imx_pinctrl_desc
-> imx_pinctrl_probe_dt
-> imx_pinctrl_parse_groups
-> 把每一个设备树里面描述pin的值解析出来,存下来。
-> imx_pinconf_set函数设置pin的电气属性
-> imx_pmx_set函数设置pin的复用

gpio子系统

使用gpio子系统来使用gpio

&usdhc1 {
	pinctrl-names = "default", "state_100mhz", "state_200mhz";
	pinctrl-0 = <&pinctrl_usdhc1>;//根据SD卡速度的不同,分别做了三组初始化
	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";
	//no-1-8-v;
};

gpio1: gpio@0209c000 {
	compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
	reg = <0x0209c000 0x4000>;
	interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
				<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
	gpio-controller;
	#gpio-cells = <2>;
	interrupt-controller;
	#interrupt-cells = <2>;
};

* Freescale i.MX/MXC GPIO controller

Required properties:
- compatible : Should be "fsl,<soc>-gpio"
- reg : Address and length of the register set for the device
- interrupts : Should be the port interrupt shared by all 32 pins, if
  one number.  If two numbers, the first one is the interrupt shared
  by low 16 pins and the second one is for high 16 pins.
- gpio-controller : Marks the device node as a gpio controller.
- #gpio-cells : Should be two.  The first cell is the pin number(编号) and
  the second cell is used to specify the gpio polarity:
      0 = active high(高电平有效)
      1 = active low(低电平有效)
- interrupt-controller: Marks the device node as an interrupt controller.
- #interrupt-cells : Should be 2.  The first cell is the GPIO number.
  The second cell bits[3:0] is used to specify trigger type and level flags:
      1 = low-to-high edge triggered.
      2 = high-to-low edge triggered.
      4 = active high level-sensitive.
      8 = active low level-sensitive.

Example:

gpio0: gpio@73f84000 {
	compatible = "fsl,imx51-gpio", "fsl,imx35-gpio";
	reg = <0x73f84000 0x4000>;
	interrupts = <50 51>;
	gpio-controller;
	#gpio-cells = <2>;
	interrupt-controller;
	#interrupt-cells = <2>;
};

定义了cd-gpios属性,<&gpio1 19 GPIO_ACTIVE_LOW>这一段是属性,GPIO1,引脚19,低电平有效
此处使用GPIO控制器
去看他们家的banding说明

如何获取设备树中节点的信息
  1. 获取到gpio所处的设备节点,譬如of_find_node_by_path
  2. 之后通过这个函数获取gpio编号
  3. 用编号去申请gpio,用完之后释放掉
  4. 设置gpio的方向
  5. 读取输入/输出高低电平

gpiolib.c是一个连接应用层和底层驱动的接口
它提供gpioadd等函数用于连接具体的soc厂家,譬如gpio-mxc.c

mxc_gpio_probe
mxc_gpio_get_hw(获取6ull的寄存器组)
bgpio_init
gpiochip_add(向系统添加gpio)

使用gpio子系统下,我们要这么写
设备树下:

&iomuxc {
	imx6ul-evk
	{
		//添加一个子节点,设置pinctrl属性
		pinctrl_gpioled: ledgrp{
			fsl,pins = <MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0>;
		};
	};
	

};

驱动文件里:
获取设备节点gpioled
of_find_node_by_path
新建一个设备节点指针gpioled.nd来存放得到的节点信息
of_get_named_gpio
新建一个gpio编号gpioled.led_gpio来存放得到的io号
申请gpio,申请失败的话,去看一看设备树里有没有io冲突
之后就可以设置IO口的输入输出了
gpio_direction_output
其他和之前的设备树io口驱动一样
输出高低电平将使用gpio_set_value函数来控制IO口

并发与竞争

相比比较简单的操作系统来说,Linux的并发与竞争【临界区保护】的处理更加复杂。

  • 多线程并发访问
  • 抢占式并发访问
  • 中断程序并发访问
  • SMP核间并发访问

目的就是要保证临界区是原子访问的

Linux解决并发与竞争的工具

介绍的有以下几种:

  1. 原子变量操作
  2. 自旋锁
  3. 信号量
  4. 互斥量
原子操作

针对整形变量,实现了各种各样的原子操作【读-写-改等】
原子操作的意思是不能再拆分了,就是一个整体。

数据结构

 typedef struct {
 int counter;
 } atomic_t;

列举了这些操作
在这里插入图片描述
针对位操作,也有一系列的API【位操作都是直接操作内存】
在这里插入图片描述

自旋锁

原子变量只能保护整形变量,局限性很大,这时候就需要锁机制出现了

自旋锁的意思就是当锁被一个线程持有,另一个线程需要获取锁时,就必须在原地等着锁被释放,进而访问共享资源。

使用的注意事项:

  1. 锁持有时间不要过长,会降低系统的性能
  2. 锁机制保护的临界区内不能调用任何导致线程休眠的API,会导致系统死锁
  3. 不能递归申请自旋锁
  4. 考虑一致性,建议一律按照多核处理器编写驱动

自旋锁还有不同的种类
5. 基础自旋锁
6. 读写自旋锁【多见于Linux系统编程】
7. 顺序自旋锁【多见于Linux系统编程】

基础自旋锁

数据结构

typedef struct spinlock {
union {
struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;

基本的API函数

由于系统运行中断状态非常复杂,建议不要使用不处理中断的自旋锁API,容易造成系统死锁

在这里插入图片描述
涉及到中断处理的自旋锁API
在这里插入图片描述
下半部处理函数【这个现在的章节没有介绍~】
在这里插入图片描述

读写自旋锁

一种一次只能一个写,但是允许多个并发读取的锁机制

简单说就是读取的时候没有线程修改,修改的时候没有线程读取就可以

数据结构

typedef struct {
 arch_rwlock_t raw_lock; 
 } rwlock_t;

API有这些
在这里插入图片描述

顺序自旋锁

基于读写锁,顺序锁允许在写的时候进行读操作,也就是同时读写,但是不允许同时并发的写操作

如果在读的过程中发生了写操作,最好重新读取,保证数据完整性。另外保护的资源不能是指针,如果写的时候指针无效,如果读取恰好访问到指针就GG了。

数据结构

typedef struct {
 arch_rwlock_t raw_lock; 
 } rwlock_t;

API有这些
在这里插入图片描述

信号量

这个原理性的就不多介绍了,用过普通的RTOS都应该知道

数据结构

struct semaphore {
 raw_spinlock_t lock;
 unsigned int count;
 struct list_head wait_list;
};

API列表
在这里插入图片描述

互斥量

这个原理性的就不多介绍了,用过普通的RTOS都应该知道

数据结构

struct mutex {
 /* 1: unlocked, 0: locked, negative: locked, possible waiters */
 atomic_t count;
 spinlock_t wait_lock;
};

这个有使用注意事项

  • 使用互斥体可以导致休眠,中断里面就不能用,只能用自旋锁
  • 互斥体保护的临界区内可以调用引起阻塞的函数
  • 一次只有一个线程可以持有互斥体,持有者才能释放,不能递归上锁解锁

定时器

中断

申请中断

每一个中断都有中断号,使用中断首先需要使用request_irq申请

申请函数可能导致休眠,禁止在中断上下文或者其他禁止睡眠的代码段中使用该函数

int request_irq(
		   unsigned int irq,//中断号
           irq_handler_t handler,//中断处理函数
           unsigned long flags, //中断处理的标志
           const char *devname, //中断名
           void *dev_id//传入中断处理函数的参数指针
           )
中断处理的标志
  • IRQF_DISABLED:
    内核在处理中断处理程序本身的时候,禁止了其他所有的中断。
  • IRQF_TIMER
    系统定时器的中断处理
  • IRQF_SHARED
    多个中断处理程序之间可以共享中断线
释放中断

中断用完以后需要释放,使用free_irq函数

void free_irq(
unsigned int irq, //要释放的中断号
void *dev_id//要卸载的中断服务函数
)
中断服务函数

其函数原型是irqreturn_t (*irq_handler_t)(int,void *)

开关中断

分为全局开关和指定中断开关,这个和MCU类似

void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)

local_irq_enable()
local_irq_disable()

void disable_irq_nosave(unsigned int irq)

当然,开关中断也是会有临界区保护的问题的,所以Linux也提供了对应的API

local_irq_save(flags)
local_irq_restore(flags)

上半部与下半部

与MCU不同,Linux为了提高中断的响应与处理,将中断处理机制分为两部分处理

  • 上半部
  • 下半部
    上半部类似于MCU的中断处理函数,里面处理实时的操作
    下半部是处理比较需要耗时间的操作

处理内容不希望被其他中断打断,放上半部
处理对时间敏感的任务,放上半部
之外的任务,放下半部

下半部机制

软中断机制

结构体定义

struct softirq_action
{
	void (*action)(struct softirq_acion *);
};
默认情况下,一共定义了10个软中断
```c
static struct softirq_acion softirq[NR_SOFTIRQS];

这10个软中断的定义如下:

enum  
{  
    HI_SOFTIRQ=0, //优先级最高的软中断,用于tasklet  
    TIMER_SOFTIRQ,  //定时器的软中断
    NET_TX_SOFTIRQ, //发送网络数据的软中断  
    NET_RX_SOFTIRQ,  
    BLOCK_SOFTIRQ,  
    BLOCK_IOPOLL_SOFTIRQ,  
    TASKLET_SOFTIRQ, //tasklet软中断  
    SCHED_SOFTIRQ,  
    HRTIMER_SOFTIRQ,  
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */  
  
    NR_SOFTIRQS   //该枚举值就是当前Linux内核允许注册的最大软中断数  
};

要使用软中断,需要先初始化软中断,这一步在内核中已经完成了
用户只需要打开并注册对应的软中断就可以了

void open_softirq(unsigned int nr,void(*action)(struct softirq_action *));
void raise_softirq(unsigned int nr);

Tasklet微线程

Tasklet是基于软中断实现的
对要推迟的函数进行组织的一种机制

struct tasklet_struct {
struct tasklet_struct *next;        /*指向链表中的下一个结构*/
     unsigned long state;            /* 小任务的状态 */
     atomic_t count;    /* 引用计数器 */
     void (*func) (unsigned long);            /* 要调用的函数 */
     unsigned long data;           /* 传递给函数的参数 */
}

类似于软中断,也是要初始化,但是不一样的是在上半部里需要调用tasklet_schedule来调度tasklet在合适的时间执行

使用的模板

struct tasklet_struct my_tasklet;

void my_tasklet_handler(unsigned long data)
{

}

irqreturn_t my_irq(int irq,void *dev_id)
{
	tasklet_schedule(&my_tasklet);
}

static int __init xxxx_init(void)
{
	tasklet_init(&my_tasklet,my_tasklet_handler,data);
	request_irq(xxx_irq,my_irq,0,"xxx",&xxx_dev);
}

工作队列

在进程上下文执行,所以可以允许睡眠或者重新调度

结构体定义

struct work_struct
{
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
};

用法和Tasklet类似,也是要在上半调用一个调度函数的
使用的模板

struct work_struct my_work;

void my_work_handler(unsigned long data)
{

}

irqreturn_t my_irq(int irq,void *dev_id)
{
	schedule_work(&my_work);
}

static int __init xxxx_init(void)
{
	INIT_WORK(&my_work,my_tasklet_handler);
	request_irq(xxx_irq,my_irq,0,"xxx",&xxx_dev);
}

设备树的中断信息节点

也是of函数读取设备树的中b断配置信息
怎么写去看binding
在imx6ul.dtsi中
找到intc【中断控制器节点】

intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
//第一个Cell中断类型,0是SPI中断,1是PPI中断
//第二个Cell中断类型,中断号
//第三个Cell中断类型,bit[3:0]表示触发类型
//1是上升沿触发
//2是下降沿触发
//4是高电平触发
//8是低电平触发
interrupt-controller;//表示是中断控制器
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};

找到gpio5【GPIO节点,可作为中断】

gpio5: gpio@020ac000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x020ac000 0x4000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
//SPI中断 中断号74 高电平触发【这个高低电平触发在这里不起作用的,到具体的IO中断才起作用】
<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
/*CELL为2!!*/
};

去imx6ull-alientek-emmc.dts里找到

fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt-parent = <&gpio5>;//父中断gpio5
interrupts = <0 8>;//引脚号0 8是低电平触发【这里起作用】
};

获取中断号的方法就是irq_of_parse_and_map 或者gpio_to_irq【特定GPIO中断】

阻塞与非阻塞

概念定义

阻塞

当应用层无法获取到设备的使用权的时候,会将对应的线程挂起,直到资源可获取为止
在这里插入图片描述

非阻塞

当应用层无法获取到设备的使用权的时候,会一直轮询等待,不断的尝试读取,返回故障码,直到资源可获取为止
应用层需要在打开设备的时候,选择非阻塞打开

fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */

在这里插入图片描述

阻塞访问的实现
等待队列

为了实现在设备不可操作的时候让线程进入休眠,就必须要让线程挂起,之后可以操作的时候唤醒【一般在中断里面唤醒】
如果要使用等待队列,就必须要在驱动里面定义并初始化一个等待队列头

等待队列头
 struct __wait_queue_head {
 spinlock_t lock;
 struct list_head task_list;
 };
 typedef struct __wait_queue_head wait_queue_head_t;

//初始化等待队列头
void init_waitqueue_head(wait_queue_head_t *q)
//宏初始化
DECLARE_WAIT_QUEUE_HEAD
等待队列项

每一个进入等待队列的进程都是一个队列项,当设备不可访问的时候就要把线程挂到等待队列项上

struct __wait_queue {
 unsigned int flags;
 void *private;
 wait_queue_func_t func;
 struct list_head task_list;
 };
typedef struct __wait_queue wait_queue_t;
//宏初始化,name是等待队列的名字,tsk表示等待队列属于哪一个任务,一般是current,表示当前进程。
DECLARE_WAITQUEUE(name, tsk)

添加/删除等待队列项

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

等待的进程唤醒

void wake_up(wait_queue_head_t *q)//可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程
void wake_up_interruptible(wait_queue_head_t *q)//只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程

等待事件满足唤醒
在这里插入图片描述
一个读按键值的例子

static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue=0;
	unsigned char releasekey=0;
	struct key_dev *dev = (struct key_dev *)filp->private_data;

	//定义队列项
	DECLARE_WAITQUEUE(wait,current);//定义等待队列项 
	
	//按键没有按下,进入休眠状态
	add_wait_queue(&dev->r_wait,&wait);
	//设置当前进程可被打断
	__set_current_state(TASK_INTERRUPTIBLE);
	//切换
	schedule();

	//唤醒以后从这里运行
	//判断当前进程是不是有信号处理
	if(signal_pending(current))
	{
		ret = -ERESTART;
		goto out;
	}

	__set_current_state(TASK_RUNNING);//设置当前线程为运行状态
	remove_wait_queue(&dev->r_wait,&wait);//将对应的队列项从等待队列头中移除
	

	keyvalue = atomic_read(&dev->keyvalue);//读取按键值
	releasekey = atomic_read(&dev->releasekey);//读取释放值

	if(releasekey)//有效按键值,已经出现释放了
	{
		if(keyvalue & 0x80)//如果已经按下
		{
			keyvalue&=~0x80;//标志重新设置
			// printk("[APP]KEY0 push    %x\r\n",keyvalue);
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		}
		else
		{
			ret = -EINVAL;
			goto data_err;
		}
		atomic_set(&dev->releasekey,0);//按下标志清0
	}
	else
	{
		ret = -EINVAL;
		goto data_err;//不是有效按键
	}
	
	return ret;
out:
	__set_current_state(TASK_RUNNING);//设置当前线程为运行状态
	remove_wait_queue(&dev->r_wait,&wait);//将对应的队列项从等待队列头中移除
data_err:
	return ret;
}
非阻塞访问的实现

应用程序需要非阻塞访问,那么驱动也要实现非阻塞才行,也就是实现poll接口函数

select函数
int select
(
int nfds, //所要监视的这三类文件描述集合中,最大文件描述符加 1。
fd_set *readfds, //监视指定描述符集的读变化,NULL为不需要监测该项
fd_set *writefds,//监视指定描述符集的写变化,NULL为不需要监测该项
fd_set *exceptfds, //监视指定描述符集的异常变化,NULL为不需要监测该项
struct timeval *timeout//轮询超时,可以设置的单位为秒和微秒,NULL为无限等
)

返回值

  • 0 超时
  • -1 错误
  • 其他 可以操作的文件描述符个数

fd_set变量用之前需要进行一定的设置,用的时候也会需要读取。

void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)

一个select的例子【应用层部分】

	while (1)
	{

		FD_ZERO(&readfds);
		FD_SET(fd, &readfds);

		timeout.tv_sec = 1;
		timeout.tv_usec = 500000; //500ms

		//select
		ret = select(fd + 1, &readfds, NULL, NULL, &timeout);

		switch (ret)
		{
		case 0:
		{
			//超时
			printf("timeout !!\r\n");
			break;
		}
		case -1:
		{
			//错误
			break;
		}
		default:
		{
			//可以读取
			if (FD_ISSET(fd, &readfds))
			{
				ret = read(fd, &keyvalue, sizeof(keyvalue));
				if (ret < 0)
				{
				}
				else
				{
					if (keyvalue > 0)
					{													 /* KEY0 */
						printf("KEY0 Press, value = %#X\r\n", keyvalue); /* 按下 */
					}
				}
			}
			break;
		}
		}
	}

一个select的例子【驱动层部分】

	// 	/* 阻塞的方式 */
	// 	//定义队列项
...
	 	DECLARE_WAITQUEUE(wait, current); //定义等待队列项

	 	//按键没有按下,进入休眠状态
	 	add_wait_queue(&dev->r_wait, &wait);
	 	//设置当前进程可被打断
	 	__set_current_state(TASK_INTERRUPTIBLE);
	 	//切换
	 	schedule();

	 	//唤醒以后从这里运行
	 	//判断当前进程是不是有信号处理
	 	if (signal_pending(current))
	 	{
	 		ret = -ERESTART;
	 		goto out;
	 	}

	 	 __set_current_state(TASK_RUNNING);		//设置当前线程为运行状态
	 	 remove_wait_queue(&dev->r_wait, &wait); //将对应的队列项从等待队列头中移除
...
poll函数

select函数是有限制的,监视的描述符最多1024,于是poll函数便出现了,但是poll没有限制
poll函数原型

struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
 short revents; /* 返回的事件 */
};
// POLLIN 有数据可以读取。
// POLLPRI 有紧急的数据需要读取。
// POLLOUT 可以写数据。
// POLLERR 指定的文件描述符发生错误。
// POLLHUP 指定的文件描述符挂起。
// POLLNVAL 无效的请求。
// POLLRDNORM 等同于 POLLIN

int poll
(
struct pollfd *fds, //要监视的文件描述符集合以及要监视的事件
nfds_t nfds, //poll 函数要监视的文件描述符数量。
int timeout
)

返回值

  • 0 超时
  • <0 错误
  • 其他 发生事件的文件描述符个数

一个poll的例子【应用层部分】

	//循环读取按键值数据
	while (1)
	{

		fds.fd = fd;
		fds.events = POLLIN;//监视读事件
		ret = poll(&fds,1,500);
		if(ret==0)
		{
			//超时
		}
		else if(ret<0)
		{	
			//错误
		}
		else
		{
			//正常
			//可以读取
			if (fds.events|POLLIN)
			{
				ret = read(fd, &keyvalue, sizeof(keyvalue));
				if (ret < 0)
				{
				}
				else
				{
					if (keyvalue > 0)
					{													 /* KEY0 */
						printf("KEY0 Press, value = %#X\r\n", keyvalue); /* 按下 */
					}
				}
			}
		}
		
	}

一个poll的例子【驱动层部分】

...
	if (filp->f_flags & O_NONBLOCK)
	{
		//非阻塞的方式
		if(atomic_read(&dev->releasekey)==0)
		{
			//说明按键无效
			return -EAGAIN;
		}
	}
	else
	{
		wait_event_interruptible(dev->r_wait,atomic_read(&dev->releasekey));//等待按键有效
	}
...
epoll函数

传统的poll在高并发下不好用,又出现了epoll

int epoll_create(int size)
//size无意义,大于0就可以

用epoll_ctl函数添加需要监视的文件描述符与事件

//操作
// EPOLL_CTL_ADD 向 epfd 添加文件参数 fd 表示的描述符。
// EPOLL_CTL_MOD 修改参数 fd 的 event 事件。
// EPOLL_CTL_DEL 从 epfd 中删除 fd 描述符。

/* epoll 事件 */
// EPOLLIN 有数据可以读取。
// EPOLLOUT 可以写数据
// EPOLLPRI 有紧急的数据需要读取。
// EPOLLERR 指定的文件描述符发生错误。
// EPOLLHUP 指定的文件描述符挂起。
// EPOLLET 设置 epoll 为边沿触发,默认触发模式为水平触发。
// EPOLLONESHOT 一次性的监视,当监视完成以后还需要再次监视某个 fd,那么就需要将fd 重新添加到 epoll 里面。

struct epoll_event {
uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户数据 */
};

int epoll_ctl
(
 int epfd, //create返回的句柄
 int op, //操作
 int fd,//文件描述符
 struct epoll_event *event//事件类型
 )

返回值:

  • 0,成功;
  • -1,失败,并且设置 errno 的值为相应的错误码。

等待函数

int epoll_wait
(
int epfd, //要等待的 epoll
struct epoll_event *events,//指向 epoll_event 结构体的数组
int maxevents, //events 数组大小,必须大于 0。
int timeout//超时时间ms
)

返回值:

  • 0,成功;
  • -1,失败,并且设置 errno 的值为相应的错误码。
  • 其他 发生事件的文件描述符个数

异步通知

之前的两种方式阻塞非阻塞

  • 休眠等待设备可用
  • 不断轮询查看设备是否可用
    这两种方式都需要应用程序主动查询,信号的概念便应运而生。系统通过信号来模拟硬件的中断的效果。

什么是信号

软件层次模拟硬件的中断,驱动程序主动向应用程序上报信号的方式表示自己可以被访问,而应用程序这边收到对应的信号旧可以从驱动程序读取/写入数据了。

信号有很多种,最最常用的信号就是SIGKILL【9】和SIGSTOP【19】

  • 杀死进程的SIGKILL【9】信号
  • CTRL+C发送应用层序的SIGSTOP【19】信号

不同的信号对应不同的中断,对应的处理也不同,驱动给应用程序发不同的信号实现不同的功能

如何使用信号

应用层代码

使用signal函数进行设置信号与信号的处理函数

sighandler_t signal(int signum, sighandler_t handler)

信号处理函数的原型为:

typedef void (*sighandler_t)(int)

fcbtl函数告知当前进程的进程号,进程的状态,启动异步通知功能
它具备五种功能

  1. 复制一个现有的描述符(cmd=F_DUPFD).
  2. 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  3. 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  4. 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  5. 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);         
int fcntl(int fd, int cmd, struct flock *lock);

应用层使用范例[需要调用以下函数]

fcntl(fd, F_SETOWN, getpid()); /* 将当前进程的进程号告诉给内核 */
flags = fcntl(fd, F_GETFD); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC);/* 设置进程启用异步通知功能 */
驱动层代码

file_operations需要实现fasync和release函数,并在合适的位置释放信号
release函数
应用程序调用 close 函数关闭驱动设备文件的时候此函数就会执行,在此函数中释放掉 fasync_struct 指针变量

 static int imx6uirq_release(struct inode *inode, struct file *filp)
 {
 	return imx6uirq_fasync(-1, filp, 0);
 }

fasync函数
调用一下 fasync_helper

 static int imx6uirq_fasync(int fd, struct file *filp, int on)
 {
 	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
 	//初始化前面定义的 fasync_struct 结构体
 	return fasync_helper(fd, filp, on, &dev->async_queue);
 }

释放信号[本例是在按键处理的定时器回调函数里面,检测到完整的一次按键过程出现后]

if(dev->async_queue)
{
	kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}

Platform驱动开发

为什么要引入平台的概念

  • 提高系统代码的重用性
  • 分离驱动与系统,简化驱动程序的设计
    在这里插入图片描述

驱动的分层

不同的层负责不同的内容,例如input层负责所有与输入的有关的驱动。
Linux引入了

  • 总线
  • 驱动
  • 设备

总线

总线的定义

struct bus_type { 
	const char *name; /* 总线名字 */
	const char *dev_name; 
	struct device *dev_root; 5 struct device_attribute *dev_attrs; 6 const struct attribute_group **bus_groups; /* 总线属性 */
	const struct attribute_group **dev_groups; /* 设备属性 */
	const struct attribute_group **drv_groups; /* 驱动属性 */

	int (*match)(struct device *dev, struct device_driver *drv);
	/*match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备*/
	/*device 和 device_driver 类型,也就是设备和驱动。*/
	int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
	int (*probe)(struct device *dev);
	int (*remove)(struct device *dev);
	void (*shutdown)(struct device *dev);

	int (*online)(struct device *dev);
	int (*offline)(struct device *dev);
	int (*suspend)(struct device *dev, pm_message_t state);
	int (*resume)(struct device *dev);
	const struct dev_pm_ops *pm;
	const struct iommu_ops *iommu_ops;
	struct subsys_private *p;
	struct lock_class_key lock_key;
};

驱动和设备的匹配方式有四种

  • OF函数
  • ACPI匹配
  • id_table匹配
  • 直接匹配驱动和设备的name

驱动

//platform_driver 
 struct platform_driver 
 { 
    /*设备与驱动匹配后会执行的probe函数,需要驱动设计者实现*/
	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; 
	/*id table表*/
	const struct platform_device_id *id_table; 
	bool prevent_deferred_probe;
};


//platform_device_id 
struct platform_device_id { 
	char name[PLATFORM_NAME_SIZE];
	kernel_ulong_t driver_data;
};

//device_driver 
struct device_driver { 
	const char *name; 
	struct bus_type *bus; 
	struct module *owner; 
	const char *mod_name; /* used for built-in modules */
	bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
	const struct of_device_id *of_match_table;//of_device_id 
	const struct acpi_device_id *acpi_match_table;
	int (*probe) (struct device *dev);
	int (*remove) (struct device *dev);
	void (*shutdown) (struct device *dev);
	int (*suspend) (struct device *dev, pm_message_t state);
	int (*resume) (struct device *dev);
	const struct attribute_group **groups;
	const struct dev_pm_ops *pm;
	struct driver_private *p;
};

//of_device_id 
 struct of_device_id 
 { 
	 char name[32];
	 char type[32];
	 //通过设备节点的 compatible 属性值和 of_match_table 中每个项目的 compatible 成员变量进行比较
	 char compatible[128];
	 const void *data; 
 };

在编写platform驱动的时候,先要定义一个platform_driver结构体变量,实现内部的哥哥方法,设备与驱动匹配成功后probe函数执行,可以在里面初始化驱动等等。。。

会用到下面的函数

int platform_driver_register (struct platform_driver *driver)
void platform_driver_unregister(struct platform_driver *drv)

设备

如果内核支持设备树的话就不要再使用 platform_device 来描述设备了,因为改用设备树去描述了

设备的定义

//platform_device 
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;
};

//resource 
struct resource {
	resource_size_t start;//起始地址
	resource_size_t end;//结束地址
	const char *name;
	unsigned long flags;//资源类型
	struct resource *parent, *sibling, *child;
};

在不支持设备树的Linux中,用户需要编写platform_device描述设备信息,用platform_device_register 函数将设备信息注册到 Linux 内核中

会用到下面的函数

int platform_device_register(struct platform_device *pdev)
void platform_device_unregister(struct platform_device *pdev)

在这种模式下,设备和驱动分离,要各自编写一个模块文件ko并加载

成功加载之后
查看/sys/bus/platform/devices目录下保存着当前板子 platform 总线下的设备
查看/sys/bus/platform/drivers目录下保存着当前板子 platform 总线下的驱动

Linux系统下的LED灯驱动

要使用这个功能需要内核配置了LED的驱动功能,然后编译

-> Device Drivers
->  LED Support (NEW_LEDS [=y])
->    LED Support for GPIO connected LEDs

就驱动本身来说,就是正常的platform平台驱动架构的套路。

使用自带的LED驱动的注意事项
  1. 设备树中的LED的compatible属性必须为gpio-leds
  2. 设置label属性,一般用来区分LED的颜色,名字等等
  3. 每个LED字节点必须设置gpios的属性值
  4. 设置default-state属性【on,off,keep】,其中keep表示保持
  5. 设置linux,default-trigger属性值

可选值如下:

  1. backlight: LED 灯作为背光。
  2. default-on: LED 灯打开
  3. heartbeat: LED 灯作为心跳指示灯,可以作为系统运行提示灯。
  4. ide-disk: LED 灯作为硬盘活动指示灯。
  5. timer: LED 灯周期性闪烁,由定时器驱动,闪烁频率可以修改
//一个LED设备树范例
    dtsleds {
    compatible = "gpio-leds";
    led0 {
    label = "red";
    gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
    linux,default-trigger = "heartbeat";
    default-state = "on";
    };
   };

MISC杂项设备驱动

杂项设备的主设备号都是10,不同设备使用不同的次设备号,MISC设备支持自动创建cdev,这个比纯粹的platform驱动要方便多了。

miscdevice设备结构体

struct miscdevice {
int minor; /* 子设备号 */
const char *name; /* 设备名字 */
const struct file_operations *fops; /* 设备操作集 */
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};

主要关注minor,name,fops这三个字段,需要用户实现这三个字段。
如果连次设备号都不想自己想,Linux已经自带了一些次设备号

#define PSMOUSE_MINOR 1
#define MS_BUSMOUSE_MINOR 2 /* unused */
#define ATIXL_BUSMOUSE_MINOR 3 /* unused */
/*#define AMIGAMOUSE_MINOR 4 FIXME OBSOLETE */
#define ATARIMOUSE_MINOR 5 /* unused */
#define SUN_MOUSE_MINOR 6 /* unused */
...
#define MISC_DYNAMIC_MINOR 255
MISC杂项设备下的API函数
int misc_register(struct miscdevice * misc)

这个函数可以代替一大堆cdev字符设备注册相关的函数

int misc_deregister(struct miscdevice *misc)

这个函数可以代替一大堆cdev字符设备注销相关的函数

INPUT子系统

和之前的杂项设备类似,都是针对某一类设备创建的框架,驱动编写者不需要关心应用层的事情,只需要上报输入事件就可以
系统分为

  1. input驱动层【输入设备的驱动程序】
  2. input核心层【承上启下,为驱动提供接口,为上层提供通知】
  3. input事件处理层【与用户空间进行交互】

input设备的主设备号为13
类似其他子系统,也是注册/反注册这些套路

注册INPUT子系统

一个input事件包含两个基本要素

  1. 输入事件类型evbit
  2. 输入事件值xxxbit,譬如keybit
    linux里面有若干大数组【位图】对应各种类型,然后数组中的每一个元素对应输入的事件值
    输入设备的定义

下面有好多数组【位图】
在这里插入图片描述

事件类型的定义

在这里插入图片描述

事件值的定义
在这里插入图片描述

如何定义input设备
  1. input_allocate_device申请一个input_dev
  2. 初始化要用到的事件类型evbit和事件值keybit,调用 input_register_device
  3. 用完以后,卸载驱动的时候调用input_unregister_device,然后释放input_free_device
如何设置事件和事件值

三种方法
在这里插入图片描述

如何上报输入事件

input_event函数
在这里插入图片描述
还可以用针对某种特定类型设备的上报函数
在这里插入图片描述
上报以后还需要告诉内核上报结束了
在这里插入图片描述
在这里插入图片描述

如何在应用层读取输入

定义input_event 结构体变量,读取type【事件类型】,再读code【事件值】和value【输入值】
在这里插入图片描述
在这里插入图片描述

如何在用户空间用input设备

在这里插入图片描述

如何在使用内核自带的按键驱动
  1. 编译Kconfig选了GPIO Button
  2. 设备树要按要求填,而且IO不能有冲突
  3. 就驱动本身,就是platform驱动的套路

在这里插入图片描述

设备树的要求

主要是前三点!!!
在这里插入图片描述

查看按键值的另类方法

hexdump /dev/input/event1
在这里插入图片描述

RTC时钟

这个驱动也是框架式的,而且imx6ull已经帮编了
RTC设备被抽象成rtc_device 结构体,走的也是申请,初始化,注册rtc_device_register,反注册 rtc_device_unregister的玩法
在这里插入图片描述
主要看rtc_class_ops,这个包含RTC设备的最底层设备操作函数集合,需要用户编写
在这里插入图片描述
Linux的通用驱动里面的玩法也是和字符设备的套路类似
在这里插入图片描述

rtc_dev_ioctl 最终会通过操作 rtc_class_ops 中的 read_time、set_time 等函数来对具体 RTC 设备
的读写操作

套路如下
在这里插入图片描述
SOC自带的驱动也是一个标准的platform驱动,但是它操作底层用了regmap来操作底层寄存器

一套方便的 API 函数去操作底层硬件寄存器,以提高代码的可重用性

驱动本身走的也是初始化,申请中断,等等的基本流程

用户空间如何查看时间

date命令

用户空间如何设置时间并保存

date -s “时间”,然后hwclock -w保存

I2C

I2C总线驱动

也就是SOC上I2C外设驱动
Linux中对I2C外设的抽象是i2c_adapter结构体和i2c_algorithm结构体
要使用I2C首先要注册I2C外设,用完也是要删除
在这里插入图片描述

I2C设备驱动

也就是接的I2C设备的驱动
在Linux中i2c_client是描述i2c设备的,i2c_driver描述驱动内容
用法也是注册和删除
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

怎么匹配上设备和驱动

不使用设备树
使用i2c_board_info结构体描述具体I2C设备
必须要设置设备名和I2C器件地址
在这里插入图片描述
使用设备树
在这里插入图片描述
在这里插入图片描述

怎么进行收发

使用i2c_transfer函数收发,i2c_msg结构体构造对应的i2c消息
在这里插入图片描述
i2c_msg结构体
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

SPI

SPI在Linux中的使用和I2C有点类似
对于SOC来说,Linux为其定义了spi_master主机驱动结构体
在这里插入图片描述
这里面对SOC中比较重要的函数就是transfertransfer_one_message
而结构体本身的使用也是遵循那种“申请和释放”,“注册和销毁”的套路
在这里插入图片描述
在这里插入图片描述
这里引出了一个重要的结构体spi_messagespi_driver
spi_driver
在这里插入图片描述
类似I2C,也是需要用户去实现这些函数,然后遵循“注册和销毁”的套路
在这里插入图片描述
对于设备和驱动的匹配
也是类似于I2C,分设备树和非设备树
对于imx6ull,其中的一个SPI外设的设备树和设备节点的范例如下:

ecspi3: ecspi@02010000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
	reg = <0x02010000 0x4000>;
	interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ECSPI3>,
	<&clks IMX6UL_CLK_ECSPI3>;
	clock-names = "ipg", "per";
	dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
	dma-names = "rx", "tx";
	status = "disabled";
};


&ecspi1 {
	fsl,spi-num-chipselects = <1>;
	cs-gpios = <&gpio4 9 0>;//片选信号信息,如果这么写就是Linux驱动接管片选
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi1>;//使用的gpio信息
	status = "okay";
		flash: m25p80@0 {//这里0表示使用ECSPI的0通道
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "st,m25p32";
		spi-max-frequency = <20000000>;//SPI总线的频率
		reg = <0>;//这里0表示使用ECSPI的0通道
		};
};

&ecspi3 {
	fsl,spi-num-chipselects = <1>;
	cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>; //可以写cs-gpio这样就是自己控制片选
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi3>;
	status = "okay";
		spidev: icm20608@0 {
		compatible = "alientek,icm20608";
		spi-max-frequency = <8000000>;
		reg = <0>;
		};
};

spi_message结构体
在这里插入图片描述
用之前也是初始化
在这里插入图片描述
发送的话就把填写好的结构体,添加到发送队列的尾部
在这里插入图片描述
必须选同步或者异步传输,也就是等待传输完成还是不等待传输完成
在这里插入图片描述
在完成上面这些以后就可以编写函数进行SPI通信了
在这里插入图片描述

串口

这个也是SOC已经写好了的

串口驱动在进入系统以后,对应生成/dev/ttymxcX(X=0….n)文件

在Linux中,串口驱动用uart_driver结构体来表示
在这里插入图片描述
每一个串口也是遵循“注册和销毁”的套路。

int uart_register_driver(struct uart_driver *drv)注册,void uart_unregister_driver(struct uart_driver *drv)销毁

每一个串口的定义在uart_port结构体
在这里插入图片描述
通过uart_add_one_port函数连接驱动和具体的串口,删除的时候用uart_remove_one_port。
在这里插入图片描述

对应串口肯定要很多操作函数,在uart_ops结构体中定义了这些操作函数,NXP的开发人员就会利用这个编写对应imx的串口驱动。

struct uart_ops {
	unsigned int (*tx_empty)(struct uart_port *);
	void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
	unsigned int (*get_mctrl)(struct uart_port *);
	void (*stop_tx)(struct uart_port *);
	void (*start_tx)(struct uart_port *);
	void (*throttle)(struct uart_port *);
	void (*unthrottle)(struct uart_port *);
	void (*send_xchar)(struct uart_port *, char ch);
	void (*stop_rx)(struct uart_port *);
	void (*enable_ms)(struct uart_port *);
	void (*break_ctl)(struct uart_port *, int ctl);
	int (*startup)(struct uart_port *);
	void (*shutdown)(struct uart_port *);
	void (*flush_buffer)(struct uart_port *);
	void (*set_termios)(struct uart_port *, struct ktermios *new,
	struct ktermios *old);
	void (*set_ldisc)(struct uart_port *, struct ktermios *);
	void (*pm)(struct uart_port *, unsigned int state,
	unsigned int oldstate);

	/*
	* Return a string describing the type of the port
	*/
	const char *(*type)(struct uart_port *);

	/*
	* Release IO and memory resources used by the port.
	* This includes iounmap if necessary.
	*/
	void (*release_port)(struct uart_port *);

	/*
	* Request IO and memory resources used by the port.
	* This includes iomapping the port if necessary.
	*/
	int (*request_port)(struct uart_port *);
	void (*config_port)(struct uart_port *, int);
	int (*verify_port)(struct uart_port *, struct serial_struct *);
	int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
	#ifdef CONFIG_CONSOLE_POLL
	int (*poll_init)(struct uart_port *);
	void (*poll_put_char)(struct uart_port *, unsigned char);
	int (*poll_get_char)(struct uart_port *);
	#endif
};

后面就是讲串口的小工具minicom的编译和安装了

CAN

教程上也没写太多的了
主要是CAN的辅助性的工具讲解
驱动部分没有提
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

网络部分

驱动框架

用net_device结构体表示一个具体的网络设备,然后初始化完成以后的net_device注册到Linux内核中。

net_device_flag是网络接口标志,反映网络的状态
在这里插入图片描述
net_device结构体也是那种申请-注册-释放的套路。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

net_device里有一个类似字符设备的函数指针集合的结构体net_device_ops结构体,定义了针对网络设备的各种函数。
在这里插入图片描述
在Linux中,网络数据以sk_buff结构体来传输,它包含了需要的各种信息。类似于LWIP的pbuf结构体那样,它是那种链表的玩法,增加/删除/申请/释放/调整大小。

NAPI处理机制

Linux 里面的网络数据接收也轮询和中断两种

中断的好处就是响应快,数据量小的时候处理及时,速度快,但是一旦当数据量大,而且都是短帧的时候会导致中断频繁发生,消耗大量的 CPU 处理时间在中断自身处理上。

轮询恰好相反,响应没有中断及时,但是在处理大量数据的时候不需要消耗过多的 CPU 处理时间。

Linux 在这两个处理方式的基础上提出了另外一种网络数据接收的处理方法:NAPI(New API)。

核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。

NAPI的套路是初始化,删除,使能,关闭。
在这里插入图片描述
在这里插入图片描述

网络外设的设备树

在这里插入图片描述
在这里插入图片描述
后面的驱动函数详细解释,不打算写了。

块设备

  • 块设备只能以块为单位进行读写访问
  • 在结构上是可以进行随机访问的,对于设备的读写按块进行,块设备使用缓冲区来暂时存放数据,等待满足一定条件之后才写入真正的物理存储设备之中,减少块设备的擦除次数,提高块设备的寿命。
  • IO算法与其他没有机械结构的设备不同
block_device结构体

在这里插入图片描述
在这里插入图片描述
也是那套注册-注销的玩法

gendisk结构体

用来描述一个磁盘设备
在这里插入图片描述
走的是申请-删除-注册到内核的玩法
多了一个设置容量。。
类似其他的设备,也是有一个操作集注册的结构体,注册操作函数

块设备IO请求过程

块设备没有read和write函数,是通过请求队列的方式实现的,请求队列里有很多请求,请求又包含bio,bio结构体又保存了相关的数据。
既然是队列,那么就要走请求,初始化,删除,分配的套路
多了一个绑定制造请求的函数,就是将申请到的request_queue与制造请求函数绑定在一起。
既然有了请求,那么就要获取请求->开启请求。。。
当然内核也设计了一步到位的处理请求函数

bio结构

每个 request 里面里面会有多个 bio,bio 保存着最终要读写的数据、地址等信息。上层应用
程序对于块设备的都写会被构造成一个或多个 bio 结构,bio 结构描述了要读写的起始扇区、要
读写的扇区数量、是读取还是写入、页偏移、数据长度等等信息。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值