Bootstrap

Linux驱动API说明及使用

本文档是自己在驱动开发过程中所使用到的api,本文档将会跟随本人开发持续更新

定时器相关

init_timer

// 初始化定时器:在驱动的初始化阶段,你需要调用init_timer()函数来初始化一个struct timer_list类型的定时器变量
struct timer_list my_timer;
init_timer(&my_timer);

timer_setup

void timer_setup(struct timer_list *timer,
                 void (*callback)(struct timer_list *),
                 unsigned int flags);
//void function_name(unsigned long data)

该函数用于初始化一个定时器结构体 timer,并设置其回调函数为 callback,以及一些标志位 flags。具体来说,该函数会将 timer 结构体中的成员变量 functiondata 分别设置为 callbacktimer 本身,将 expires 设置为 0,将 flags 设置为 flags | TIMER_IRQSAFE

设置定时器超时事件

unsigned long timeout = jiffies + msecs_to_jiffies(1000);  // 设置定时器超时时间为1秒
mod_timer(&my_timer, timeout);
// jiffies是一个全局变量,用于表示系统启动以来的时钟滴答数(tick)。它是内核用于跟踪时间的基本单位。

del_timer

// 注销定时器:在驱动的退出阶段,你应该调用del_timer()函数来注销定时器,以防止定时器在驱动退出后继续运行
del_timer(&my_timer);

ktime_set

ktime_t ktime_set(const s64 secs, const unsigned long nsecs);

ktime_set() 是 Linux 内核中用于创建 ktime_t 类型时间值的函数,定义在 include/linux/ktime.h 文件中

hrtimer_start

hrtimer_start() 是 Linux 内核提供的一个函数,用于启动一个高精度定时器。该函数的原型定义如下:

int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode);

其中,timer 是一个指向 hrtimer 结构体的指针,表示要启动的定时器;tim 是一个 ktime_t 类型的时间值,表示定时器的超时时间;mode 是一个枚举类型,表示定时器的模式,可以是 HRTIMER_MODE_REL(相对时间模式)或 HRTIMER_MODE_ABS(绝对时间模式)。

在启动定时器之前,需要先初始化 hrtimer 结构体并设置定时器超时时的回调函数,例如:

struct hrtimer my_timer;
my_timer.function = my_timer_callback;

其中,my_timer_callback 是一个函数指针,表示定时器超时时要执行的回调函数。

然后,可以调用 hrtimer_start() 函数启动定时器,例如:

ktime_t timeout = ktime_set(0, 1000000000);  // 1 秒钟的超时时间
hrtimer_start(&my_timer, timeout, HRTIMER_MODE_REL);

这个例子中,定时器超时时间为 1 秒钟,模式为相对时间模式。当定时器超时时,会自动调用 my_timer_callback 函数执行相应的操作。

坐标相关

  1. 首先,在驱动的probe函数中注册输入设备,指定设备类型为EV_ABS,表示该设备支持绝对坐标输入。代码如下:
input_dev = input_allocate_device();
input_dev->name = "my_multitouch_device";
input_dev->id.bustype = BUS_USB;
input_dev->id.vendor = 0x1234;
input_dev->id.product = 0x5678;
input_dev->id.version = 1;
input_set_capability(input_dev, EV_ABS, ABS_X);
input_set_capability(input_dev, EV_ABS, ABS_Y);
input_set_capability(input_dev, EV_ABS, ABS_MT_POSITION_X);
input_set_capability(input_dev, EV_ABS, ABS_MT_POSITION_Y);
input_set_abs_params(input_dev, ABS_X, 0, 1023, 0, 0);
input_set_abs_params(input_dev, ABS_Y, 0, 767, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, 1023, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, 767, 0, 0);
input_mt_init_slots(input_dev, 2);
input_register_device(input_dev);
  1. 接下来,在驱动的中断处理函数中,获取多点触控事件的坐标信息,并调用input_mt_report_slot_state函数上报事件。代码如下:
