Linux知识整理

Linux驱动

字符设备驱动框架

​ 字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

image-20210807221217663
字符设备结构体

在 Linux 中使用 cdev 结构体表示一个字符设备

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;	//设备文件操作集合
	struct list_head list;
	dev_t dev;	//设备号
	unsigned int count;	//设备数目
};

其中dev是设备号,包含主设备号和次设备号信息。主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备 。Linux系统中用dev_t来定义设备号,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。其中高 12 位为主设备号, 低 20 位为次设备号。

设备号的分配

**静态分配:**设备号由驱动开发者静态的指定,通过函数register_chrdev_region向内核申请使用,若该设备号已经被使用,则申请失败。

**动态分配:**在注册字符设备之前先使用alloc_chrdev_region 函数申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。

注销字符设备之后要释放掉设备号,设备号释放函数 unregister_chrdev_region

设备文件操作函数集合 struct file_operations *ops

struct file_operations *ops 是函数指针集合,定义能够在设备上进行的操作

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);//用于修改文件当前的读写位置
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//用于读取设备文件。
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//用于向设备文件写入(发送)数据
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);//轮询函数,用于查询设备是否可以进行非阻塞的读写
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//提供对于设备的控制功能,与应用程序中的 ioctl 函数对应
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);//用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
	int (*mremap)(struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);//用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
};
字符驱动初始化步骤

1、定义字符设备结构体struct cdev

2、使用 void cdev_init(struct cdev *cdev, const struct file_operations *fops)函数对cdev经行初始化

3、使用int cdev_add(struct cdev *p, dev_t dev, unsigned count) 函数向linux系统添加字符设备。

4、创建设备节点

  1. 创建类:使用struct class *class_create (struct module *owner, const char *name) 函数创建类
  2. 创建设备:使用 struct device *device_create(....) 函数在类下面创建设备

5、设置文件私有数据

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等 ,在编写驱动的时候可以将其写成一个结构体,编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中。

/* 设备结构体 */
struct test_dev{
	dev_t devid; /* 设备号 */
	struct cdev cdev; /* cdev */
	struct class *class; /* 类 */
	struct device *device; /* 设备 */
	int major; /* 主设备号 */
	int minor; /* 次设备号 */
};
struct test_dev testdev;
/* open 函数 */
static int test_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &testdev; /* 设置私有数据 */
	return 0;
}

在 open 函数里面设置好私有数据以后,在 write、 read、 close 等函数中直接读取 private_data即可得到设备结构体。

驱动注销
/* 注销字符设备 */
cdev_del(&newchrled.cdev);/* 删除 cdev */
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);//注销设备号devid
device_destroy(newchrled.class, newchrled.devid);//删除设备device
class_destroy(newchrled.class);//删除类class
完整的驱动范例
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define DTSLED_CNT			1		  	/* 设备号个数 */
#define DTSLED_NAME			"dtsled"	/* 名字 */
#define LEDOFF 					0			/* 关灯 */
#define LEDON 					1			/* 开灯 */

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* dtsled设备结构体 */
struct dtsled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
};

