2024年12月10日 03:48

公告:

欢迎来到havte!站点已升级完成,域名已迁移至dev.havte.com,大家收藏访问dev.havte.com站点。


最新帖子

1 2 3 ... 7
1
Android驱动层 / Linux v4l2框架分析
Last post by admin - 2022年04月19日 14:24
1.概述
V4L2(Video for Linux 2):Linux内核中关于视频设备驱动的框架,对上向应用层提供统一的接口,对下支持各类复杂硬件的灵活扩展; V4L2框架,主要包括v4l2-core、meida framework、videobuf2等模块.
2. v4l2-core
2.1 应用视角
先从应用的角度来看如何使用v4l2吧:
假如要进行视频数据采集
打开设备文件/dev/videoX; 根据打开的设备,查询设备能力集; 设置视频数据的格式、参数等; 分配buffer,这个buffer可以是用户态分配的,也可以是从内核中获取的; 开始视频流采集工作; 将buffer enqueue到v4l2框架,底层负责将视频数据填充后,应用层再将buffer dequeue以便获取数据,然后再将buffer enqueue,如此循环往复;
通常一个camera的模组,通常包括Lens、Sensor、CSI接口等,其中CSI接口用于视频数据的传输; SoC的Mipi接口对接Camera,并通过I2C/SPI控制camera模组; Camera模组中也可以包含ISP模块,用于对图像进行处理,有的SoC中也集成了ISP的IP,接收camera的raw数据后,进行图像处理;
2.2 数据结构
对摄像头的硬件该怎么来抽象呢?没错,就是以v4l2_device和v4l2_subdev来进行抽象,以v4l2_device来代表整个输入设备,以v4l2_subdev来代表子模块,比如CSI、Sensor等;
v4l2_device:对视频设备的整体进行抽象,可以看成是一个纽带,将各个子设备联系在一起,通常它会嵌入在其他结构体中以提供v4l2框架的功能,比如strcut isp_device; v4l2_subdev:对子设备进行抽象,该结构体中包含的struct v4l2_subdev_ops是一个完备的操作函数集,用于对接各种不同的子设备,比如video、audio、sensor等,同时还有一个核心的函数集struct v4l2_subdev_core_ops,提供更通用的功能。子设备驱动根据设备特点实现该函数集中的某些函数即可; video_device:用于向系统注册字符设备节点,以便用户空间可以进行交互,包括各类设置以及数据buffer的获取等,在该结构体中也能看到struct v4l2_ioctl_ops和struct vb2_queue结构体字段,这些与上文中的应用层代码编写息息相关; 如果子设备不需要与应用层交互,struct v4l2_subdev中内嵌的video_device也可以不向系统注册字符设备; video_device结构体,可以内嵌在其他结构体中,以便提供用户层交互的功能,比如struct isp_video; 针对图中回调函数集,v4l2-core提供了一些实现,所以driver在实现时,非特殊情况下可以不用重复造轮子;
2.3 流程分析
来进一步看一下内部的注册,及调用流程吧:
在驱动实现中,驱动结构体中内嵌struct video_device,同时实现struct v4l2_file_operations结构体中的函数,最终通过video_register_device向提供注册; v4l2_register_device函数通过cdev_add向系统注册字符设备,并指定了file_operations,用户空间调用open/read/write/ioctl等接口,便可回调到驱动实现中; v4l2_register_device函数中,通过device_register向系统注册设备,会在/sys文件系统下创建节点; 完成注册后,用户空间便可通过文件描述符来进行访问,从应用层看,大部分都是通过ioctl接口来完成。
用户层的ioctl回调到__video_do_ioctl中,该函数会对系统提供的struct v4l2_ioctl_info v4l2_ioctls[]表进行查询,找到对应的项后进行调用; 驱动做的工作就是填空题,实现对应的回调,在合适的时候被调用; 下一个小节,让我们看看更复杂一点的情况。
3. media framework
3.1 问题引入
为了更好的描述,本节以omap3isp为例:
CSI:camera接口,接收图像数据,RGB/YUV/JPEG等; CCDC:视频处理前端,CCDC为图像传感器和数字视频源提供接口,并处理图像数据; Preview/Resizer:视频处理后端,Preview提供预览功能,可针对不同类型的传感器进行定制,Resizer提供将输入图像数据按所需的显示或视频编码分辨率调整大小的方法; H3A/HIST:静态统计模块,H3A支持AF、AWB、AE的回路控制,HIST根据输入数据,提供各种3A算法所需的统计数据; 上述硬件模块,可以对应到驱动结构体struct isp_device中的各个字段。
omap3isp的硬件模块,支持多种数据流通路,它并不是唯一的,以RGB为例,:
Raw RGB数据进入ISP模块后,可以在运行过程中,根据实际的需求进行通路设置; 所以,重点是:它需要动态设置路径! 那么,软件该如何满足这种需求呢?
3.2 框架
没错,pipeline框架的引入可以解决这个问题。说来很巧,我曾经也实现过一个类似的框架,在阅读media framework时有一种似曾相识的感觉,核心的思想大体一致。
模块之间相互独立,通过struct media_entity来进行抽象,通常会将struct media_entity嵌入到其他结构中,以支持media framework功能; 模块包含struct media_pad,pad可以认为是端口,与其他模块进行联系的媒介,针对特定模块来说它是确定的; pad通过struct media_link来建立连接,指定source和sink,即可将通路建立起来; 各个模块之间最终建立一条数据流,便是一条pipeline了,同一条pipeline中的模块,可以根据前一个模块查找到下一个模块,因此也可以很方便进行遍历,并做进一步的设置操作; 因此,只需要将struct media_entity嵌入到特定子模块中,最终便可以将子模块串联起来,构成数据流。所以,omap3isp的驱动中。
video devnode代表video device,也就是前文中提到的导出到用户空间的节点,用于与用户进行控制及数据交互; 每个模块分别有source pad和sink pad,数据通路灵活多变; 至于数据通路选择问题,可以在驱动初始化的时候进行链接创建,比如isp_create_links; 还是看一下数据结构吧:
media_device:与v4l2_device类似,也是负责将各个子模块集中进行管理,同时在注册的时候,会向系统注册设备节点,方便用户层进行操作; media_entity、media_pad、media_link等结构体的功能在上文中描述过,注意,这几个结构体会添加到media_device的链表中,同时它们结构体的开始字段都需是struct media_gobj,该结构中的mdev将会指向它所属的media_device。这种设计方便结构之间的查找; media_entity中包含多个media_pad,同时media_pad又会指向它所属的media_entity; media_graph和media_pipeline是media_entity的集合,直观来理解,就是由一些模块构成的一条数据通路,由一个统一的数据结构来组织管理; 罗列一下常见的几个接口吧,细节不表了:
程序代码 [选择]
/* 初始化entity的pads */
int media_entity_pads_init(struct media_entity *entity, u16 num_pads,
      struct media_pad *pads);

/* 在两个entity之间创建link */
int media_create_pad_links(const struct media_device *mdev,
   const u32 source_function,
   struct media_entity *source,
   const u16 source_pad,
   const u32 sink_function,
   struct media_entity *sink,
   const u16 sink_pad,
   u32 flags,
   const bool allow_both_undefined);

/* 开始graph的遍历,从指定的entity开始 */
void media_graph_walk_start(struct media_graph *graph,
    struct media_entity *entity);

/* 启动pipeline */
__must_check int media_pipeline_start(struct media_entity *entity,
      struct media_pipeline *pipe);

将media framework和v4l2_device及v4l2_subdev结合起来,就可以将各个子设备构建pipeline。
4. videobuf2
4.1 框架分析
框架可以分成两个部分看:控制流+数据流,上文已经大概描述了控制流,数据流的部分就是video buffer了。 V4L2的buffer管理是通过videobuf2来完成的,它充当用户空间和驱动之间的中间层,并提供low-level,模块化的内存管理功能;
videobuf2的框架; vb2_queue:核心的数据结构,用于描述buffer的队列,其中struct vb2_buffer *bufs[]是存放buffer节点的数组,该数组中的成员代表了vb2 buffer,并将在queued_list和done_list两个队列中进行流转; struct vb2_buf_ops:buffer的操作函数集,由驱动来实现,并由框架通过call_bufop宏来对特定的函数进行调用; struct vb2_mem_ops:内存buffer分配函数接口,buffer类型分为三种:1)虚拟地址和物理地址都分散,可以通过dma-sg来完成;2)物理地址分散,虚拟地址连续,可以通过vmalloc分配;3)物理地址连续,可以通过dma-contig来完成;三种类型也vb2框架中都有实现,框架可以通过call_memop来进行调用; struct vb2_ops:vb2队列操作函数集,由驱动来实现对应的接口,并在框架中通过call_vb_qop宏被调用;
4.2 流程分析
本节以omap3isp为例进行简要分析,流程如下:
buffer申请
buffer enqueue
buffer dequeue
stream on
2
做Android系统开发多年,"工欲善其事,必先利其器",要想提高团队整体的开发效率,那么必须要配备一套硬件配置给力的用于编译开发。源码编译服务器硬件配置的高低,直接影响着系统固件升级和ROM版本发布的速度和效率。

有做过android系统项目基本的都知道,大部分公司开发环境都是入职就搭建好了,入职时拿个账号密码就直接开始搞开发了,但有些刚起步做android项目的公司,或者已有编译环境但随着公司团队规模越来越大,原有较低配置影响到开发进度,要升级编译硬件配置。
由于目前Google发布的最新版本的Android系统源码体积越来越大,因此,越是定制高版本的系统,对编译服务器的硬件配置要求就越高,理论上配置当然是越高编译速度越快,但成本也会升高,这里根据调研,结合自己多年从事系统行业开发,给出Android源码定制开发的基本配置,大概预算,具体参考一下以下的一些建议。

一、直接买整机
优点是简单方便,售后服务较好,但价格会贵一些
1、中配
戴尔(DELL) PowerEdge R740/R750 2U机架式服务器虚拟化主机 R740 2*银牌4214R 24核48线程 64G内存/4*8TB SAS/H750

价格3万8左右
京东购买链接: 京东链接

2、低配
戴尔(DELL) PowerEdge R740/R750 2U机架式服务器虚拟化主机 R740 2*银牌4210R 20核40线程 32G内存/3*4TB SAS/H750

价格2万8左右
京东购买链接:京东链接

二、自己购买配件,找本地电脑服务商前来安装组装
配置高低可以灵活选择,价格会便宜很多

三、自己购买配件,自己组装
需要动手能力强的自行装机,价钱最低

未完待续
3
Android驱动层 / Linux条件变量
Last post by admin - 2022年01月12日 08:32
Linux条件变量
相关函数
程序代码 [选择]
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);

