嵌入式linux/鸿蒙开发板(IMX6ULL)开发(三十五)驱动程序基石(中)

简介: 嵌入式linux/鸿蒙开发板(IMX6ULL)开发(三十五)驱动程序基石

1.3.4 应用编程


应用程序要做的事情有这几件:

① 编写信号处理函数:

static void sig_func(int sig)
{
  int val;
  read(fd, &val, 4);
  printf("get button : 0x%x\n", val);
}


② 注册信号处理函数:

signal(SIGIO, sig_func);


③ 打开驱动:

fd = open(argv[1], O_RDWR);


④ 把进程ID告诉驱动:

fcntl(fd, F_SETOWN, getpid());


⑤ 使能驱动的FASYNC功能:

flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);


1.3.5 现场编程


1.3.6 上机编程


1.3.7 异步通知机制内核代码详解


还没写


1.4 阻塞与非阻塞


所谓阻塞,就是等待某件事情发生。比如调用read读取按键时,如果没有按键数据则read函数不会返回,它会让线程休眠等待。

使用poll时,如果传入的超时时间不为0,这种访问方法也是阻塞的。


使用poll时,可以设置超时时间为0,这样即使没有数据它也会立刻返回,这就是非阻塞方式。能不能让read函数既能工作于阻塞方式,也可以工作于非阻塞方式?可以!

APP调用open函数时,传入O_NONBLOCK,就表示要使用非阻塞方式;默认是阻塞方式。

注意:对于普通文件、块设备文件,O_NONBLOCK不起作用。

注意:对于字符设备文件,O_NONBLOCK起作用的前提是驱动程序针对O_NONBLOCK做了处理。


只能在open时表明O_NONBLOCK吗?在open之后,也可以通过fcntl修改为阻塞或非阻塞。


使用GIT命令载后,本节源码位于这个目录下:

01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\
06_gpio_irq\
    06_read_key_irq_poll_fasync_block


1.4.1 应用编程


open时设置:

int  fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK);  /* 非阻塞方式 */
int  fd = open(“/dev/xxx”, O_RDWR );  /* 阻塞方式 */


open之后设置:

int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);  /* 非阻塞方式 */
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);  /* 阻塞方式 */


1.4.2 驱动编程


以drv_read为例:

static ssize_t drv_read(struct file *fp, char __user *buf, size_t count, loff_t *ppos)
if (queue_empty(&as->queue) && fp->f_flags & O_NONBLOCK)
  return -EAGAIN;
    wait_event_interruptible(apm_waitqueue, !queue_empty(&as->queue));
    ……


从驱动代码也可以看出来,当APP打开某个驱动时,在内核中会有一个struct file结构体对应这个驱动,这个结构体中有f_flags,就是打开文件时的标记位;可以设置f_flasgs的O_NONBLOCK位,表示非阻塞;也可以清除这个位表示阻塞。

驱动程序要根据这个标记位决定事件未就绪时是休眠和还是立刻返回。


1.4.3 驱动开发原则


驱动程序程序“只提供功能,不提供策略”。就是说驱动程序可以提供休眠唤醒、查询等等各种方式,,驱动程序只提供这些能力,怎么用由APP决定。


1.5 定时器


使用GIT命令载后,本节源码位于这个目录下:

01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\
06_gpio_irq\
    07_read_key_irq_poll_fasync_block_timer


1.5.1 内核函数


所谓定时器,就是闹钟,时间到后你就要做某些事。有2个要素:时间、做事,换成程序员的话就是:超时时间、函数。

在内核中使用定时器很简单,涉及这些函数(参考内核源码include\linux\timer.h):

① setup_timer(timer, fn, data):

设置定时器,主要是初始化timer_list结构体,设置其中的函数、参数。

② void add_timer(struct timer_list *timer):

向内核添加定时器。timer->expires表示超时时间。

当超时时间到达,内核就会调用这个函数:timer->function(timer->data)。

③ int mod_timer(struct timer_list *timer, unsigned long expires):

修改定时器的超时时间,

它等同于:del_timer(timer); timer->expires = expires; add_timer(timer);

但是更加高效。

④ int del_timer(struct timer_list *timer):

删除定时器。


1.5.2 定时器时间单位


编译内核时,可以在内核源码根目录下用“ls -a”看到一个隐藏文件,它就是内核配置文件。打开后可以看到如下这项:

CONFIG_HZ=100


