我尽量讲的更详细,为了关注我的粉丝!!!
本章使用通过设置一个定时器来实现周期性的闪烁 LED 灯,因此本章例程就使用到了一个LED 灯。
这里我们以毫秒为单位,所以要用msecs_to_jiffies这个函数。
如果是2s就是msecs_to_jiffies(2000)。
在linux函数定时器里面,判断它定时多久都是和系统的HZ作比较,2HZ就是2s,msecs_to_jiffies(2000)也是2s,毫秒为单位。
1.修改设备树文件
这一步骤在前面已经有过,所以跟着前面文章的同学应该很熟悉!
在 stm32mp157d-atk.dts 文件的根节点"/"下创建 LED 灯节点,节点名为"timer",节点内容如下:
发现节点已经有了!
2.LED 灯驱动程序编写
之前的博客也是跟大家按照肌肉记忆来编写程序!一步一步按照思路来编写!
总代码会放在最后。
为了让大家更能明白,可以先对着总代码,进行对我的写代码流程更加详细得当!
放心,我也是一步一步打的代码,不是复制粘贴!!!
2.1头文件
比以前的代码添加了
#include <linux/timer.h>
#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 <linux/semaphore.h>
#include <linux/timer.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
2.2驱动入口函数和出口函数及MODULE函数

2.3创建设备结构体
在跟之前一样,为了后续的注册设备字符,要在开发板内核里面显示的话,要申请设备号,便包括主设备号和次设备号;为了显得更专业一点,就要使用结构体来统一有关设备的相关父子类结构体信息。后面也可以添加更多的子类信息!
这里看总代码就知道我放在哪个位置了!
dev_t
是一个用于表示设备编号的数据类型,它在 <sys/types.h>
头文件中被定义。
timer
:这是一个变量名,它的类型是 struct timer_dev
。也就是说,timer
是一个 timer_dev
结构体类型的变量,通过这个变量可以存储和操作该结构体所定义的各种数据。
比如timer.devid,timer.major,timer.minor。等等。
后续有很多子类会在这里!
例如:
struct timer_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行分别以给定设备号和未给定设备号的情况进行编程。其中timer.major,timer.minor便是利用了结构体内的变量子类,MKDEV
是 Linux 内核提供的一个宏,用于根据主设备号和次设备号生成一个 dev_t
类型的设备编号。其定义通常位于 <linux/kdev_t.h>
头文件中,register_chrdev_region
函数的原型定义在 <linux/fs.h>
头文件中,若是给了设备号就是用这个函数,从gpio.devid所赋值的设备号告诉内核进行相应设备号的注册。第二个位置就是设备号个数,第三个位置就是设备号名称。
alloc_chrdev_region
不需要手动指定主设备号,内核会自动分配一个未被使用的主设备号给驱动程序。&timer.devid是指针类型,原型是dev_t *dev
,要通过&取地址来存储设备号,第二个位置是起始的此设备号。
既然有创建字符设备,那就要有删除字符设备。
2.4.1补充设备结构体
完成字符设备注册后,就是在设备结构体中添加字符设备对象。
2.5初始化cdev
很显然cdev也是变量timer的子类timer.cdev。同时在内核里面cdev里面包括很多子类信息。
这里是内核里面的定义。不用管,只要配置cdev的子类即可。
对cdev进行初始化,这个是字符设备对象。
timer.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
目录下)来访问这个字符设备。
即后续的App 可以执行到/dev
2.6.1注销字符设备对象
目前已经注册和注销了字符设备,同时也注册了字符设备对象,所以要进行字符设备对象的注销。
依然按照逻辑顺序进行注销。
2.7创建设备类和设备节点
2.7.1补充类与节点的定义
这两者作用在后续内核中创建/dev/timer。方便执行代码程序,传入到设备执行,关联设备号
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:即本文的timer.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(timer.cls)
返回 true
时,代码会执行 return PTR_ERR(timer.cls);
,这会将 timer.cls
转换为对应的错误码并返回给调用者。调用者可以根据这个错误码进行相应的错误处理,例如打印错误信息、释放已经分配的资源等。
同样:
2.7.5注销类和设备节点