irqreturn_t my_multitouch_interrupt(int irq, void *dev_id)
{
    struct input_dev *input_dev = dev_id;
    int i;
    for (i = 0; i < num_touches; i++) {
        input_mt_slot(input_dev, i);
        input_mt_report_slot_state(input_dev, MT_TOOL_FINGER, true);
        input_report_abs(input_dev, ABS_MT_POSITION_X, touches[i].x);
        input_report_abs(input_dev, ABS_MT_POSITION_Y, touches[i].y);
    }
    input_sync(input_dev);
    return IRQ_HANDLED;
}

input_set_capability

int input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code);

该函数的作用是为输入设备对象添加一个输入事件类型和输入事件代码的组合,以指示该输入设备支持该事件类型和代码。例如,可以使用该函数将输入设备对象标记为支持按键事件、鼠标事件或触摸屏事件等。

  • type:要添加的输入事件类型,如EV_KEY表示按键事件,EV_ABS表示绝对事件,EV_REL表示相对事件等。
  • code:要添加的输入事件代码,如KEY_A表示A键,ABS_X表示X轴绝对坐标,REL_X表示X轴相对坐标等。

input_set_abs_params

void input_set_abs_params(struct input_dev *dev, unsigned int axis, int min, int max, int fuzz, int flat);

该函数用于设置输入设备对象中绝对输入事件的参数。

  • 参数axis是指要设置的绝对输入事件的轴的编号
  • 参数minmax分别是该轴的最小值和最大值
  • 参数fuzzflat分别是该轴的模糊度和平坦度。一般默认为零

input_mt_init_slots

int input_mt_init_slots(struct input_dev *dev, unsigned int num_slots, unsigned int flags);
  • num_slots:要为多点触摸设备分配的触点数。
  • flags:指定多点触摸设备的属性标志,如是否支持手势等。我目前使用的这个选项可以为空

input_mt_report_slot_state

void input_mt_report_slot_state(struct input_dev *dev, unsigned int slot, bool touching);

该函数的第一个参数dev是一个指向输入设备的指针,第二个参数slot表示输入槽的编号(触摸源类型,例如手、笔),第三个参数touching表示输入槽的状态,如果为true表示该输入槽正在被触摸,如果为false表示该输入槽没有被触摸。
slot的类型:MT_TOOL_FINGER、MT_TOOL_PEN、MT_TOOL_PALM、MT_TOOL_MAX

等待队列

#include <linux/wait.h>

等待队列头使用流程

  1. 定义等待队列头
    在驱动程序中,需要定义一个等待队列头(wait_queue_head_t),用于存储等待队列中的等待进程信息。

  2. 初始化等待队列头
    在驱动程序的初始化函数中,需要初始化等待队列头,可以使用宏 init_waitqueue_head() 来完成。该函数将等待队列头的自旋锁初始化,并将任务列表初始化为空。

  3. 将进程加入等待队列

    add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait): 将等待队列项添加到等待队列头的任务列表中。该函数会获取等待队列头的自旋锁,并将等待队列项链接到任务列表的末尾。

    当共享资源不可用时,需要将当前进程加入等待队列中,可以使用宏 wait_event_interruptible()wait_event_timeout() 来完成。这些宏会将当前进程加入等待队列,并使其进入睡眠状态,直到共享资源可用或超时。

  4. 从等待队列中唤醒进程
    当共享资源可用时,需要从等待队列中唤醒等待进程,可以使用宏 wake_up_interruptible()wake_up_all() 来完成。这些宏会将等待队列中的进程唤醒,并使其从睡眠状态中醒来。
    wake_up(wait_queue_head_t *q): 唤醒等待队列头中的所有等待者。该函数会获取等待队列头的自旋锁,并遍历任务列表,逐个唤醒等待队列中的任务。
    wake_up_interruptible(wait_queue_head_t *q): 唤醒等待队列头中的所有等待者,但只唤醒处于可中断状态(TASK_INTERRUPTIBLE)的任务。


1. DECLARE_WAIT_QUEUE_HEAD(name);//:定义等待队列头,其中 `name` 是等待队列头的名称。

