致敬英雄!
中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate),单位是 Hz。系统节拍率是可以设置的,在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率。
make menuconfig
,依次选择Kernel Features -> Timer frequency,切换到100Hz,按下空格,进行选中!jiffies
在上一步,我们采用了 100Hz 的节拍率,这样时间精度就是 10ms。不管是 32 位的系统还是 64 位系统,都可以使用 jiffies来记录系统从启动以来的系统节拍数。(初始化默认为0)
100HZ 表示1秒有100个节拍, jiffies 表示系统运行的总节拍数。那么后者除以前者,即可得到系统的运行时间。不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,因此该现象称之为绕回现象。处理 jiffies 的绕回显得尤为重要,Linux 内核提供了如下表所示的几个 API 函数来处理绕回。
函数 | 描述 |
---|---|
time_after(unkown, known) | unkown > kown,返回真 |
time_before(unkown, known) | unkown < kown,返回真 |
time_after_eq(unkown, known) | unkown ≥ kown,返回真 |
time_before_eq(unkown, known) | unkown ≤ kown,返回真 |
注:表中的unkown 通常为 jiffies, known 通常是需要对比的值。
为了方便开发, Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数,如下表
函数 | 描述 |
---|---|
int jiffies_to_msecs(const unsigned long j) | jiffies转化为对应的ms |
int jiffies_to_usecs(const unsigned long j) | jiffies转化为对应的us |
u64 jiffies_to_nsecs(const unsigned long j) | jiffies转化为对应的ns |
long msecs_to_jiffies(const unsigned int m) | ms转化为对应的jiffies |
long usecs_to_jiffies(const unsigned int u) | us转化为对应的jiffies |
unsigned long nsecs_to_jiffies(u64 n) | ns转化为对应的jiffies |
这里再补充一下Linux 内核短延时函数
函数 | 描述 |
---|---|
void ndelay(unsigned long nsecs) | ns延时 |
void udelay(unsigned long usecs) | us延时 |
void mdelay(unsigned long mseces) | ms延时 |
Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行。要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
Linux 内核使用 timer_list 结构体表示内核定时器,定义如下
struct timer_list {
struct list_head entry;
unsigned long expires; /* 定时器超时时间,单位是节拍数 */
struct tvec_base *base;
void (*function)(unsigned long); /* 定时处理函数 */
unsigned long data; /* 要传递给 function 函数的参数 */
int slack;
};
比如我们要定义一个周期为2s的定时器,那么expires = jiffies + msecs_to_jiffies(timerperiod)
,
定时器相关API函数
函数 | 描述 |
---|---|
init_timer | 初始化 timer_list 类型变量 |
add_timer | 向 Linux 内核注册定时器 |
del_timer | 删除一个定时器(不管有没有激活,立即删除)(不常用) |
del_timer_sync | 使用完定时器再删除,不能使用在中断上下文 |
mod_timer | 修改定时值(会激活定时器,一般放到中断函数尾,用于周期定时) |
使用流程
struct timer_list timer; /* 定义定时器 */
/* 定时器回调函数 */
void function(unsigned long arg)
{
/*
* 定时器处理代码
*/
/* 如果需要定时器周期性运行的话就使用 mod_timer
* 函数重新设置超时值并且启动定时器。
*/
mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
}
/* 初始化函数 */
void init(void)
{
init_timer(&timer); /* 初始化定时器 */
timer.function = function; /* 设置定时处理函数 */
timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */
add_timer(&timer); /* 启动定时器 */
}
/* 退出函数 */
void exit(void)
{
del_timer(&timer); /* 删除定时器 */
/* 或者使用 */
del_timer_sync(&timer);
}
ioctl 系统调用主要用于增加系统调用的硬件控制能力,它可以构建自己的命令,也能接受参数。通过 ioctl 控制硬件 I/O,必须在驱动中为 ioctl()系统调用设计一些控制命令,通过不同的命令实现不同的硬件控制。更加深入研究,可参考<这里>。
用户空间的 ioctl 函数原型如下所示,
int ioctl (int fd, unsigned long cmd, ...)
内核空间 iotcl 函数原型如下所示,定义的 ioctl 命令通过 cmd 传递,数据通过 arg 传递。驱动得到 cmd 命令和 arg 参数后,须首先用解析 ioctl 命令的宏定义对命令和参数进行解析判断,没有问题再进行后续处理。
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
ioctl 操作与硬件平台相关,使用 ioctl 的驱动需要包含
例如,0x82187201,它的二进制如下表所示。所以含义为:读:_IOR;参数长度536;幻数114,ASCII为r,功能号1.
字段 | 31~30 | 29~16 | 15~8 | 7~0 |
---|---|---|---|---|
二进制 | 10 | 00 0010 0001 1000 | 0111 0010 | 0000 0001 |
实际上这个命令是#define VFAT_IOCTL_READDIR_BOTH _IOR('r', 1, struct __fat_dirent[2])
为驱动构造 ioctl 命令,首先要为驱动选择一个可用的幻数作为驱动的特征码,以区分不同驱动的命令。内核已经使用了很多幻数,为了防止冲突,最好不要再使用这些系统已经占用的幻数来作为驱动的特征码。已经被使用的幻数列表详见内核源码目录Documentation/ioctl/ioctl-number.txt
文件。
在不同平台上,幻数所使用情况都不同,为防止冲突,可以选择其它平台使用的幻数来用。选定幻数后,可以这样来进行定义:
#define LED_IOC_MAGIC 'Z'
ioctl 命令字段的 bit[31:30]表示命令的方向,分别表示使用_IO、 _IOW、 _IOR 和_IOWR
这几个宏定义,分别用于构造不同的命令,具体见下表:
命令 | 描述 |
---|---|
_IO(type,nr) | 构造无参数的命令编号 |
_IOW(type,nr,size) | 构造往驱动写入数据的命令编号 |
_IOR(type,nr,size) | 构造从驱动中读取数据的命令编号 |
_IOWR(type,nr,size) | 构造双向传输的命令编号 |
其中, type 是幻数, nr 是功能号, size 是数据大小。
例如,为 LED 驱动构造 ioctl 命令,由于控制 LED 无需数据传输,可以这样定义:
#define SET_LED_ON _IO(LED_IOC_MAGIC, 0)
#define SET_LED_OFF _IO(LED_IOC_MAGIC, 1)
例如,如果想在 ioctl 中往驱动写入一个 int 型的数据,可以这样定义:
#define CHAR_WRITE_DATA _IOW(CHAR_IOC_MAGIC, 2, int)
例如,要从驱动中读取 int 型的数据,则定义为:
#define CHAR_READ_DATA _IOR(CHAR_IOC_MAGIC, 3, int)
注意:同一份驱动的 ioctl 命令定义,无论有无数据传输以及数据传输方向是否相同,各命令的序号都不能相同
定义完全部所需命令后,还需定义一个命令的最大的编号,防止传入参数超过编号范围。
驱动程序必须对传入的命令进行解析,包括传输方向、命令类型、命令编号以及参数大
小,分别可以通过下表的宏定义完成:
宏定义 | 描述 |
---|---|
_IOC_DIR(nr) | 解析命令的传输方向 |
_IOC_TYPE(nr) | 解析命令类型 |
_IOC_NR(nr) | 解析命令序号 |
_IOC_SIZE(nr) | 解析参数大小 |
如果解析发现命令出错,可以返回-ENOTTY,如:
if (_IOC_TYPE(cmd) != LED_IOC_MAGIC) {
return -ENOTTY;
}
if (_IOC_NR(cmd) >= LED_IOC_MAXNR) {
return -ENOTTY;
}
参考第九节内容。
这一次将驱动程序的框架又完善了一下,认真体会!
leddrv.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEV_CNT 1 /* 设备个数 */
#define DEV_NAME "led" /* 设备名字 */
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
/* 定义led_dev设备结构体 */
struct led_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
/*GPIO子系统*/
struct gpio_desc *led_gpio; /* GPIO子系统接口 */
/*定时器*/
int timeperiod; /* 定时周期,单位为ms */
struct timer_list timer; /* 定义一个定时器*/
/*自旋锁*/
spinlock_t lock; /* 定义自旋锁 */
};
struct led_dev leddev; /* led设备 */
static int led_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
file->private_data = &leddev; /* 设置私有数据 */
leddev.timeperiod = 1000; /* 默认周期为1s */
gpiod_direction_output(leddev.led_gpio, 1); /* 初始化LED - on */
return 0;
}
static long led_drv_unlocked_ioctl (struct file *file, unsigned int cmd, unsigned long arg)
{
struct led_dev *dev = (struct led_dev *)file->private_data;
int timerperiod;
unsigned long flags;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
switch (cmd) {
case CLOSE_CMD: /* 关闭定时器 */
del_timer_sync(&dev->timer);
break;
case OPEN_CMD: /* 打开定时器 */
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;
}
/* 定义自己的file_operations结构体 */
static struct file_operations led_drv = {
.owner = THIS_MODULE,
.open = led_drv_open,
.unlocked_ioctl = led_drv_unlocked_ioctl,
};
/* 定时器回调函数 */
void timer_function(unsigned long arg)
{
struct led_dev *dev = (struct led_dev *)arg;
static int sta = 1;
int timerperiod;
unsigned long flags;
sta = !sta; /* 每次都取反,实现LED灯反转 */
gpiod_set_value(dev->led_gpio, sta);/* 用的时候需要强制转化为struct led_dev*,并且只能用->运算符 */
/* 重启定时器 */
spin_lock_irqsave(&dev->lock, flags);
timerperiod = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));
}
/* 从platform_device获得GPIO
* 把file_operations结构体告诉内核:注册驱动程序
*/
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 1、从设备树中获取资源。设备树中定义有: led-gpios=<...>; */
leddev.led_gpio = gpiod_get(&pdev->dev, "led", 0);
if (IS_ERR(leddev.led_gpio)) {
dev_err(&pdev->dev, "Failed to get GPIO for led\n");
return PTR_ERR(leddev.led_gpio);
}
/* 2、注册字符设备驱动 */
/* ①、创建设备号 */
if (leddev.major) { /* 定义了设备号 */
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(leddev.devid, DEV_CNT, DEV_NAME);
} else { /* 没有定义设备号 */
alloc_chrdev_region(&leddev.devid, 0, DEV_CNT, DEV_NAME); /* 申请设备号 */
leddev.major = MAJOR(leddev.devid); /* 获取分配号的主设备号 */
leddev.minor = MINOR(leddev.devid); /* 获取分配号的次设备号 */
}
/* ②、初始化cdev */
leddev.cdev.owner = THIS_MODULE;
cdev_init(&leddev.cdev, &led_drv);
/* ③、添加一个cdev */
cdev_add(&leddev.cdev, leddev.devid, DEV_CNT);
/* ④、创建类 */
leddev.class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(leddev.class)) {
return PTR_ERR(leddev.class);
}
/* ⑤、创建设备 */
leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, DEV_NAME);
if (IS_ERR(leddev.device)) {
return PTR_ERR(leddev.device);
}
/* 初始化自旋锁 */
spin_lock_init(&leddev.lock);
/* 初始化timer,设置定时器处理函数,还未设置周期,所以不会激活定时器 */
init_timer(&leddev.timer);
leddev.timer.function = timer_function;
/* 注意leddev类型是结构体led_dev,这里取地址然后强制转化为unsigned long,用的时候需要强制转化为struct led_dev* */
leddev.timer.data = (unsigned long)&leddev;
return 0;
}
static int chip_demo_gpio_remove(struct platform_device *pdev)
{
gpiod_set_value(leddev.led_gpio, 0);/* 卸载驱动的时候关闭LED */
gpiod_put(leddev.led_gpio);
del_timer_sync(&leddev.timer); /* 删除timer */
/* 注销字符设备驱动 */
cdev_del(&leddev.cdev);/* 删除cdev */
unregister_chrdev_region(leddev.devid, DEV_CNT); /* 注销设备号 */
device_destroy(leddev.class, leddev.devid);
class_destroy(leddev.class);
return 0;
}
static const struct of_device_id ask100_leds[] = {
{ .compatible = "100ask,leddrv" },
{ },
};
/* 1. 定义platform_driver */
static struct platform_driver chip_demo_gpio_driver = {
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "100ask_led",
.of_match_table = ask100_leds,
},
};
/* 2. 在入口函数注册platform_driver */
static int __init led_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&chip_demo_gpio_driver);
return err;
}
/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
* 卸载platform_driver
*/
static void __exit led_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&chip_demo_gpio_driver);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
需要说明的:
①、灯的状态
②、使用自旋锁
③、ioctl
'∩'
!(不是小写字母n,数学符号交集)switch-case
选择!④、私有数据
⑤、修改定时器
dev->timeperiod
⑥、强制转化
所有LED相关的放到了一个设备结构体里,然后引入私有数据的思想,值得认真体会!
ledtest.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 "linux/ioctl.h"
/* 命令值 */
#define CLOSE_CMD (_IO(0XEF, 0x1)) /* 关闭定时器 */
#define OPEN_CMD (_IO(0XEF, 0x2)) /* 打开定时器 */
#define SETPERIOD_CMD (_IO(0XEF, 0x3)) /* 设置定时器周期命令 */
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) { /* 参数输入错误 */
gets(str); /* 防止卡死 */
}
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) { /* 参数输入错误 */
gets(str); /* 防止卡死 */
}
}
ioctl(fd, cmd, arg); /* 控制定时器的打开和关闭 */
}
close(fd);
}
需要说明的
编译程序没有问题后,运行qemu虚拟开发板,并做好准备工作!将
cp *.ko ledtest ~/linux/qemu/NFS/
insmod leddrv.ko
在qemu中加载最后一个模块时,会出现下面的提示信息,但是ctrl+c之后,似乎测试还是可以用的,不知道是怎么回事。知道的朋友,可以在下面留言一起探讨!
./ledtest /dev/led
同时,可以看到,qemu模拟板的第一个小灯,又白色变成红色表示打开。同时终端会提示让继续输入命令,我们尝试输入2,打开定时器,观察小灯闪烁!【无法录屏,这里就不放图了】
接着输入1,关闭定时器,取消LED闪烁!
最后输入3,自定义LED闪烁时间为2000ms!
大功告成,还是很完美的!