【Linux驱动开发】irq中断配置API及中断应用 阻塞休眠和非阻塞的驱动操作

embedded/2024/11/21 16:49:10/

【Linux驱动开发】irq中断配置API及中断应用 阻塞休眠和非阻塞的驱动操作

文章目录

  • 中断操作
    • 注册和释放中断
    • 中断服务函数
    • 使能和禁止
    • 上半部和下半部
      • 软中断
      • tasklet
      • 工作队列
  • 设备节点中的中断
    • 中断号API函数
      • 获取中断号
      • 获取中断信息
  • 中断应用
    • 读取设备树节点
    • 获取中断号
    • 获取中断触发方式
    • 定义中断服务函数
    • 申请中断
    • 开启中断
    • 注销中断
  • 阻塞休眠和非阻塞驱动开发
    • 阻塞
    • 非阻塞
  • 附录:嵌入式Linux驱动开发基本步骤
    • 开发环境
    • 驱动文件
      • 编译驱动
      • 安装驱动
      • 自动创建设备节点文件
    • 驱动开发
      • 驱动设备号
      • 地址映射,虚拟内存和硬件内存地址
      • 字符驱动
        • 旧字符驱动
        • 新字符驱动
    • 应用程序开发

中断操作

与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_mapgpio_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.komodprobe test
建议用modprobe 原因是可以解决依赖关系
在这里插入图片描述

查看已安装的模块:
使用lsmodcat /proc/devices查看 其中 还能看到已安装的驱动设备号(新安装的不能重复)

创建设备节点文件:(如果自动创建就不需要)

mknod /dev/test c 200 0

查看节点文件:

ls /dev/test -l

在这里插入图片描述
最后如果不需要了 则卸载
卸载模块:
rmmod testmodprobe -r test

自动创建设备节点文件

使用udevmdev即可实现自动创建
如果要使用 则在驱动开发中写入到驱动入口函数中
(一般在 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文件 然后安装后即可调用

驱动设备号

驱动主要有主设备号 次设备号和驱动名
可以自定义 也可以自动申请
自定义的话 主设备号不能用冲突

查看已安装的模块:
使用lsmodcat /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_chrdevunregister_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_usercopy_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

最后进行测试即可


http://www.ppmy.cn/embedded/139384.html

相关文章

Linux下多线程

在Linux下的底层里并没有多线程这个概念&#xff0c;取而代之的是轻量级进程的概念。应为在Llinu下内核下并没有TCB,而只有PCB。 线程是什么 在⼀个程序⾥的⼀个执⾏路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程是“⼀个进程内部 的控制序…

【Git从入门到精通】——Git分支介绍与GitHub相关知识总结

&#x1f3bc;个人主页&#xff1a;【Y小夜】 &#x1f60e;作者简介&#xff1a;一位双非学校的大二学生&#xff0c;编程爱好者&#xff0c; 专注于基础和实战分享&#xff0c;欢迎私信咨询&#xff01; &#x1f386;入门专栏&#xff1a;&#x1f387;【MySQL&#xff0…

C++中的组合模式

组合模式&#xff08;Composite Pattern&#xff09; 组合模式是一种结构型设计模式&#xff0c;它将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端可以统一地处理单个对象和对象组合。这个模式特别适用于需要表示层次结构的场景&#xff0c;例如文件…

用Python“拍立淘”:在1688的海洋里寻找宝藏

想象一下&#xff0c;你是一名勇敢的探险家&#xff0c;手持一张神秘的藏宝图&#xff0c;准备在阿里巴巴的1688海洋中寻找那些隐藏的宝贝。但这次&#xff0c;你的武器不是传统的铲子和罗盘&#xff0c;而是Python爬虫。我们将一起编写一段代码&#xff0c;让它成为我们的“拍…

C++小白实习日记——Day 5 gitee怎么删文件,测试文件怎么写循环

昨晚一直内耗&#xff0c;一个程序写了三天写不出来&#xff0c;主要是耗时太多了&#xff0c;老板一直不满意。想在VScode上跑一下&#xff0c;昨晚一直报错。今天来公司重新搞了一下&#xff0c; 主要工作有&#xff1a; 1&#xff0c;读取当前时间用tscns 2&#xff0c;输…

Java项目实战II基于微信小程序的课堂助手(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 在数字化教…

MySQL 架构概览

show processlist;命令查询所有正在运行的线程 MySql架构从上到下分为网络连接层&#xff0c;系统服务层&#xff0c;存储引擎层&#xff0c;文件系统层 1.网络连接层主要负责维护客户端的连接&#xff0c;保存session会话&#xff0c;以及多线程下的连接管理。 2.系统服务层…

【Vim/Vi/Gvim操作】:列操作

文章目录 列操作1. 列选择模式1.1 visual 模式1.2 visual line模式1.3 visual block模式 2.列操作2.1 行首插入操作2.2 替换操作 列操作 1. 列选择模式 1.1 visual 模式 命令行状态下按下小写v&#xff0c;默认进入到visual模式&#xff0c;该模式相当于在window中按下鼠标左…