发布时间:2023-08-08 08:00
板子: 正点原子imx6ull
字符设备驱动的编写就是编写驱动对应的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是用来让内核查找设备是不是支持的
没有设备树的话要找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来设置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
<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 */
>;
};
....
};
通过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,
};
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
&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 = <®_sd1_vmmc>;
status = "okay";
//no-1-8-v;
};
gpio1: gpio@0209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
interrupts = ,
;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
* Freescale i.MX/MXC GPIO controller
Required properties:
- compatible : Should be "fsl,-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说明
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 = ;
};
};
};
驱动文件里:
获取设备节点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的并发与竞争【临界区保护】的处理更加复杂。
目的就是要保证临界区是原子访问的
介绍的有以下几种:
针对整形变量,实现了各种各样的原子操作【读-写-改等】
原子操作的意思是不能再拆分了,就是一个整体。
数据结构
typedef struct {
int counter;
} atomic_t;
列举了这些操作
针对位操作,也有一系列的API【位操作都是直接操作内存】
原子变量只能保护整形变量,局限性很大,这时候就需要锁机制出现了
自旋锁的意思就是当锁被一个线程持有,另一个线程需要获取锁时,就必须在原地等着锁被释放,进而访问共享资源。
使用的注意事项:
自旋锁还有不同的种类
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;
基于读写锁,顺序锁允许在写的时候进行读操作,也就是同时读写,但是不允许同时并发的写操作
如果在读的过程中发生了写操作,最好重新读取,保证数据完整性。另外保护的资源不能是指针,如果写的时候指针无效,如果读取恰好访问到指针就GG了。
数据结构
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;
这个原理性的就不多介绍了,用过普通的RTOS都应该知道
数据结构
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
这个原理性的就不多介绍了,用过普通的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//传入中断处理函数的参数指针
)
中断用完以后需要释放,使用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为了提高中断的响应与处理,将中断处理机制分为两部分处理
处理内容不希望被其他中断打断,放上半部
处理对时间敏感的任务,放上半部
之外的任务,放下半部
结构体定义
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是基于软中断实现的
对要推迟的函数进行组织的一种机制
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接口函数
int select
(
int nfds, //所要监视的这三类文件描述集合中,最大文件描述符加 1。
fd_set *readfds, //监视指定描述符集的读变化,NULL为不需要监测该项
fd_set *writefds,//监视指定描述符集的写变化,NULL为不需要监测该项
fd_set *exceptfds, //监视指定描述符集的异常变化,NULL为不需要监测该项
struct timeval *timeout//轮询超时,可以设置的单位为秒和微秒,NULL为无限等
)
返回值
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); //将对应的队列项从等待队列头中移除
...
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
)
返回值
一个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));//等待按键有效
}
...
传统的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//事件类型
)
返回值:
等待函数
int epoll_wait
(
int epfd, //要等待的 epoll
struct epoll_event *events,//指向 epoll_event 结构体的数组
int maxevents, //events 数组大小,必须大于 0。
int timeout//超时时间ms
)
返回值:
之前的两种方式阻塞和非阻塞
软件层次模拟硬件的中断,驱动程序主动向应用程序上报信号的方式表示自己可以被访问,而应用程序这边收到对应的信号旧可以从驱动程序读取/写入数据了。
信号有很多种,最最常用的信号就是SIGKILL【9】和SIGSTOP【19】
不同的信号对应不同的中断,对应的处理也不同,驱动给应用程序发不同的信号实现不同的功能
使用signal函数进行设置信号与信号的处理函数
sighandler_t signal(int signum, sighandler_t handler)
信号处理函数的原型为:
typedef void (*sighandler_t)(int)
fcbtl函数告知当前进程的进程号,进程的状态,启动异步通知功能
它具备五种功能
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);
}
不同的层负责不同的内容,例如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;
};
驱动和设备的匹配方式有四种
//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 总线下的驱动
要使用这个功能需要内核配置了LED的驱动功能,然后编译
-> Device Drivers
-> LED Support (NEW_LEDS [=y])
-> LED Support for GPIO connected LEDs
就驱动本身来说,就是正常的platform平台驱动架构的套路。
可选值如下:
//一个LED设备树范例
dtsleds {
compatible = "gpio-leds";
led0 {
label = "red";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
linux,default-trigger = "heartbeat";
default-state = "on";
};
};
杂项设备的主设备号都是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
这个函数可以代替一大堆cdev字符设备注册相关的函数
这个函数可以代替一大堆cdev字符设备注销相关的函数
和之前的杂项设备类似,都是针对某一类设备创建的框架,驱动编写者不需要关心应用层的事情,只需要上报输入事件就可以
系统分为
input设备的主设备号为13
类似其他子系统,也是注册/反注册这些套路
一个input事件包含两个基本要素
事件类型的定义
input_event函数
还可以用针对某种特定类型设备的上报函数
上报以后还需要告诉内核上报结束了
定义input_event 结构体变量,读取type【事件类型】,再读code【事件值】和value【输入值】
这个驱动也是框架式的,而且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保存
也就是SOC上I2C外设驱动
Linux中对I2C外设的抽象是i2c_adapter结构体和i2c_algorithm结构体
要使用I2C首先要注册I2C外设,用完也是要删除
也就是接的I2C设备的驱动
在Linux中i2c_client是描述i2c设备的,i2c_driver描述驱动内容
用法也是注册和删除
不使用设备树
使用i2c_board_info结构体描述具体I2C设备
必须要设置设备名和I2C器件地址
使用设备树
使用i2c_transfer函数收发,i2c_msg结构体构造对应的i2c消息
i2c_msg结构体
SPI在Linux中的使用和I2C有点类似
对于SOC来说,Linux为其定义了spi_master主机驱动结构体
这里面对SOC中比较重要的函数就是transfer和transfer_one_message了
而结构体本身的使用也是遵循那种“申请和释放”,“注册和销毁”的套路
这里引出了一个重要的结构体spi_message 和spi_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的辅助性的工具讲解
驱动部分没有提
用net_device结构体表示一个具体的网络设备,然后初始化完成以后的net_device注册到Linux内核中。
net_device_flag是网络接口标志,反映网络的状态
net_device结构体也是那种申请-注册-释放的套路。
net_device里有一个类似字符设备的函数指针集合的结构体net_device_ops结构体,定义了针对网络设备的各种函数。
在Linux中,网络数据以sk_buff结构体来传输,它包含了需要的各种信息。类似于LWIP的pbuf结构体那样,它是那种链表的玩法,增加/删除/申请/释放/调整大小。
Linux 里面的网络数据接收也轮询和中断两种
中断的好处就是响应快,数据量小的时候处理及时,速度快,但是一旦当数据量大,而且都是短帧的时候会导致中断频繁发生,消耗大量的 CPU 处理时间在中断自身处理上。
轮询恰好相反,响应没有中断及时,但是在处理大量数据的时候不需要消耗过多的 CPU 处理时间。
Linux 在这两个处理方式的基础上提出了另外一种网络数据接收的处理方法:NAPI(New API)。
核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。
用来描述一个磁盘设备
走的是申请-删除-注册到内核的玩法
多了一个设置容量。。
类似其他的设备,也是有一个操作集注册的结构体,注册操作函数
块设备没有read和write函数,是通过请求队列的方式实现的,请求队列里有很多请求,请求又包含bio,bio结构体又保存了相关的数据。
既然是队列,那么就要走请求,初始化,删除,分配的套路
多了一个绑定制造请求的函数,就是将申请到的request_queue与制造请求函数绑定在一起。
既然有了请求,那么就要获取请求->开启请求。。。
当然内核也设计了一步到位的处理请求函数
每个 request 里面里面会有多个 bio,bio 保存着最终要读写的数据、地址等信息。上层应用
程序对于块设备的都写会被构造成一个或多个 bio 结构,bio 结构描述了要读写的起始扇区、要
读写的扇区数量、是读取还是写入、页偏移、数据长度等等信息。