2.7.6配置操作集函数
这里不用write,read。用上static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
是一个典型的 Linux 内核驱动程序中的 ioctl
系统调用处理函数。ioctl
(Input/Output Control)是一种用于设备驱动程序和用户空间程序之间进行特殊控制操作的接口。这个函数允许用户空间程序向内核空间的设备驱动发送特定的命令(cmd
),并传递相关的数据(arg
),以实现对设备的各种控制。
复制上一节的操作函数集
static int timer_open(struct inode *inode, struct file *filp)
{
int ret = 0;
filp->private_data = &timerdev;
return 0;
}
static long timer_unlocked_ioctl(struct file *filp,
unsigned int cmd, unsigned long arg)
{
struct timer_dev *dev = (struct timer_dev *)filp->private_data;
return 0;
}
static int timer_release(struct inode *inode, struct file *filp)
{
struct timer_dev *dev = filp->private_data;
return 0;
}
/*操作集*/
static const struct file_operations timer_fops = {
.owner = THIS_MODULE,
.open = timer_open,
.release = timer_release,
.unlocked_ioctl = timer_unlocked_ioctl,
};
2.8获取设备节点(设备树属性)
这里和前面的文章不同,我们分别利用timer_init,led_init来编程,timer_init用于注册设备相关的,led_init用于gpio的属性配置。
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配置错误处理
按照以前的习惯,从后面进行处理错误信息,按照逻辑!!!
timer_init:
led_init:
2.9初始化自旋锁
这里我们用自旋锁来保证需要的代码不会受到中断的干扰!
因为这里用的是线程,所以用
spin_lock_irqsave(&gpioled.lock, flags);/*上锁*/
spin_unlock_irqrestore(&gpioled.lock, flags);/解锁 /
//这是为了防止在持有锁期间被中断服务程序打断,避免出现死锁或数据不一致的问题
2.10配置定时器

2.10.1初始化定时器

这里应用timer_function是回调函数。
/*
`通过 mod_timer 函数设置了定时器的到期时间后,一旦系统时钟的节拍数(jiffies)`
`达到了设定的到期时间,内核就会触发定时器,进而调用注册的回调函数 timer_function,后面会不断去执行timer_function`
*/
知道了回调函数的作用,直接可以在这写定时器周期执行想要实现的功能!
其中sta是静态变量,sta虽然初始化为整数变量,会保留上一次调用结束的值。
后面不停的执行实现led灯的反转。不过也要等时间过后才会执行。
2.11配置操作函数

46行代码定义定时器周期为1s,单位用的是ms,所以后面用msecs_to_jiffies(timerperiod),跟HZ作比较,jiffies/HZ=1000的时候就是1s。
48行代码初始化led_init,y因为在timer_init中并没有初始化led相关代码,所以在这里初始化led。
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
是一个典型的 Linux 内核驱动程序中的 ioctl
系统调用处理函数。ioctl
(Input/Output Control)是一种用于设备驱动程序和用户空间程序之间进行特殊控制操作的接口。这个函数允许用户空间程序向内核空间的设备驱动发送特定的命令(cmd
),并传递相关的数据(arg
),以实现对设备的各种控制。
所以可以利用switch(cmd)来执行特定的命令,传递相关的数据。
就是允许App(用户空间程序),./App /dev/传递相关数据arg。
timer_exit注销了timer字符设备相关的代码,但是并没有释放gpio相关的信息,释放资源。
所以这里就是在timer_release执行gpio相关代码,这里也关闭定时器!
这里同时删除定时器!
2.12程序效果

