Linux driver development | kernel timer

Posted by Flames on Mon, 01 Nov 2021 04:19:56 +0100

Linux kernel timer

1. Kernel time management

Many functions in Linux kernel need time management, such as periodic scheduler, delay program and timer. The hardware timer provides the clock source. The frequency of the clock source can be set. After setting, the timing interrupt will be generated periodically. The system uses the timing interrupt to time. The frequency generated periodically by interrupts is the system frequency, also known as the tick rate. The system beat rate can be set in Hz. When compiling the Linux kernel, you can set the system beat rate through the graphical interface, and open the configuration interface according to the following path:

-> Kernel Features
	-> Timer frequency (<choice>[=y])


By default, the system beat rate is selected as 100Hz. After setting, it can be seen in the. config file under the root directory of Linux kernel source code that the system beat rate is set as 100Hz

The Linux kernel uses CONFIG_HZ to set its own system clock. The file include / ASM generic / param. H contains the following:

#undef HZ 
#define HZ CONFIG_HZ 
#define USER_HZ 100 9 
#define CLOCKS_PER_SEC (USER_HZ)

The Linux kernel uses the global variable jiffies to record the system beats since the system was started. When the system is started, jiffies will be initialized to 0. Jiffies are defined in the file include/linux/jiffies.h, as follows:

extern u64 __jiffy_data jiffies_64;		//Define a 64 bit jiffies_64 for 64 bit systems
extern unsigned long volatile __jiffy_data jiffies;		//32-bit jiffies for 32-bit systems

HZ indicates the number of beats per second and jiffies indicates the number of jiffies beats of the system. Therefore, jiffies/HZ is the system running time, in seconds. Both 32-bit and 64 bit jiffies have the risk of overflow. After overflow, the count will start from 0 again, which is equivalent to wrapping back. Therefore, this phenomenon is also called wrapping back. If the HZ is the maximum value of 1000, the 32-bit jiffies only need 49.7 days to detour. For 64 bit jiffies, it will take about 580 million years to detour, so jiffies_ The detour of 64 is ignored

It is particularly important to handle the 32-bit jiffies' round robin. The Linux kernel provides several API functions as shown below to handle the round robin

If unkown exceeds known, time_ The after function returns true, otherwise it returns false. If unkown does not exceed known time_ The before function returns true, otherwise it returns false. time_after_eq function and time_ The after function is similar, except that more judgment is equal to this condition. Similarly, time_before_eq function and time_ The before function is similar.

To determine whether the execution time of a piece of code has timed out, you can use the following code:

unsigned long timeout;
timeout = jiffies + (2 * HZ); /* Time point of timeout */
/*************************************
Specific code
************************************/
/* Determine whether there is a timeout */
if(time_before(jiffies, timeout)) {
	/* Timeout did not occur */
} else {
	/* Timeout occurred */
}

In order to facilitate development, the Linux kernel provides several conversion functions between jiffies and ms, us and ns, as shown below

2. Kernel timer

Timer is a very common function, which is used for work requiring periodic processing. Linux kernel timer is implemented by system clock, not PIT and other hardware timers. The Linux kernel timer is very simple to use. You only need to provide the timeout time (equivalent to the timing value) and the timing processing function. When the timeout time reaches, the set timing processing function will execute

The kernel timer does not run periodically. It will close automatically after timeout. If you want to realize periodic timing, you need to restart the timer in the timing processing function. The Linux kernel uses timer_ The list structure represents the kernel timer, timer_list is defined in the file include/linux/timer.h as follows:

struct timer_list { 
	struct list_head entry; 
	unsigned long expires; 				/* Timer timeout, in beats */ 
	struct tvec_base *base; 
	void (*function)(unsigned long); 	/* Timing processing function */ 
	unsigned long data; 				/* Parameters to pass to the function function */ 
	int slack; 
}; 

To use the kernel timer, first define a timer_ The list variable represents the timer. After defining the timer, you need to initialize the timer through a series of API functions. These functions are as follows:

  • init_timer function: initializing timer_list type variable
void init_timer(struct timer_list *timer)
//Timer: timer to initialize
//No return value
  • add_timer function: register the timer with the Linux kernel. After registration, the timer starts running
void add_timer(struct timer_list *timer)
//Timer: timer to register
//No return value
  • del_timer function: deletes a timer. It can be deleted whether it is activated or not
int del_timer(struct timer_list * timer)
//Timer: timer to delete
//Return value: 0, the timer has not been activated, 1, the timer has been activated
  • del_timer_sync function: del_ The synchronous version of timer function will wait for other processors to use the timer before deleting it
int del_timer_sync(struct timer_list * timer)
//Timer: timer to delete
//Return value: 0, the timer has not been activated, 1, the timer has been activated
  • mod_timer function: modify the timer value. If the timer has not been activated, this function will activate the timer
int mod_timer(struct timer_list *timer, unsigned long expires)
//Timer: the timer to modify the timeout (timing value)
//expires: the modified timeout

The general usage of kernel timer is as follows:

struct timer_list timer; /* Define timer */ 
 