这表示内核每秒中会发生100次系统滴答中断(tick),这就像人类的心跳一样,这是Linux系统的心跳。每发生一次tick中断,全局变量jiffies就会累加1。

CONFIG_HZ=100表示每个滴答是10ms。

定时器的时间就是基于jiffies的,我们修改超时时间时,一般使用这2种方法:

① 在add_timer之前,直接修改:

timer.expires = jiffies + xxx;   // xxx表示多少个滴答后超时,也就是xxx*10ms
timer.expires = jiffies + 2*HZ;  // HZ等于CONFIG_HZ,2*HZ就相当于2秒


② 在add_timer之后,使用mod_timer修改:

mod_timer(&timer, jiffies + xxx);   // xxx表示多少个滴答后超时,也就是xxx*10ms
mod_timer(&timer, jiffies + 2*HZ);  // HZ等于CONFIG_HZ,2*HZ就相当于2秒


1.5.3 使用定时器处理按键抖动


在实际的按键操作中,可能会有机械抖动:

1670925015053.jpg

按下或松开一个按键,它的GPIO电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。

如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。

怎么处理?

① 在按键中断程序中,可以循环判断几十亳秒,发现电平稳定之后再上报

② 使用定时器

显然第1种方法太耗时,违背“中断要尽快处理”的原则,你的系统会很卡。

怎么使用定时器?看下图:

1670925039503.jpg

核心在于:在GPIO中断中并不立刻记录按键值,而是修改定时器超时时间,10ms后再处理。

如果10ms内又发生了GPIO中断,那就认为是抖动,这时再次修改超时时间为10ms。

只有10ms之内再无GPIO中断发生,那么定时器的函数才会被调用。

在定时器函数中记录按键值。


1.5.4 现场编程、上机


1.5.5 深入研究:定时器的内部机制


初学者会用定时器就行,本节不用看。

怎么实现定时器,逻辑上很简单:每发生一次硬件中断时,硬件中断处理完后就会看看有没有软件中断要处理。

定时器就是通过软件中断来实现的,它属于TIMER_SOFTIRQ软中断。

对于TIMER_SOFTIRQ软中断,初始化代码如下:

void __init init_timers(void)
{
  init_timer_cpus();
  init_timer_stats();
  open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}


当发生硬件中断时,硬件中断处理完后,内核会调用软件中断的处理函数。对于TIMER_SOFTIRQ,会调用run_timer_softirq,它的函数如下:

run_timer_softirq
__run_timers(base);
    while (time_after_eq(jiffies, base->clk)) {
        ……
expire_timers(base, heads + levels);
    fn = timer->function;
    data = timer->data;
    call_timer_fn(timer, fn, data);
        fn(data);


简单地说,add_timer函数会把timer放入内核里某个链表;

在TIMER_SOFTIRQ的处理函数中,会从链表中把这些超时的timer取出来,执行其中的函数。

怎么判断是否超时?jiffies大于或等于timer->expires时,timer就超时。

内核中有很多timer,如果高效地找到超时的timer?这是比较复杂的,可以看看这文章:

https://blog.csdn.net/tianmohust/article/details/8707162


我们以后如果要深入讲解timer的话,会用视频来讲解。


1.5.6 深入研究:找到系统滴答


这只是一些笔记,初学者不用看。

在开发板执行以下命令,可以看到CPU0下有一个数值变化特别快,它就是滴答中断:

# cat /proc/interrupts
           CPU0
 16:       2532       GPC  55 Level     i.MX Timer Tick
 19:         22       GPC  33 Level     2010000.ecspi
 20:        384       GPC  26 Level     2020000.serial
 21:          0       GPC  98 Level     sai


以100ASK_IMX6ULL为做,滴答中断名字就是“i.MX Timer Tick”。

在Linux内核源码目录下执行以下命令:

$ grep "i.MX Timer Tick" * -nr
drivers/clocksource/timer-imx-gpt.c:319:        act->name = "i.MX Timer Tick";


打开timer-imx-gpt.c 319行左右,可得如下源码:

act->name = "i.MX Timer Tick";
act->flags = IRQF_TIMER | IRQF_IRQPOLL;
act->handler = mxc_timer_interrupt;
act->dev_id = ced;
return setup_irq(imxtm->irq, act);


mxc_timer_interrupt应该就是滴答中断的处理函数,代码如下:

static irqreturn_t mxc_timer_interrupt(int irq, void *dev_id)
{
  struct clock_event_device *ced = dev_id;
  struct imx_timer *imxtm = to_imx_timer(ced);
  uint32_t tstat;
  tstat = readl_relaxed(imxtm->base + imxtm->gpt->reg_tstat);
  imxtm->gpt->gpt_irq_acknowledge(imxtm);
  ced->event_handler(ced);
  return IRQ_HANDLED;
}


在上述代码中没看到对jiffies的累加操作啊,应该是在ced->event_handler(ced)中进行。

ced->event_handler(ced)是哪一个函数?不太好找,我使用QEMU来调试内核,在mxc_timer_interrupt中打断点跟踪代码(以后的课程会讲怎么用QEMU调试内核),发现它对应tick_handle_periodic。


tick_handle_periodic位于kernel\time\tick-common.c中,它里面的调用关系如下:

tick_handle_periodic
tick_periodic(cpu);
    do_timer(1);
        jiffies_64 += ticks;  // jiffies就是jiffies_64


你为何说jiffies就是jiffies_64?在arch\arm\kernel\vmlinux.lds.S有如下代码:

#ifndef __ARMEB__
jiffies = jiffies_64;
#else
jiffies = jiffies_64 + 4;
#endif


上述代码说明了,对于大字节序的CPU,jiffies指向jiffies_64的高4字节;对于小字节序的CPU,jiffies指向jiffies_64的低4字节。

对jiffies_64的累加操作,就是对jiffies的累加操作。


1.6 中断下半部tasklet


使用GIT命令载后,本节源码位于这个目录下:

01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\
06_gpio_irq\
    08_read_key_irq_poll_fasync_block_timer_tasklet


在前面我们介绍过中断上半部、下半部。中断的处理有几个原则:

① 不能嵌套;

② 越快越好。

在处理当前中断时,即使发生了其他中断,其他中断也不会得到处理,所以中断的处理要越快越好。但是某些中断要做的事情稍微耗时,这时可以把中断拆分为上半部、下半部。

在上半部处理紧急的事情,在上半部的处理过程中,中断是被禁止的;

在下半部处理耗时的事情,在下半部的处理过程中,中断是使能的。


中断上半部、下半部的关系机制,请回顾第18.2.5节。


1.6.1 内核函数


1.6.1.1 定义tasklet


中断下半部使用结构体tasklet_struct来表示,它在内核源码include\linux\interrupt.h中定义:

struct tasklet_struct
{
  struct tasklet_struct *next;
  unsigned long state;
  atomic_t count;
  void (*func)(unsigned long);
  unsigned long data;
};


其中的state有2位:

① bit0表示TASKLET_STATE_SCHED

等于1时表示已经执行了tasklet_schedule把该tasklet放入队列了;tasklet_schedule会判断该位,如果已经等于1那么它就不会再次把tasklet放入队列。

② bit1表示TASKLET_STATE_RUN

等于1时,表示正在运行tasklet中的func函数;函数执行完后内核会把该位清0。


其中的count表示该tasklet是否使能:等于0表示使能了,非0表示被禁止了。对于count非0的tasklet,里面的func函数不会被执行。


使用中断下半部之前,要先实现一个tasklet_struct结构体,这可以用这2个宏来定义结构体:

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }


使用DECLARE_TASKLET定义的tasklet结构体,它是使能的;

使用DECLARE_TASKLET_DISABLED定义的tasklet结构体,它是禁止的;使用之前要先调用tasklet_enable使能它。


也可以使用函数来初始化tasklet结构体:

extern void tasklet_init(struct tasklet_struct *t,
    void (*func)(unsigned long), unsigned long data);


1.6.1.2 使能/禁止tasklet


static inline void tasklet_enable(struct tasklet_struct *t);
static inline void tasklet_disable(struct tasklet_struct *t);


tasklet_enable把count增加1;tasklet_disable把count减1。


1.6.1.3 调度tasklet


static inline void tasklet_schedule(struct tasklet_struct *t);


把tasklet放入链表,并且设置它的TASKLET_STATE_SCHED状态为1。


1.6.1.4 kill tasklet


extern void tasklet_kill(struct tasklet_struct *t);


如果一个tasklet未被调度,tasklet_kill会把它的TASKLET_STATE_SCHED状态清0;

如果一个tasklet已被调度,tasklet_kill会等待它执行完华,再把它的TASKLET_STATE_SCHED状态清0。

通常在卸载驱动程序时调用tasklet_kill。