具体说明
条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。
条件变量上的基本操作有:
触发条件(当条件变为 true 时) 等待条件,挂起线程直到其他线程触发条件 条件变量要和互斥量相联结,以避免出现条件竞争:一个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件
上述API具体说明如下:
程序代码 [选择]
pthread_cond_init 使用 cond_attr 指定的属性初始化条件变量 cond,当 cond_attr 为 NULL 时,使用缺省的属性
LinuxThreads 实现条件变量不支持属性,因此 cond_attr 参数实际被忽略
pthread_cond_t 类型的变量也可以用 PTHREAD_COND_INITIALIZER 常量进行静态初始化
pthread_cond_signal 使在条件变量上等待的线程中的一个线程重新开始。如果没有等待的线程,则什么也不做。如果有多个线程在等待该条件,只有一个能重启动,但不能指定哪一个。
pthread_cond_broadcast 重新启动等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。
pthread_cond_wait 自动解锁互斥量(如同执行了 pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用 CPU 时间,直到条件变量被触发。在调用 pthread_cond_wait 之前,应用程序必须加锁互斥量。pthread_cond_wait 函数返回前,自动重新对互斥量加锁(如同执行了 pthread_lock_mutex)。

互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,线程需要对互斥量加锁 这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发 pthread_cond_timedwait 和 pthread_cond_wait 一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间。如果在 abstime 指定的时间内 cond 未触发,互斥量 mutex 被重新加锁,且 pthread_cond_timedwait 返回错误 ETIMEDOUT abstime 参数指定一个绝对时间,时间原点与 time 和 gettimeofday 相同:abstime = 0 表示 1970 年 1 月 1 日 00:00:00 GMT pthread_cond_destroy 销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程 在 LinuxThreads 的实现中,条件变量不联结资源,除检查有没有等待的线程外,pthread_cond_destroy 实际上什么也不做
取消
程序代码 [选择]
pthread_cond_wait 和 pthread_cond_timedwait 是取消点。如果一个线程在这些函数上挂起时被取消,线程立即继续执行,然后再次对 pthread_cond_wait 和 pthread_cond_timedwait 的 mutex 参数加锁,最后执行取消。因此,当调用清除处理程序时,可确保,mutex 是加锁的

异步信号安全(Async-signal Safety) 条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用
特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁。
返回值 在执行成功时,所有条件变量函数都返回 0,错误时返回非零的错误代码
错误代码
程序代码 [选择]
pthread_cond_init、pthread_cond_signal、pthread_cond_broadcast 和 pthread_cond_wait 从不返回错误代码。
pthread_cond_timedwait 函数出错时返回下列错误代码:

ETIMEDOUT:abstime 指定的时间超时时,条件变量未触发
EINTR:pthread_cond_timedwait 被触发中断

pthread_cond_destroy 函数出错时返回下列错误代码:
程序代码 [选择]
EBUSY:某些线程正在等待该条件变量

举例
设有两个共享的变量 x 和 y,通过互斥量 mut 保护,当 x > y 时,条件变量 cond 被触发。
程序代码 [选择]
int x,y;
int x,y;
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
等待直到 x > y 的执行流程:

pthread_mutex_lock(&mut);
while (x <= y) {
    pthread_cond_wait(&cond, &mut);
}
/* 对 x、y 进行操作 */
pthread_mutex_unlock(&mut);
对 x 和 y 的修改可能导致 x > y,应当触发条件变量:

pthread_mutex_lock(&mut);
/* 修改 x、y */
if (x > y) pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);

如果能够确定最多只有一个等待线程需要被唤醒(例如,如果只有两个线程通过 x、y 通信),则使用 pthread_cond_signal 比 pthread_cond_broadcast 效率稍高一些。如果不能确定,应当用pthread_cond_broadcast 要等待在 5 秒内 x > y,则需要这样处理:
程序代码 [选择]
struct timeval now;
struct timespec timeout;
int retcode;

pthread_mutex_lock(&mut);
gettimeofday(&now);
timeout.tv_sec = now.tv_sec + 5;
timeout.tv_nsec = now.tv_usec * 1000;
retcode = 0;
while (x <= y && retcode != ETIMEDOUT) {
    retcode = pthread_cond_timedwait(&cond, &mut, &timeout);
}
if (retcode == ETIMEDOUT) {
    /* 发生超时 */
} else {
    /* 操作 x 和  y */
}
pthread_mutex_unlock(&mut);
4
设备树
通用
查找节点
通过名字查找
程序代码 [选择]
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字(不是table和name属性)。
返回值: 找到的节点,如果为 NULL 表示查找失败。

通过device_type 属性查找
程序代码 [选择]
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,即 device_type 属性值。
返回值: 找到的节点,如果为 NULL 表示查找失败。

根据 device_type 和 compatible查找
程序代码 [选择]
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,即 device_type 属性值(若为 NULL则表示忽略 device_type 属性)
compatible: 要查找的节点所对应的 compatible 属性列表。
返回值: 找到的节点,如果为 NULL 表示查找失败

通过 of_device_id 匹配表来查找
程序代码 [选择]
struct device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
matches: of_device_id 匹配表,也就是在此匹配表里面查找节点。
match: 找到的匹配的 of_device_id。
返回值: 找到的节点,如果为 NULL 表示查找失败

通过路径查找
程序代码 [选择]
inline struct device_node *of_find_node_by_path(const char *path)
path:带有全路径的节点名,可以使用节点的别名。
返回值: 找到的节点,如果为 NULL 表示查找失败

查找指定节点的父节点
程序代码 [选择]
struct device_node *of_get_parent(const struct device_node *node)
node:要查找的父节点的节点。
返回值: 找到的父节点。

查找指定节点的子节点
程序代码 [选择]
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
返回值: 找到的下一个子节点。

提取属性
查找节点中的指定属性
程序代码 [选择]
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
np:设备节点。
name: 属性名字。
lenp:属性值的字节数,一般为NULL
返回值: 找到的属性。

获取属性中元素的数量
程序代码 [选择]
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:每个元素的长度。(如果元素为u32类型则此处填sizeof(u32))
返回值: 得到的属性元素数量。

从属性中获取指定标号的 u32 类型数据值
程序代码 [选择]
int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值: 0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

读取属性中 u8、 u16、 u32 和 u64 类型的数组数据
程序代码 [选择]
int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)
np:设备节点。
proname: 要读取的属性名字。
out_values:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
sz: 要读取的数组元素数量。
返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

读取只有一个整形值的属性
程序代码 [选择]
int of_property_read_u8(const struct device_node *np,const char *propname, u8 *out_value)
int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value)
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value)
int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value)
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值。
返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

读取属性中字符串值
程序代码 [选择]
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值: 0,读取成功,负值,读取失败。

获取#address-cells 属性值
程序代码 [选择]
int of_n_addr_cells(struct device_node *np)
np:设备节点。
返回值: 获取到的#address-cells 属性值。

获取#size-cells 属性值
程序代码 [选择]
int of_n_size_cells(struct device_node *np)
np:设备节点。
返回值: 获取到的#size-cells 属性值。

其他常用函数
查看节点的 compatible 属性是否有包含指定的字符串
程序代码 [选择]
int of_device_is_compatible(const struct device_node *device, const char *compat)
device:设备节点。
compat:要查看的字符串。
返回值: 0,节点的 compatible 属性中不包含 compat 指定的字符串; 正数,节点的compatible属性中包含 compat 指定的字符串。

获取地址相关属性
主要是"reg"或者"assigned-addresses"属性值
程序代码 [选择]
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags)
dev:设备节点。
index:要读取的地址标号。
size:地址长度。
flags:参数,比如 IORESOURCE_IO、 IORESOURCE_MEM 等
返回值: 读取到的地址数据首地址,为 NULL 的话表示读取失败。

将从设备树读取到的地址转换为物理地址
程序代码 [选择]
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
dev:设备节点。
in_addr:要转换的地址。
返回值: 得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。

从设备树里面提取资源值
本质上是将 reg 属性值转换为 resource 结构体类型
程序代码 [选择]
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
dev:设备节点。
index:地址资源标号。
r:得到的 resource 类型的资源值。
返回值: 0,成功;负值,失败。

直接内存映射(获取内存地址所对应的虚拟地址 )
本质上是将 reg 属性中地址信息转换为虚拟地址(将原来的先提取属性在映射结合起来),如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段
程序代码 [选择]
void __iomem *of_iomap(struct device_node *np, int index)
np:设备节点。
index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为0。(从0开始,一次映射一对,即一个地址一个长度)
返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

GPIO子系统
of函数
获取设备树某个属性中定义 GPIO 的个数(空的 GPIO 信息(即值为0)也会被统计到)
程序代码 [选择]
int of_gpio_named_count(struct device_node *np, const char *propname)
np:设备节点。
propname:要统计的 GPIO 属性。
返回值: 正值,统计到的 GPIO 数量;负值,失败。

获取设备树gpios属性中定义 GPIO 的个数(空的 GPIO 信息(即值为0)也会被统计到)
程序代码 [选择]
int of_gpio_count(struct device_node *np)

获取 GPIO 编号
程序代码 [选择]
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
index: GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO 的编号,如果只有一个 GPIO 信息的话此参数为 0

驱动层函数
申请GPIO
程序代码 [选择]
int gpio_request(unsigned gpio, const char *label)
gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数返回值
label:给 gpio 设置个名字。
返回值: 0,申请成功;其他值,申请失败。

释放GPIO
程序代码 [选择]
void gpio_free(unsigned gpio)

设置方向
程序代码 [选择]
int gpio_direction_input(unsigned gpio)
int gpio_direction_output(unsigned gpio, int value)
返回值: 0,设置成功;负值,设置失败

设置值
程序代码 [选择]
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
返回值: 非负值,得到的 GPIO 值;负值,获取失败

获取值
程序代码 [选择]
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)

获取 gpio 对应的中断号
程序代码 [选择]
int gpio_to_irq(unsigned int gpio)
gpio: 要获取的 GPIO 编号
返回值: GPIO 对应的中断号

中断相关
提取 interrupts 属性中的中断号
程序代码 [选择]
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
dev: 设备节点
index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息
返回值:中断号

获取 gpio 对应的中断号(与上面的函数功能一样)
程序代码 [选择]
int gpio_to_irq(unsigned int gpio)
gpio: 要获取的 GPIO 编号,由gpio_request申请而来
返回值: GPIO 对应的中断号

总结
以上汇总四篇,总结常用内核API接口,其中有较为详细的注解,经常看一看,对于熟练编写驱动, 有非常重要的作用,当然这不是让你去记这些API接口。而是经常看看熟悉它们。
5
Android框架 / 在高通平台Android环境下编译内核模块
Last post by admin - 2022年01月06日 12:10
高通Android环境中Linux内核会作为Android的一部分进行编译,直接使用make即可一次性从头编到尾。
编译内核模块之前必须先编译内核,编译内核之前必须先指定内核配置。在独立编译内核情况下,编译一遍内核后,可以直接使用 make module 来编译内核模块,如果修改了相应模块文件,使用相同的命令也能很快的进行增量编译。
而在高通环境下,由于内核的编译过程已经被集成到Android的编译中,所以每次编译内核或者内核模块时,都必须通过Android的编译环境进行启用。虽然Android提供诸如 make bootimage 命令,可以只编译bootimage相关内容,但是Android庞大的编译体系在初始化时也会占用很多的时间。前段时间在调试一个独立的内核模块时就一直被这个问题困扰着,每次修改模块代码后都必须通过 make bootimage 来编译。虽然只有一个文件,但是每次编译都花费至少1min30sec,严重影响了开发进度。为此,自己参考内核模块独立编译的Makefile和Android的环境特点写了一个内核模块编译Makefile。