2. void init_waitqueue_head(wait_queue_head_t *q);//:初始化等待队列头,其中 `q` 是等待队列头的指针。

3. void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);//:将进程加入等待队列,其中 `q` 是等待队列头的指针,`wait` 是进程的等待队列项。

4. void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);//:从等待队列中移除进程,其中 `q` 是等待队列头的指针,`wait` 是进程的等待队列项。

5. int wait_event_interruptible(wait_queue_head_t *q, long condition);//:等待事件的发生,其中 `q` 是等待队列头的指针,`condition` 是事件的条件。如果事件已经发生,则该函数立即返回;否则,该函数将当前进程加入等待队列,并将其挂起,直到事件发生或者进程被信号中断。

6. int wait_event_timeout(wait_queue_head_t *q, long condition, long timeout);//:等待事件的发生,并设置超时时间,其中 `q` 是等待队列头的指针,`condition` 是事件的条件,`timeout` 是超时时间。如果事件已经发生,则该函数立即返回;否则,该函数将当前进程加入等待队列,并将其挂起,直到事件发生、进程被信号中断或者超时时间到达。

7. void wake_up(wait_queue_head_t *q);//:唤醒等待队列中的所有进程,其中 `q` 是等待队列头的指针。

8. void wake_up_interruptible(wait_queue_head_t *q);//:唤醒等待队列中的所有可中断等待的进程,其中 `q` 是等待队列头的指针。

等待队列项

在Linux内核中,等待队列(Wait Queue)是一种机制,用于实现进程或线程之间的同步和通信。等待队列允许一个或多个任务等待某个条件满足或事件发生,然后在条件满足或事件发生时被唤醒。

等待队列的基本结构是一个双向链表,每个等待队列项表示一个等待者(任务)。等待队列项通常与特定的条件或事件相关联。在Linux内核中,等待队列项的定义如下:

typedef struct __wait_queue wait_queue_t;

struct __wait_queue {
    unsigned int        flags;
    void                *private;
    wait_queue_func_t   func;
    struct list_head    task_list;
};
  • flags:等待队列项的标志位,用于指定等待的状态或行为。
  • private:指向私有数据的指针,用于存储与等待队列项相关的任何附加信息。
  • func:等待队列项的回调函数,用于在唤醒时执行特定的操作。
  • task_list:等待队列项在等待队列中的链接,使用双向链表连接等待队列项。

等待队列的使用通常涉及以下几个步骤:

  1. 定义等待队列项:使用DECLARE_WAITQUEUE宏或手动创建wait_queue_t结构体来定义等待队列项。
  2. 添加等待队列项到等待队列:使用add_wait_queue函数将等待队列项添加到特定的等待队列中。等待队列可以是全局的或与特定条件或事件相关的局部队列。
  3. 设置任务状态:使用__set_current_state函数将当前任务的状态设置为等待状态,例如TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE
  4. 调用调度函数:使用schedule函数将CPU的控制权交给其他可运行的任务,当前任务将被挂起,直到被唤醒。
  5. 唤醒等待者:当某个条件满足或事件发生时,使用wake_upwake_up_interruptible函数唤醒等待队列中的任务。
  6. 移除等待队列项:使用remove_wait_queue函数从等待队列中移除等待队列项。

等待队列的使用场景包括但不限于以下几种情况:

  • 等待设备就绪:当设备完成某个操作或达到某个状态时,等待队列可用于阻塞任务,直到设备就绪。
  • 等待资源释放:当某个共享资源被占用时,等待队列可用于阻塞任务,直到资源被释放。
  • 等待事件发生:当某个事件尚未发生时,等待队列可用于阻塞任务,直到事件发生。

等待队列是Linux内核中实现同步和通信的重要机制之一,它提供了一种有效的方式,允许任务在特定条件满足或事件发生之前等待,并在条件满足或事件发生时被唤醒。这种机制有助于提高系统的效率和响应性。

使用注意

if (wait_event_interruptible(async_queue, write_index < buffer_size)) {
return -ERESTARTSYS;
}