1.6.2 tasklet使用方法

先定义tasklet,需要使用时调用tasklet_schedule,驱动卸载前调用tasklet_kill。

tasklet_schedule只是把tasklet放入内核队列,它的func函数会在软件中断的执行过程中被调用。


1.6.3 tasklet内部机制


作为初学者,可以不看本节。

tasklet属于TASKLET_SOFTIRQ软件中断,入口函数为tasklet_action,这在内核kernel\softirq.c中设置:

1670925382425.jpg

当驱动程序调用tasklet_schedule时,会设置tasklet的state为TASKLET_STATE_SCHED,并把它放入某个链表:

1670925396774.jpg

当发生硬件中断时,内核处理完硬件中断后,会处理软件中断。对于TASKLET_SOFTIRQ软件中断,会调用tasklet_action函数。

执行过程还是挺简单的:从队列中找到tasklet,进行状态判断后执行func函数,从队列中删除tasklet。

从这里可以看出:

① tasklet_schedule调度tasklet时,其中的函数并不会立刻执行,而只是把tasklet放入队列;

② 调用一次tasklet_schedule,只会导致tasklnet的函数被执行一次;

③ 如果tasklet的函数尚未执行,多次调用tasklet_schedule也是无效的,只会放入队列一次。

tasklet_action函数解析如下:

1670925405686.jpg


1.7 工作队列


使用GIT命令载后,本节源码位于这个目录下:

01_all_series_quickstart\
05_嵌入式Linux驱动开发基础知识\source\
06_gpio_irq\
    09_read_key_irq_poll_fasync_block_timer_tasklet_workqueue


前面讲的定时器、下半部tasklet,它们都是在中断上下文中执行,它们无法休眠。当要处理更复杂的事情时,往往更耗时。这些更耗时的工作放在定时器或是下半部中,会使得系统很卡;并且循环等待某件事情完成也太浪费CPU资源了。

如果使用线程来处理这些耗时的工作,那就可以解决系统卡顿的问题:因为线程可以休眠。

在内核中,我们并不需要自己去创建线程,可以使用“工作队列”(workqueue)。内核初始化工作队列是,就为它创建了内核线程。以后我们要使用“工作队列”,只需要把“工作”放入“工作队列中”,对应的内核线程就会取出“工作”,执行里面的函数。

在2.xx的内核中,工作队列的内部机制比较简单;在现在4.x的内核中,工作队列的内部机制做得复杂无比,但是用法是一样的。

工作队列的应用场合:要做的事情比较耗时,甚至可能需要休眠,那么可以使用工作队列。

缺点:多个工作(函数)是在某个内核线程中依序执行的,前面函数执行很慢,就会影响到后面的函数。

在多CPU的系统下,一个工作队列可以有多个内核线程,可以在一定程度上缓解这个问题。

我们先使用看看怎么使用工作队列。


1.7.1 内核函数


内核线程、工作队列(workqueue)都由内核创建了,我们只是使用。使用的核心是一个work_struct结构体,定义如下:

1670925431074.jpg

使用工作队列时,步骤如下:

① 构造一个work_struct结构体,里面有函数;

② 把这个work_struct结构体放入工作队列,内核线程就会运行work中的函数。


1.7.1.1 定义work


参考内核头文件:include\linux\workqueue.h

#define DECLARE_WORK(n, f)      \
  struct work_struct n = __WORK_INITIALIZER(n, f)
#define DECLARE_DELAYED_WORK(n, f)      \
  struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)


第1个宏是用来定义一个work_struct结构体,要指定它的函数。

第2个宏用来定义一个delayed_work结构体,也要指定它的函数。所以“delayed”,意思就是说要让它运行时,可以指定:某段时间之后你再执行。

如果要在代码中初始化work_struct结构体,可以使用下面的宏:

#define INIT_WORK(_work, _func)


1.7.1.2 使用work:schedule_work


调用schedule_work时,就会把work_struct结构体放入队列中,并唤醒对应的内核线程。内核线程就会从队列里把work_struct结构体取出来,执行里面的函数。

