【嵌入式Linux驅動開發】十四、瞭解Linux內核定時器使用流程,實現LED閃爍

   致敬英雄!


一、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選擇!
  • ④、私有數據

    • 一般在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!

大功告成,還是很完美的!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章