Makefile:
程序代码 [选择]
CROSS_ARCH:=ARCH=arm CROSS_COMPILE="$(ARM_EABI_TOOLCHAIN)/arm-eabi-"
KDIR:=$(ANDROID_PRODUCT_OUT)/obj/KERNEL_OBJ/
PWD:=$(shell pwd)

obj-m:= my_module.o
.PHONY: modules package clean
all:package
modules:
    @if [ "$(ANDROID_BUILD_TOP)_yes" = "_yes" ]; then echo "You have to run \". build/envsetup.sh\" to init enviroment first. \nAnd then you have to run \"choosecombo\" to setup the project."&&exit 1; fi
    @if [ ! -d $(KDIR) ]; then echo "Build kernle first."&&cd $(ANDROID_BUILD_TOP)&&make bootimage&&cd -; fi
    $(MAKE) $(CROSS_ARCH) -C $(KDIR) M=$(PWD) modules

package:modules
    @mkdir -p ./package
    @cp $(obj-m:.o=.ko) ./package

clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.order *.symvers package


该Makefile默认会将当前目录下的my_module.c文件编译为内核模块。同时,在编译时会强制检查Android的环境是否正确配置,如果没有配置它会进行相应提示后退出编译处理。编译模块时使用的内核配置是编译Android时指定项目所配置的内核配置。如果内核还没有编译,则在编译模块之前会自动编译内核主体。如果一切OK,则每次只会编译修改过的模块文件。编译好后会将模块文件单独拷贝到当前目录下的 package 目录中,方便使用。
6
常用框架API
platform
platform设备相关:

程序代码 [选择]
void platform_device_unregister(struct platform_device *pdev)//卸载platform设备
int platform_device_register(struct platform_device *pdev)//注册platform设备

platform驱动相关:

程序代码 [选择]
void platform_driver_unregister(struct platform_driver *drv)//卸载platform驱动
int platform_driver_register (struct platform_driver *driver)//注册platform驱动

MISC
MISC 驱动也叫做杂项驱动,当某些外设无法进行分类的时候就可以使用 MISC 驱动。 MISC 驱动其实就是最简单的字符设备驱动,通常嵌套在 platform 总线驱动中,实现复杂的驱动
所有的 MISC 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号。MISC 设备会自动创建 cdev

程序代码 [选择]
int misc_register(struct miscdevice * misc)//注册MISC驱动
int misc_deregister(struct miscdevice *misc)//销毁MISC驱动

INPUT
初始化及卸载 申请及释放:

程序代码 [选择]
struct input_dev *input_allocate_device(void)//申请input_dev
void input_free_device(struct input_dev *dev)//释放input_dev

注册及注销:

程序代码 [选择]
int input_register_device(struct input_dev *dev)//注册
void input_unregister_device(struct input_dev *dev)//注销

修改input_dev:

程序代码 [选择]
struct input_dev *inputdev;
inputdev = input_allocate_device(); /* 申请 input_dev */
inputdev->name = "test_inputdev"; /* 设置 input_dev 名字 */

/*********第一种设置事件和事件值的方法***********/
__set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
__set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
__set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
/************************************************/

/*********第二种设置事件和事件值的方法***********/
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
/************************************************/

/*********第三种设置事件和事件值的方法***********/
keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
/************************************************/

/* 注册 input_dev */
input_register_device(inputdev);

上报数据 上报指定的事件以对应值:

程序代码 [选择]
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)

dev:需要上报的 input_dev type:上报的事件类型,比如 EV_KEY code:事件码,即注册的按键值,比如 KEY_0、 KEY_1 等等 value:事件值,比如 1 表示按键按下, 0 表示按键松开
专用上报函数(本质是调用input_event):

程序代码 [选择]
void input_report_key(struct input_dev *dev, unsigned int code, int value)//上报按键
void input_report_rel(struct input_dev *dev, unsigned int code, int value)
void input_report_abs(struct input_dev *dev, unsigned int code, int value)
void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
void input_report_switch(struct input_dev *dev, unsigned int code, int value)
void input_mt_sync(struct input_dev *dev)

同步事件:

程序代码 [选择]
void input_sync(struct input_dev *dev)

应用层相关
应用层通过获得 input_event 结构体来获取input子系统发送的输入事件

程序代码 [选择]
struct input_event {
    struct timeval time;
    __u16 type;
    __u16 code;
    __s32 value;
};

time:时间,即此事件发生的时间,timeval 结构体类型:

程序代码 [选择]
typedef long __kernel_long_t;
typedef __kernel_long_t __kernel_time_t;
typedef __kernel_long_t __kernel_suseconds_t;

struct timeval {
    __kernel_time_t tv_sec; /* 秒 */
    __kernel_suseconds_t tv_usec; /* 微秒 */
};

type: 事件类型,比如 EV_KEY 表示此次事件为按键事件 code: 事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,如: KEY_0、KEY_1等 value:值,比如 EV_KEY 事件中 value 就是按键值,为 1 表示按键按下,为 0 的话表示按键没有被按下
应用层读取方式:

程序代码 [选择]
static struct input_event inputevent;
int err = 0;
err = read(fd, &inputevent, sizeof(inputevent));
if (err > 0) { /* 读取数据成功 */
    switch (inputevent.type) {
        case EV_KEY:
            if (inputevent.code < BTN_MISC) { /* 键盘键值 */
                printf("key %d %s\r\n", inputevent.code, inputevent.value ? "press" : "release");
            } else {
                printf("button %d %s\r\n", inputevent.code, inputevent.value ? "press" : "release");
            }
            break;
            /* 其他类型的事件,自行处理 */
        case EV_REL:
            break;
        case EV_ABS:
            break;
        case EV_MSC:
            break;
        case EV_SW:
            break;
    }
} else {
    printf("读取数据失败\r\n");
}

多点触摸
linux内核中讲解多点电容触摸屏协议文档路径:Documentation/input/multitouch-protocol.txt 老版本(2.x 版本)的 linux内核不支持多点电容触摸(Multi-touch,简称 MT)
MT 协议分为两种类型: Type A:适用于触摸点不能被区分或者追踪,此类型的设备上报原始数据(此类型在实际使用中非常少) Type B:适用于有硬件追踪并能区分触摸点的触摸设备,此类型设备通过 slot 更新某一个触摸点的信息,一般的多点电容触摸屏 IC 都有此能力
Type A
步骤(时序)如下:

程序代码 [选择]
ABS_MT_POSITION_X x[0]//上报第一个点的x坐标 input_report_abs()
ABS_MT_POSITION_Y y[0]//上报第一个点的y坐标
SYN_MT_REPORT// input_mt_sync()

ABS_MT_POSITION_X x[1]//上报第二个点的x坐标 input_report_abs()
ABS_MT_POSITION_Y y[1]//上报第二个点的y坐标
SYN_MT_REPORT// input_mt_sync()

SYN_REPORT// input_sync() 该轮数据发送完毕

例子:drivers/input/touchscreen/st1232.c
Type B
步骤(时序)如下:

程序代码 [选择]
ABS_MT_SLOT 0// input_mt_slot()
ABS_MT_TRACKING_ID 45// input_mt_report_slot_state()
ABS_MT_POSITION_X x[0]//上报第一个点的x坐标 input_report_abs()
ABS_MT_POSITION_Y y[0]//上报第一个点的y坐标

ABS_MT_SLOT 1// input_mt_slot()
ABS_MT_TRACKING_ID 46// input_mt_report_slot_state()
ABS_MT_POSITION_X x[1]//上报第二个点的x坐标 input_report_abs()
ABS_MT_POSITION_Y y[1]//上报第二个点的y坐标

SYN_REPORT// input_sync() 该轮数据发送完毕

例子:drivers/input/touchscreen/ili210x.c
相关函数:
初始化 MT 的输入 slots(初始化时使用):

程序代码 [选择]
int input_mt_init_slots(struct input_dev *dev, unsigned int num_slots, unsigned int flags)

dev: MT 设备对应的 input_dev,因为 MT 设备隶属于 input_dev num_slots:设备要使用的 SLOT 数量,也就是触摸点的数量 flags: 其他一些 flags 信息

程序代码 [选择]
#define INPUT_MT_POINTER 0x0001 /* pointer device, e.g. trackpad */
#define INPUT_MT_DIRECT 0x0002 /* direct device, e.g. touchscreen */
#define INPUT_MT_DROP_UNUSED0x0004 /* drop contacts not seen in frame */
#define INPUT_MT_TRACK 0x0008 /* use in-kernel tracking */
#define INPUT_MT_SEMI_MT 0x0010 /* semi-mt device, finger count handled manually */

返回值: 0,成功;负值,失败
产生 ABS_MT_SLOT 事件:

程序代码 [选择]
static inline void input_mt_slot(struct input_dev *dev, int slot)

dev: MT 设备对应的 input_dev slot:当前发送的是哪个 slot 的坐标信息,也就是哪个触摸点产生ABS_MT_TRACKING_ID和ABS_MT_TOOL_TYPE事件:

程序代码 [选择]
void input_mt_report_slot_state(struct input_dev *dev, unsigned int tool_type, bool active)

dev: MT 设备对应的 input_dev tool_type:上报触摸工具类型(即ABS_MT_TOOL_TYPE事件),目前的协议支持MT_TOOL_FINGER(手指)、 MT_TOOL_PEN(笔)和 MT_TOOL_PALM(手掌) active:true,连续触摸,添加一个新的触摸点,linux 内核自动分配一个 ABS_MT_TRACKING_ID;false,触摸点抬起,移除一个触摸点,ABS_MT_TRACKING_ID由内核设为-1上传真实的触摸点数量:

程序代码 [选择]
void input_mt_report_pointer_emulation(struct input_dev *dev, bool use_count)

dev: MT 设备对应的 input_dev use_count:true,有效的触摸点数量(上报数量就是当前数模点数量); false,追踪到的触摸点数量多于当前上报的数量(使用 BTN_TOOL_TAP 事件通知用户空间当前追踪到的触摸点总数量)
举个例子:硬件能够追踪5个触摸点,无论是否有触摸,硬件都会有5个值输出,此时use_count就是false,即无论触摸的数量为多少,追踪到的(硬件输出的5个值)总比上报的(真实触摸数量)多 例子:

程序代码 [选择]
static irqreturn_t ft5x06_handler(int irq, void *dev_id)
{
......
/* 读取FT5X06触摸点坐标从0X02寄存器开始,连续读取29个寄存器 */
ret = ft5x06_read_regs(multidata, FT5X06_TD_STATUS_REG, rdbuf, FT5X06_READLEN);
   
/* 上报每一个触摸点坐标 */
for (i = 0; i < MAX_SUPPORT_POINTS; i++) {
u8 *buf = &rdbuf[i * tplen + offset];

/* 以第一个触摸点为例,寄存器TOUCH1_XH(地址0X03),各位描述如下:
* bit7:6  Event flag  0:按下 1:释放 2:接触 3:没有事件
* bit5:4  保留
* bit3:0  X轴触摸点的11~8位。
*/
type = buf[0] >> 6;     /* 获取触摸类型 */
if (type == TOUCH_EVENT_RESERVED)
continue;
 
/* 我们所使用的触摸屏和FT5X06是反过来的 */
x = ((buf[2] << 8) | buf[3]) & 0x0fff;
y = ((buf[0] << 8) | buf[1]) & 0x0fff;

/* 以第一个触摸点为例,寄存器TOUCH1_YH(地址0X05),各位描述如下:
* bit7:4  Touch ID  触摸ID,表示是哪个触摸点
* bit3:0  Y轴触摸点的11~8位。
*/
id = (buf[2] >> 4) & 0x0f;
down = type != TOUCH_EVENT_UP;//是否按下 1:按下 0:松开

input_mt_slot(multidata->input, id);
input_mt_report_slot_state(multidata->input, MT_TOOL_FINGER, down);

if (!down)
continue;

input_report_abs(multidata->input, ABS_MT_POSITION_X, x);
input_report_abs(multidata->input, ABS_MT_POSITION_Y, y);
}

input_mt_report_pointer_emulation(multidata->input, true);
input_sync(multidata->input);
    ......
}