struct dtsled_dev dtsled;	/* led设备 */

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &dtsled; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations dtsled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	u32 val = 0;
	int ret;
	u32 regdata[14];
	const char *str;
	struct property *proper;

	/* 获取设备树中的属性数据 */
	/* 1、获取设备节点:alphaled */
	dtsled.nd = of_find_node_by_path("/alphaled");

	/* 初始化LED */
	。。。。
        
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (dtsled.major) {		/*  定义了设备号 */
		dtsled.devid = MKDEV(dtsled.major, 0);
		register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME);	/* 申请设备号 */
		dtsled.major = MAJOR(dtsled.devid);	/* 获取分配号的主设备号 */
		dtsled.minor = MINOR(dtsled.devid);	/* 获取分配号的次设备号 */
	}
	printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);	
	
	/* 2、初始化cdev */
	dtsled.cdev.owner = THIS_MODULE;
	cdev_init(&dtsled.cdev, &dtsled_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

	/* 4、创建类 */
	dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
	if (IS_ERR(dtsled.class)) {
		return PTR_ERR(dtsled.class);
	}

	/* 5、创建设备 */
	dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
	if (IS_ERR(dtsled.device)) {
		return PTR_ERR(dtsled.device);
	}
	
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
	/* 取消映射 */
    
	/* 注销字符设备驱动 */
	cdev_del(&dtsled.cdev);/*  删除cdev */
	unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */

	device_destroy(dtsled.class, dtsled.devid);
	class_destroy(dtsled.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("sqy");

pinctrl和GPIO子系统

传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。 pinctrl 子系统就是为了解决这个问题而引入的, pinctrl 子系统主要工作内容如下:

​ ①、获取设备树中 pin 信息。
​ ②、根据获取到的 pin 信息来设置 pin 的复用功能
​ ③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等

使用的时候需要在设备树下的iomuxc节点下追加pinctrl子节点,如下图

&iomuxc {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_hog_1>;
	imx6ul-evk {
        /*  LED */
        pinctrl_led: ledgrp {
            fsl,pins = <
            	MX6UL_PAD_GPIO1_IO03__GPIO1_IO03        0x10B0 /* LED0 */
            >;
        };
    ......
    };

MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0MX6UL_PAD_GPIO1_IO03复用为GPIO1_IO03,电气属性配置被0x10b0

gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等。驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO 。

如下图设备树根节点下的LED设备节点

gpioled {
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "atkalpha-gpioled";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_led>;		//添加pinctrl节点
		led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;	//gpio相关信息
		status = "okay";
	};

gpio 子系统常用API函数

int gpio_request(unsigned gpio, const char *label);	//用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request进行申请
void gpio_free(unsigned gpio);	//如果不使用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放
int gpio_direction_input(unsigned gpio);	//用于设置某个 GPIO 为输入
int gpio_direction_output(unsigned gpio, int value);	//用于设置某个 GPIO 为输出
int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);

Linux 内核提供了几个与 GPIO 有关的 OF 函数,常用的几个 OF 函数如下:

int of_gpio_named_count(struct device_node *np, const char *propname)//用于获取设备树某个属性里面定义了几个 GPIO 信息
int of_gpio_count(struct device_node *np);//和 of_gpio_named_count 函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属性的 GPIO 数量,
int of_get_named_gpio(struct device_node *np,const char *propname,int index);	//此函数获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的 GPIO 编号

使用pinctrl子系统和gpio子系统,在设备树添加pinctrl节点和字符设备节点中添加gpio信息就可以从设备树中获取节点并初始化。

	/* 设置BEEP所使用的GPIO */
	/* 1、获取设备节点:beep */
	beep.nd = of_find_node_by_path("/beep");

	/* 2、 获取设备树中的gpio属性,得到BEEP所使用的gpio编号 */
	beep.beep_gpio = of_get_named_gpio(beep.nd, "beep-gpio", 0);
	printk("led-gpio num = %d\r\n", beep.beep_gpio);

	/* 3、设置GPIO5_IO01为输出,并且输出高电平,默认关闭BEEP */
	ret = gpio_direction_output(beep.beep_gpio, 1);

Linux中断

​ 在裸机中使用中断我们需要做一大堆的工作,比如配置寄存器,使能 IRQ 等等。 Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可。

Linux中断框架

1、向设备树里设备节点中添加中断信息

interrupt-parent = <&gpio1>;	//设置 interrupt-parent 属性值为“gpio1”,也就是设置当前中断的 GPIO 中断控制器为 gpio1。
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;	//设置 interrupts 属性,也就是设置中断源,第一个 cells 的 18 表示 GPIO1 组的 18号 IO。 IRQ_TYPE_EDGE_BOTH 表示中断触发方式为边沿触发

2、获取中断号

​ 每个中断都有一个中断号,通过中断号即可区分不同的中断 ,在 Linux 内核中使用一个 int 变量表示中断号 。

​ 中断信息已经写到了设备树里面,因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号。 函数返回中断号。

​ 如果使用 GPIO 的话,可以使用int gpio_to_irq(unsigned int gpio) 函数来获取 gpio 对应的中断号 。

3、申请中断

​ 在 Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断, request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。 request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断 。

int request_irq(unsigned int irq,	//中断号
				irq_handler_t handler,	//中断服务函数
				unsigned long flags,	//中断标志
				const char *name,	//中断名字
				void *dev)	//一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数

使用free_irq 函数释放掉相应的中断。free_irq 会删除中断处理函数并且禁止中断 。

4、编写中断处理函数

irqreturn_t (*irq_handler_t) (int, void *) 第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。

中断上半部和下半部

软中断和硬中断

1 .软中断是执行中断指令产生的,而硬中断是由外设引发的。
2 .硬中断的中断号是由中断控制器提供的,软中断的中断号由指令直接指出,无需使用中断控制器。
3 .硬中断是可屏蔽的,软中断不可屏蔽。
4 .硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为上半部。
5 .软中断处理硬中断未完成的工作,是一种推后执行的机制,属于下半部。

​ Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,上半部处理时不可以被中断。剩下的所有工作都可以放到下半部去执行 。

​ ①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。

​ ②、如果要处理的任务对时间敏感,可以放到上半部。

​ ③、如果要处理的任务与硬件有关,可以放到上半部
​ ④、除了上述三点以外的其他任务,优先考虑放到下半部

中断下半部实现:软中断、tasklet、工作队列。

总线设备驱动框架

platform驱动

​ platform 驱动框架分为总线、设备和驱动,其中总线是 Linux 内核提供的,在使用设备树的时候,设备的描述被放到了设备树中,因此 platform_device 就不需要我们去编写了,我们只需要实现 platform_driver 即可。

1、在设备树中创建设备节点

​ 先在设备树中创建设备节点来描述设备信息,重点是要设置好 compatible属性的值,因为 platform 总线需要通过设备节点的 compatible 属性值来匹配驱动!

2、编写 platform 驱动的时候要注意兼容属性

​ 使用设备树的时候 platform 驱动会通过 of_match_table 来保存兼容性值,也就是表明此驱动兼容哪些设备。所以, of_match_table 将会尤为重要

3、编写 platform 驱动

​ 当驱动和设备匹配成功以后就会执行 probe 函数。我们需要在 probe 函数里面执行字符设备驱动那一套,当注销驱动模块的时候 remove 函数就会执行。

设备树下的platform驱动范例如下

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define LEDDEV_CNT		1				/* 设备号长度 	*/
#define LEDDEV_NAME		"dtsplatled"	/* 设备名字 	*/
#define LEDOFF 			0
#define LEDON 			1

/* leddev设备结构体 */
struct leddev_dev{
	dev_t devid;				/* 设备号	*/
	struct cdev cdev;			/* cdev		*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备		*/
	int major;					/* 主设备号	*/	
	struct device_node *node;	/* LED设备节点 */
	int led0;					/* LED灯GPIO标号 */
};

struct leddev_dev leddev; 		/* led设备 */

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led0_switch(u8 sta)
{
	if (sta == LEDON )
		gpio_set_value(leddev.led0, 0);
	else if (sta == LEDOFF)
		gpio_set_value(leddev.led0, 1);	
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &leddev; /* 设置私有数据  */
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[2];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {

		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	
	ledstat = databuf[0];
	if (ledstat == LEDON) {
		led0_switch(LEDON);
	} else if (ledstat == LEDOFF) {
		led0_switch(LEDOFF);
	}
	return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.write = led_write,
};

/*
 * @description		: flatform驱动的probe函数,当驱动与
 * 					  设备匹配以后此函数就会执行
 * @param - dev 	: platform设备
 * @return 			: 0,成功;其他负值,失败
 */
static int led_probe(struct platform_device *dev)
{	
	printk("led driver and device was matched!\r\n");
	/* 1、设置设备号 */
	if (leddev.major) {
		leddev.devid = MKDEV(leddev.major, 0);
		register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
	} else {
		alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
		leddev.major = MAJOR(leddev.devid);
	}

	/* 2、注册设备      */
	cdev_init(&leddev.cdev, &led_fops);
	cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);

	/* 3、创建类      */
	leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
	if (IS_ERR(leddev.class)) {
		return PTR_ERR(leddev.class);
	}

	/* 4、创建设备 */
	leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
	if (IS_ERR(leddev.device)) {
		return PTR_ERR(leddev.device);
	}

	/* 5、初始化IO */	
	leddev.node = of_find_node_by_path("/gpioled");
	if (leddev.node == NULL){
		printk("gpioled node nost find!\r\n");
		return -EINVAL;
	} 
	
	leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);
	if (leddev.led0 < 0) {
		printk("can't get led-gpio\r\n");
		return -EINVAL;
	}

	gpio_request(leddev.led0, "led0");
	gpio_direction_output(leddev.led0, 1); /* led0 IO设置为输出,默认高电平	*/
	return 0;
}

/*
 * @description		: platform驱动的remove函数,移除platform驱动的时候此函数会执行
 * @param - dev 	: platform设备
 * @return 			: 0,成功;其他负值,失败
 */
static int led_remove(struct platform_device *dev)
{
	gpio_set_value(leddev.led0, 1); 	/* 卸载驱动的时候关闭LED */

	cdev_del(&leddev.cdev);				/*  删除cdev */
	unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */
	device_destroy(leddev.class, leddev.devid);
	class_destroy(leddev.class);
	return 0;
}

/* 匹配列表 */
static const struct of_device_id led_of_match[] = {
	{ .compatible = "atkalpha-gpioled" },
	{ /* Sentinel */ }
};

/* platform驱动结构体 */
static struct platform_driver led_driver = {
	.driver		= {
		.name	= "imx6ul-led",			/* 驱动名字,用于和设备匹配 */
		.of_match_table	= led_of_match, /* 设备树匹配表 		 */
	},
	.probe		= led_probe,
	.remove		= led_remove,
};
		
/*
 * @description	: 驱动模块加载函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init leddriver_init(void)
{
	return platform_driver_register(&led_driver);
}

/*
 * @description	: 驱动模块卸载函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit leddriver_exit(void)
{
	platform_driver_unregister(&led_driver);
}

module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("sqy");

I2C驱动

Linux内核将 I2C 驱动分为两部分:
①、 I2C 总线驱动, I2C 总线驱动就是 SOC 的 I2C 控制器驱动,也叫做 I2C 适配器驱动。
②、 I2C 设备驱动, I2C 设备驱动就是针对具体的 I2C 设备而编写的驱动。

I2C 总线驱动

Linux系统编程

进程

init进程

ps -aux显示当前系统上运行的所有进程

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.7 0.1 119792 6016 ? Ss 21:35 0:06 /sbin/init

​ init进程为1号进程,是祖先进程。可以把init进程看作为操作系统的进程管理器,它是其他所有进程的祖先进程。 我们将要看到的其他系统进程要么是由init进程启动的,要么是由被init进程启动的其他进程启动的。

程序与进程

程序(program)是一个普通文件,是为了完成特定任务而准备好的指令序列与数据的集合, 这些指令和数据以”可执行映像”的格式保存在磁盘中。

进程(process)则是程序执行的具体实例,那么在程序执行的过程中,它享有系统的资源, 至少包括进程的运行环境、CPU、外设、内存、进程ID等资源与信息。程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行, 这种执行的程序称之为进程,也就是说进程是系统进行资源分配和调度的一个独立单位, 每个进程都有自己单独的地址空间。

程序转变为进程

正如我们运行一个程序(可执行文件),通常在Shell中输入命令运行就可以了, 在这运行的过程中包含了程序到进程转换的过程,整个转换过程主要包含以下3个步骤:

  1. 查找命令对应程序文件的位置。
  2. 使用 fork()函数为启动一个新进程。
  3. 在新进程中调用 exec族函数装载程序文件,并执行程序文件中的main()函数。
进程状态转换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xduvOBA3-1629002288876)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210808222100316.png)]

创建进程

在Linux中启动一个进程有多种方法,比如可以使用system()函数, 也可以使用fork()函数去启动

fork()函数:创建一个子进程

fork()函数用于从一个已存在的进程中启动一个新进程, 新进程称为子进程,而原进程称为父进程。

在父进程中的fork()调用后返回的是新的子进程的PID。子进程中的fork()函数调用后返回的是0

子进程与父进程一致的内容:

  • 进程的地址空间。
  • 进程上下文、代码段。
  • 进程堆空间、栈空间,内存信息。
  • 进程的环境变量。
  • 标准 IO 的缓冲区。
  • 打开的文件描述符。
  • 信号响应函数。
  • 当前工作路径。

子进程独有的内容:

  • 进程号 PID。 PID 是身份证号码,是进程的唯一标识符。
  • 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
  • 挂起的信号。这些信号是已经响应但尚未处理的信号,也就是”悬挂”的信号, 子进程也不会继承这些信号。

exec系列函数

​ 使用fork()函数启动一个子进程是并没有太大作用的,因为子进程跟父进程都是一样的,exec系列函数可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段, 在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换。

​ exec系列函数是直接将当前进程给替换掉的, 当调用exec系列函数后,当前进程将不会再继续执行。因此我们可以通过调用fork()复制启动一个子进程,并且在子进程中调用exec系列函数替换子进程, 这样把fork()和exec系列函数结合在一起使用就是创建一个新进程所需要的一切了。

int execl(const char *path, const char *arg, ...)
int execlp(const char *file, const char *arg, ...)
int execle(const char *path, const char *arg, ..., char *const envp[])
int execv(const char *path, char *const argv[])
int execvp(const char *file, char *const argv[])
int execve(const char *path, char *const argv[], char *const envp[])
  • 名称包含 l 字母的函数(execl、execlp和execle)接收参数列表“list”作为调用程序的参数。
  • 名称包含 p 字母的函数(execvp 和 execlp)可接受一个程序名作为参数, 它会在当前的执行路径和环境变量“PATH”中搜索并执行这个程序(即可使用相对路径); 名字不包含p字母的函数在调用时必须指定程序的完整路径(即要求绝对路径)。
  • 名称包含 v 字母的函数(execv、execvp 和 execve)的子程序参数通过一个数组“vector”装载。
  • 名称包含 e 字母的函数(execve 和 execle)比其它函数多接收一个指明环境变量列表的参数, 并且可以通过参数envp传递字符串数组作为新程序的环境变量, 这个envp参数的格式应为一个以 NULL 指针作为结束标记的字符串数组, 每个字符串应该表示为“environment = virables”的形式。
终止进程

正常终止

  • 从main函数返回。
  • 调用exit()函数终止。
  • 调用_exit()函数终止。

异常终止

  • 调用abort()函数异常终止。
  • 由系统信号终止。
proces014

​ 从图中可以看出,_exit()函数的作用最为简单:直接通过系统调用使进程终止运行,在终止进程的时候会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构;exit()函数在调用exit系统调用之前要检查文件的打开情况, 把文件缓冲区中的内容写回文件,这就是“清除I/O缓冲”

等待进程
pid_t wait(int *wstatus);

​ wait()函数在被调用的时候,系统将暂停父进程的执行,直到有信号来到或子进程结束, 如果在调用wait()函数时子进程已经结束,则会立即返回子进程结束状态值。 子进程的结束状态信息会由参数wstatus返回,与此同时该函数会返子进程的PID, 它通常是已经结束运行的子进程的PID。

pid_t waitpid(pid_t pid, int *wstatus, int options);

​ waitpid()函数的作用和wait()函数一样,但它并不一定要等待第一个终止的子进程, 它还有其他选项,比如指定等待某个pid的子进程、提供一个非阻塞版本的wait()功能等。 实际上wait()函数只是 waitpid() 函数的一个特例,在 Linux内部实现 wait函数时直接调用的就是 waitpid 函数。

信号

​ 信号(signal),又称为软中断信号,用于通知进程发生了异步事件, 它是Linux系统响应某些条件而产生的一个事件, 它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,在原理上, 一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

实时信号与非实时信号

​ 信号值为1~31的信号属性非实时信号,它主要是因为这类信号不支持排队, 因此信号可能会丢失。比如发送多次相同的信号,进程只能收到一次, 也只会处理一次,因此剩下的信号将被丢弃。而实时信号(信号值为34~64的信号)则不同, 它是支持排队的,发送了多少个信号给进程,进程就会处理多少次。

信号捕获相关函数

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

​ signal()需要提前设置一个回调函数,即进程接收到信号后将要跳转执行的响应函数

sigaction()函数

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
            void     (*sa_handler)(int);//捕获信号后的处理函数
            void     (*sa_sigaction)(int, siginfo_t *, void *);//扩展信号处理函数
            sigset_t   sa_mask;//信号掩码,它指定了在执行信号处理函数期间阻塞的信号的掩码,被设置在该掩码中的信号, 在进程响应信号期间被临时阻塞。
            int        sa_flags;//用于修改信号处理过程行为的标志
            void     (*sa_restorer)(void);
        };

发送信号相关函数

int kill(pid_t pid, int sig);
/*
pid的取值如下:
pid > 1:将信号sig发送到进程ID值为pid指定的进程。
pid = 0:信号被发送到所有和当前进程在同一个进程组的进程。
pid = -1:将sig发送到系统中所有的进程,但进程1(init)除外。
pid < -1:将信号sig发送给进程组号为-pid (pid绝对值)的每一个进程。
sig:要发送的信号值。
函数返回值:
0:发送成功。
-1:发送失败。
*/

int raise(int sig);
/*
raise()函数也是发送信号函数,不过与 kill()函数所不同的是, raise()函数只是进程向自身发送信号的,而没有向其他进程发送信号, 可以说kill(getpid(),sig)等同于raise(sig)
*/

unsigned int alarm(unsigned int seconds);
/*
alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间seconds到时, 它就向进程发送SIGALARM信号。
如果在seconds秒内再次调用了alarm()函数设置了新的闹钟,则新的设置将覆盖前面的设置, 即之前设置的秒数被新的闹钟时间取代。它的返回值是之前闹钟的剩余秒数,如果之前未设闹钟则返回0。
*/

管道

​ 当数据从一个进程连接流到另一个进程时,这之间的连接就是一个管道(pipe)。 我们通常是把一个进程的输出通过管道连接到另一个进程的输入。于shell命令来说,命令的连接是通过管道字符来完成的,正如“ ps -aux | grep root ”命令一样,它实际上就是执行以下过程:

  • shell负责安排两个命令的标准输入和标准输出。
  • ps的标准输入来自终端鼠标、键盘等。
  • ps的标准输出传递给grep,作为grep的标准输入。
  • grep的标准输出连接到终端,即输出到显示器屏幕,最终我们看到grep的输出结果。

​ 管道的实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间, 管道占用的是内存空间,因此Linux上的管道就是一个操作方式为文件的内存缓冲区

匿名管道PIPE

匿名管道有以下的特征:

  • 没有名字,因此不能使用open()函数打开,但可以使用close()函数关闭。
  • 只提供单向通信(半双工),也就是说,两个进程都能访问这个文件,假设进程1往文件内写东西,那么进程2就只能读取文件的内容。
  • 只能用于具有血缘关系的进程间通信,通常用于父子进程建通信 。
  • 管道是基于字节流来通信的。
  • 依赖于文件系统,它的生命周期随进程的结束而结束。
  • 写入操作不具有原子性,因此只能用于一对一的简单通信情形。
  • 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read()和write()等函数。 但是它又不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中,因此不能使用lseek()来定位。
int pipe(int pipefd[2]);//创建匿名管道
/*数组pipefd是用于返回两个引用管道末端的文件描述符, 它是一个由两个文件描述符组成的数组的指针。pipefd[0] 指管道的读取端,pipefd[1]指向管道的写端。*/

匿名管道创建成功以后,创建该匿名管道的进程(父和子进程)同时掌握着管道的读取端和写入端, 但是想要父子进程间有数据交互,则需要以下操作:

  • 如果想要从父进程将数据传递给子进程,则父进程需要关闭读取端,子进程关闭写入端,
  • 如果想要从子进程将数据传递给父进程,则父进程需要关闭写入端,子进程关闭读取端,
  • 当不需要管道的时候,就在进程中将未关闭的一端关闭即可。
命名管道FIFO

命名管道有以下的特征:

  • 有名字,存储于普通文件系统之中。
  • 任何具有相应权限的进程都可以使用 open()来获取命名管道的文件描述符。
  • 跟普通文件一样:使用统一的 read()/write()来读写。
  • 跟普通文件不同:不能使用 lseek()来定位,原因是数据存储于内存中。
  • 具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏。
  • 遵循先进先出(First In First Out)原则,最先被写入 FIFO的数据,最先被读出来。
int mkfifo(const char * pathname,mode_t mode);	//创建命名管道

mkfifo()会根据参数pathname建立特殊的FIFO文件,而参数mode为该文件的模式与权限。

mkfifo()创建的FIFO文件其他进程都可以进行读写操作,可以使用读写一般文件的方式操作它, 如open,read,write,close等。

mode模式及权限参数说明:

  • O_RDONLY:读管道。
  • O_WRONLY:写管道。
  • O_RDWR:读写管道。
  • O_NONBLOCK:非阻塞。
  • O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限。
  • O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是否存在。

使用FIFO的过程中,当一个进程对管道进行读操作时:

  • 若该管道是阻塞类型,且当前 FIFO内没有数据,则对读进程而言将一直阻塞到有数据写入。
  • 若该管道是非阻塞类型,则不论 FIFO内是否有数据,读进程都会立即执行读操作。 即如果FIFO内没有数据,读函数将立刻返回 0。

使用FIFO的过程中,当一个进程对管道进行写操作时:

  • 若该管道是阻塞类型,则写操作将一直阻塞到数据可以被写入。
  • 若该管道是非阻塞类型而不能写入全部数据,则写操作进行部分写入或者调用失败

消息队列

消息队列基本概念

消息队列、共享内存 和 信号量 被统称为 system-V IPC

​ 消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。 我们可以通过发送消息来避免命名管道的同步和阻塞问题。

消息队列与信号的对比:

  • 信号承载的信息量少,而消息队列可以承载大量自定义的数据

消息队列与管道的对比:

  • 消息队列跟命名管道有不少的相同之处,它与命名管道一样,消息队列进行通信的进程可以是不相关的进程, 同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write(),接收数据用read(), 则在消息队列中,发送数据用msgsnd(),接收数据用msgrcv(),消息队列对每个数据都有一个最大长度的限制
  • 消息队列也可以独立于发送和接收进程而存在,在进程终止时,消息队列及其内容并不会被删除。
  • 管道只能承载无格式字节流,消息队列提供有格式的字节流,可以减少了开发人员的工作量。
  • 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级,接收程序可以通过消息类型有选择地接收数据, 而不是像命名管道中那样,只能默认地接收。
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的顺序接收,也可以按消息的类型接收。

消息队列的实现包括创建或打开消息队列、发送消息、接收消息和控制消息队列这4 种操作。

消息队列控制API

int msgget(key_t key, int msgflg);创建或者获取获取一个消息队列对象, 并返回消息队列标识符。

  • key:消息队列的关键字值,多个进程可以通过它访问同一个消息队列。 例如收发进程都使用同一个键值即可使用同一个消息队列进行通讯。 其中有个特殊值IPC_PRIVATE,它用于创建当前进程的私有消息队列。

  • msgflg:表示创建的消息队列的模式标志参数,主要有IPC_CREAT,IPC_EXCL和权限mode,

    • 如果是 IPC_CREAT 为真表示:如果内核中不存在关键字与key相等的消息队列,则新建一个消息队列; 如果存在这样的消息队列,返回此消息队列的标识符。
    • 而如果为 IPC_CREAT | IPC_EXCL 表示如果内核中不存在键值与key相等的消息队列,则新建一个消息队列; 如果存在这样的消息队列则报错。
    • mode指IPC对象存取权限,它使用Linux文件的数字权限表示方式,如0600,0666等。

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);发送消息函数