/* Timer callback function */ 
void function(unsigned long arg){ 
	/* 
	 * Timer processing code 
	 */ 
 
	/* If the timer needs to run periodically, use mod_timer 
	 * Function resets the timeout value and starts the timer. 
	 */ 
	mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000)); 
} 
 
/* Initialization function */ 
void init(void){ 
	init_timer(&timer); 	/* Initialization timer */ 
 
	timer.function = function; 		/* Set timing processing function */ 
	timer.expires=jffies + msecs_to_jiffies(2000);		/* Timeout 2 seconds */ 
	timer.data = (unsigned long)&dev; 		/* Take the equipment structure as a parameter */ 
 
	add_timer(&timer); 				/* Start timer */ 
} 
 
/* Exit function */ 
void exit(void) { 
	del_timer(&timer); 		/* Delete timer */ 
	/* Or use */ 
	del_timer_sync(&timer); 
}

Sometimes it is necessary to implement short delay in the kernel. The Linux kernel provides millisecond, microsecond and nanosecond delay functions, as shown in the following table:

3. Preparation of experimental program

In this experiment, the kernel timer is used to turn on and off the LED on the development board periodically. The flashing cycle of the LED is set by the kernel timer. The test application can control the cycle of the kernel timer

3.1 modify equipment tree file

For the addition of equipment tree node information, please refer to Development of LED driver under device tree One article

3.2 Timer Driver Programming

After the device tree is ready, you can write the driver. Create a new "timer" folder, create a vscode project in the folder, create a new timer.c file, and write the program

#define TIMER_CNT 		 one 					/*  Number of equipment numbers 	*/
#define TIMER_NAME 		 "timer" 				/*  name 		*/
#define CLOSE_CMD  		 (_IO(0XEF, 0x1)) 	/*  Turn off the timer*/
#define OPEN_CMD 		 (_IO(0XEF, 0x2)) 	/*  Turn on the timer*/
#define SETPERIOD_CMD 	 (_IO(0XEF, 0x3)) 	/*  Set timer cycle command*/
#define LEDON  			 one 					/*  Turn on the light*/
#define LEDOFF  			 0 					/*  Turn off the lights*/

/* timer Equipment structure */
struct timer_dev{
	dev_t devid;			/* Equipment number 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* class 		*/
	struct device *device;	/* equipment 	 */
	int major;				/* Main equipment No	  */
	int minor;				/* Secondary equipment No   */
	struct device_node	*nd; /* Device node */
	int led_gpio;			/* key GPIO number used		*/
	int timeperiod; 		/* Timing period, in ms */
	struct timer_list timer;/* Define a timer*/
	spinlock_t lock;		/* Define spin lock */
};

struct timer_dev timerdev;	/* timer equipment */

/*
 * @description	: Initialize the LED light IO, when the open function turns on the driver
 * 				  Initialize the GPIO pin used by the LED.
 * @param 		: nothing
 * @return 		: nothing
 */
static int led_init(void)
{
	int ret = 0;

	timerdev.nd = of_find_node_by_path("/gpioled");
	if (timerdev.nd== NULL) {
		return -EINVAL;
	}

	timerdev.led_gpio = of_get_named_gpio(timerdev.nd ,"led-gpio", 0);
	if (timerdev.led_gpio < 0) {
		printk("can't get led\r\n");
		return -EINVAL;
	}
	
	/* IO used to initialize led */
	gpio_request(timerdev.led_gpio, "led");		/* Request IO 	*/
	ret = gpio_direction_output(timerdev.led_gpio, 1);
	if(ret < 0) {
		printk("can't set gpio!\r\n");
	}
	return 0;
}

/*
 * @description		: open device
 * @param - inode 	: inode passed to driver
 * @param - filp 	: The device file has a file structure called private_ Member variable of data
 * 					  Generally, private is used when open ing_ Data points to the device structure.
 * @return 			: 0 success; Other failures
 */
static int timer_open(struct inode *inode, struct file *filp)
{
	int ret = 0;
	filp->private_data = &timerdev;	/* Set private data */

	timerdev.timeperiod = 1000;		/* The default period is 1s */
	ret = led_init();				/* Initialize LED IO */
	if (ret < 0) {
		return ret;
	}

	return 0;
}

/*
 * @description		: ioctl Function,
 * @param - filp 	: Device file to open (file descriptor)
 * @param - cmd 	: Commands sent by the application
 * @param - arg 	: parameter
 * @return 			: 0 success; Other failures
 */
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:		/* off timer  */
			del_timer_sync(&dev->timer);
			break;
		case OPEN_CMD:		/* Turn on the timer */
			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: /* Set timer cycle */
			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;
}

/* Device operation function */
static struct file_operations timer_fops = {
	.owner = THIS_MODULE,
	.open = timer_open,
	.unlocked_ioctl = timer_unlocked_ioctl,
};