相关文章
|
5月前
|
存储 网络协议 Ubuntu
【Linux开发实战指南】基于UDP协议的即时聊天室:快速构建登陆、聊天与退出功能
UDP 是一种无连接的、不可靠的传输层协议,位于IP协议之上。它提供了最基本的数据传输服务,不保证数据包的顺序、可靠到达或无重复。与TCP(传输控制协议)相比,UDP具有较低的传输延迟,因为省去了建立连接和确认接收等过程,适用于对实时性要求较高、但能容忍一定数据丢失的场景,如在线视频、语音通话、DNS查询等。 链表 链表是一种动态数据结构,用于存储一系列元素(节点),每个节点包含数据字段和指向下一个节点的引用(指针)。链表分为单向链表、双向链表和循环链表等类型。与数组相比,链表在插入和删除操作上更为高效,因为它不需要移动元素,只需修改节点间的指针即可。但访问链表中的元素不如数组直接,通常需要从
307 2
|
1月前
|
人工智能 安全 物联网
Linux操作系统的演变与未来:从开源精神到万物互联的基石###
本文是关于Linux操作系统的演变、现状与未来的深度探索。Linux,这一基于Unix的开源操作系统,自1991年由林纳斯·托瓦兹(Linus Torvalds)学生时代创造以来,已经彻底改变了我们的数字世界。文章首先追溯了Linux的起源,解析其作为开源项目的独特之处;随后,详细阐述了Linux如何从一个小众项目成长为全球最广泛采用的操作系统之一,特别是在服务器、云计算及嵌入式系统领域的主导地位。此外,文章还探讨了Linux在推动技术创新、促进协作开发模式以及保障信息安全方面的作用,最后展望了Linux在未来技术趋势中的角色,包括物联网、人工智能和量子计算等前沿领域的潜在影响。 ###
|
2月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
116 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
3月前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
4月前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
58 6
|
4月前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
61 5
|
4月前
|
存储 监控 安全
Linux存储安全:物理安全基石
【8月更文挑战第17天】在数字化时代,数据安全至关重要。Linux存储安全的物理防护作为基石,通过选择安全的数据中心、实施严格的访问控制、环境监控、物理隔离及设备锁定等措施,有效防范未授权访问和环境威胁。结合具体实施方法与案例代码,能大幅提升系统的物理安全性,确保数据安全无虞。
63 10
|
4月前
|
编解码 安全 Linux
基于arm64架构国产操作系统|Linux下的RTMP|RTSP低延时直播播放器开发探究
这段内容讲述了国产操作系统背景下,大牛直播SDK针对国产操作系统与Linux平台发布的RTMP/RTSP直播播放SDK。此SDK支持arm64架构,基于X协议输出视频,采用PulseAudio和Alsa Lib处理音频,具备实时静音、快照、缓冲时间设定等功能,并支持H.265编码格式。此外,提供了示例代码展示如何实现多实例播放器的创建与管理,包括窗口布局调整、事件监听、视频分辨率变化和实时快照回调等关键功能。这一技术实现有助于提高直播服务的稳定性和响应速度,适应国产操作系统在各行业中的应用需求。
143 3
|
5月前
|
Web App开发 缓存 Linux
FFmpeg开发笔记(三十六)Linux环境安装SRS实现视频直播推流
《FFmpeg开发实战》书中第10章提及轻量级流媒体服务器MediaMTX,适合测试RTSP/RTMP协议,但不适合生产环境。推荐使用SRS或ZLMediaKit,其中SRS是国产开源实时视频服务器,支持多种流媒体协议。本文简述在华为欧拉系统上编译安装SRS和FFmpeg的步骤,包括安装依赖、下载源码、配置、编译以及启动SRS服务。此外,还展示了如何通过FFmpeg进行RTMP推流,并使用VLC播放器测试拉流。更多FFmpeg开发内容可参考相关书籍。
137 2
FFmpeg开发笔记(三十六)Linux环境安装SRS实现视频直播推流
|
5月前
|
Linux
FFmpeg开发笔记(三十四)Linux环境给FFmpeg集成libsrt和librist
《FFmpeg开发实战》书中介绍了直播的RTSP和RTMP协议,以及新协议SRT和RIST。SRT是安全可靠传输协议,RIST是可靠的互联网流传输协议,两者于2017年发布。腾讯视频云采用SRT改善推流卡顿。以下是Linux环境下为FFmpeg集成libsrt和librist的步骤:下载安装源码,配置、编译和安装。要启用这些库,需重新配置FFmpeg,添加相关选项,然后编译和安装。成功后,通过`ffmpeg -version`检查版本信息以确认启用SRT和RIST支持。详细过程可参考书中相应章节。
119 1
FFmpeg开发笔记(三十四)Linux环境给FFmpeg集成libsrt和librist