Notes on Driver Development of Embedded Linux Devices (3)

Keywords: Linux Android

I. Interrupt Source and Interrupt Number

Interrupt is generated by hardware circuit, so if a peripheral has no independent interrupt line, it can not produce interrupt.

The work of the interrupt controller is to collect the interrupts generated by the hardware, and then submit them to the cpu according to the pre-set priority.
For arm processor, the development of interrupt controller has gone through three stages:
PIC (Programmable Interrupt Controller)
VIC (Vector Interrupt Controller)
GIC (General Interrupt Controller)

Interrupt source: hardware concept; interrupt number: software concept, from 0 to back. In the linux kernel, the interrupt number corresponds to the interrupt source. The interrupt source of each processor is different, and the conversion from the interrupt source to the interrupt sign is different. Interrupt numbers are defined in mach/irqs.h. Two different methods can be used to find interrupt numbers:

(1) Internal peripherals of chips
Firstly, the name of the device is defined, and then the corresponding interrupt number is found in irqs.h by name matching.
For example, the interrupt number of the watchdog device is IRQ_WDT, and the RTC hardware corresponds to IRQ_RTC_ALARAM/IRQ_RTC_TIC.

(2) Devices for External Connection of Chips
Because the device's interrupt pins are usually connected to GPIO, the GPIO number can be used to find the interrupt number: the interrupt number = gpio_to_irq(GPIO number)

When registering an interrupt handler, you can include the header file and refer to its macros as follows:

#include <mach/irqs.h>

Interrupt Processing in Linux

(1) Core structure

A,irq_desc

Defined in "linux/irqdesc.h"
Corresponds to an interrupt number. The linux kernel allocates an irq_desc array at startup, which has a total of NR_IRQS members. Each irq_desc records various kinds of information corresponding to interrupts, such as the interrupt processing function, the number of interrupts and so on.
irq_desc is prepared by the kernel.

B,irqaction

Defined in "linux/interrupt.h"
Each irqaction is used to encapsulate an interrupt handler. The structure is assigned by the driver.
irqaction contains interrupt number; interrupt handler function pointer; interrupt execution flag; interrupt name, etc.

C,irq_handler_t

Defined in "linux/interrupt.h", as follows:

typedef irqreturn_t (*irq_handler_t)(int, void *);

The type of interrupt handler, which is implemented by the driver, is recorded in irqaction.
irqreturn_t has only two values, IRQ_NONE/IRQ_HANDLED. If the interrupt is not caused by the device, return IRQ_NONE or IRQ_HANDLED.
The function parameter irq is an interrupt sign and void* is the parameter passed to the interrupt processing function, corresponding to irqaction - > dev_id.

(2) Interrupt Handling Function

When designing interrupt handling functions, drivers should follow the following requirements:
A. Nested non-reentrant
B. Sleeplessness
C. If the hardware has an interrupt status register, the software is responsible for clearing the interrupt flag bit. Generally speaking, if the mark is not cleared, the device can not produce interruption again.

kzalloc(size, GFP_KERNEL); //Possible Sleep
kzalloc(size, GFP_ATOMIC); //Can't Sleep

D. Registration and Logoff of Interrupt Processing Functions

#include <linux/interrupt.h>
#Include <mach/irqs.h>// Inside and Outside Devices
#Include <linux/gpio.h>// peripherals
#include <mach/gpio.h>

//Determine the interrupt number
#Define KEY_IRQ gpio_to_irq (gpio number);

//Interrupt handler
static irqreturn_t key_service(int irq, void *dev_id)
{
    //Complete work according to hardware and software requirements
    ...
    return IRQ_HANDLED or IRQ_NONE;
}

//To register an interrupt handler, you must check the return value
//The request_irq internal assigns and initializes irqaction
//If an interrupt is generated by an internal device, it is generally not necessary to share or configure IO, then flags can be 0

u32 flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;

int ret;
ret = request_irq(KEY_IRQ, /* interrupt number */
                  key_service, /* Interrupt handler */
                  flags, /* Sign of interruption */
                  "xxx", /* Name of interrupt handler */
                  dev_id);