加判断的作用:如果等待过程中进程被中断或者收到信号,则 wait_event_interruptible() 函数会返回一个错误码 -ERESTARTSYS,表示进程需要重新启动。在这种情况下,调用 wait_event_interruptible() 函数的进程不会被加入到等待队列中,也不会挂起,可以继续执行其他操作。

只对当前线程有影响

线程是在进程内部创建和管理的执行单元,它们共享进程的地址空间和其他资源。当进程中的某个线程调用wait_event_interruptible函数时,只有该线程会进入休眠状态,而其他线程仍然可以继续执行。这是因为线程是在用户空间中调度的,而不是内核空间中。因此,wait_event_interruptible函数只会让调用它的线程进入休眠状态,而不会影响其他线程的执行。

异步通知相关(信号)

set_current_state(TASK_INTERRUPTIBLE) 函数用于将当前进程的状态设置为可中断状态,表示该进程可以被信号中断。在这种状态下,进程不会占用 CPU 时间,而是等待某些事件的发生。在本例中,等待的事件是收到 SIGUSR1 信号。

schedule() 函数用于让出 CPU 时间,以便其他进程可以运行。在等待信号的过程中,进程需要等待信号的到来,因此不需要占用 CPU 时间。通过调用 schedule() 函数,进程可以让出 CPU 时间,以便其他进程可以运行。当信号到达时,驱动程序会调用 wake_up_process() 函数来唤醒等待信号的进程,使其重新变为可运行状态。这样,该进程就可以继续执行了。

内存映射

void __iomem *of_iomap(struct device_node *np, int index);

np 是指向设备节点的指针,index 是指定设备寄存器的索引号。如果设备有多个寄存器,可以使用不同的索引号来映射这些寄存器。

of_iomap() 函数返回一个指向内核虚拟地址空间中映射的设备寄存器的指针。返回的指针是一个虚拟地址,需要使用 ioread*() 和 iowrite*() 等函数来访问设备寄存器。

必须保证设备节点中的 "reg" 属性与设备的资源信息一致

设备树相关

unsigned int irq_of_parse_and_map(struct device_node *node, int index);

node 是指向设备节点的指针,index 是指定设备中断的索引号。如果设备有多个中断,可以使用不同的索引号来映射这些中断。

irq_of_parse_and_map() 函数返回一个内核中断编号,可以使用 request_irq() 函数注册中断处理函数。

使用 irq_of_parse_and_map() 函数映射设备中断时,必须保证设备节点中的 "interrupts" 属性与硬件设计一致

struct reset_control *devm_reset_control_get_shared(struct device *dev,
    const char *id);

dev 是指向设备结构体的指针,id 是指定复位控制器的标识符。如果设备有多个复位控制器,可以使用不同的标识符来获取这些复位控制器。

devm_reset_control_get_shared() 函数返回一个指向复位控制器结构体的指针。可以使用 reset_control_deassert() 函数来取消设备的复位状态。

使用 devm_reset_control_get_shared() 函数获取设备的复位控制器时,必须保证设备节点中的 “resets” 属性与硬件设计一致

互斥锁

工作项

Linux 内核的工作项(work item)是一种轻量级的异步执行机制,可以在内核中延迟执行一些任务,以避免阻塞当前进程或线程。工作项通常用于处理一些需要异步执行的任务,例如处理网络数据包、处理中断请求等。

工作项的实现基于内核中的 tasklet 机制,但是相对于 tasklet,工作项具有更高的灵活性和可靠性。工作项可以在任何上下文中被调度执行,而不仅仅是在中断上下文中。此外,工作项可以被取消或重新安排执行时间,以满足实际需求。

工作项的实现基于 struct work_struct 结构体,该结构体定义如下:

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
};

其中,data 是一个原子变量,用于存储工作项的状态信息;entry 是一个链表节点,用于将工作项链接到内核的工作队列中;func 是一个函数指针,指向实际执行工作项的函数。

工作项的创建和初始化通常通过 INIT_WORK() 宏来实现,该宏定义如下:

#define INIT_WORK(_work, _func) \
    do { \
        INIT_LIST_HEAD(&(_work)->entry); \
        (_work)->func = (_func); \
        atomic_long_set(&(_work)->data, 0); \
    } while (0)

其中,_work 是一个指向工作项结构体的指针,_func 是一个指向实际执行工作项的函数的指针。

下面是一个使用工作项的示例代码:

#include <linux/workqueue.h>

static void my_work_handler(struct work_struct *work)
{
    /* 实际执行工作项的代码 */
    printk(KERN_INFO "my_work_handler() called\n");
}

static DECLARE_WORK(my_work, my_work_handler);

static int my_device_probe(struct platform_device *pdev)
{
    /* 将工作项添加到内核的工作队列中 */
    schedule_work(&my_work);

    return 0;
}

在上述代码中,首先定义了一个名为 my_work_handler() 的函数,用于实际执行工作项的代码。然后,使用 DECLARE_WORK() 宏定义了一个名为 my_work 的工作项,并将其初始化为调用 my_work_handler() 函数。最后,在设备的 probe() 函数中,将工作项添加到内核的工作队列中,以便异步执行。

需要注意的是,工作项的执行顺序不能保证,因此在实际使用时需要小心谨慎。此外,工作项的执行时间可能会受到内核负载和系统资源的影响,因此需要合理规划工作项的执行时间和优先级。

void INIT_WORK(struct work_struct *work, void (*func)(struct work_struct *));

参数work是指向要初始化的工作项的指针,参数func是指向工作项执行函数的指针。工作项执行函数的原型应该是void func(struct work_struct *work),其中,参数work是指向要执行的工作项的指针。

bool flush_work(struct work_struct *work);

该函数的参数是一个指向工作项的指针。如果该工作项已经被执行完毕并且从工作队列中删除,则返回true;否则返回false

#define INIT_LIST_HEAD(ptr) do { \
    (ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)

使用该宏定义可以方便地初始化一个双向链表的头节点,例如:

struct list_head my_list;
INIT_LIST_HEAD(&my_list);

这样就可以得到一个空的双向链表,头节点的nextprev指针都指向自己,表示该链表为空。

中断

gpiod_get_value_cansleep

int gpiod_get_value_cansleep(const struct gpio_desc *desc);

其中,desc 是指向 GPIO 描述符的指针。

调用 gpiod_get_value_cansleep 函数后,它会返回 GPIO 的当前电平值。通常情况下,返回值为 0 表示低电平(逻辑 0),返回值为 1 表示高电平(逻辑 1)。

devm_request_any_context_irq

int devm_request_any_context_irq(struct device *dev, unsigned int irq,
                                 irq_handler_t handler, unsigned long irqflags,
                                 const char *name, void *dev_id);

参数 dev 是一个指向设备结构的指针,表示要请求中断的设备。

参数 irq 是要请求的中断号。

参数 handler 是一个中断处理函数,当中断发生时将被调用。中断处理函数的原型为 irqreturn_t (*)(int, void *)

参数 irqflags 是一个标志位,用于指定中断的行为和属性。

参数 name 是一个字符串,表示中断的名称。

参数 dev_id 是一个指针,用于传递给中断处理函数的设备标识数据。

该函数用于请求一个中断,并将中断处理函数与设备关联起来。中断处理函数将在中断发生时被调用,以处理中断事件。

irq_set_irq_type

int irq_set_irq_type(unsigned int irq, unsigned int type);

其中,irq 是中断号,type 是所需的触发类型。

调用 irq_set_irq_type 函数后,它会设置指定中断的触发类型为所需的类型。返回值表示函数执行的结果,通常为 0 表示成功,负值表示错误。

设备相关

devm_add_action

int devm_add_action(struct device dev, void (action)(void *), void *data);

延迟工作

工作函数

void (*work_func)(struct work_struct *work)

工作结构体

定义工作结构体:创建一个工作结构体,通常是一个包含work_struct成员的自定义结构体。

DECLARE_DELAYED_WORK

DECLARE_DELAYED_WORKINIT_DELAYED_WORK都是用于初始化延迟工作的