参数说明:

  • msqid:消息队列标识符。

  • msgp:发送给队列的消息。msgp可以是任何类型的结构体,但第一个字段必须为long类型, 即表明此发送消息的类型,msgrcv()函数则根据此接收消息。msgp定义的参照格式如下:

    /*msgp定义的参照格式*/
    struct s_msg{
        long type;  /* 必须大于0,消息类型 */
        char mtext[];  /* 消息正文,可以是其他任何类型 */
    } msgp;
    
    • msgsz:要发送消息的大小,不包含消息类型占用的4个字节,即mtext的长度。
    • msgflg:如果为0则表示:当消息队列满时,msgsnd()函数将会阻塞,直到消息能写进消息队列; 如果为IPC_NOWAIT则表示:当消息队列已满的时候,msgsnd()函数不等待立即返回; 如果为IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。

接收消息函数

/*接收消息:从标识符为msqid的消息队列读取消息并将消息存储到msgp中, 读取后把此消息从消息队列中删除*/
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数说明:

  • msqid:消息队列标识符。
  • msgp:存放消息的结构体,结构体类型要与msgsnd()函数发送的类型相同。
  • msgsz:要接收消息的大小,不包含消息类型占用的4个字节。
  • msgtyp有多个可选的值:如果为0则表示接收第一个消息,如果大于0则表示接收类型等于msgtyp的第一个消息, 而如果小于0则表示接收类型等于或者小于msgtyp绝对值的第一个消息。
  • msgflg用于设置接收的处理方式,