//The final parameter dev_id is passed to the interrupt handler, which is usually set as a pointer to a private structure rather than NULL. In fact, dev_id can be NULL for non-shared interrupts.

if (ret) {
    printk("Cannot register interrupt handler\n");
    return -1;
}

//Log-off interrupt handler
free_irq(irq, dev_id);
//The parameters are interrupt number and dev_id. Dev_id must be consistent with the last parameter in request_irq.

//You can manually close (mask)/open an interrupt:
disable_irq(int irq);
enable_irq(int irq);
//The above two functions support nesting. That is to say, if disable_irq is called three times, enable_irq is needed three times to enable interruption. Make sure that disable_irq is called first, then enable_irq is called.

//If you want to block the interruption of the whole cpu, you can use:
local_irq_disable();
local_irq_enable();

(3) Interruption of the lower half

A. The Meaning of the Lower Part

This interrupt is closed by default before entering the interrupt handler. For some interrupts requiring rapid response or high data throughput, it is necessary to consider dividing the work of interrupt processing functions into two parts, the upper and lower parts of interrupts, respectively.

There are many ways to implement the second half, including softirq, tasklet and work queue.
For drivers, only tasklet and work queue are used.

The lower half of this cpu can be turned on or off:

local_bh_enable();
local_bh_disable();

B,tasklet

tasklet characteristics
a. Execute immediately after the first half of the execution, but the interruption is fully opened at this time;
b. The kernel is still in the interrupt context when tasklet is executed, so it cannot sleep.
c. The execution function of tasklet will not reentrant.
d. If scheduling occurs again during the execution of tasklet, the second scheduling is invalid.

#include <linux/interrupt.h>

//Declare tasklet structure
struct tasklet_struct mytask;

//tasklet's execution function
void bo_service(unsigned long data)
{
    ...
}   

//Execution function in the upper half
irqreturn_t up_service(int irq, void *dev_id)
{
    //First, do important things like interacting with hardware.
    //Triggering the lower half of tasklet
    tasklet_schedule(&mytask);
    //Or tasklet_hi_schedule (& mytask);
    ...
    return IRQ_HANDLED or IRQ_NONE;
}

//Initialize tasklet
tasklet_init(&mytask, bo_service, (unsigned long)dev);

C. work queue

Task queue characteristics
a. Push it back to the process context where the interrupt is fully open;
b. work is executed in the context of the process, so it can sleep.
The execution function of c and work will not reentrant.
d. If scheduling occurs again during the execution of the work, the second scheduling is invalid.

If it is redhat, the kernel creates a set of kernel threads named events/x, which are responsible for executing the lower half of the functions.
x is the number of the cpu, which can be 0, 1, etc.

If it's Android, the name of the kernel thread created by the kernel is kworker/x:y.
x is the CPU number, y should be the number of kernel threads on the cpu;

#include <linux/interrupt.h>
#include <linux/workqueue.h>

//Declare tasklet structure
struct work_struct mywork;

//Execution function of work queue
void bo_service(struct work_struct *data)
{
    ...
}   

//Execution function in the upper half
irqreturn_t up_service(int irq, void *dev_id)
{
    //First, do important things like interacting with hardware.
    //By default, the upper and lower parts are executed on the same cpu
    //You can wake up the lower half of thread execution on a given cpu
    //1 represents CPU 1, 0 represents CPU 0
    schedule_work(&mywork);
    //Or schedule_work_on (1, & mywork);
    ...
    return IRQ_HANDLED or IRQ_NONE;
}

//Initialization work queue
INIT_WORK(&mywork, bo_service);

//Upper half of registration
ret = request_irq(KEY_IRQ, up_service, flags, "wq_test", &mywork);
if (ret) {
    printk("cannot request irq %d\n", KEY_IRQ);
    return ret;
}

The implementation of the work queue is in "linux/workqueue.h".

Posted by Xajel on Fri, 21 Jun 2019 14:00:41 -0700