INIT_DELAYED_WORK

#define INIT_DELAYED_WORK(work, func) \
    do { \
        INIT_WORK(&(work)->work, func); \
        (work)->timer.function = delayed_work_timer_fn; \
        (work)->timer.data = (unsigned long)(work); \
    } while (0)

其中,work 是一个指向 struct delayed_work 结构的指针,表示要初始化的延迟工作。

func 是一个回调函数,表示延迟工作执行时要调用的函数。

宏内部通过调用 INIT_WORK 宏初始化了 work 中的 work 字段,该字段是一个普通工作(Work)结构,用于表示延迟工作的执行函数和相关数据。

然后,宏设置了 work 中的 timer 字段,该字段是一个内核定时器(Timer)结构,用于实现延迟工作的调度。设置 timer.function 字段为 delayed_work_timer_fn,表示延迟工作的定时器回调函数。设置 timer.data 字段为 work 的地址,以便在回调函数中获取到对应的延迟工作。

使用 INIT_DELAYED_WORK 宏可以方便地初始化延迟工作,并指定执行函数。接下来,可以将延迟工作添加到适当的工作队列中,以便在指定的延迟时间后执行。

mod_delayed_work

int mod_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay);

其中,wq 是指向工作队列(workqueue)的指针,dwork 是指向延迟工作的指针,delay 是新的延迟时间。

调用 mod_delayed_work 函数后,延迟工作将会在新的延迟时间点上执行。如果延迟工作当前正在执行或已经取消,则调用 mod_delayed_work 函数可能会产生不确定的结果。

需要注意的是,mod_delayed_work 函数只能修改已经提交的延迟工作的延迟时间,无法修改工作的其他属性。如果需要修改其他属性,可能需要使用其他相关的函数或方法。

总结

最后,通过调用schedule_delayed_work函数,将延迟工作提交到系统的工作队列中,并指定了延迟的时间。工作函数将在指定的延迟时间过后被调度执行。

需要注意的是,延迟工作的执行时间是近似的,它依赖于系统的调度和负载情况。因此,无法保证工作函数会在指定的延迟时间精确执行。此外,延迟工作使用了系统的工作队列机制,因此需要确保在使用延迟工作之前,工作队列机制已经初始化和可用。

gpio

gpiod_get_value_cansleep

int gpiod_get_value_cansleep(const struct gpio_desc *desc);

其中,desc 是指向 GPIO 描述符的指针。

调用 gpiod_get_value_cansleep 函数后,它会返回 GPIO 的当前电平值。通常情况下,返回值为 0 表示低电平(逻辑 0),返回值为 1 表示高电平(逻辑 1)。

需要注意的是,gpiod_get_value_cansleep 函数可以在睡眠状态下调用,这意味着它可以在内核的睡眠上下文中使用。如果需要在不允许睡眠的上下文中使用,可以考虑使用 gpiod_get_value 函数代替。

gpiod_is_active_low

bool gpiod_is_active_low(struct gpio_desc *desc);

参数 desc 是一个指向 struct gpio_desc 结构的指针,表示要检查的 GPIO 描述符。

该函数返回一个布尔值,如果 GPIO 被配置为活动低电平,则返回 true,否则返回 false

gpiod_set_debounce

int gpiod_set_debounce(struct gpio_desc *desc, unsigned int debounce_us);

参数 desc 是一个指向 struct gpio_desc 结构的指针,表示要设置去抖动时间的 GPIO 描述符。

参数 debounce_us 是去抖动时间,以微秒为单位。它指定了 GPIO 输入信号稳定后延迟处理的时间。通常,较长的去抖动时间可以更好地过滤掉抖动信号,但也会增加响应时间。

该函数返回一个整数值,表示设置去抖动时间的结果。如果设置成功,返回 0;如果发生错误,返回一个负数错误码。

功耗相关

pm_stay_awake

pm_stay_awake 可以是一个函数或宏,其具体实现取决于代码的上下文和内核版本。它的作用是向内核发出请求,告知系统需要保持唤醒状态,以防止系统进入低功耗模式。

;