【Linux驱动开发】多线程调用驱动时的并发与竞争(原子操作、自旋锁、信号量、互斥锁)
【Linux驱动开发】多线程调用驱动时的并发与竞争(原子操作、自旋锁、信号量、互斥锁)
文章目录
- 多线程调用驱动
- 原子操作
- 原子变量操作
- 原子位操作
- 调用方法
- 自旋锁
- 读写锁
- 顺序锁
- 调用方法
- 信号量
- 互斥锁(互斥体)
- 中断中使用
- 附录:嵌入式Linux驱动开发基本步骤
- 开发环境
- 驱动文件
- 编译驱动
- 安装驱动
- 自动创建设备节点文件
- 驱动开发
- 驱动设备号
- 地址映射,虚拟内存和硬件内存地址
- 字符驱动
- 旧字符驱动
- 新字符驱动
- 应用程序开发
多线程调用驱动
所谓多线程调用 一方面指的是在Linux应用开发中 可以通过thread创建多个线程来open驱动进行操作
另一方面也可以使某一个应用在后台运行占用驱动时 打开另外一个应用调用驱动
由于涉及到某些驱动只能被占用一次 所以在驱动开发中可以使用相关的线程锁来阻止应用获取
线程安全这个问题在应用开发中很常见 但这里是跟驱动相关的 需要用到不同的方法 但基本原理都与应用开发中的多线程相似
可以使用以下四种方法来进行驱动的多线程安全操作:
原子操作、自旋锁、信号量、互斥锁
原子操作
这里的原子操作 多作为一个变量用于内核中的状态判定 但也可以作为一种普通变量来使用
其操作方式与普通变量的区别为:
在C语言中 给一个变量赋值 会转成三句汇编
比如 a=3
实际上就是:
ldr r0, =0X30000000 /* 变量 a 地址 */
ldr r1, = 3 /* 要写入的值 */
str r1, [r0] /* 将 3 写入到 a 变量中 */
而如果有两个线程同时调用了a这个变量 则可能在这两个线程中的汇编语句交替执行 导致赋值错误

但如果使用原子变量 并通过原子变量API来进行操作 则可以将每次调用时的语句完全执行完后才能执行另外一个线程的调用
其实相当于:
mutex.lock();
a=3;
mutex.unlock();
但本身要方便得多 占用资源也少 只需要一行API函数即可
原子变量操作
原子变量结构体为atomic_t类型 初始化则为ATOMIC_INIT方法:
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0
操作函数有:

同样支持64位的原子变量atomic64_t类型 以及64位的API函数
原子位操作
对位操作则不进行定义 直接对某个地址下的某一位操作即可
如:


调用方法
譬如在安装驱动后 可以将一个原子变量定义为1
每次打开驱动时 使该变量-1 如果为0 则表示可以运行 否则 则表示被其他驱动打开过了
如:
原子变量初始化:
/* 1、初始化原子变量 */
gpioled.lock = (atomic_t)ATOMIC_INIT(0);
/* 2、原子变量初始值为 1 */
atomic_set(&gpioled.lock, 1);
通过atomic_dec_and_test函数执行减一操作 如果为0则正常 小于0则+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 被使用,返回忙 */
}
自旋锁
自旋锁的定义类型为spinlock_t 其实与互斥锁大同小异
但是互斥锁在锁住占用时 线程可以进行休眠(CPU可以进行其他的任务调度 直到被其他线程解锁后使用)
自旋锁则是使线程原地等待 相当于while(flag)
所以 中断中可以使用自旋锁
但使用自旋锁的时间不宜过长 因为while(1)本身会极度占用系统资源
自旋锁执行后 不能调用任何可以引起休眠的函数 也不能递归自己锁自己 避免死锁
相关API如下

在中断中使用自旋锁要避免死锁发生
如线程和中断都调用了同一个锁:

所以在自旋锁上锁时 可以调用以下API来关闭中断