Framebuffer
fb_info结构体:

程序代码 [选择]
struct fb_info *framebuffer_alloc(size_t size, struct device *dev)

void framebuffer_release(struct fb_info *info) size:分配完framebuffer结构体后附加的额外空间(一般用于存放用户私有数据) dev:最终会绑定到fb_info->device上,可以设为NULL
注册与卸载:

程序代码 [选择]
int register_framebuffer(struct fb_info *fb_info)
int unregister_framebuffer(struct fb_info *fb_info)

显存分配与释放:

程序代码 [选择]
static inline void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *dma_addr, gfp_t gfp)
static inline void dma_free_writecombine(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_addr)

RTC
申请并注册rtc_device:

程序代码 [选择]
struct rtc_device *rtc_device_register(const char *name, struct device *dev, const struct rtc_class_ops *ops, struct module *owner)

name:设备名字 dev: 设备 ops: RTC 底层驱动函数集 owner:驱动模块拥有者
注销rtc_device:

程序代码 [选择]
void rtc_device_unregister(struct rtc_device *rtc)

IIC
适配器注册和注销: 一般不会用到,SOC厂商会写好这部分代码
//注册

程序代码 [选择]
int i2c_add_adapter(struct i2c_adapter *adapter)/* 使用动态总线号 */
int i2c_add_numbered_adapter(struct i2c_adapter *adap)/* 使用静态总线号 */

//注销

程序代码 [选择]
void i2c_del_adapter(struct i2c_adapter * adap)

i2c驱动注册与注销:

程序代码 [选择]
#define i2c_add_driver(driver) i2c_register_driver(THIS_MODULE, driver)
void i2c_del_driver(struct i2c_driver *driver)

iic通信
内核驱动 内核文档:Documentation\i2c\i2c-protocol、Documentation\i2c\smbus-protocol
其中smbus-protocol是i2c-protocol的一个子集,官方更加推荐使用后者的smbus-protoco中的函数
收发函数:

程序代码 [选择]
//S Addr Rd [A] [Data] NA S Addr Wr [A] Data [A] P
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)

adap: 所使用的 I2C 适配器, i2c_client 会保存其对应的 i2c_adapter msgs: I2C 要发送的一个或多个消息

程序代码 [选择]
struct i2c_msg {
    __u16 addr; /* 从机地址 */
    __u16 flags; /* 标志 */
    #define I2C_M_TEN 0x0010
    #define I2C_M_RD 0x0001
    #define I2C_M_STOP 0x8000
    #define I2C_M_NOSTART 0x4000
    #define I2C_M_REV_DIR_ADDR 0x2000
    #define I2C_M_IGNORE_NAK 0x1000
    #define I2C_M_NO_RD_ACK 0x0800
    #define I2C_M_RECV_LEN 0x0400
    __u16 len; /* 消息(本 msg)长度 */
    __u8 *buf; /* 消息数据 */
};

num: 消息数量,即 msgs 的数量 返回值: 负值,失败,其他非负值,发送的 msgs 数量
发送函数(最终调用i2c_transfer):

程序代码 [选择]
//S Addr Wr [A] Data [A] Data [A] ... [A] Data [A] P
int i2c_master_send(const struct i2c_client *client, const char *buf, int count)

client: I2C 设备对应的 i2c_client buf:要发送的数据 count: 要发送的数据字节数,必须小于 64KB(i2c_msg 的 len 成员变量是一个 u16(无符号 16 位)类型的数据) 返回值: 负值,失败,其他非负值,发送的字节数
接收函数(最终调用i2c_transfer):

程序代码 [选择]
//S Addr Rd [A] [Data] A [Data] A ... A [Data] NA P
int i2c_master_recv(const struct i2c_client *client, char *buf, int count)

client: I2C 设备对应的 i2c_client buf:要接收的数据 count: 要接收的数据字节数,必须小于 64KB(i2c_msg 的 len 成员变量是一个 u16(无符号 16 位)类型的数据) 返回值: 负值,失败,其他非负值,发送的字节数
smbus-protoco中的函数:

程序代码 [选择]
i2c_smbus_read_byte()//S Addr Rd [A] [Data] NA P
i2c_smbus_write_byte()//S Addr Wr [A] Data [A] P
i2c_smbus_read_byte_data()//S Addr Wr [A] Comm [A] S Addr Rd [A] [Data] NA P
i2c_smbus_read_word_data()//S Addr Wr [A] Comm [A] S Addr Rd [A] [DataLow] A [DataHigh] NA P
i2c_smbus_write_byte_data()//S Addr Wr [A] Comm [A] Data [A] P
i2c_smbus_write_word_data()//S Addr Wr [A] Comm [A] DataLow [A] DataHigh [A] P
i2c_smbus_read_block_data()//S Addr Wr [A] Comm [A] S Addr Rd [A] [Count] A [Data] A [Data] A ... A [Data] NA P
i2c_smbus_write_block_data()//S Addr Wr [A] Comm [A] Count [A] Data [A] Data [A] ... [A] Data [A] P
i2c_smbus_read_i2c_block_data()//S Addr Wr [A] Comm [A] S Addr Rd [A] [Data] A [Data] A ... A [Data] NA P
i2c_smbus_write_i2c_block_data()//S Addr Wr [A] Comm [A] Data [A] Data [A] ... [A] Data [A] P

应用层直接访问
内核文档:Documentation\i2c\dev-interface 通常,I2C设备由设备驱动来控制,但是通过/dev接口也可以提供用户空间直接访问适配器上的设备。前提是需要加载I2C-DEV内核模块
在应用层,i2c-tools工具包帮你写好了接口,下载链接,镜像仓库,只需要包含该库的头文件include\linux\i2c-dev.h即可直接控制iic(最新版本的4.1没有发现该文件)。其本质是调用的内核自带的驱动模块I2C-DEV。该模块位于\drivers\i2c\i2c-dev.c,通过宏CONFIG_I2C_CHARDEV配置,内核menuconfig路径为Device Drivers-> I2C support,一般默认为通过模块加载
i2c-tools工具包的本质是通过调用ioctl打开实现各种功能: //include\linux\i2c-dev.h部分源码

程序代码 [选择]
static inline __s32 i2c_smbus_access(int file, char read_write, __u8 command, int size, union i2c_smbus_data *data)
{
    struct i2c_smbus_ioctl_data args;

    args.read_write = read_write;
    args.command = command;
    args.size = size;
    args.data = data;
    return ioctl(file,I2C_SMBUS,&args);//本质就是调用ioctl
}


static inline __s32 i2c_smbus_write_quick(int file, __u8 value)
{
    return i2c_smbus_access(file,value,0,I2C_SMBUS_QUICK,NULL);
}

上述部分源码中file就是需要访问的iic控制器,如:/dev/i2c-0,与自己写的驱动程序类似
模板

程序代码 [选择]
/* 设备结构体 */
struct xxx_dev {
    ......
        void *private_data; /* 私有数据,一般会设置为 i2c_client */
};

/*
* @description : 读取 I2C 设备多个寄存器数据
* @param - dev : I2C 设备
* @param - reg : 要读取的寄存器首地址
* @param - val : 读取到的数据
* @param - len : 要读取的数据长度
* @return : 操作结果
*/
static int xxx_read_regs(struct xxx_dev *dev, u8 reg, void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev->private_data;

    /* msg[0],第一条写消息,发送要读取的寄存器首地址 */
    msg[0].addr = client->addr; /* I2C 器件地址 */
    msg[0].flags = 0; /* 标记为发送数据 */
    msg[0].buf = &reg; /* 读取的首地址 */
    msg[0].len = 1; /* reg 长度 */

    /* msg[1],第二条读消息,读取寄存器数据 */
    msg[1].addr = client->addr; /* I2C 器件地址 */
    msg[1].flags = I2C_M_RD; /* 标记为读取数据 */
    msg[1].buf = val; /* 读取数据缓冲区 */
    msg[1].len = len; /* 要读取的数据长度 */
    ret = i2c_transfer(client->adapter, msg, 2);
    if(ret == 2) {
        ret = 0;
    } else {
        ret = -EREMOTEIO;
    }
    return ret;
}

/*
* @description : 向 I2C 设备多个寄存器写入数据
* @param - dev : 要写入的设备结构体
* @param - reg : 要写入的寄存器首地址
* @param - val : 要写入的数据缓冲区
* @param - len : 要写入的数据长度
* @return : 操作结果
*/
static s32 xxx_write_regs(struct xxx_dev *dev, u8 reg, u8 *buf, u8 len)
{
    u8 b[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *)
        dev->private_data;

    b[0] = reg; /* 寄存器首地址 */
    memcpy(&b[1],buf,len); /* 将要发送的数据拷贝到数组 b 里面 */

    msg.addr = client->addr; /* I2C 器件地址 */
    msg.flags = 0; /* 标记为写数据 */

    msg.buf = b; /* 要发送的数据缓冲区 */
    msg.len = len + 1; /* 要发送的数据长度 */

    return i2c_transfer(client->adapter, &msg, 1);
}

SPI
spi_master注册和注销: 一般不会用到,SOC厂商会写好这部分代码
//注册

程序代码 [选择]
struct spi_master *spi_alloc_master(struct device *dev,unsigned size)//申请
int spi_register_master(struct spi_master *master)//注册 spi_bitbang_start

//注销

程序代码 [选择]
void spi_master_put(struct spi_master *master)//释放
void spi_unregister_master(struct spi_master *master)//注销 spi_bitbang_stop

spi驱动注册与注销:

程序代码 [选择]
int spi_register_driver(struct spi_driver *sdrv);
void spi_unregister_driver(struct spi_driver *sdrv)

相关结构体:

程序代码 [选择]
/*-----------------------spi_message-----------------------*/
struct spi_message {
    struct list_head    transfers;
    struct spi_device   *spi;
    unsigned        is_dma_mapped:1;
    /* REVISIT:  we might want a flag affecting the behavior of the
     * last transfer ... allowing things like "read 16 bit length L"
     * immediately followed by "read L bytes".  Basically imposing
     * a specific message scheduling algorithm.
     *
     * Some controller drivers (message-at-a-time queue processing)
     * could provide that as their default scheduling algorithm.  But
     * others (with multi-message pipelines) could need a flag to
     * tell them about such special cases.
     */
    /* completion is reported through a callback */
    void            (*complete)(void *context);/*异步传输完成后,会调用该函数*/
    void            *context;
    unsigned        frame_length;
    unsigned        actual_length;
    int         status;
    /* for optional use by whatever driver currently owns the
     * spi_message ...  between calls to spi_async and then later
     * complete(), that's the spi_master controller driver.
     */
    struct list_head    queue;
    void            *state;
};
/*-----------------------spi_transfer-----------------------*/
struct spi_transfer {
    /* it's ok if tx_buf == rx_buf (right?)
     * for MicroWire, one buffer must be null
     * buffers must work with dma_*map_single() calls, unless
     *   spi_message.is_dma_mapped reports a pre-existing mapping
     */
    const void  *tx_buf;/* 要发送的数据 */
    void        *rx_buf;/* 保存接收到的数据 */
    unsigned    len;/* 进行传输的数据长度 */
    dma_addr_t  tx_dma;
    dma_addr_t  rx_dma;
    struct sg_table tx_sg;
    struct sg_table rx_sg;
    unsigned    cs_change:1;
    unsigned    tx_nbits:3;
    unsigned    rx_nbits:3;
#define SPI_NBITS_SINGLE    0x01 /* 1bit transfer */
#define SPI_NBITS_DUAL      0x02 /* 2bits transfer */
#define SPI_NBITS_QUAD      0x04 /* 4bits transfer */
    u8      bits_per_word;
    u16     delay_usecs;
    u32     speed_hz;
    struct list_head transfer_list;
};

spi通信
初始化:

程序代码 [选择]
int spi_setup(struct spi_device *spi)//初始化时钟和SPI模式
void spi_message_init(struct spi_message *m)
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)//将spi_transfer添加到spi_message队列中

同步传输(阻塞,会等待SPI数据传输完成)

程序代码 [选择]
int spi_sync(struct spi_device *spi, struct spi_message *message)

异步传输(不会阻塞,不会等到SPI数据传输完成)
异步传输需要设置 spi_message 中的complete成员变量,当 SPI 异步传输完成以后complete函数就会被调用

程序代码 [选择]
int spi_async(struct spi_device *spi, struct spi_message *message)

模板

程序代码 [选择]
/* SPI 多字节发送 */
static int spi_send(struct spi_device *spi, u8 *buf, int len)
{
    int ret;
    struct spi_message m;
    struct spi_transfer t = {
        .tx_buf = buf,
        .len = len,
    };
    spi_message_init(&m); /* 初始化 spi_message */
    spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
    ret = spi_sync(spi, &m); /* 同步传输 */
    return ret;
}
/* SPI 多字节接收 */
static int spi_receive(struct spi_device *spi, u8 *buf, int len)
{
    int ret;
    struct spi_message m;
    struct spi_transfer t = {
        .rx_buf = buf,
        .len = len,
    };
    spi_message_init(&m); /* 初始化 spi_message */
    spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
    ret = spi_sync(spi, &m); /* 同步传输 */
    return ret;
}
7
阻塞与非阻塞
阻塞:访问资源时,如果资源不可用,将会挂起线程,直到资源可用再唤醒。open函数中大部分flag均为阻塞访问
非阻塞:访问资源时,如果资源不可用,将会直接返回错误码。open函数中flag参数O_NONBLOCK为非阻塞访问
等待队列
一般用于在中断中唤醒阻塞操作而引起的线程挂起
等待队列头 等待队列的头部
定义:
程序代码 [选择]
struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

初始化:
程序代码 [选择]
void init_waitqueue_head(wait_queue_head_t *q)//仅初始化
DECLARE_WAIT_QUEUE_HEAD//宏,定义+初始化

等待队列项
每个访问设备的进程都需要创建一个队列项,当设备不可用的时候需要将这些进程对应的等待队列项添加到等待队列里面
定义:
程序代码 [选择]
struct __wait_queue {
    unsigned int flags;
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;

初始化:
程序代码 [选择]
DECLARE_WAITQUEUE(name, tsk)//宏,定义并初始化
name:等待队列项的名字
tsk:这个等待队列项属于哪个任务(进程),一般设置为current(全局变量,表示当前进程)

添加/移除等待队列
当设备不可用的时候需要将该进程对应的等待队列项添加到等待队列里面
程序代码 [选择]
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)//添加
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)//删除
q: 等待队列项要加入的等待队列头
wait:要加入/删除的等待队列项

等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程
程序代码 [选择]
void wake_up(wait_queue_head_t *q)//唤醒队列中的所有进程(包括TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE状态)
void wake_up_interruptible(wait_queue_head_t *q)//唤醒队列中的所有进程(仅唤醒TASK_INTERRUPTIBLE状态进程)

等待事件
当事件满足以后能够自动唤醒等待队列中的进程
程序代码 [选择]
wait_event(wq, condition)//若condition为真,则唤醒等待队列,否则一直阻塞。(会将进程设置为TASK_UNINTERRUPTIBLE状态)
wait_event_timeout(wq, condition, timeout)//与wait_event类似,timeout为超时时间,单位为jiffies。返回0:超时时间到,且condition为假;返回1:condition为真
wait_event_interruptible(wq, condition)//与wait_event类似,此函数会将进程设置为TASK_INTERRUPTIBLE,即可以被信号打断
wait_event_interruptible_timeout(wq, condition, timeout)//与wait_event_timeout类似,此函数会将进程设置为TASK_INTERRUPTIBLE,即可以被信号打断

轮询
一般用于非阻塞操作
poll、 epoll 和 select 用于处理轮询。 应用程序通过 select、 epoll 或 poll 来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行
select
单线程中 select 函数能够监视的文件描述符数量一般最大为1024,可以修改该数量但会降低效率
程序代码 [选择]
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
nfds: 要操作的文件描述符个数
readfds:指向描述符集合,fd_set类型。用于监视指定描述符集的读变化,所指定集合中有一个文件可以读取,则返回大于 0 的值,否则根据 timeout 参数来判断是否超时。设置为 NULL,表示不关心任何文件的读变化
writefds:指向描述符集合,fd_set类型。用于监视指定描述符集的写变化,所指定集合中有一个文件可以写入,则返回大于 0 的值,否则根据 timeout 参数来判断是否超时。设置为 NULL,表示不关心任何文件的写变化
exceptfds:指向描述符集合,fd_set类型。用于监视指定描述符集的异常变化,所指定集合中有一个文件异常,则返回大于 0 的值,否则根据 timeout 参数来判断是否超时。设置为 NULL,表示不关心任何文件的异常变化

fd_set变量定义相关宏:
程序代码 [选择]
void FD_ZERO(fd_set *set)//将 fd_set 变量的所有位都清零
void FD_SET(int fd, fd_set *set)//将 fd_set 变量的某个位置 1(向 fd_set 添加一个文件描述符), fd:要加入的文件描述符
void FD_CLR(int fd, fd_set *set)//将 fd_set 变量的某个位清零(向 fd_set 删除一个文件描述符), fd:要删除的文件描述符
int FD_ISSET(int fd, fd_set *set)//测试 fd_set 的某个位是否置 1(判断某个文件是否可以进行操作), fd:要判断的文件描述符
timeout:超时时间,设为 NULL 表示无限期等待。timeval结构体类型

struct timeval {
 long tv_sec; /* 秒 */
 long tv_usec; /* 微秒 */
};
返回值: 0,超时发生,没有任何文件描述符可以进行操作; -1,发生错误;其他值,可以进行操作的文件描述符个数

select使用示例:
程序代码 [选择]
void main(void)
{
    int ret, fd; /* 要监视的文件描述符 */
    fd_set readfds; /* 读操作文件描述符集 */
    struct timeval timeout; /* 超时结构体 */
    fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
    FD_ZERO(&readfds); /* 清除 readfds */
    FD_SET(fd, &readfds); /* 将 fd 添加到 readfds 里面 */
    /* 构造超时时间 */
    timeout.tv_sec = 0;
    timeout.tv_usec = 500000; /* 500ms */
    ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
    switch (ret) {
        case 0: /* 超时 */
            ......
            break;
        case -1: /* 错误 */
            ......
            break;
        default: /* 可以读取数据 */
            if(FD_ISSET(fd, &readfds)) { /* 判断是否为 fd 文件描述符 */
                /* 使用 read 函数读取数据 */
            }
            break;
    }
}

poll
本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制
int poll(struct pollfd *fds, nfds_t nfds, int timeout) fds: 要监视的文件描述符集合以及要监视的事件,为结构体pollfd数组类型,pollfd类型定义:
程序代码 [选择]
struct pollfd {
    int fd; /* 文件描述符 */
    short events; /* 请求的事件 */
    short revents; /* 返回的事件 */
};

fd:要监视的文件描述符,如果 fd 无效则 events 监视事件也无效,并且 revents 返回 0
events:要监视的事件,类型如下:
程序代码 [选择]
POLLIN 有数据可以读取
POLLPRI 有紧急的数据需要读取
POLLOUT 可以写数据
POLLERR 指定的文件描述符发生错误
POLLHUP 指定的文件描述符挂起
POLLNVAL 无效的请求
POLLRDNORM 等同于POLLIN

revents:返回参数,由 Linux 内核设置具体的返回事件 nfds:poll 函数要监视的文件描述符数量 timeout:超时时间,单位为 ms 返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,即发生事件或错误的文件描述符数量; 0,超时; -1,发生错误,并设置相应的错误码
poll使用示例:
程序代码 [选择]
void main(void)
{
    int ret;
    int fd; /* 要监视的文件描述符 */
    struct pollfd fds;
    fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
    /* 构造结构体 */
    fds.fd = fd;
    fds.events = POLLIN; /* 监视数据是否可以读取 */
    ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
    if (ret) { /* 数据有效 */
        ......
        /* 读取数据 */
        ......
    } else if (ret == 0) { /* 超时 */
        ......
    } else if (ret < 0) { /* 错误 */
        ......
    }
}

epoll
selcet 和 poll 函数会随着所监听的 fd 数量的增加,而导致效率的降低,且遍历所有的描述符比较浪费时间。epoll 专为处理大并发而准备,一般用于网络编程中。 其相关函数如下:
创建 epoll 句柄:
int epoll_create(int size) size:Linux2.6.8 后此参数已无意义,大于 0 即可 返回值: epoll 句柄,如果为-1 的话表示创建失败 向 epoll 句柄中添加要监视的文件描述符以及监视的事件:
程序代码 [选择]
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd:epoll 句柄(即epoll_create的返回值)

op:对 epfd 的操作,可以设置为:
程序代码 [选择]
EPOLL_CTL_ADD 向epfd添加文件参数fd表示的描述符
EPOLL_CTL_MOD 修改参数fd的event事件
EPOLL_CTL_DEL 从epfd中删除fd描述符

fd:要监视的文件描述符 event:要监视的事件类型,epoll_event结构体指针类型:
程序代码 [选择]
struct epoll_event {
    uint32_t events; /* epoll 事件 */
    epoll_data_t data; /* 用户数据 */
};

events:要监视的事件:
程序代码 [选择]
EPOLLIN 有数据可以读取
EPOLLPRI 有紧急的数据需要读取
EPOLLOUT 可以写数据
EPOLLERR 指定的文件描述符发生错误
EPOLLHUP 指定的文件描述符挂起
EPOLLET 设置epoll为边沿触发(默认为水平触发)
EPOLLONESHOT 一次性监视(当监视完成以后需要再次监视某个fd,就需要将fd重新添加到epoll里面)

返回值: 0,成功; -1,失败,并设置相应的错误码
等待事件发生:
程序代码 [选择]
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
epfd:epoll 句柄
events:指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调用者可以根据 events 判断发生了哪些事件
maxevents:events 数组大小(必须大于 0)
timeout:超时时间,单位为 ms

返回值: 0,超时; -1,错误;其他值,准备就绪的文件描述符数量
驱动中的poll函数 函数原型:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait) filp: 要打开的设备文件(文件描述符)
wait: 结构体 poll_table_struct 类型指针, 由应用程序传递进来的。一般将此参数传递给 poll_wait 函数
返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:
程序代码 [选择]
POLLIN 有数据可以读取
POLLPRI 有紧急的数据需要读取
POLLOUT 可以写数据
POLLERR 指定的文件描述符发生错误
POLLHUP 指定的文件描述符挂起
POLLNVAL 无效的请求
POLLRDNORM 等同于POLLIN,普通数据可读

添加到poll_table:
非阻塞函数,不会引起阻塞,只是将应用程序添加到 poll_table 中
程序代码 [选择]
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
wait_address:要添加到 poll_table 中的等待队列头
p:poll_table,就是file_operations 中 poll 函数的 wait 参数

驱动通用模板:
程序代码 [选择]
struct xxxx_dev {
    ......
    struct wait_queue_head_t r_wait; /* 等待队列头 */
};

unsigned int xxx_poll(struct file *filp, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    struct xxxx_dev *dev = (struct xxxx_dev *)filp->private_data;

    poll_wait(filp, &dev->r_wait, wait);

    if(......) { /* 相关条件满足 */
        mask = POLLIN | POLLRDNORM; /* 返回 PLLIN */
    }
    return mask;
}

异步通知(信号)
Linux中的信号宏:
程序代码 [选择]
#define SIGHUP 1 /* 终端挂起或控制进程终止 */
#define SIGINT 2 /* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 3 /* 终端退出(Ctrl+\组合键) */
#define SIGILL 4 /* 非法指令 */
#define SIGTRAP 5 /* debug 使用,有断点指令产生 */
#define SIGABRT 6 /* 由 abort(3)发出的退出指令 */
#define SIGIOT 6 /* IOT 指令 */
#define SIGBUS 7 /* 总线错误 */
#define SIGFPE 8 /* 浮点运算错误 */
#define SIGKILL 9 /* 杀死、终止进程 */
#define SIGUSR1 10 /* 用户自定义信号 1 */
#define SIGSEGV 11 /* 段违例(无效的内存段) */
#define SIGUSR2 12 /* 用户自定义信号 2 */
#define SIGPIPE 13 /* 向非读管道写入数据 */
#define SIGALRM 14 /* 闹钟 */
#define SIGTERM 15 /* 软件终止 */
#define SIGSTKFLT 16 /* 栈异常 */
#define SIGCHLD 17 /* 子进程结束 */
#define SIGCONT 18 /* 进程继续 */
#define SIGSTOP 19 /* 停止进程的执行,只是暂停 */
#define SIGTSTP 20 /* 停止进程的运行(Ctrl+Z 组合键) */
#define SIGTTIN 21 /* 后台进程需要从终端读取数据 */
#define SIGTTOU 22 /* 后台进程需要向终端写数据 */
#define SIGURG 23 /* 有"紧急"数据 */
#define SIGXCPU 24 /* 超过 CPU 资源限制 */
#define SIGXFSZ 25 /* 文件大小超额 */
#define SIGVTALRM 26 /* 虚拟时钟信号 */
#define SIGPROF 27 /* 时钟信号描述 */
#define SIGWINCH 28 /* 窗口大小改变 */
#define SIGIO 29 /* 可以进行输入/输出操作 */
#define SIGPOLL SIGIO
/* #define SIGLOS 29 */
#define SIGPWR 30 /* 断点重启 */
#define SIGSYS 31 /* 非法的系统调用 */
#define SIGUNUSED 31 /* 未使用信号 */

上述信号中,除了 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被忽略外,其他的信号都可以忽略
应用程序API 指定信号的处理函数:
程序代码 [选择]
sighandler_t signal(int signum, sighandler_t handler)

signum:要设置处理函数的信号 handler: 信号的处理函数,函数原型:
程序代码 [选择]
typedef void (*sighandler_t)(int)

返回值: 设置成功返回信号的前一个处理函数,设置失败返回 SIG_ERR
应用程序的一般模板:
程序代码 [选择]
fd = open(filename, O_RDWR);
if (fd < 0) {
    printf("Can't open file %s\r\n", filename);
    return -1;
}

/* 设置信号 SIGIO 的处理函数 */
signal(SIGIO, sigio_signal_func);

fcntl(fd, F_SETOWN, getpid()); /* 将当前进程的进程号告诉给内核 */
flags = fcntl(fd, F_GETFD); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC);/* 设置进程状态为 FASYNC 启用异步通知功能(此时会调用驱动中的fasync函数) */

while(1) {
    sleep(2);
}

close(fd);
return 0;

驱动程序API
异步相关结构体:
程序代码 [选择]
struct fasync_struct {
    spinlock_t fa_lock;
    int magic;
    int fa_fd;
    struct fasync_struct *fa_next;
    struct file *fa_file;
    struct rcu_head fa_rcu;
};

向应用程序发送中断信号:
程序代码 [选择]
void kill_fasync(struct fasync_struct **fp, int sig, int band)

fp:要操作的 fasync_struct 结构体 sig: 要发送的信号 band: 可读时设置为 POLL_IN,可写时设置为 POLL_OUT 返回值: 无
file_operations操作集中异步接口函数: 应用程序通过fcntl(fd, F_SETFL, flags | FASYNC)改变fasync标记的时候,该函数就会执行
程序代码 [选择]
int (*fasync) (int fd, struct file *filp, int on)

初始化 fasync_struct 结构体:
程序代码 [选择]
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

前三个参数与fasync函数中参数一致 fapp:要初始化的 fasync_struct 结构体指针变量
驱动一般模板:
程序代码 [选择]
struct xxx_dev {
    ......
    struct fasync_struct *async_queue; /* 异步相关结构体 */
};

static int xxx_fasync(int fd, struct file *filp, int on)
{
    struct xxx_dev *dev = (xxx_dev)filp->private_data;

    if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)//直接调用该函数即可,不用干其他事
        return -EIO;
    return 0;
}

static struct file_operations xxx_ops = {
    ......
    .fasync = xxx_fasync,
    .release = xxx_release,
    ......
};

static int xxx_release(struct inode *inode, struct file *filp)
{
    return xxx_fasync(-1, filp, 0); /* 删除异步通知 */
}
8
并发与竞争
原子操作
定义:
程序代码 [选择]
typedef struct {
    int counter;
} atomic_t;

整形操作:
程序代码 [选择]
ATOMIC_INIT(int i);//定义时初始化
int atomic_read(atomic_t *v);
void atomic_set(atomic_t *v, int i)
void atomic_add(int i, atomic_t *v)
void atomic_sub(int i, atomic_t *v)
void atomic_dec(atomic_t *v)//自减
void atomic_inc(atomic_t *v)//自增
int atomic_dec_return(atomic_t *v)//自减并返回v
int atomic_inc_return(atomic_t *v)//自增并返回v
int atomic_sub_and_test(int i, atomic_t *v)//(v-i)==0?1:0(返回真、假)
int atomic_dec_and_test(atomic_t *v)//(v--)==0?1:0(返回真、假)
int atomic_inc_and_test(atomic_t *v)//(v++)==0?1:0(返回真、假)
int atomic_add_negative(int i, atomic_t *v)//(v+i)<0?1:0(返回真、假)

位操作(直接对内存操作):
程序代码 [选择]
void set_bit(int nr, void *p)//将p地址的第nr位置 1
void clear_bit(int nr,void *p)
void change_bit(int nr, void *p)//翻转
int test_bit(int nr, void *p)//获取
int test_and_set_bit(int nr, void *p)//将p地址的第nr位置1,并返回nr位原来的值
int test_and_clear_bit(int nr, void *p)//...清零...
int test_and_change_bit(int nr, void *p)//...翻转...

自旋锁
基本API
DEFINE_SPINLOCK(spinlock_t lock)//定义并初始化 int spin_lock_init(spinlock_t *lock)//初始化 void spin_lock(spinlock_t *lock)//加锁 void spin_unlock(spinlock_t *lock)//解锁 int spin_trylock(spinlock_t *lock)//尝试加锁 int spin_is_locked(spinlock_t *lock)//检查是否加锁,是返回非 0,否返回 0
中断相关:
一般在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用spin_lock/spin_unlock
程序代码 [选择]
void spin_lock_irq(spinlock_t *lock)//禁止本地中断,并获取自旋锁
void spin_unlock_irq(spinlock_t *lock)//激活本地中断,并释放自旋锁
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)//保存中断状态,禁止本地中断,并获取自旋锁
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)//恢复中断状态,并且激活本地中断,释放自旋锁

中断下半部相关:
程序代码 [选择]
void spin_lock_bh(spinlock_t *lock)//关闭下半部,并获取自旋锁
void spin_unlock_bh(spinlock_t *lock)//打开下半部,并释放自旋锁

信号量
程序代码 [选择]
DEFINE_SEAMPHORE(name)//定义信号量 并设值为1
void sema_init(struct semaphore *sem, int val)//初始化,并设值为val
void down(struct semaphore *sem)//获取信号量(会导致休眠,不能在中断中使用,不能被信号打断)
int down_interruptible(struct semaphore *sem)//获取信号量(会导致休眠,不能在中断中使用,可以被信号打断)
int down_trylock(struct semaphore *sem)//尝试获取信号量(成功返回 0。否则返回非 0)
void up(struct semaphore *sem)//释放信号量

互斥体
会导致休眠,不能在中断中使用 mutex,中断中只能使用自旋锁
程序代码 [选择]
DEFINE_MUTEX(name)//定义并初始化
void mutex_init(mutex *lock)//初始化
void mutex_lock(struct mutex *lock)//上锁,失败则休眠,不可以被信号打断
int mutex_lock_interruptible(struct mutex *lock)//上锁,失败则休眠,可以被信号打断
void mutex_unlock(struct mutex *lock)//解锁
int mutex_trylock(struct mutex *lock)//尝试加锁,成功返回1,失败返回0
int mutex_is_locked(struct mutex *lock)//判断是否加锁,是返回1,否返回0

字符设备驱动
常用宏
程序代码 [选择]
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author");
//printk
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */

注册与注销
老方法,不推荐:
程序代码 [选择]
//注册字符设备(浪费设备号)
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
//注销字符设备
static inline void unregister_chrdev(unsigned int major, const char *name);
name:设备名字,指向一串字符串

新方法,推荐:
程序代码 [选择]
/*注册(方法一,自己确定设备号)*/
int register_chrdev_region(dev_t from, unsigned count, const char *name);//详见设备号API
cdev.owner = THIS_MODULE;
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
/*注册(方法二,系统分配设备号)*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);//详见设备号API
cdev.owner = THIS_MODULE;
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
/*注销*/
void cdev_del(struct cdev *p);
void unregister_chrdev_region(dev_t from, unsigned count);

设备节点
类相关:
程序代码 [选择]
class_create(owner, name)//创建类 owner一般为THIS_MODULE
void class_destroy(struct class *cls);//卸载类

设备相关:
程序代码 [选择]
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);//创建设备 parent=NULL drvdata=NULL
void device_destroy(struct class *class, dev_t devt)//卸载设备

设备号
相关宏:
程序代码 [选择]
MAJOR(dev)//dev_t -> 主设备号
MINOR(dev)//dev_t -> 次设备号
MKDEV(ma,mi)//主设备号+次设备号 -> dev_t

动态分配设备号:
系统分配设备号+自动注册设备号
程序代码 [选择]
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev:存放起始设备编号的指针,当注册成功,*dev就会等于分配到的起始设备编号,可以通过MAJOR()和MINNOR()宏来提取主次设备号
baseminor:次设备号基地址,也就是起始次设备号
count:要申请的数量,一般都是一个
name:字符设备名称
返回值小于0,表示注册失败

注册设备号:
手动确定设备号+手动注册设备号
程序代码 [选择]
int register_chrdev_region(dev_t from, unsigned count, const char *name)
from:要申请的起始设备号,也就是给定的设备号
count:要申请的数量,一般都是一个
name:设备名字
当返回值小于0,表示注册失败

用户空间与内核空间
内核空间->用户空间
程序代码 [选择]
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
to:用户空间指针
from:内核空间指针
n:从内核空间向用户空间拷贝数据的字节数
成功返回0,失败返回失败数目

用户空间->内核空间
程序代码 [选择]
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
to:内核空间指针
from:用户空间指针
n:从用户空间向内核空间拷贝数据的字节数
成功返回0,失败返回失败数目

地址映射
程序代码 [选择]
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
phys_addr:要映射的物理起始地址。
size:要映射的内存空间大小。
void iounmap (volatile void __iomem *addr)

对映射后的内存进行读写操作的函数:
//读
程序代码 [选择]
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

//写
程序代码 [选择]
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

Linux常用命令
程序代码 [选择]
#查看设备号
cat /proc/devices
#创建设备节点
mknod /dev/xxx c 主设备号 次设备号
#查看设备树节点
ls /proc/device-tree
ls /sys/firmware/devicetree/base
#查看platform相关
ls /sys/bus/platform/devices # 设备
ls /sys/bus/platform/drivers # 驱动
#查看misc相关
ls /sys/class/misc # 驱动
#驱动相关
depmod
modprobe xxx.ko
insmod xxx.ko
lsmod
rmmod xxx.ko

内核定时器
程序代码 [选择]
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

相关变量:
HZ:宏,就是CONFIG_HZ,系统频率,通过menuconfig配置,默认100 jiffies、jiffies_64:系统运行的节拍数,由宏HZ确定,默认10ms累加一次 jiffies/HZ:系统运行时间(s) 时间先后函数:
//unkown 通常为 jiffies,known 通常是需要对比的值
程序代码 [选择]
time_after(unkown, known)//unkown 时间上超过 known 则返回真
time_before(unkown, known)//unkown 时间上滞后 known 则返回真
time_after_eq(unkown, known)//unkown 时间上超过或等于 known 则返回真
time_before_eq(unkown, known)//unkown 时间上滞后或等于 known 则返回真

时间转换函数:
程序代码 [选择]
int jiffies_to_msecs(const unsigned long j)//jiffies类型j --> ms
int jiffies_to_usecs(const unsigned long j)//jiffies类型j --> us
u64 jiffies_to_nsecs(const unsigned long j)//jiffies类型j --> 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

短延时函数:
程序代码 [选择]
void ndelay(unsigned long nsecs)
void udelay(unsigned long usecs)
void mdelay(unsigned long mseces)

定时器类型:
程序代码 [选择]
struct timer_list {
    struct list_head entry;
    unsigned long expires; /* 定时器超时时间,单位是节拍数 */
    struct tvec_base *base;
    void (*function)(unsigned long); /* 定时处理函数 */
    unsigned long data; /* 要传递给 function 函数的参数 */
    int slack;
};

定时器函数:
程序代码 [选择]
void init_timer(struct timer_list *timer);//初始化
void add_timer(struct timer_list *timer);//注册并使能
int del_timer(struct timer_list * timer);//删除(需等待 定时处理函数 退出)
int del_timer_sync(struct timer_list *timer);//删除(需等待其他处理器使用完定时器,中断上下文勿用)
int mod_timer(struct timer_list *timer, unsigned long expires);//修改定时值(会使能定时器)
返回值: 0,定时器未被使能; 1,定时器已经使能

中断
上半部
申请
程序代码 [选择]
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
irq:要申请中断的中断号。
handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
flags:中断标志,可以在文件include/linux/interrupt.h里面查看所有的中断标志。

常用的中断标志:
标志 描述
程序代码 [选择]
IRQF_SHARED 多个设备共享一个中断线,共享的所有中断都必须指定此标志。 如果使用共享中断的话,request_irq函数的 dev 参数是唯一区分他们的标志
IRQF_ONESHOT 单次中断,中断执行一次就结束
IRQF_TRIGGER_NONE 无触发
IRQF_TRIGGER_RISING 上升沿触发
IRQF_TRIGGER_FALLING 下降沿触发
IRQF_TRIGGER_HIGH 高电平触发
IRQF_TRIGGER_LOW 低电平触发
name:中断名字,设置以后可以在/proc/interrupts文件中看到对应的中断名字
dev: 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数
返回值: 0 中断申请成功;其他负值:中断申请失败;-EBUSY:中断已经被申请了

对于一种不太重要而又比较耗时的中断,可以使用以下函数进行申请:
程序代码 [选择]
int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id)

该函数会使得中断线程化(中断使用下半部虽然可以被延迟处理,但是依旧先于线程执行,中断线程化可以让这些比较耗时的下半部与进程进行公平竞争) 带devm_前缀表明申请到的资源可以由系统自动释放,无需手动处理 注意:并不是所有的中断都可以被线程化。该函数有利有弊,具体是否使用需要根据实际情况来衡量(触摸中断一般会用) 释放
程序代码 [选择]
void free_irq(unsigned int irq, void *dev)
irq: 要释放的中断
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉

中断处理函数 格式:
程序代码 [选择]
irqreturn_t (*irq_handler_t) (int, void *)

返回值:
一般为return IRQ_RETVAL(IRQ_HANDLED)
程序代码 [选择]
enum irqreturn {
    IRQ_NONE = (0 << 0),
    IRQ_HANDLED = (1 << 0),
    IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;

使能与禁止
程序代码 [选择]
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)//需等待 正在执行的中断处理函数执行完
void disable_irq_nosync(unsigned int irq)//无需等待 正在执行的中断处理函数执行完

//不推荐
程序代码 [选择]
local_irq_disable()//关闭全局中断
local_irq_enable()//使能全局中断

//推荐
程序代码 [选择]
local_irq_save(flags)//保存中断标志到flags并 关闭全局中断
local_irq_restore(flags)//根据flags设置中断标志并 使能全局中断

下半部 推荐使用tasklet实现
软中断 定义:
程序代码 [选择]
struct softirq_action
{
    void (*action)(struct softirq_action *);
};

系统定义的10个软中断:
多核处理器中,各个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断,但是所执行的软中断服务函数却是相同的:
程序代码 [选择]
enum
{
    HI_SOFTIRQ=0,//高优先级软中断
    TIMER_SOFTIRQ,//定时器软中断
    NET_TX_SOFTIRQ,//网络数据发送软中断
    NET_RX_SOFTIRQ,//网络数据接收软中断
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,//tasklet 软中断
    SCHED_SOFTIRQ,//调度软中断
    HRTIMER_SOFTIRQ,//高精度定时器软中断
    RCU_SOFTIRQ,//RCU 软中断
    NR_SOFTIRQS
};

static struct softirq_action softirq_vec[NR_SOFTIRQS];

注册软中断:
软中断必须在编译的时候静态注册。Linux内核默认会在软中断初始化函数softirq_init中打开TASKLET_SOFTIRQ和HI_SOFTIRQ软中断
程序代码 [选择]
void open_softirq(int nr, void (*action)(struct softirq_action *))
nr:要开启的软中断,在上述enum中选择一个
action:软中断对应的处理函数

触发软中断:
程序代码 [选择]
void raise_softirq(unsigned int nr)

tasklet 定义:
程序代码 [选择]
struct tasklet_struct
{
    struct tasklet_struct *next; /* 下一个 tasklet */
    unsigned long state; /* tasklet 状态 */
    atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
    void (*func)(unsigned long); /* tasklet 执行的函数,用户定义,相当于中断处理函数 */
    unsigned long data; /* 函数 func 的参数 */
};

初始化:
程序代码 [选择]
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);//初始化
DECLARE_TASKLET(name, func, data)//宏,定义并初始化
func:tasklet 的处理函数
data:要传递给 func 函数的参数

调度函数:
一般在中断的上半部中调用
程序代码 [选择]
void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name

工作队列 工作队列在进程上下文执行,即,将要推后的工作交给一个内核线程去执行,因此工作队列允许睡眠或重新调度。
如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet
定义:
工作->工作队列->工作者线程
//工作(重点关注):
程序代码 [选择]
struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func; /* 工作队列处理函数 */
};

//工作队列:
程序代码 [选择]
struct workqueue_struct {
    struct list_head pwqs;
    struct list_head list;
    struct mutex mutex;
    int work_color;
    int flush_color;
    atomic_t nr_pwqs_to_flush;
    struct wq_flusher *first_flusher;
    struct list_head flusher_queue;
    struct list_head flusher_overflow;
    struct list_head maydays;
    struct worker *rescuer;
    int nr_drainers;
    int saved_max_active;
    struct workqueue_attrs *unbound_attrs;
    struct pool_workqueue *dfl_pwq;
    char name[WQ_NAME_LEN];
    struct rcu_head rcu;
    unsigned int flags ____cacheline_aligned;
    struct pool_workqueue __percpu *cpu_pwqs;
    struct pool_workqueue __rcu *numa_pwq_tbl[];
};

//工作者线程
程序代码 [选择]
struct worker {
    union {
        struct list_head entry;
        struct hlist_node hentry;
    };
    struct work_struct *current_work;
    work_func_t current_func;
    struct pool_workqueue *current_pwq;
    bool desc_valid;
    struct list_head scheduled;
    struct task_struct *task;
    struct worker_pool *pool;
    struct list_head node;
    unsigned long last_active;
    unsigned int flags;
    int id;
    char desc[WORKER_DESC_LEN];
    struct workqueue_struct *rescue_wq;
};

初始化:
程序代码 [选择]
#define INIT_WORK(_work, _func)//初始化,需要自己创建work_struct
#define DECLARE_WORK(n, f)//创建和初始化,无需自己创建work_struct

调度:
一般在中断的上半部中调用
程序代码 [选择]
bool schedule_work(struct work_struct *work)
work: 要调度的工作
返回值: 0 成功,其他值 失败

篇幅较长,拆分成多篇
9
其他 / 芯片设计之ARM64架构
Last post by admin - 2021年12月31日 09:42

1. ARM64架构介绍
1.1 ARMv8-A架构介绍

ARMv8-A是ARM公司发布的第一代支持64位处理器的指令集和架构。它在扩充64位寄存器的同时提供对上一代架构指令集的兼容,因而能同时提供运行32位和64位应用移序的执行环境。ARMv8-A架构除了提高了处理能力外,还引入了很多吸引人的新特性。

