8.2linux竞争与并发驱动LED灯原子操作实验_csdn
8.2linux竞争与并发驱动LED灯(原子操作实验)_csdn
本例程我们在第二十五章的 基于pinctrl与gpio子系统驱动LED灯实验 的基础上完成。在本节使用中我们使用原子操作来实现对 LED 这个设备的互斥访问,也就是一次只允许一个应用程序可以使用 LED 灯。
原子操作属于临界区,临界区必须保证一次只有一个线程访问。
1.修改设备树文件
这一步骤在前面已经有过,所以跟着前面文章的同学应该很熟悉!
在 stm32mp157d-atk.dts 文件的根节点“/”下创建 LED 灯节点,节点名为“gpioled”,节点内容如下:
发现节点已经有了!
2.LED 灯驱动程序编写
之前的博客也是跟大家按照肌肉记忆来编写程序!一步一步按照思路来编写!
总代码会放在最后。
为了让大家更能明白,可以先对着总代码,进行对我的写代码流程更加详细得当!
放心,我也是一步一步打的代码,不是复制粘贴!!!
2.1头文件
#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 <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
2.2驱动入口函数和出口函数及MODULE函数
这里的MODULE_LICENCE要改为 MODULE_LICENSE 。
2.3创建设备结构体
在跟之前一样,为了后续的注册设备字符,要在开发板内核里面显示的话,要申请设备号,便包括主设备号和次设备号;为了显得更专业一点,就要使用结构体来统一有关设备的相关父子类结构体信息。后面也可以添加更多的子类信息!
这里看总代码就知道我放在哪个位置了!
dev_t
是一个用于表示设备编号的数据类型,它在
<sys/types.h>
头文件中被定义。
gpioled
:这是一个变量名,它的类型是
struct gpioled_dev
。也就是说,
gpioled
是一个
gpioled_dev
结构体类型的变量,通过这个变量可以存储和操作该结构体所定义的各种数据。
比如gpioled.devid,gpioled.major,gpioled,minor。等等。
后续有很多子类会在这里!
例如:
struct gpioled_dev {
dev_t devid; // 设备号
struct cdev cdev; // 字符设备对象
struct class *class; // 设备类(sysfs接口)
struct device *device; // 设备节点(/dev目录下)
int major; // 主设备号
int minor; // 次设备号
struct device_node *nd; // 设备树节点指针
int led_gpio; // GPIO编号(从设备树解析) };
2.4注册字符设备
这个很早就说了,在驱动入口就开始注册字符了,打开驱动就是注册!
33行是先让主设备号为0,防止以前的实验占用及未注销的设备号继续利用。
34-41行分别以给定设备号和未给定设备号的情况进行编程。其中gpioled.major,gpioled.minor便是利用了结构体内的变量子类,
MKDEV
是 Linux 内核提供的一个宏,用于根据主设备号和次设备号生成一个
dev_t
类型的设备编号。其定义通常位于
<linux/kdev_t.h>
头文件中,
register_chrdev_region
函数的原型定义在
<linux/fs.h>
头文件中,若是给了设备号就是用这个函数,从gpio.devid所赋值的设备号告诉内核进行相应设备号的注册。第二个位置就是设备号个数,第三个位置就是设备号名称。
alloc_chrdev_region
不需要手动指定主设备号,内核会自动分配一个未被使用的主设备号给驱动程序。&gpioled.devid是指针类型,原型是
dev_t *dev
,要通过&取地址来存储设备号,第二个位置是起始的此设备号。
既然有创建字符设备,那就要有删除字符设备。
2.4.1补充设备结构体
完成字符设备注册后,就是在设备结构体中添加字符设备对象。
2.5初始化cdev
很显然cdev也是变量gpioled的子类gpioled.cdev。同时在内核里面cdev里面包括很多子类信息。
这里是内核里面的定义。不用管,只要配置cdev的子类即可。
对cdev进行初始化,这个是字符设备对象。
gpioled.cdev.owner = THIS_MODULE;
的作用
- 内核利用这个关联来管理模块的引用计数。当有用户空间程序对该字符设备进行操作(如打开、读写)时,内核会增加当前模块的引用计数,以确保在设备被使用期间模块不会被卸载。当所有使用该设备的操作结束后,内核会减少引用计数,当引用计数降为 0 时,模块可以被安全卸载。
cdev_init
函数的核心功能是对一个
struct cdev
结构体实例进行初始化,并且把一组文件操作函数(由
struct file_operations
结构体定义)和该字符设备关联起来。这样一来,当用户空间的程序通过文件操作接口(像
open
、
read
、
write
等)对该字符设备进行操作时,内核就能调用相应的函数来处理这些请求。
2.5.1配置file_operations
同理,上面的&led_fops需要配置。
图片中还有些未进行修改 , 不用管,后面对字符添加操作集子类函数时改,先初始化cdev。
2.6添加cdev
cdev_add
函数的主要作用是将一个已经初始化好的字符设备(
struct cdev
结构体实例)添加到内核的字符设备管理系统中,使该字符设备正式在系统中可用。一旦调用
cdev_add
成功,用户空间的程序就可以通过设备文件(通常位于
/dev
目录下)来访问这个字符设备。
即后续的./ledApp 可以执行到/dev
2.6.1注销字符设备对象
目前已经注册和注销了字符设备,同时也注册了字符设备对象,所以要进行字符设备对象的注销。
依然按照 逻辑顺序 进行注销。
2.7创建设备类和设备节点
2.7.1补充类与节点的定义
这两者作用在后续./dev/beep 1/0中创建/dev/beep。方便执行代码程序,传入到设备执行,关联设备号
2.7.2创建类
其中定义
#define class_create(owner, name)
owner和结构体内的owner都是module模块的,一般来说是THIS_MODULE;name就是设备名字。
2.7.3创建设备节点
以便访问设备设备树根节点的信息,与下文的获取设备节点相互呼应。
代码如下:
struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata,const char *fmt, ...);
struct device *parent
,指向父设备的指针。如果该设备没有父设备,可以传入
NULL
。
dev_t :即本文的gpioled.devid。
void *drvdata
,是一个指向设备驱动私有数据的指针。可以传入自定义的数据结构指针,用于在设备驱动中存储和管理设备相关的信息,这里同样给NULL。
const char *fmt-
fmt
是一个格式化字符串,
...
表示可变参数列表。类似于
printf
函数的用法,用于指定设备节点的名称。作用是指定要创建的设备节点在
/dev
目录下的名称,可以使用格式化字符串动态生成名称。
2.7.4补充错误信息
IS_ERR
宏通过比较指针的值和
(unsigned long)-MAX_ERRNO
的大小来判断该指针是否为错误指针。如果指针的值大于等于
(unsigned long)-MAX_ERRNO
,则认为它是一个==错误指针,返回
true
;==否则返回
false
。
PTR_ERR
:当
IS_ERR(gpioled.cls)
返回
true
时,代码会执行
return PTR_ERR(gpioled.cls);
,这会将
gpioled.cls
转换为对应的错误码并返回给调用者。调用者可以根据这个错误码进行相应的错误处理,例如打印错误信息、释放已经分配的资源等。
同样:
2.7.5注销类和设备节点
2.7.6配置操作集函数
复制上一节的操作函数集
static int dtsled_open(struct inode *inode, struct file *filp)
{
filp->private_data = &dtsled;
return 0;
}
static int dtsled_release(struct inode *inode, struct file *filp)
{
struct dtsled_dev *dev = (struct dtsled_dev*)filp->private_data;
return 0;
}
static ssize_t dtsled_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
struct dtsled_dev *dev = (struct dtsled_dev*)filp->private_data;
int retvalue;
unsigned char databuf[1];
retvalue = copy_from_user(databuf,buf,count);
if (retvalue < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
/*判断开灯还是关灯*/
led_switch(databuf[0]);
return 0;
}
2.8获取设备节点(设备树属性)
2.8.1配置设备树结构体
2.8.2获取设备树节点
2.8.3获取led所对应的GPIO编号
这里知道了有关led驱动的gpio信息,仅仅是能知道信息,并没有驱动能力,所以要向内核申请权限来驱动gpio口。
2.8.4申请IO
相应需要注销函数时释放,不然占用资源。
2.8.5使用IO,设置为输出
2.8.6配置错误处理
按照以前的习惯,从后面进行处理错误信息,按照逻辑!!!
2.9配置私有数据进行write设备
提供宏定义给LEDAPP进行操作。
图中:
struct gpioled_dev *dev = filp->private_data;
与
struct gpioled_dev gpioled; /*LED*/
类似,其中可以看作gpioled为全局变量,而dev为局部变量。而私有变量写法要加箭头
dev = filp->private_data
这是个整体,在前面加个指针,后面要用这个就dev->给私有数据。
如
gpioled.led_gpio
和
dev->led_gpio
一样调用。和全局变量,局部变量一样。
同理要在输出口设置关灯。
以上就是可以点亮led灯。
下面结合原子操作,编写相关代码程序!
2.10添加原子操作
原子操作属于临界区,临界区必须保证一次只有一个线程访问。
这里我们重点分析一下,因为这里有很多API函数,我们可以多拿几个分析。
2.10.1配置原子操作设备结构体
2.10.2初始化原子操作
2.10.3配置操作函数
当原子变量没有被使用时,gpioled.lock=1,一旦被上图程序执行,会被减1。后面释放原子变量会回到1。
这里也可以用别的API函数,只要逻辑可以就行!(根据lock初始化为1,所以后面要回到1)
例如 :
static int led_open(struct inode *inode, struct file *filp)
{
/* 通过判断原子变量的值来检查 LED 有没有被别的应用使用 */
if (!atomic_dec_and_test(&gpioled.lock)) {
atomic_inc(&gpioled.lock);/* 小于 0 的话就加 1,使其原子变量等于 0 */
return -EBUSY; /* LED 被使用,返回忙 */
filp->private_data = &gpioled; /* 设置私有数据 */
}
return 0;
}
/*这里的atomic_dec_and_test(&gpioled.lock),假如没有被使用,gpioled.lock=1,则减1,变为0,返回真,又因为“!”,变为假,就不执行return -EBUSY;但是已经变为0,后面释放会加到1,如果还没释放就又要使用这个原子变量,就会执行这个if,跟上面的相反。*/
/*这样就保证了每次就只允许一个应用使用 LED 灯。*/
这里加原子变量是因为限制一个应用进来操作。例如gpioled,beep等多个线程同时要执行,就要一个一个来,获得原子变量。原子变量是贯穿内核的。
测试结果:
./atomicApp /dev/gpioled 1 &//其中&表示在后台运行 atomicApp 这个软件
./atomicApp /dev/gpioled 0
我发现./atomicApp /dev/gpioled 1不可以执行到原子变量的效果,
但是发现/atomicApp /dev/gpioled 1&可以
2.10.4两种命令的区别
./atomicApp /dev/gpioled 1
:这是一个在前台执行程序的命令。当你运行这个命令时,终端会被该程序占用,直到程序执行完毕才会返回命令提示符,你在程序执行期间无法在同一终端输入其他命令。./atomicApp /dev/gpioled 1 &
:&
符号的作用是将程序放到后台执行。程序启动后,终端不会被其占用,你可以立即输入其他命令,同时该程序会在后台继续运行。可以看出, atomicApp 运行正常,输出了“App running times:1”和“App running times:2”等字符串,这就是模拟 25S 占用,说明 atomicApp 这个软件正在使用 LED 灯,假如在模拟占用25s内再输入命令./atomicApp /dev/gpioled 0关闭 LED 灯,则会失败。
打开/dev/gpioled 失败!原因运行的 atomicAPP软件正在占用/dev/gpioled,如果再次运行atomicApp 软件去操作/dev/gpioled 肯定会失败。 必须等待atomicApp运行结束,也就是25S结束以后其他软件才能去操作/dev/gpioled 。这个就是采用原子变量实现一次只能有一个应用程序访问 LED 灯。
2.11总代码
gpioled.c
#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 <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define GPIOLED_NAME "gpioled"
#define GPIOLED_CNT 1
#define LEDON 1
#define LEDOFF 0
/*gpioled设备结构体*/
struct gpioled_dev{
dev_t devid; //dev_t 是一个用于表示设备编号的数据类型,设备号
int major;//主设备号
int minor;//次设备号
struct cdev cdev;//设备关联,字符设备对象
struct class *class;//设备类
struct device *device;//设备节点
struct device_node *nd;//设备树节点指针
int led_gpio;//GPIO编号(从设备树上获取)
atomic_t lock; /* 原子变量 */
};
struct gpioled_dev gpioled;//设备
static int led_open(struct inode *inode, struct file *filp)
{
/*通过判断原子变量的值来检查 LED 有没有被别的应用使用*/
if(atomic_read(&gpioled.lock) <= 0){
return -EBUSY; /* LED 被使用,返回忙 */
}else {
atomic_dec(&gpioled.lock);//LED没有被使用,就减1,表示现在要用这个原子操作
}
filp->private_data = &gpioled;
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
/* 关闭驱动文件的时候释放原子变量 */
atomic_inc(&dev->lock);//加1变成1,表示用完,释放原子变量
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
unsigned char databuf[1];
struct gpioled_dev *dev=filp->private_data;
ret = copy_from_user(databuf,buf,count);
if(ret < 0){
return -EINVAL;
}
if(databuf[0] == LEDON){
gpio_set_value(dev->led_gpio,0);//开灯
}else if(databuf[0] == LEDOFF){
gpio_set_value(dev->led_gpio,1);//关灯
}
return 0;
}
/*操作集*/
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_write,
.open = led_open,
.release = led_release,
};
/*驱动入口函数*/
static int __init led_init(void)
{
int ret;
/*1.注册字符设备*/
gpioled.major = 0;
if(gpioled.major){//若给定主设备号
gpioled.devid = MKDEV(gpioled.major,0);//0为次设备号
ret = register_chrdev_region(gpioled.devid,GPIOLED_CNT,GPIOLED_NAME);
}else{//若给定主设备号
ret = alloc_chrdev_region(&gpioled.devid,0,GPIOLED_CNT,GPIOLED_NAME);
//0为次设备号,起始的次设备号
gpioled.major = MAJOR(gpioled.devid);
gpioled.minor = MINOR(gpioled.devid);
}
if(ret < 0){
goto fail_devid;
}
printk("major=%d minor=%d\r\n",gpioled.major,gpioled.minor);
/*2.初始化cdev*/
gpioled.cdev.owner= THIS_MODULE;//模块关联与引用计数管理
cdev_init(&gpioled.cdev, &led_fops);//字符设备操作函数绑定
/*3.添加cdev*/
ret = cdev_add(&gpioled.cdev,gpioled.devid,GPIOLED_CNT);//即后续的./ledApp 可以执行到/dev
if(ret < 0){
goto fail_cdev;
}
/*4.创建类class*/
gpioled.class = class_create(THIS_MODULE,GPIOLED_NAME);//创建/dev/beep
if(IS_ERR(gpioled.class)){
ret = PTR_ERR(gpioled.class);
goto fail_class;
}
/*5.创建设备节点device*/
gpioled.device = device_create(gpioled.class,NULL,gpioled.devid,NULL,GPIOLED_NAME);
//创建/dev/beep
if(IS_ERR(gpioled.device)){
ret = PTR_ERR(gpioled.device);
goto fail_device;
}
/*6.获取设备树节点*/
gpioled.nd = of_find_node_by_path("/gpioled");//根节点
if(gpioled.nd ==NULL ){
ret = -EINVAL;
goto fail_findnode;
}
/*7.获取gpioled根节点下的属性*/
gpioled.led_gpio = of_get_named_gpio(gpioled.nd,"led-gpio",0);
if(gpioled.led_gpio < 0){
printk("can't find led gpio\r\n");
ret = -EINVAL;
goto fail_findnode;
}
printk("led gpio num = %d\r\n",gpioled.led_gpio);
/*8.申请IO*/
ret = gpio_request(gpioled.led_gpio,"led-gpio");
if(ret){
printk("Failed to request the led gpio\r\n");
ret = -EINVAL;
goto fail_findnode;
}
/*9.设置IO口电气属性,设置为输出模式*/
ret = gpio_direction_output(gpioled.led_gpio,1);//默认高电平
if (ret < 0) {
ret = -EINVAL;
goto fail_setoutput;
}
/*10.初始化原子变量*/
gpioled.lock = (atomic_t)ATOMIC_INIT(0);//gpioled.lock=0
/*11.原子变量初始值为1*/
atomic_set(&gpioled.lock, 1);
return 0;
fail_setoutput:/*设置输出失败,释放已申请的IO,避免资源浪费*/
gpio_free(gpioled.led_gpio);
fail_findnode:
device_destroy(gpioled.class,gpioled.devid);
fail_device:
class_destroy(gpioled.class);
fail_class:
cdev_del(&gpioled.cdev);
fail_cdev:
unregister_chrdev_region(gpioled.devid,GPIOLED_CNT);
fail_devid:
return ret;
}
/*驱动出口函数*/
static void __exit led_exit(void)
{
/*关灯*/
gpio_set_value(gpioled.led_gpio,1);
/*释放IO*/
gpio_free(gpioled.led_gpio);
/*注销设备节点*/
device_destroy(gpioled.class,gpioled.devid);
/*注销设备类*/
class_destroy(gpioled.class);
/*注销字符设备对象驱动*/
cdev_del(&gpioled.cdev);
/*注销字符设备驱动*/
unregister_chrdev_region(gpioled.devid,GPIOLED_CNT);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("chensir");
MODULE_INFO(intree,"Y");//INFO,信息
LEDAPP.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h" /*open执行所需头文件,根文件目录终端输入man 2 open*/
#include "sys/stat.h" /*open执行所需头文件,根文件目录终端输入man 2 open*/
#include "fcntl.h" /*open执行所需头文件,根文件目录终端输入man 2 open*/
#include "stdlib.h"
#include "string.h" /*memcpy执行所需头文件,根文件目录终端输入man 3 memcpy*/
/*
*argc:应用程序参数个数
*argv[]:具体的参数内容,字符串形式
*./ledAPP <fliename> <0:1> 0表示关灯,1表示开灯
*./ledAPP /dev/gpioled 0 关灯
*./ledAPP /dev/gpioled 1 开灯
*/
#define LEDOFF 0
#define LEDON 1
int main(int argc,char *argv[])
{
int fd,retvalue;/*fd:类似文件的ID号;retvalue:返回值。*/
unsigned char databuf[1];/*这种数组可以存储数字或字符*/
char *filename;
unsigned char cnt = 0;
if(argc !=3)
{
printf("Error usage!\r\n");
return -1;
}
filename = argv[1];
/*打开文件*/
/* int open (const char *pathname,int flags)*/
fd = open(filename,O_RDWR);/*以读写模式打开文件,fd用于打开文件获取ID号*/
if(fd<0)
{
printf("file %s open failed!\r\n",filename);
return -1;
}
databuf[0] = atoi(argv[2]);/*将字符转换为数字*/
retvalue=write(fd,databuf,sizeof(databuf));
if(retvalue<0)
{
printf("LED Control Failed!\r\n");
close(fd);
}
while(1) {
sleep(5);
cnt++;
printf("App running times:%d\r\n", cnt);
if(cnt >= 5) break;
}
close(fd);
return 0;
}
makefile
KERNELDIR := /home/chensir/linux/atk-mp1/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := atomic.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
3.总结
1.添加pinctrl信息。
2.检查当前设备树中要使用的IO有没有被其他设备使用,如果有的话要处理。
3.添加设备节点,在设备节点中创建一个属性,吃属性描述所使用的gpio。
4.编写驱动,获取对应的gpio编号,并申请IO,成功后即可使用此IO。
申请失败绝大部分情况是被其他外设给占用了!!!这种情况在根节点或者设备树检查一下pinctrl复用情况。
加油!!!