一般在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock
另外 下半部竞争也需要避免 可以使用以下API:

读写锁
使用rwlock_t类型的读写锁可以防止重复读写的操作


顺序锁
上面的读写锁的读写没法同时工作 但用顺序锁可以解决这个问题
其结构体变量为seqlock_t

调用方法
初始化:
spin_lock_init(&gpioled.lock);
在打开驱动时 可以将某个状态位+1 同时在+1之前上锁保证不会被其他线程调用 从而保护dev_stats变量
spin_lock_irqsave(&gpioled.lock, flags); /* 上锁 */
if (gpioled.dev_stats) { /* 如果设备被使用了 */
spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */
return -EBUSY;
}
gpioled.dev_stats++; /* 如果设备没有打开,那么就标记已经打开了 */
spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */
同时 在关闭驱动时 也可以对齐进行-1操作的保护
spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
if (dev->dev_stats) {
dev->dev_stats--;
}spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */
信号量
与应用开发中的信号量概念一致 其原理就是申请x个信号空间 每次调用时拿走y个空间 如果能拿走且剩余大于等于0 则可以进行 否则就相当于上锁
如果x为1 每次拿信号的y为1 则信号量永远只有0和1两个状态 就与互斥锁相同了
信号量的定义为semaphore
操作很简单:
struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */

同样也是在打开驱动时 使用信号量
if (down_interruptible(&gpioled.sem)) { /* 获取信号量,进入休眠状态的进程可以被信号打断,这时 count 就为 0 */
return -ERESTARTSYS;
}
互斥锁(互斥体)
互斥锁的定义实际上就是一个原子变量+一个自旋锁
struct mutex {atomic_long_t owner;spinlock_t wait_lock;
};
相关API:

struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */mutex_lock(&lock); /* 上锁 *//* 临界区 */
mutex_unlock(&lock); /* 解锁 */
使用时 与信号量调用大同小异
if (mutex_lock_interruptible(&gpioled.lock)) {
return -ERESTARTSYS;
}
中断中使用
如果在中断中要进行上锁操作 那么需要注意以下几点
自旋锁、原子变量可以在中断中使用
自旋不会使中断休眠 而原子变量本身都不算是一个锁
但中断中不要使用信号量和互斥锁的上锁功能
如果在中断中因为上锁而被占用 则中断直接休眠 完全无法退出
附录:嵌入式Linux驱动开发基本步骤
开发环境
首先需要交叉编译器和Linux环境
这里如果是ARM内核 则需要采用ARM的交叉编译器编译器:
arm-none-linux-gnueabihf-gcc
同时需要目标ARM板子的Linux系统内核环境
并编译内核:
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- stm32mp1_atk_defconfig
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- uImage vmlinux dtbs LOADADDR=0xC2000040 -j4
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- stm32mp1_atk_defconfig
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- modules -j4
如果是第一次编译 则可能有所不同 需要根据实际手册来
以下是我编译好 打包好的虚拟机
通过百度网盘分享的文件:适用于STM32MP135开发板的开发环境虚拟机
链接:https://pan.baidu.com/s/1Sf_wk2gEPj0JlQ7X_rpQcg
提取码:d9sj
驱动文件
对于已完成的驱动开发 需要进行编译后进行安装
所有驱动文件在开发上都需要进行驱动入口和出口开发
譬如需要编写驱动入口和退出函数
static int __init xxx_init(void)
static void __exit xxx_exit(void)
然后再模块注册 需要调用到以下函数:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
最后在结尾添加作者和许可信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
MODULE_INFO(intree, "Y");
为了欺骗内核,给本驱动添加 intree 标记,如果不加就会有“loading out-of-tree module taints kernel.”这个警告。
然后才能编译驱动
编译驱动
编译前要配置环境变量:
source /etc/profile
需要先在此文件中 指定环境所在目录
Makefile
KERNELDIR := /home/alientek/linux/atk-mp135/linux/my_linux/linux-5.15.24
CURRENT_PATH := $(shell pwd)obj-m := test.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf-
安装驱动
将编译好的驱动推荐放置到ARM板子的/lib/modules/<kernel-version>目录下
加载驱动:
insmod test.ko 或 modprobe test
建议用modprobe 原因是可以解决依赖关系

查看已安装的模块:
使用lsmod或cat /proc/devices查看 其中 还能看到已安装的驱动设备号(新安装的不能重复)
创建设备节点文件:(如果自动创建就不需要)
mknod /dev/test c 200 0
查看节点文件:
ls /dev/test -l

最后如果不需要了 则卸载
卸载模块:
rmmod test 或 modprobe -r test
自动创建设备节点文件
使用udev 或 mdev即可实现自动创建
如果要使用 则在驱动开发中写入到驱动入口函数中
(一般在 cdev_add 函数后面添加自动创建设备节点相关代码 一些具体的变量和说明见后文新字符驱动开发)
完成开发后 安装驱动时就自动帮你创建好驱动设备节点文件
否则就需要手动去添加
首先要创建一个 class 类,class 是个结构体,定义在文件include/linux/device/class.h 里面。class_create 是类创建函数,class_create 是个宏定义
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
然后使用 device_create 函数在类下面创建设备
device_create(struct class *cls,struct device *parent,dev_t devt,void *drvdata,const char *fmt, ...);
参数 cls 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。
卸载则调用:
void device_destroy(struct class *cls, dev_t devt);
如在已知设备号的情况下进行注册:
struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */ /* 驱动入口函数 */static int __init xxx_init(void)
{/* 创建类 */
class = class_create(THIS_MODULE, "xxx");
/* 创建设备 */
device = device_create(class, NULL, devid, NULL, "xxx");
return 0;
}/* 驱动出口函数 */static void __exit led_exit(void)
{/* 删除设备 */device_destroy(newchrled.class, newchrled.devid);/* 删除类 */
class_destroy(newchrled.class);}module_init(led_init);
module_exit(led_exit);
以上这些设备号、类、驱动等变量太多 可以用一个结构体来表示
/* 设备结构体 */
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;
}
驱动开发
通过开发字符驱动等设备 编译成驱动*.ko文件 然后安装后即可调用
驱动设备号
驱动主要有主设备号 次设备号和驱动名
可以自定义 也可以自动申请
自定义的话 主设备号不能用冲突
查看已安装的模块:
使用lsmod或cat /proc/devices查看 其中 还能看到已安装的驱动设备号(新安装的不能重复)
如果不采用分配的方式进行 直接自定义的话 就不需要看这一节下面的内容了
但如果要分配设备号的话 这里引入dev_t类型的设备号变量:
动态分配则用以下函数申请:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:
dev:保存申请到的设备号。
baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这
些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递
增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count:要申请的设备号数量。
name:设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
或者采用以下两个函数都能来进行申请 第二个函数首先得是确定了主设备号的
//无设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//给定了设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
如:
int major; /* 主设备号 */
int minor; /* 次设备号 */
dev_t devid; /* 设备号 */if (major) { /* 定义了主设备号 */devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0*/
register_chrdev_region(devid, 1, "test");
} else { /* 没有定义设备号 */
alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */major = MAJOR(devid); /* 获取分配号的主设备号 */minor = MINOR(devid); /* 获取分配号的次设备号 */}
如果 major 有效的话就使用 MKDEV 来构建设备号,次设备号选择 0。
如果 major 无效,那就表示没有给定设备号。此时就要使用 alloc_chrdev_region
函数来申请设备号。设备号申请成功以后使用 MAJOR 和 MINOR 来提取出主设备号和次设备
号
注销字符设备之后要释放掉设备号 则是调用:
void unregister_chrdev_region(dev_t from, unsigned count)
直接传入设备号数量即可
地址映射,虚拟内存和硬件内存地址
Linux设备如果最后要操作寄存器进行开发的话 不可避免的会使用内核寄存器
Linux设备如今大多已支持直接从硬件地址读写 但不建议直接采用
对于安装了MMU的设备 可以通过MMU映射到虚拟内存地址 然后对虚拟内存读写后内核则进行物理地址操作
ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
卸载则用:
void iounmap (volatile void __iomem *addr)
Linux设备最好是通过虚拟内存来访问 并且用以下的几组函数来操作内存
使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
读:
u8 readb(const volatile void __iomem *addr)u16 readw(const volatile void __iomem *addr)u32 readl(const volatile void __iomem *addr)
写:
void writeb(u8 value, volatile void __iomem *addr)void writew(u16 value, volatile void __iomem *addr)void writel(u32 value, volatile void __iomem *addr)
字符驱动
其中 所有的外设、驱动等 都可以用字符驱动来开发 但不一定方便
因为字符驱动只能进行简单的打开 销毁 读写等
虽然本质上驱动的开发也是寄存器的读写 但用字符设备还是限制性很大
字符驱动可以实现open close write read等操作
另外字符驱动的文件结构体file中
有一个private_data变量 也就是私有变量 可以在初始化时将一些外部参数初始化成该变量存入
设置好好以后 就可以在在 write、read、close 等函数中直接读取 private_data即可得到设备结构体
旧字符驱动
字符驱动就是file文件驱动 在应用层用open read write close等函数来操作
字符驱动注册和注销需要:
static inline int register_chrdev(unsigned int major,
const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major,
const char *name)
需要编写驱动入口和退出函数
static int __init xxx_init(void)
static void __exit xxx_exit(void)
然后再模块注册 需要调用到以下函数:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
在驱动入口和退出函数中调用register_chrdev和unregister_chrdev函数进行字符驱动的注册与注销
其中 注册时需要传参设备号、名称和file_operations结构体
结构体中需要指定函数名称 该结构体下全是回调函数(函数指针)但也不是全部都要写 不过必须得几项必须要填
如:
static struct file_operations test_fops = {.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,.write = chrtest_write,
.release = chrtest_release,
};
另外 在write和read函数中 用户不得直接访问内存空间 所以要借助copy_from_user和copy_to_user来进行操作
最后在结尾添加作者和许可信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
MODULE_INFO(intree, "Y");
为了欺骗内核,给本驱动添加 intree 标记,如果不加就会有“loading out-of-tree module taints kernel.”这个警告。
完整的代码如:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/gpio.h>
#include <linux/init.h>
#include <linux/module.h>/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbase.c
作者 : 正点原子
版本 : V1.0
描述 : chrdevbase驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2020/12/26 正点原子创建
***************************************************************/#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */static char readbuf[100]; /* 读缓冲区 */
static char writebuf[100]; /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};/** @description : 打开设备* @param - inode : 传递给驱动的inode* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量* 一般在open的时候将private_data指向设备结构体。* @return : 0 成功;其他 失败*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{//printk("chrdevbase open!\r\n");return 0;
}/** @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符)* @param - buf : 返回给用户空间的数据缓冲区* @param - cnt : 要读取的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{int retvalue = 0;/* 向用户空间发送数据 */memcpy(readbuf, kerneldata, sizeof(kerneldata));retvalue = copy_to_user(buf, readbuf, cnt);if(retvalue == 0){printk("kernel senddata ok!\r\n");}else{printk("kernel senddata failed!\r\n");}//printk("chrdevbase read!\r\n");return 0;
}/** @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符* @param - buf : 要写给设备写入的数据* @param - cnt : 要写入的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue = 0;/* 接收用户空间传递给内核的数据并且打印出来 */retvalue = copy_from_user(writebuf, buf, cnt);if(retvalue == 0){printk("kernel recevdata:%s\r\n", writebuf);}else{printk("kernel recevdata failed!\r\n");}//printk("chrdevbase write!\r\n");return 0;
}/** @description : 关闭/释放设备* @param - filp : 要关闭的设备文件(文件描述符)* @return : 0 成功;其他 失败*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{//printk("chrdevbase release!\r\n");return 0;
}/** 设备操作函数结构体*/
static struct file_operations chrdevbase_fops = {.owner = THIS_MODULE, .open = chrdevbase_open,.read = chrdevbase_read,.write = chrdevbase_write,.release = chrdevbase_release,
};/** @description : 驱动入口函数 * @param : 无* @return : 0 成功;其他 失败*/
static int __init chrdevbase_init(void)
{int retvalue = 0;/* 注册字符设备驱动 */retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);if(retvalue < 0){printk("chrdevbase driver register failed\r\n");}printk("chrdevbase init!\r\n");return 0;
}/** @description : 驱动出口函数* @param : 无* @return : 无*/
static void __exit chrdevbase_exit(void)
{/* 注销字符设备驱动 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase exit!\r\n");
}/* * 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);/* * LICENSE和作者信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
然后就可以开始编译
新字符驱动
新字符驱动可以自动生成设备树文件等 比较方便 开发的方式大同小异
在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体在 include/linux/cdev.h 文件中
的定义如下:
示例代码 9.1.2.1 cdev 结构体
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
可以看到 里面包含了file_operations 结构体 以及dev_t 变量等等
定义了cdev变量后 需要进行初始化
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
这里就需要传参file_operations变量了
这两个结构体的.owner都要为THIS_MODULE
如:
struct cdev testcdev;/* 设备操作函数 */
static struct file_operations test_fops = {.owner = THIS_MODULE,/* 其他具体的初始项 */};testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops);
cdev_add(&testcdev, devid, 1);
初始化后 使用以下函数往cdev中添加dev设备号变量
这里要注意 虽然cdev中有dev变量 但不能直接赋值 需要使用cdev_add函数来添加
事实上 无论是写入dev还是读取dev 都不可直接在cdev中进行操作
(如果是C++ 就可以规定私有属性了 但C语言这里不行)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
卸载时则需要删除cdev
void cdev_del(struct cdev *p)
同时也要用unregister_chrdev_region函数去注销外部的dev变量
加上自动创建设备树等功能 则完整代码为:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : newchrled.c
作者 : 正点原子
版本 : V1.0
描述 : LED驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2020/11/24 正点原子团队创建
***************************************************************/
#define NEWCHRLED_CNT 1 /* 设备号个数 */
#define NEWCHRLED_NAME "newchrled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 *//* 寄存器物理地址 */
#define PERIPH_BASE (0x40000000)
#define MPU_AHB4_PERIPH_BASE (PERIPH_BASE + 0x10000000)
#define RCC_BASE (MPU_AHB4_PERIPH_BASE + 0x0000)
#define RCC_MP_AHB4ENSETR (RCC_BASE + 0XA28)
#define GPIOI_BASE (MPU_AHB4_PERIPH_BASE + 0xA000)
#define GPIOI_MODER (GPIOI_BASE + 0x0000)
#define GPIOI_OTYPER (GPIOI_BASE + 0x0004)
#define GPIOI_OSPEEDR (GPIOI_BASE + 0x0008)
#define GPIOI_PUPDR (GPIOI_BASE + 0x000C)
#define GPIOI_BSRR (GPIOI_BASE + 0x0018)/* 映射后的寄存器虚拟地址指针 */
static void __iomem *MPU_AHB4_PERIPH_RCC_PI;
static void __iomem *GPIOI_MODER_PI;
static void __iomem *GPIOI_OTYPER_PI;
static void __iomem *GPIOI_OSPEEDR_PI;
static void __iomem *GPIOI_PUPDR_PI;
static void __iomem *GPIOI_BSRR_PI;/* newchrled设备结构体 */
struct newchrled_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */
};struct newchrled_dev newchrled; /* 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(GPIOI_BSRR_PI);val |= (1 << 19); writel(val, GPIOI_BSRR_PI);}else if(sta == LEDOFF) {val = readl(GPIOI_BSRR_PI);val|= (1 << 3); writel(val, GPIOI_BSRR_PI);}
}/** @description : 取消映射* @return : 无*/
void led_unmap(void)
{/* 取消映射 */iounmap(MPU_AHB4_PERIPH_RCC_PI);iounmap(GPIOI_MODER_PI);iounmap(GPIOI_OTYPER_PI);iounmap(GPIOI_OSPEEDR_PI);iounmap(GPIOI_PUPDR_PI);iounmap(GPIOI_BSRR_PI);
}/** @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 = &newchrled; /* 设置私有数据 */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 newchrled_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;/* 初始化LED *//* 1、寄存器地址映射 */MPU_AHB4_PERIPH_RCC_PI = ioremap(RCC_MP_AHB4ENSETR, 4);GPIOI_MODER_PI = ioremap(GPIOI_MODER, 4);GPIOI_OTYPER_PI = ioremap(GPIOI_OTYPER, 4);GPIOI_OSPEEDR_PI = ioremap(GPIOI_OSPEEDR, 4);GPIOI_PUPDR_PI = ioremap(GPIOI_PUPDR, 4);GPIOI_BSRR_PI = ioremap(GPIOI_BSRR, 4);/* 2、使能PI时钟 */val = readl(MPU_AHB4_PERIPH_RCC_PI);val &= ~(0X1 << 8); /* 清除以前的设置 */val |= (0X1 << 8); /* 设置新值 */writel(val, MPU_AHB4_PERIPH_RCC_PI);/* 3、设置PI3通用的输出模式。*/val = readl(GPIOI_MODER_PI);val &= ~(0X3 << 3); /* bit0:1清零 */val |= (0X1 << 3); /* bit0:1设置01 */writel(val, GPIOI_MODER_PI);/* 3、设置PI3为推挽模式。*/val = readl(GPIOI_OTYPER_PI);val &= ~(0X1 << 3); /* bit0清零,设置为上拉*/writel(val, GPIOI_OTYPER_PI);/* 4、设置PI3为高速。*/val = readl(GPIOI_OSPEEDR_PI);val &= ~(0X3 << 3); /* bit0:1 清零 */val |= (0x2 << 3); /* bit0:1 设置为10*/writel(val, GPIOI_OSPEEDR_PI);/* 5、设置PI3为上拉。*/val = readl(GPIOI_PUPDR_PI);val &= ~(0X3 << 3); /* bit0:1 清零*/val |= (0x1 << 3); /*bit0:1 设置为01*/writel(val,GPIOI_PUPDR_PI);/* 6、默认关闭LED */val = readl(GPIOI_BSRR_PI);val |= (0x1 << 3);writel(val, GPIOI_BSRR_PI);/* 注册字符设备驱动 *//* 1、创建设备号 */if (newchrled.major) { /* 定义了设备号 */newchrled.devid = MKDEV(newchrled.major, 0);ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);if(ret < 0) {pr_err("cannot register %s char driver [ret=%d]\n",NEWCHRLED_NAME, NEWCHRLED_CNT);goto fail_map;}} else { /* 没有定义设备号 */ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); /* 申请设备号 */if(ret < 0) {pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", NEWCHRLED_NAME, ret);goto fail_map;}newchrled.major = MAJOR(newchrled.devid); /* 获取分配号的主设备号 */newchrled.minor = MINOR(newchrled.devid); /* 获取分配号的次设备号 */}printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor); /* 2、初始化cdev */newchrled.cdev.owner = THIS_MODULE;cdev_init(&newchrled.cdev, &newchrled_fops);/* 3、添加一个cdev */ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);if(ret < 0)goto del_unregister;/* 4、创建类 */newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);if (IS_ERR(newchrled.class)) {goto del_cdev;}/* 5、创建设备 */newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);if (IS_ERR(newchrled.device)) {goto destroy_class;}return 0;destroy_class:class_destroy(newchrled.class);
del_cdev:cdev_del(&newchrled.cdev);
del_unregister:unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
fail_map:led_unmap();return -EIO;}/** @description : 驱动出口函数* @param : 无* @return : 无*/
static void __exit led_exit(void)
{/* 取消映射 */led_unmap();/* 注销字符设备驱动 */cdev_del(&newchrled.cdev);/* 删除cdev */unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */device_destroy(newchrled.class, newchrled.devid);class_destroy(newchrled.class);
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
然后就可以去编译了
应用程序开发
所谓应用程序 就是调用驱动就行各种任务 这里是Linux C应用开发
当然 如果你用Python啥的去调用驱动也可以
应用程序可以对/dev/下的驱动进行读写等操作 前提是已经安装了驱动
开发后 使用一条简单的命令即可编译
测试的应用程序采用open等函数进行驱动操作 写好后执行编译
arm-none-linux-gnueabihf-gcc test_app.c -o test_app
最后进行测试即可
相关文章:
【Linux驱动开发】多线程调用驱动时的并发与竞争(原子操作、自旋锁、信号量、互斥锁)
【Linux驱动开发】多线程调用驱动时的并发与竞争(原子操作、自旋锁、信号量、互斥锁) 文章目录 多线程调用驱动原子操作原子变量操作原子位操作调用方法 自旋锁读写锁顺序锁调用方法 信号量互斥锁(互斥体)中断中使用附录…...
qt QComboBox详解
QComboBox是一个下拉选择框控件,用于从多个选项中选择一个。通过掌握QComboBox 的用法,你将能够在 Qt 项目中轻松添加和管理组合框组件,实现复杂的数据选择和交互功能。 重要方法 addItem(const QString &text):将一个项目添…...
redis做缓存,mysql的数据怎么与redis进行同步(双写一致性)
基于业务做选择,强一致性和允许延迟再加消息队列 强一致性:当修改了数据库的数据同时更新缓存的数据,缓存和数据库的数据保持一致 读操作:缓存命中,直接返回数据,缓存没有命中,查询数据库,写入缓存,设定过期时间 写操作:延迟双删 :先删除缓存,修改数据库,等待延迟(数据库主从节…...
WPF触发器
WPF触发器 触发器(Trigger)通常指的是一种事件驱动机制,用于响应特定的事件或条件。WPF触发器是WPF中一种强大的功能,允许开发者在样式和模板中定义条件逻辑,以响应属性值的变化。WPF提供了多种触发器来实现样式和模板…...
反序列化漏洞的运行原理及防御方法
反序列化漏洞是当前网络安全领域中的一种严重安全威胁,其运行原理和防御方法对于保障应用程序和系统安全至关重要。本文将深入探讨反序列化漏洞的运行原理,并提出有效的防御策略。 ### 反序列化漏洞的运行原理 序列化是指将对象的状态信息转换为可以存储…...
护眼大路灯哪个牌子好?口碑最好的护眼灯品牌
护眼大路灯哪个牌子好?作为一名专业的测评师,我发现有大量的家长都反应自己家孩子在学习时经常出现,揉眼睛、频繁眨眼、眼睛痒等问题,而这些问题多半是与不良光线有光,这些现象使我更加关注护眼大路灯的选择࿰…...
Redis 初学者指南
Redis 初学者指南 1. 什么是 Redis?2. Redis 的基本概念3. 安装 Redis3.1 使用 Docker 安装3.2 从源码编译安装 4. 基本操作4.1 启动 Redis 服务4.2 连接 Redis 客户端4.3 常用命令 5. Redis 的数据结构5.1 字符串5.2 列表5.3 集合5.4 散列5.5 有序集合 6. 高级特性…...
node.js_npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1
这个错误信息表明 PowerShell 因为执行策略的限制而阻止了 npm.ps1 脚本的运行。PowerShell 的执行策略是一种安全功能,用于限制哪些脚本可以运行,以防止恶意脚本的执行。 要解决这个问题,你可以按照以下步骤操作: 查看当前的执行…...
技术星河中的璀璨灯塔 —— 青云交的非凡成长之路
💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...
使用 Git 命令将本地项目上传到 GitLab
步骤详解 1. 在 GitLab 上创建一个新项目 登录你的 GitLab 账号。点击“New project”创建一个新的空项目。为项目设置名称、描述等信息。 2. 关联远程 Git 仓库 1.初始化本地 Git 仓库 git init 2.关联远程仓库: git remote add origin https://gitlab-lizz…...
JavaScript的第十三天
目录 一、使用jQuery操作属性值 操作案例: 表格的全选和反选 二、设置宽高 1、宽高属性 2、使用方式 三、offset() 和position()的区别 1、.offset():获取到document的距离,也就是窗口边…...
el-table 滚动条重置 手动控制滚动条
最近在使用 el-table 的时候,出现一个问题: 表头过长的时候,会有左右滑动的操作,当我们把表格拉到最右侧,这个时候重新请求数据的话,表格位置还是在最右侧,不会恢复原位。 那我们想恢复原位&a…...
详细分析Vue3中的provide和inject基本知识(附Demo)
目录 前言1. 基本知识2. Demo3. 拓展 前言 原先写过一篇父传子,推荐阅读:详细分析Vue3中的props用法(父传子) 实战中也常用这种方式,今天突然发现还有另外一种方式,对此进行深入探讨学习下 1. 基本知识 …...
spring集成kafka
Kafka 是一个分布式流处理平台,广泛用于构建实时数据流管道和流应用程序。它以高吞吐量、可扩展性和可靠性著称。以下是 Kafka 的实现原理详解及其在 Spring Boot 中的集成示例。 一、Kafka 实现原理 1. 架构概述 Kafka 的架构主要由以下几个组件组成:…...
el-form表单中含有el-input按回车自动刷新如何阻止
场景: 在Vue.js中使用Element UI的el-input组件时,如果按下Enter键导致页面刷新,这通常是因为表单的默认提交行为被触发了。要避免这种情况,你可以在el-input所在的表单上监听键盘事件,并阻止默认行为。 先解释一下时间…...
Spring Boot2.x教程:(十)从Field injection is not recommended谈谈依赖注入
从Field injection is not recommended谈谈依赖注入 1、问题引入2、依赖注入的三种方式2.1、字段注入(Field Injection)2.2、构造器注入(Constructor Injection)2.3、setter注入(Setter Injection) 3、为什…...
在 Android Studio 上运行 Java 的 main 函数
直接写了个main函数运行的时候提示 这时我们需要在idea目录下找到gradle.xml文件 添加 <option name"delegatedBuild" value"false" />搞定...
【Nas】X-DOC:Mac mini 安装 ZeroTier 并替换 planet 实现内网穿透
【Nas】X-DOC:Mac mini 安装 ZeroTier 并替换 planet 实现内网穿透 1、下载客户端 ZeroTier One2、安装过程3、更换planet备份原planet4、重启服务5、加入网络6、NAT内网穿透 1、下载客户端 ZeroTier One https://www.zerotier.com/download/ 选择 MacOS 适用版本&…...
Spring Boot 集成 RabbitMQ
在现代分布式系统中,消息队列扮演着至关重要的角色。它能够实现系统间的异步通信、解耦组件以及提高系统的可扩展性和可靠性。RabbitMQ 作为一款广泛使用的开源消息中间件,具有强大的功能和灵活的配置。而 Spring Boot 则是一种流行的 Java 开发框架&…...
存在sql注入的公网站点
此数据为博主在新手阶段练习sql注入时发现的站点,漏洞可能修复,备注可能错误 url: https://www.uni-1.com.hk/about_en.php?id2 注入点类型:数值 sql报错回显:无 sql语句执行:[order by] [union] 字段数:1…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...
基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...
UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...