通过超大物理地址空间,提供超过4GB物理内存的访问。
具有64位宽的虚拟地址空间。32位处理器中只能提供4GB大小的虚拟地址空间,这极大限制了桌面系统和服务器等应用的发挥。64位宽的虚拟地址空间可以提供超大的访问空间。
提供31个64位宽的通用寄存器,可以减少对栈的访问,从而提高性能。
提供16KB和64KB的页面,有助于降低TLB的未命中率(miss rate)。
具有全新的异常处理模型,有助于降低操作系统和虚拟化的实现复杂度。
具有全新的加载-获取、存储-释放指令(load-acquire,store-release instruction),专为C++11、C11以及Java内存模型而设计。


1.2常见的ARMv8处理器
下面介绍市面上常见的ARMv8架构的处理器内核。

Cortex-A53处理器内核:ARM公司第一款采用ARMv8-A架构设计的处理器 内核,专为低功耗而设计,通常可以使用1~4个Cortex-A53处理器组成处理器簇(cluster),也可以和Cortex-A57/Cortex-A72等高性能处理器组成大小核架构。
Cortex-A57处理器内核:采用64位ARMv8-A架构设计的CPU,而且通过AArch32执行状态来保持与ARMv7架构的完全后向兼容性。除了ARMv8的架构优势之外,Cortex-A57还提高了单个时钟周期的性能,比高性能的Cortex-A15CPU高出20%~40%。CortexA57还改进了二级高速缓存的设计以及内存系统的其他组件,极大地提高了能效。
Cortex-A72处理器内核:2015年年初正式发布的基于ARMv8-A架构并对Cortex-A57处理器做了大量优化和改进的一款处理器内核。在相同的移动设备电池寿命限制下,Cortex-A72相比基于Cortex-A15的设备能提供3.5倍的性能提升,展现出优异的整体功耗效率。

1.3 ARM64的基本概念
ARM处理器实现的是精简指令集计算机(ReducedInstruction Set Computer,RISC)架构。本节介绍ARMv8-A架构中的一些基本概念。

处理单元
ARM公司的官方技术手册中提到了一个概念,可以把处理器处理事务的过程抽象为处理单元(Processing Element,(PE)。

执行状态
执行状态(execution state)是处理器运行时的环境,包括寄存器的位宽、支持的指令集异常模型、内存管理以及编程模型等。ARMv8架构定义了两种执行模式。

AArch64:64位的执行状态。提供31个64位的通用寄存器。提供64位的程序计数(PC)寄存器、栈指针(SP)寄存器以及异常链接寄存器(ELR)。提供A64指令集。定义ARMv8异常模型,支持4个异常等级ELO~EL3。提供64位的内存模型定义一组处理器状态(PSTATE)用来保存PE的状态。
AArch32:32位的执行状态。提供13个32位的通用寄存器,再加上PC寄存器、SP寄存器、链接寄存器(LR)。支持两套指令集,分别是A32和T32指令集(Thumb指令集)。支持ARMv7-A异常模型,基于PE模式并映射到ARMv8的异常模型。提供32位的虚拟内存访问机制。定义一组处理器状态(PSTATE)用来保存PE的状态。
ARMv8指令集
ARMv8架构根据不同的执行状态提供对不同指令集的支持。

A64指令集:运行在AArch64状态,提供64位指令集支持。
A32指令集:运行在AArch32状态,提供32位指令集支持。
T32指令集:运行在AArch32状态,提供16和32位指令集支持。
系统寄存器命名
在AArch64状态下,很多系统寄存器会根据不同的异常等级提供不同的变种寄存器

_ELx, where x is 0,1, 2, or 3

比如,SPELO表示ELO下的栈指针寄存器,SP_EL1表示EL1下的栈指针寄存器。


1.4ARMv8处理器的运行状态
ARMv8处理器支持两种运行状态--AArch64状态和AArch32状态。AArch64状态是ARMv8新增的64位运行状态,而AArch32状态是为了兼容ARMv7架构而保留的32位运行状态。当处理器运行在AArch64状态时执行A64指令集;而当运行在AArch32状态时,可以执行A32指令集或T32指令集。 AArch64架构的异常等级(exception level)确定了处理器当前运行的特权级别,类似于ARMv7架构中的特权等级.

EL0:用户特权,用于运行普通用户程序。
EL1:系统特权,通常用于运行操作系统。
EL2:运行虚拟化扩展的虚拟监控程序(hypervisor)。
EL3:运行安全世界中的安全监控器(secure monitor).
在ARMv8架构里允许切换应用程序的运行模式。比如,在运行64位操作系统的ARMv8处理器中,我们可以同时运行A64指令集的应用程序和A32指令集的应用程序。但是在这行32位操作系统的ARMv8处理器中,就不能执行A64指令集的应用程序了。当需要执行 A32指令集的应用程序时,需要通过管理员调用(Supervisor Call, SVC)指令切换到EL1操作系统会执行任务的切换并且返回到AArch32的ELO,这时候系统便为这个应用程序备好了AArch32的运行环境。


1.5 ARMv8架构支持的数据宽度
ARMv8架构支持如下几种数据宽度。

字节(byte):1字节等于8位。
半字(halfword):16位。
字(word):32位。
双字(doubleword):64位。
4字(quadword):128位。


1.6不对齐访问
不对齐访问有两种情况。一种是指令对齐,另一种是数据对齐。A64指令集要求指令存放的位置必须以字(word,32位宽)为单位对齐。访问存储位置不是以字为单位对齐的指令会导致PC对齐异常(PCaligment fault)。 对于数据访问,需要区分不同的内存类型。对内存类型是设备内存的不对齐访问会触发对齐异常(alignment fault)。 对于访问普通内存,除了使用独占。加载/独占-存储(load-exclusive/store-exclusive)指令或加载-获取/存储-释放(load-acquire/store-release)指令外,还可使用其他的用于加载或存储单个或多个寄存器的所有指令。如果访问地址和要访问的数据元素大小不对齐,那么可以根据以下两种情况进行处理:

若对应的异常等级中的SCTLR_ELx寄存器的A域设置为1,则说明打开了地址对齐检查功能,因而会触发对齐异常。

若对应的异常等级中的SCTLRELx寄存器的A域设置为0,则说明处理器支持不对齐访问。 当然,处理器支持的不对齐访问也有一些限制。

不能保证单次访问原子地完成,有可能复制多次。

不对齐访问比对齐访问需要更多的处理时间。

不对齐的地址访问可能会引发中止(abort)。
10
1. 内联汇编做了什么
在Linux内核中,我们时常见到内嵌汇编语言的函数,向这种在C语言函数内部嵌入汇编代码的做法我们称之为内联汇编。尽管现在的编译器对程序的优化都很优秀了,但在某些对效率要求较高的场合仍需要手动优化,我们暂时先不关心具体的内联汇编的规则,先来看看内联汇编是如何提高效率的。
1.1. 优化对比测试
测试环境:imx6ull
编译器:arm-linux-gnueabihf-gcc
反汇编:arm-linux-gnueabihf-objdump
Makefile:
程序代码 [选择]
test: main.c
        arm-linux-gnueabihf-gcc -o $@ $^

        arm-linux-gnueabihf-objdump -D test > test.dis
clean:
        rm test *.dis
1.1.1. 非内联汇编
写测试函数如下: main.c:
程序代码 [选择]
#include <stdio.h>
#include <stdlib.h>

int add(int a, int b)
{
return a + b;
}

int main(int argc, char **argv)
{
int a;
int b;
char *endptr;

if (argc != 3)
{
printf("Usage: %s <val1> <val2>\n", argv[0]);
return -1;
}

a = (int)strtol(argv[1], NULL, 0);
b = (int)strtol(argv[2], NULL, 0);

printf("%d + %d = %d\n", a, b, add(a, b));
}

查看反汇编:
这是我们写的add函数:
程序代码 [选择]
00010404 <add>:
   10404:       b480            push    {r7}
   10406:       b083            sub     sp, #12
   10408:       af00            add     r7, sp, #0
   1040a:       6078            str     r0, [r7, #4]
   1040c:       6039            str     r1, [r7, #0]
   1040e:       687a            ldr     r2, [r7, #4]
   10410:       683b            ldr     r3, [r7, #0]
   10412:       4413            add     r3, r2
   10414:       4618            mov     r0, r3
   10416:       370c            adds    r7, #12
   10418:       46bd            mov     sp, r7
   1041a:       f85d 7b04       ldr.w   r7, [sp], #4
   1041e:       4770            bx      lr
然后在main函数中被调用:
程序代码 [选择]
00010420 <main>:
#...省略
10470:       f7ff ffc8       bl      10404 <add>
#...省略
1.1.2. 内联汇编
写测试函数如下: main.c:
程序代码 [选择]
#include <stdio.h>
#include <stdlib.h>

int add(int a, int b)
{
int sum;
__asm__ volatile (
"add %0, %1, %2"
:"=r"(sum)
:"r"(a), "r"(b)
:"cc"
);
return sum;
}

int main(int argc, char **argv)
{
int a;
int b;

if (argc != 3)
{
printf("Usage: %s <val1> <val2>\n", argv[0]);
return -1;
}

a = (int)strtol(argv[1], NULL, 0);
b = (int)strtol(argv[2], NULL, 0);

printf("%d + %d = %d\n", a, b, add(a, b));
return 0;
}

程序代码 [选择]
00010474 <add>:
   10474:       1840            adds    r0, r0, r1
   10476:       4770            bx      lr
然后在main函数中被调用:
程序代码 [选择]
00010404 <main>:
#...省略
 10454:       f000 f80e       bl      10474 <add>
#...省略
1.2. 结论
可见,实现相同的功能,内联汇编add函数的指令相比纯C语言写的add函数大大简化,效率自然大大提高。
2. 语法规则
2.1. 内联汇编规则
程序代码 [选择]
asm [Qualifiers] (
                  ``AssemblerTemplate``
                  : ``OutputOperands``
                  : ``InputOperands``
                  : ``Clobbers`` 
                )

asm [Qualifiers] goto (
                      ``AssemblerTemplate``
                      : /* No outputs. */
                      : ``InputOperands``
                      : ``Clobbers``
                      : ``GotoLabels``
                    )
2.2. 示例
程序代码 [选择]
#include <stdio.h>
#include <stdlib.h>

int test(int a, int b)
{
__asm__ goto (

"cmp %1, %0\n\t"
    /* BLE指令:b比a小则跳转 */
"blt %l2"
: /* No outputs. */
: "r"(a), "r"(b)
: "cc"
: carry);

return 0;

carry:
return 1;
}

int main(int argc, char **argv)
{
int a;
int b;

if (argc != 3)
{
printf("Usage: %s <val1> <val2>\n", argv[0]);
return -1;
}

a = (int)strtol(argv[1], NULL, 0);
b = (int)strtol(argv[2], NULL, 0);

printf("test return is %d\n",test(a, b));
return 0;
}
2.3. imx6ull测试结果
BLE指令实现功能:b比a小则跳转
程序代码 [选择]
./test  1 2
test return is 0

./test  3 2
test return is 1

./test  2 2
test return is 0
1 2 3 ... 7