控制消息队列函数

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数说明:

  • msqid:消息队列标识符。
  • cmd 用于设置使用什么操作命令,它的取值有多个:
    • IPC_STAT 获取该 MSG 的信息,获取到的信息会储存在结构体 msqid_ds类型的buf中。
    • IPC_SET 设置消息队列的属性,要设置的属性需先存储在结构体msqid_ds类型的buf中, 可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,储存在结构体msqid_ds中。
    • IPC_RMID 立即删除该 MSG,并且唤醒所有阻塞在该 MSG上的进程,同时忽略第三个参数。
    • IPC_INFO 获得关于当前系统中 MSG 的限制值信息。
    • MSG_INFO 获得关于当前系统中 MSG 的相关资源消耗信息。
    • MSG_STAT 同 IPC_STAT,但 msgid为该消息队列在内核中记录所有消息队列信息的数组的下标, 因此通过迭代所有的下标可以获得系统中所有消息队列的相关信息。
  • buf:相关信息结构体缓冲区。

FreeRTOS

在freertos中使用了大量的链表结构,节点的定义如下

struct xLIST_ITEM
{
	TickType_t xItemValue; /* 辅助值,用于帮助节点做顺序排列 */ (1)
	struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */ (2)
	struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */ (3)
	void * pvOwner; /* 指向拥有该节点的内核对象,通常是 TCB */(4)
	void * pvContainer; /* 用于指向该节点所在的链表,通常指向链表的根节点 */ (5)
};
typedef struct xLIST_ITEM ListItem_t; /* 节点数据类型重定义 */ (6)

