致敬英雄!
文章目錄
一、Linux內核定時器初探
1.1、圖形界面配置系統節拍率
中斷週期性產生的頻率就是系統頻率,也叫做節拍率(tick rate),單位是 Hz。系統節拍率是可以設置的,在編譯 Linux 內核的時候可以通過圖形化界面設置系統節拍率。
- 進入Linux內核源碼目錄,終端輸入
make menuconfig
,依次選擇Kernel Features -> Timer frequency,切換到100Hz,按下空格,進行選中!
- 設置好之後,保存退出。在內核源碼根目錄,查看.cofig文件內容,可以看到有如下宏定義!
1.2、重要全局變量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延時 |
1.3、內核定時器中斷
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);
}
1.4、ioctl 簡單介紹
ioctl 系統調用主要用於增加系統調用的硬件控制能力,它可以構建自己的命令,也能接受參數。通過 ioctl 控制硬件 I/O,必須在驅動中爲 ioctl()系統調用設計一些控制命令,通過不同的命令實現不同的硬件控制。更加深入研究,可參考<這裏>。
1.4.1 應用程序 ioctl 函數
用戶空間的 ioctl 函數原型如下所示,
int ioctl (int fd, unsigned long cmd, ...)
- fd 是被打開的設備文件, cmd 是操作設備的命令,“ …”代表可變數目的參數表,通常用 char *argp 來定義,如果 cmd 命令不需要參數,則傳入 NULL 即可。
1.4.2 驅動程序 ioctl 函數
內核空間 iotcl 函數原型如下所示,定義的 ioctl 命令通過 cmd 傳遞,數據通過 arg 傳遞。驅動得到 cmd 命令和 arg 參數後,須首先用解析 ioctl 命令的宏定義對命令和參數進行解析判斷,沒有問題再進行後續處理。
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
- filp 表示文件描述符,cmd表示命令,arg表示與命令相關的參數,至於參數具體表達什麼含義,完全由驅動編寫者來定義。
1.4.3 ioctl 命令構成
ioctl 操作與硬件平臺相關,使用 ioctl 的驅動需要包含<linux/ioctl.h>文件。每個 ioctl 命令cmd實際上都是一個 32 位整型數,各字段和含義如下表所示。
例如,0x82187201,它的二進制如下表所示。所以含義爲:讀:_IOR;參數長度536;幻數114,ASCII爲r,功能號1.
字段 | 31~30 | 29~16 | 15~8 | 7~0 |
---|---|---|---|---|
二進制 | 10 | 00 0010 0001 1000 | 0111 0010 | 0000 0001 |
實際上這個命令是<linux/msdos_fs.h>中的 VFAT_IOCTL_READDIR_BOTH 命令:#define VFAT_IOCTL_READDIR_BOTH _IOR('r', 1, struct __fat_dirent[2])
1.4.4 構造ioctl命令
爲驅動構造 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 命令定義,無論有無數據傳輸以及數據傳輸方向是否相同,各命令的序號都不能相同
定義完全部所需命令後,還需定義一個命令的最大的編號,防止傳入參數超過編號範圍。
1.4.5 解析 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;
}
二、編寫代碼
2.1 修改、編譯、覆蓋設備樹文件
參考第九節內容。
2.2 驅動程序編寫
這一次將驅動程序的框架又完善了一下,認真體會!
leddrv.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/timer.h>
#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");
需要說明的:
-
①、燈的狀態
- 設備樹中設置低電平有效,打開-紅色-寫1 關閉-白色-寫0
-
②、使用自旋鎖
- 取出定時週期值的時候,timerperiod = dev->timeperiod;
- 設置定時週期的時候,dev->timeperiod = arg;
-
③、ioctl
- 幻數0xEF,對應十進制239,對應ASCII爲符號
'∩'
!(不是小寫字母n,數學符號交集) - 驅動程序中,爲了方便並沒有做解析,而是直接
switch-case
選擇!
- 幻數0xEF,對應十進制239,對應ASCII爲符號
-
④、私有數據
- 一般在open的時候將file結構體中的private_data指向設備結構體,即設置私有數據!
-
⑤、修改定時器
- 修改定時器會激活定時器
- msecs_to_jiffies轉換系統節拍的時候,藉助的是第三方變量,而沒有直接操作
dev->timeperiod
-
⑥、強制轉化
- 程序中涉及到結構體和unsigned long的轉化,注意體會思想,也可參考<這篇>文章裏的強制轉化進行理解!
-
所有LED相關的放到了一個設備結構體裏,然後引入私有數據的思想,值得認真體會!
2.2 應用程序編寫
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);
}
需要說明的
- gets的加入可能會讓程序編譯的時候有警告,忽略即可!
- 實現功能
- 輸入 1 表示關閉定時器
- 輸入 2 表示打開定時器
- 輸入 3 設置定時器週期
- 選擇設置定時器週期的話,接着需要輸入設置的週期值,單位爲毫秒
三、運行程序
編譯程序沒有問題後,運行qemu虛擬開發板,並做好準備工作!將
- 拷貝led.ko和ledtest到NFS中
cp *.ko ledtest ~/linux/qemu/NFS/
- 在qemu終端,加載led.ko文件
insmod leddrv.ko
在qemu中加載最後一個模塊時,會出現下面的提示信息,但是ctrl+c之後,似乎測試還是可以用的,不知道是怎麼回事。知道的朋友,可以在下面留言一起探討!
- 在qemu終端,運行應用程序
./ledtest /dev/led
同時,可以看到,qemu模擬板的第一個小燈,又白色變成紅色表示打開。同時終端會提示讓繼續輸入命令,我們嘗試輸入2,打開定時器,觀察小燈閃爍!【無法錄屏,這裏就不放圖了】
接着輸入1,關閉定時器,取消LED閃爍!
最後輸入3,自定義LED閃爍時間爲2000ms!
大功告成,還是很完美的!