这里实现了打开定时器,改变定时器周期,2s,0.5s。
2.13总代码
timer.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 <linux/semaphore.h>
#include <linux/timer.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define TIMER_CNT 1
#define TIMER_NAME "timer"
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令*/
/*timer设备结构体*/
struct timer_dev{
dev_t devid;//设备号
int major;//主设备号
int minor;//次设备号
struct cdev cdev;//关联设备,字符设备对象
struct class *class;//设备类
struct device *device;//设备节点
struct device_node *nd;//设备树节点指针
int led_gpio; /*led所使用的GPIO编号*/
spinlock_t lock; /*定义自旋锁*/
struct timer_list timer; /*定义一个定时器*/
int timeperiod; /*定时周期,单位为ms*/
};
struct timer_dev timerdev;//设备
/*初始化led*/
static int led_init(void)
{
int ret;
/*1.获取设备节点*/
timerdev.nd = of_find_node_by_path("/gpioled");//根节点下的gpioled
if(timerdev.nd == NULL){
printk("timerdev node not find!\r\n");
ret = -EINVAL;
goto fail_findnode;
}
/*2.获取led下的所对应gpio编号*/
timerdev.led_gpio = of_get_named_gpio(timerdev.nd,"led-gpio",0);
if(timerdev.led_gpio < 0){
printk("can't find led gpio\r\n");
ret = -EINVAL;
goto fail_findnode;
}
printk("led gpio num = %d\r\n",timerdev.led_gpio);
/*3.申请IO*/
ret = gpio_request(timerdev.led_gpio,"led-gpio");
if(ret){
printk("Failed to request the led gpio!\r\n");
ret = -EINVAL;
goto fail_findnode;
}
/*4.设置IO为输出*/
ret = gpio_direction_output(timerdev.led_gpio, 1);//默认关闭led
if (ret < 0) {
ret = -EINVAL;
goto fail_setoutput;
}
return 0;
fail_setoutput:
gpio_free(timerdev.led_gpio);
fail_findnode:
device_destroy(timerdev.class,timerdev.devid);
return ret;
}
static int timer_open(struct inode *inode, struct file *filp)
{
int ret = 0;
filp->private_data = &timerdev;
timerdev.timeperiod = 1000; /*默认周期为1s,这里跟系统的单位HZ比较节拍率
,不用我们管,是函数调用内部自动完成,jiffies和HZ比较,我们只要找换算单位即可*/
ret = led_init(); /*初始化 LED IO*/
if(ret < 0){
return ret;
}
return 0;
}
static long timer_unlocked_ioctl(struct file *filp,
unsigned int cmd, unsigned long arg)
{
struct timer_dev *dev = (struct timer_dev *)filp->private_data;
int timerperiod;
unsigned long flags;
switch (cmd) {
case CLOSE_CMD: /* 关闭定时器 */
del_timer_sync(&dev->timer);
break;
case OPEN_CMD: /* 打开定时器 */
/*这里其实可以mod_timer(&dev->timer, jiffies +
msecs_to_jiffies(dev->timeperiod);
但是考虑到如果执行命令过快时,dev->timeperiod会不断变化,
线程还没解锁dev->timeperiod就发生改变了。
*/
spin_lock_irqsave(&dev->lock, flags);//线程上锁
timerperiod = dev->timeperiod;//保护这个代码
spin_unlock_irqrestore(&dev->lock, flags);//线程解锁
mod_timer(&dev->timer, jiffies +
msecs_to_jiffies(timerperiod));
break;
case SETPERIOD_CMD: /* 设置定时器周期 */
spin_lock_irqsave(&dev->lock, flags);//线程上锁
dev->timeperiod = arg;//保护这个代码
spin_unlock_irqrestore(&dev->lock, flags);//线程解锁
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));
break;
default:
break;
}
return 0;
}
static int timer_release(struct inode *inode, struct file *filp)
{
struct timer_dev *dev = filp->private_data;
gpio_set_value(dev->led_gpio, 1); /* APP 结束的时候关闭 LED */
gpio_free(dev->led_gpio); /* 释放 LED 的GPIO*/
del_timer_sync(&dev->timer); /* 关闭定时器 */
return 0;
}
/*操作集*/
static const struct file_operations timer_fops = {
.owner = THIS_MODULE,
.open = timer_open,
.release = timer_release,
.unlocked_ioctl = timer_unlocked_ioctl,
};
/*定时器回调函数*/
void timer_function(struct timer_list *arg)
{//这个函数作用就是定时周期的执行程序:比如周期翻转led的状态
/* from_timer 是个宏,可以根据结构体的成员地址,获取到这个结构体的首地址。
第一个参数表示结构体,第二个参数表示第一个参数里的一个成员,第三个参数表
示第二个参数的类型,得到第一个参数的首地址。
*/
/*
通过 mod_timer 函数设置了定时器的到期时间后,一旦系统时钟的节拍数(jiffies)
达到了设定的到期时间,内核就会触发定时器,进而调用注册的回调函数 timer_function
*/
struct timer_dev *dev = from_timer(dev, arg, timer);
static int sta = 1;/*静态局部变量的生命周期从程序开始运行到程序结束
,它在函数的多次调用之间会保留其值。也就是说,当 timer_function 函数
第一次被调用时,sta 被初始化为 1,后续每次调用该函数时,sta
不会再次被初始化,而是保留上一次调用结束时的值。*/
int timerperiod;
unsigned long flags;
sta = !sta; /* 每次都取反,实现 LED 灯反转 */
gpio_set_value(dev->led_gpio, sta);
/*重启定时器*/
spin_lock_irqsave(&dev->lock, flags);//上锁
timerperiod = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);//解锁
mod_timer(&dev->timer, jiffies +msecs_to_jiffies(timerperiod));
}
/*字符设备驱动入口函数*/
static int __init timer_init(void)
{
int ret;
/*1.注册字符设备*/
timerdev.major = 0;
if(timerdev.major){//若给定了主设备号
timerdev.devid = MKDEV(timerdev.major,0);
ret = register_chrdev_region(timerdev.devid,TIMER_CNT,TIMER_NAME);
}else{//若未给定主设备号
ret = alloc_chrdev_region(&timerdev.devid,0,TIMER_CNT,TIMER_NAME);
timerdev.major = MAJOR(timerdev.devid);
timerdev.minor = MINOR(timerdev.devid);
}
if(ret < 0){//注册字符设备驱动失败
goto fail_devid;
}
printk("major = %d,minor = %d,CNT = %d,NAME = %s\r\n",
timerdev.major,timerdev.minor,TIMER_CNT,TIMER_NAME);
/*2.初始化cdev*/
timerdev.cdev.owner = THIS_MODULE;
cdev_init(&timerdev.cdev,&timer_fops);
/*3.添加cdev*/
ret = cdev_add(&timerdev.cdev,timerdev.devid,TIMER_CNT);//可以执行/dev
if(ret < 0){
goto fail_cdev;
}
/*4.创建类*/
timerdev.class = class_create(THIS_MODULE,TIMER_NAME);//创建/dev/timer
if(IS_ERR(timerdev.class)){
ret = PTR_ERR(timerdev.class);
goto fail_class;
}
/*5.创建设备节点*/
timerdev.device = device_create(timerdev.class,NULL,timerdev.devid,
NULL,TIMER_NAME);
if(IS_ERR(timerdev.device)){
ret = PTR_ERR(timerdev.device);
goto fail_device;
}
/*6.初始化自旋锁*/
spin_lock_init(&timerdev.lock);
/*7.初始化timer设置定时器处理函数,还未设置周期,所有不会激活定时器*/
timer_setup(&timerdev.timer, timer_function, 0);
return 0;
fail_device:
class_destroy(timerdev.class);
fail_class:
cdev_del(&timerdev.cdev);
fail_cdev:
unregister_chrdev_region(timerdev.devid,TIMER_CNT);
fail_devid:
return ret;
}
/*字符设备驱动出口函数*/
static void __exit timer_exit(void)
{
/*删除timer*/
del_timer_sync(&timerdev.timer);
#if 0
del_timer(&timerdev.tiemr);
#endif
/*释放IO*/
gpio_free(timerdev.led_gpio);
/*注销设备节点*/
device_destroy(timerdev.class,timerdev.devid);
/*注销设备类*/
class_destroy(timerdev.class);
/*注销字符设备对象驱动*/
cdev_del(&timerdev.cdev);
/*注销字符设备驱动*/
unregister_chrdev_region(timerdev.devid,TIMER_CNT);
}
module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("chensir");
MODULE_INFO(intree,"Y");
LEDAPP.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <sys/ioctl.h>
/*
*argc:应用程序参数个数
*argv[]:具体的参数内容,字符串形式
*./timerApp <fliename>
*./timerApp /dev/timer
*./timerApp /dev/timer
*/
/* 命令值 */
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
/*
* @description : main 主程序
* @param - argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, ret;
char *filename;
unsigned int cmd;
unsigned int arg;
unsigned char str[100];
if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
while (1) {
printf("Input CMD:");
ret = scanf("%d", &cmd);
if (ret != 1) { /* 参数输入错误 */
fgets(str, sizeof(str), stdin); /* 防止卡死 */
}
if(4 == cmd) /* 退出 APP */
goto out;
if(cmd == 1) /* 关闭 LED 灯 */
cmd = CLOSE_CMD;
else if(cmd == 2) /* 打开 LED 灯 */
cmd = OPEN_CMD;
else if(cmd == 3) {
cmd = SETPERIOD_CMD; /* 设置周期值 */
printf("Input Timer Period:");
ret = scanf("%d", &arg);
if (ret != 1) { /* 参数输入错误 */
fgets(str, sizeof(str), stdin); /* 防止卡死 */
}
}
ioctl(fd, cmd, arg); /* 控制定时器的打开和关闭 */
}
out:
close(fd);
}
makefile
KERNELDIR := /home/chensir/linux/atk-mp1/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := timer.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean