【Linux驱动开发】irq中断配置API及中断应用 阻塞休眠和非阻塞的驱动操作
文章目录
中断操作
与GPIO一样 每一个中断都有一个中断号
包含的头文件为:
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
注册和释放中断
通过
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,const char *name,
void *dev)
函数可以申请中断
其中 中断服务函数是一个回调
中断标志flag则有:
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字
最后的dev是一个指针 可以定义成结构体 用来判断设备或传参
如果使用的是共享中断 dev就是用来区分的方式
request_irq
函数会默认使能中断
另外 释放中断则是:
void free_irq(unsigned int irq, void *dev)
中断服务函数
中断服务函数原型为:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数就是中断号 第二个参数就是dev
返回值则一般为:
return IRQ_RETVAL(IRQ_HANDLED);
使能和禁止
使用函数操作:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
disable_irq
函数会等待中断完成以后才返回
在等待期间不能产生新的中断
而使用
void disable_irq_nosync(unsigned int irq)
则能立即返回
另外 开启和关闭全局中断可以使用:
local_irq_enable()
local_irq_disable()
另外 推荐使用可以恢复的全局中断
local_irq_save(flags)
local_irq_restore(flags)
上半部和下半部
中断通常需要快速返回
而将耗时的任务放在主线程中执行
中断服务函数为上半部 耗时的任务放在下半部中执行
软中断
实现下半部可以用软中断来实现
使用结构体 softirq_action
表示软中断
要使用软中断,必须先使用 open_softirq
函数注册对应的软中断处理函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
即软中断号和软中断回调
另外 通过以下函数触发软中断
void raise_softirq(unsigned int nr)
tasklet
推荐使用
tasklet也是一种软中断实现方式
结构体定义如下:
struct tasklet_struct
{
struct tasklet_struct *next; /* 下一个 tasklet */
unsigned long state; /* tasklet 状态 */
atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数 */
unsigned long data; /* 函数 func 的参数 */
};
如果要使用 tasklet,必须先定义一个 tasklet_struct
变量,然后使用 tasklet_init
函数对其进行初始化,taskled_init 函数原型如下:
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
或者用宏定义完成:
DECLARE_TASKLET(name, func, data)
在上半部,也就是中断处理函数中调用 tasklet_schedule
函数就能使 tasklet 在合适的时间运行,tasklet_schedule
函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
在中断服务函数中使用tasklet_schedule
在中断注册函数中先使用tasklet_init
再注册中断
工作队列
工作队列也可以实现软中断
简单创建工作很简单,直接定义一个 work_struct
结构体变量即可,然后使用 INIT_WORK
宏来初始化工作,INIT_WORK
宏定义如下:
#define INIT_WORK(_work, _func)
//或
#define DECLARE_WORK(n, f)
结构体定义为:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
在中断服务函数中则调用:
bool schedule_work(struct work_struct *work)
来开启工作队列
设备节点中的中断
同样 在设备节点中也有中断属性
譬如:
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
指定了该节点的中断为GIC_SPI 中断ID为36 高电平触发
该设备属于GIC中断控制器
在cortex-m4中 还有两种常用的分别是NVIC和EXTI
注意 前面32个中断号属于系统分配的SGI和PPI
32号以后的就是SPI中断(不是那个SPI)
所以 外设中断有两个中断号 分别是SPI的中断号 和 加上32的中断号
GIC控制器基本上涵盖所有能用到的中断 包括EXTI
虽然EXTI可以直接在CPU响应 但也可以通过GIC来完成
GPIO节点中就包含了EXTI
interrupt-cells=2
,表明 exti 的子节点里面第一个 cell 表示为中断号,也可以叫EXTI 事件编号,第二个 cell 表示中断标志位。其它的设备树属性和 GIC 控制器是一样的。
比如:
interrupts = <0 IRQ_TYPE_EDGE_FALLING>;
interrupt-parent = <&gpioi>;
表示该设备中断号为0 下降沿触发
interrupt-parent
属性设置中断控制器,这里是有 gpioi 作为中断控制器
那么这个就是PI0的中断号
设备节点中一共有以下几种指定中断的方式:
#interrupt-cells //指定中断源的信息 cells 个数。
interrupt-controller //表示当前节点为中断控制器。
interrupts //指定中断号,触发方式等。
interrupt-parent //指定父中断,也就是中断控制器。
interrupts-extended //指定中断控制器、中断号、中断类型和触发方式。
其中interrupts-extended
可以一次性将所有中断属性涵盖 指定中断父节点,IO 编号,中断方式等
中断号API函数
获取中断号
使用如下函数 可以获取某个设备节点的中断号
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
如果是GPIO 则通过:
int gpio_to_irq(unsigned int gpio)
来获取
获取中断信息
使用irq_get_trigger_type
函数来获取中断触发方式
中断应用
按步骤分为以下几个步骤
读取设备树节点
如:
key.nd = of_find_node_by_name(NULL,"key1");
获取中断号
通过irq_of_parse_and_map
或gpio_to_irq
获取
如:
irq_num = irq_of_parse_and_map(key.nd, 0);
获取中断触发方式
irq_flags = irq_get_trigger_type(key.irq_num);
定义中断服务函数
如:
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
...
return IRQ_HANDLED;
}
申请中断
request_irq(key.irq_num, key_interrupt, irq_flags,"Key1_IRQ", NULL);
开启中断
request_irq
函数会默认使能中断,所以不需要 enable_irq
来使能中断,当然我们也可以在申请成功之后先使用 disable_irq
函数禁用中断
注销中断
使用完以后用函数注销:
如:
free_irq(key.irq_num, NULL);
阻塞休眠和非阻塞驱动开发
在驱动开发中 有阻塞和非阻塞之分
譬如延时函数 就是阻塞的
我们之前定义的iotcl延时 就是延时以后才返回
但如果是非阻塞的 就可以立即返回 等延时时间结束后再进行读取
【Linux驱动开发】timer库下的jiffies时间戳和延时驱动编写
对于设备驱动文件的默认读取方式就是阻塞式的 如:
fd = open("/dev/xxx_dev", O_RDWR);
如果用非阻塞模式 则:
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);
如果驱动开发中 譬如读取一个io的信息
如果驱动中使用了while等语句一直读取 那么就会占用CPU
譬如 驱动中开启了定时器
定时器周期性读取某个GPIO的状态
但只有应用在读取时才有用
那么就可以引入阻塞、非阻塞的概念
在没有读取发生时就休眠
在软件上可以用线程锁来实现 但会导致某些设备卡死
本质上就是在没有操作时 休眠线程 有操作时 唤醒线程
而为了实现休眠 就必须要用到等待队列
阻塞
可以阻塞某些函数来进行
阻塞时 可以进入休眠状态
譬如在应用层 一直循环读取 一直调用read函数
但是只需要读到正确的值时才会触发
那么就可以用队列的方式 进入使其休眠 并阻塞
直到状态变化或手动唤醒后 read才返回
其实跟线程锁的使用差不多 但区别就是可以自动唤醒或手段唤醒
也就相当于单片机的低功耗
等待队列的方式即可实现阻塞时的系统休眠
譬如设置事件条件自动唤醒:
ret = wait_event_interruptible(key.r_wait, KEY_KEEP != atomic_read(&key.status));
或者手动唤醒
wake_up_interruptible(&key.r_wait);
等候队列头文件为:
#include <linux/wait.h>
等候队列的定义为wait_queue_head_t
在使用时 要先初始化:
void init_waitqueue_head(struct wait_queue_head *wq_head)
非阻塞
上面的阻塞模式就是正常的默认模式
只不过不会一直调用循环 而是会休眠
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。
poll、epoll 和 select 可以用于处理轮询,应用程序通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。
当应用程序调用 select、epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。
而非阻塞则要用到轮询等方法
在应用层需要在open时指定非阻塞打开模式 然后用select、poll、epoll函数来读取
如果在应用层指定了非阻塞访问方式 那么在驱动层就需要定义一个轮询方式的访问函数
如:
static struct file_operations key_fops = {.owner = THIS_MODULE,.open = key_open,.read = key_read,.write = key_write,.release = key_release,.poll = key_poll,
};
poll函数定义如下:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
函数返回值如下:
我们需要在驱动程序的 poll 函数中调用 poll_wait
函数,poll_wait
函数不会引起阻塞,只是
将应用程序添加到 poll_table
中,poll_wait
函数原型如下:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
参数 wait_address
是要添加到 poll_table
中的等待队列头,参数 p 就是 poll_table
,就是
file_operations
中 poll 函数的 wait 参数。
poll_wait
需要将等待队列的队列头传入 作用是将队列头移入到poll_table
中 实现监控的作用
在应用层 调用select、epoll 或 poll 函数时 收到返回值 对返回值进行判断即可知道是否可以做读取等操作
比如:
void main(void)
{
int ret;
int fd; /* 要监视的文件描述符 */
struct pollfd fds; fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 *//* 构造结构体 */
fds.fd = fd;
fds.events = POLLIN; /* 监视数据是否可以读取 */ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
if (ret) { /* 数据有效 */
}
在应用层调用轮询操作后 就会运行驱动的poll操作
其中的poll_wait
不会阻塞
如果队列头被唤醒 那么就会立即返回 否则就是等待到应用层中的超时后返回超时值
附录:嵌入式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
最后进行测试即可