链表的根节点定义如下

typedef struct xLIST
{
	UBaseType_t uxNumberOfItems; /* 链表节点计数器 */ (1)
	ListItem_t * pxIndex; /* 链表节点索引指针 */ (2)
	MiniListItem_t xListEnd; /* 链表最后一个节点 */ (3)
} List_t;

struct xMINI_LIST_ITEM
{
	TickType_t xItemValue; /* 辅助值,用于帮助节点做升序排列 */
	struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */
	struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t; /* 精简节点数据类型重定义 */

多任务系统

​ 多任务系统的事件响应是在中断中完成的,但是事件的处理是在任务中完成的。在多任务系统中, 任务跟中断一样,也具有优先级,优先级高的任务会被优先执行。当一个紧急的事件在中断被标记之后,如果事件对应的任务的优先级足够高,就会立马得到响应。

任务

​ 在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务 。

任务栈:任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组, 也可以是动态分配的一段内存空间,但它们都存在于 RAM 中 。

​ **任务控制块TCB:**多任务系统中, 任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称, 任务的形参等

typedef struct tskTaskControlBlock
{
	volatile StackType_t *pxTopOfStack; /* 栈顶 */ (1)
	ListItem_t xStateListItem; /* 任务节点:通过这个节点,可以将任务控制块挂接到各种链表中 */ (2)
	StackType_t *pxStack; /* 任务栈起始地址 */ (3)
	/* 任务名称,字符串形式 */(4)
	char pcTaskName[ configMAX_TASK_NAME_LEN ];
} tskTCB;

​ **任务创建方法:**FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。 动态创建时,任务控制块和栈的内存是创建任务时动态分配的, 任务删除时,内存可以释放。 静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内 存 , 任 务 删 除 时 , 内 存 不 能 释 放

就绪列表:List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定最 大 任 务 优 先 级 的 configMAX_PRIORITIES 决 定 ,最大支持 256 个优先级。 数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。

临界段的保护

PendSV 中断:PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,而且任何异常都可以中断它。

​ 在FreeRTOS,系统调度,最终是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换,所以还是可以归结为中断。 既然这样, FreeRTOS 对临界段的保护最终还是回到对中断的开和关的控制

Cortex-M 内核快速关中断指令

CPSID I ;//设置PRIMASK=1 ;关中断
CPSIE I ;//设置PRIMASK=0 ;开中断
CPSID F ;//设置FAULTMASK=1 ;关异常
CPSIE F ;//设置FAULTMASK=0 ;开异常

Cortex-M 内核中断屏蔽寄存器如下

寄存器名字功能描述
PRIMASK这是个只有单一比特的寄存器。 在它被置 1 后,就关掉所有可屏蔽的异常, 只剩下 NMI 和硬 FAULT可以响应。它的缺省值是 0,表示没有关中断。
FAULTMASK当它置 1 时,只有 NMI 才能响应,所有其它的 异常,甚至是硬 FAULT,也通通关闭。它的缺省值也是 0,表示没有关异 常。
BASEPRI这个寄存器最多有 9 位(由表达优先级的位数决定)。它定义了被屏蔽优先 级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关 (优先级号越大,优先级越低)。但若被设成 0,则不关闭任何中断, 0 也是 缺省值。

​ 在 FreeRTOS 中,对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽,不受FreeRTOS 管理 。

关中断函数

#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
void vPortRaiseBASEPRI( void )
{
	uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; 
	__asm
	{
		msr basepri, ulNewBASEPRI 
		dsb
		isb
	}
}
/* 带返回值的关中断函数,可以嵌套,可以在中断里面使用 */ 
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
ulPortRaiseBASEPRI( void )
{
	uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; 
	__asm
	{
		mrs ulReturn, basepri 
		msr basepri, ulNewBASEPRI 
		dsb
		isb
	}
	return ulReturn; 
}

开中断函数

/* 不带中断保护的开中断函数 */
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 ) (2)
/* 带中断保护的开中断函数 */
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x) (3)
void vPortSetBASEPRI( uint32_t ulBASEPRI ) (1)
{
	__asm
	{
		msr basepri, ulBASEPRI
	}
}

临界段代码的应用

/* 在中断场合,临界段可以嵌套 */
{
	uint32_t ulReturn;
	/* 进入临界段,临界段可以嵌套 */
	ulReturn = taskENTER_CRITICAL_FROM_ISR();
	/* 临界段代码 */
	/* 退出临界段 */
	taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}

/* 在非中断场合,临界段不能嵌套 */
{
	/* 进入临界段 */
	taskENTER_CRITICAL();
	/* 临界段代码 */
	/* 退出临界段*/
	taskEXIT_CRITICAL();
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值