/* Timer callback function */
void timer_function(unsigned long arg)
{
	struct timer_dev *dev = (struct timer_dev *)arg;
	static int sta = 1;
	int timerperiod;
	unsigned long flags;

	sta = !sta;		/* Reverse each time to reverse the LED light */
	gpio_set_value(dev->led_gpio, sta);
	
	/* restart timer  */
	spin_lock_irqsave(&dev->lock, flags);
	timerperiod = dev->timeperiod;
	spin_unlock_irqrestore(&dev->lock, flags);
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod)); 
 }

/*
 * @description	: Drive entry function
 * @param 		: nothing
 * @return 		: nothing
 */
static int __init timer_init(void)
{
	/* Initialize spin lock */
	spin_lock_init(&timerdev.lock);

	/* Register character device driver */
	/* 1,Create device number */
	if (timerdev.major) {		/*  Defines the equipment number */
		timerdev.devid = MKDEV(timerdev.major, 0);
		register_chrdev_region(timerdev.devid, TIMER_CNT, TIMER_NAME);
	} else {						/* No device number defined */
		alloc_chrdev_region(&timerdev.devid, 0, TIMER_CNT, TIMER_NAME);	/* Application equipment No */
		timerdev.major = MAJOR(timerdev.devid);	/* Gets the master device number of the assigned number */
		timerdev.minor = MINOR(timerdev.devid);	/* Get the secondary device number of the assigned number */
	}
	
	/* 2,Initialize cdev */
	timerdev.cdev.owner = THIS_MODULE;
	cdev_init(&timerdev.cdev, &timer_fops);
	
	/* 3,Add a cdev */
	cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT);

	/* 4,Create class */
	timerdev.class = class_create(THIS_MODULE, TIMER_NAME);
	if (IS_ERR(timerdev.class)) {
		return PTR_ERR(timerdev.class);
	}

	/* 5,Create device */
	timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, TIMER_NAME);
	if (IS_ERR(timerdev.device)) {
		return PTR_ERR(timerdev.device);
	}
	
	/* 6,Initialize timer and set timer processing function. If the cycle has not been set, all timers will not be activated */
	init_timer(&timerdev.timer);
	timerdev.timer.function = timer_function;
	timerdev.timer.data = (unsigned long)&timerdev;
	return 0;
}

/*
 * @description	: Drive exit function
 * @param 		: nothing
 * @return 		: nothing
 */
static void __exit timer_exit(void)
{	
	gpio_set_value(timerdev.led_gpio, 1);	/* Turn off the LED when unloading the drive */
	del_timer_sync(&timerdev.timer);		/* Delete timer */
#if 0
	del_timer(&timerdev.tiemr);
#endif

	/* Unregister character device driver */
	cdev_del(&timerdev.cdev);/*  Delete cdev */
	unregister_chrdev_region(timerdev.devid, TIMER_CNT); /* Logout equipment number */

	device_destroy(timerdev.class, timerdev.devid);
	class_destroy(timerdev.class);
}

module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("andyxi");
3.3 test program preparation

Create a new file named timerApp.c and write test code

/* Command value */
#define CLOSE_CMD  		 (_IO(0XEF, 0x1)) 	/*  Turn off the timer*/
#define OPEN_CMD 		 (_IO(0XEF, 0x2)) 	/*  Turn on the timer*/
#define SETPERIOD_CMD 	 (_IO(0XEF, 0x3)) 	/*  Set timer cycle command*/

/*
 * @description		: main main program
 * @param - argc 	: argv Number of array elements
 * @param - argv 	: Specific parameters
 * @return 			: 0 success; Other failures
 */
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) {				/* Parameter input error */
			gets(str);				/* Prevent jamming */
		}

		if(cmd == 1)				/* Turn off the LED */
			cmd = CLOSE_CMD;
		else if(cmd == 2)			/* Turn on the LED */
			cmd = OPEN_CMD;
		else if(cmd == 3) {
			cmd = SETPERIOD_CMD;	/* Set cycle value */
			printf("Input Timer Period:");
			ret = scanf("%d", &arg);
			if (ret != 1) {			/* Parameter input error */
				gets(str);			/* Prevent jamming */
			}
		}
		ioctl(fd, cmd, arg);		/* Controls the opening and closing of the timer */	
	}

	close(fd);
}
3.4 program compilation
  • Modify Makefile compilation target variable
obj-m := timer.o
  • Use "make -j32" to compile the driver module file
make -j32
  • Use the "arm linux gnueabihf GCC" command to compile the test APP
arm-linux-gnueabihf-gcc timerApp.c -o timerApp
3.5 operation test
  • Copy the driver file and APP executable file to "rootfs/lib/modules/4.1.15"
  • When loading the driver for the first time, use the "depmod" command
depmod
  • Use the "modprobe" command to load the driver
modprobe timer.ko
  • After using the ". / timerApp /dev/timer" command, the terminal displays the following

  • Enter "2" to turn on the timer, and the LED will start flashing in the default 1-second cycle

  • Input "3" to set the timing cycle. Input the cycle value to be set according to the prompt. Input "500" to set the timer cycle value to 500ms. After setting, the LED light will start flashing at an interval of 500ms

  • Enter "1" to turn off the timer

  • Use the "rmmod" command to unload the driver

rmmod timer.ko

Topics: Linux kernel timer