Quantcast
Channel: 蜗窝科技
Viewing all 218 articles
Browse latest View live

eMMC 原理 4 :总线协议

$
0
0


1. eMMC 总线接口

eMMC 总线接口定义如下图所示:

各个信号的描述如下:

CLK

CLK 信号用于从 Host 端输出时钟信号,进行数据传输的同步和设备运作的驱动。
在一个时钟周期内,CMD 和 DAT0-7 信号上都可以支持传输 1 个比特,即 SDR (Single Data Rate) 模式。此外,DAT0-7 信号还支持配置为 DDR (Double Data Rate) 模式,在一个时钟周期内,可以传输 2 个比特。
Host 可以在通讯过程中动态调整时钟信号的频率(注,频率范围需要满足 Spec 的定义)。通过调整时钟频率,可以实现省电或者数据流控(避免 Over-run 或者 Under-run)功能。 在一些场景中,Host 端还可以关闭时钟,例如 eMMC 处于 Busy 状态时,或者接收完数据,进入 Programming State 时。

CMD

CMD 信号主要用于 Host 向 eMMC 发送 Command 和 eMMC 向 Host 发送对于的 Response。Command 和 Response 的细节会在后续章节中介绍。

DAT0-7

DAT0-7 信号主要用于 Host 和 eMMC 之间的数据传输。在 eMMC 上电或者软复位后,只有 DAT0 可以进行数据传输,完成初始化后,可配置 DAT0-3 或者 DAT0-7 进行数据传输,即数据总线可以配置为 4 bits 或者 8 bits 模式。

Data Strobe

Data Strobe 时钟信号由 eMMC 发送给 Host,频率与 CLK 信号相同,用于 Host 端进行数据接收的同步。Data Strobe 信号只能在 HS400 模式下配置启用,启用后可以提高数据传输的稳定性,省去总线 tuning 过程。

NOTE:
Extended CSD byte[183] BUS_WIDTH 寄存器用于配置总线宽度和 Data Strobe

2. eMMC 总线模型

eMMC 总线中,可以有一个 Host,多个 eMMC Devices。总线上的所有通讯都由 Host 端以一个 Command 开发发起,Host 一次只能与一个 eMMC Device 通讯。

系统在上电启动后,Host 会为所有 eMMC Device 逐个分配地址(RCA,Relative device Address)。当 Host 需要和某一个 eMMC Device 通讯时,会先根据 RCA 选中该 eMMC Device,只有被选中的 eMMC Device 才会响应 Host 的 Command。

NOTE:
更详细的工作原理请参考 eMMC 工作模式 章节。

2.1 速率模式

随着 eMMC 协议的版本迭代,eMMC 总线的速率越来越高。为了兼容旧版本的 eMMC Device,所有 Devices 在上电启动或者 Reset 后,都会先进入兼容速率模式(Backward Compatible Mode)。在完成 eMMC Devices 的初始化后,Host 可以通过特定的流程,让 Device 进入其他高速率模式,目前支持以下的几种速率模式。

Mode Data Rate Bus Width Frequency Max Data Transfer (x8)
Backward Compatible Single x1, x4, x8 0-26 MHz 26 MB/s
High Speed SDR Single x1, x4, x8 0-52 MHz 52 MB/s
High Speed DDR Dual x4, x8 0-52 MHz 104 MB/s
HS200 Single x4, x8 0-200 MHz 200 MB/s
HS400 Dual x8 0-200 MHz 400 MB/s

NOTE:
Extended CSD byte[185] HS_TIMING 寄存器可以配置总线速率模式
Extended CSD byte[183] BUS_WIDTH 寄存器用于配置总线宽度和 Data Strobe

2.2 通信模型

Host 与 eMMC Device 之间的通信都是由 Host 以一个 Command 开始发起的,eMMC Device 在完成 Command 所指定的任务后,则返回一个 Response。

2.2.1 Read Data

Host 从 eMMC Device 读取数据的流程如上图所示。

如果 Host 发送的是 Single Block Read 的 Command,那么 eMMC Device 只会发送一个 Block 的数据(一个 Block 的数据的字节数由 Host 设定或者为 eMMC Device 的默认值,更多细节请参考 eMMC 工作模式 章节)。
如果 Host 发送的是 Multiple Block Read 的 Command,那么 eMMC Device 会持续发送数据,直到 Host 主动发送 Stop Command。

NOTE:
从 eMMC Device 读数据都是按 Block 读取的。

2.2.2 Write Data

Host 向 eMMC Device 写入数据的流程如上图所示。

如果 Host 发送的是 Single Block Write Command,那么 eMMC Device 只会将后续第一个 Block 的数据写入的存储器中。
如果 Host 发送的是 Multiple Block Write Command,那么 eMMC Device 会持续地将接收到的数据写入到存储器中,直到 Host 主动发送 Stop Command。

eMMC Device 在接收到一个 Block 的数据后,会进行 CRC 校验,然后将校验结果通过 CRC Token 发送给 Host。
发送完 CRC Token 后,如果 CRC 校验成功,eMMC Device 会将数据写入到内部存储器时,此时 DAT0 信号会拉低,作为 Busy 信号。Host 会持续检测 DAT0 信号,直到为高电平时,才会接着发送下一个 Block 的数据。如果 CRC 校验失败,那么 eMMC Device 不会进行数据写入,此次传输后续的数据都会被忽略。

NOTE:
向 eMMC Device 写数据都是按 Block 写入的。

2.2.3 No Data

在 Host 与 eMMC Device 的通信中,有部分交互是不需要进行数据传输的,还有部分交互甚至不需要 eMMC Device 的回复 Response。

2.2.4 Command

如上图所示,eMMC Command 由 48 Bits 组成,各个 Bits 的解析如下所示:

Description Start Bit Transmission Bit Command Index Argument CRC7 End Bit
Bit position 47 46 [45:40] [39:8] [7:1] 0
Width (bits) 1 1 6 32 7 1
Value "0" "1" x x x "1"

Start Bit 固定为 "0",在没有数据传输的情况下,CMD 信号保持高电平,当 Host 将 Start Bit 发送到总线上时,eMMC Device 可以很方便检测到该信号,并开始接收 Command。

Transmission Bit 固定为 "1",指示了该数据包的传输方向为 Host 发送到 eMMC Device。

Command Index 和 Argument 为 Command 的具体内容,不同的 Command 有不同的 Index,不同的 Command 也有各自的 Argument。 更多的细节,请参考 eMMC Commands 章节。

CRC7 是包含 Start Bit、Transmission Bit、 Command Index 和 Argument 内容的 CRC 校验值。

End Bit 为结束标志位,固定为"1"。

NOTE:
CRC 校验简单来说,是发送方将需要传输的数据“除于”(模2除)一个约定的数,并将得到的余数附在数据上一并发送出去。接收方收到数据后,再做同样的“除法”,然后校验得到余数是否与接收的余数相同。如果不相同,那么意味着数据在传输过程中发生了改变。更多的细节不在本文展开描述,感兴趣的读者可以参考 CRC wiki 中的介绍。

2.2.5 Response

eMMC Response 有两种长度的数据包,分别为 48 Bits 和 136 Bits。

Start Bit 与 Command 一样,固定为 "0",在没有数据传输的情况下,CMD 信号保持高电平,当 eMMC Device 将 Start Bit 发送到总线上时,Host 可以很方便检测到该信号,并开始接收 Response。

Transmission Bit 固定为 "0",指示了该数据包的传输方向为 eMMC Device 发送到 Host。

Content 为 Response 的具体内容,不同的 Command 会有不同的 Content。 更多的细节,请参考 eMMC Responses 章节。

CRC7 是包含 Start Bit、Transmission Bit 和 Content 内容的 CRC 校验值。

End Bit 为结束标志位,固定为"1"。

2.2.6 Data Block

Data Block 由 Start Bit、Data、CRC16 和 End Bit 组成。以下是不同总线宽度和 Data Rate 下,Data Block 详细格式。

1 Bit Bus SDR

CRC 为 Data 的 16 bit CRC 校验值,不包含 Start Bit。

4 Bits Bus SDR

各个 Data Line 上的 CRC 为对应 Data Line 的 Data 的 16 bit CRC 校验值。

8 Bits Bus SDR

各个 Data Line 上的 CRC 为对应 Data Line 的 Data 的16 bit CRC 校验值。

4 Bits Bus DDR

8 Bits Bus DDR

在 DDR 模式下,Data Line 在时钟的上升沿和下降沿都会传输数据,其中上升沿传输数据的奇数字节 (Byte 1,3,5 ...),下降沿则传输数据的偶数字节(Byte 2,4,6 ...)。
此外,在 DDR 模式下,1 个 Data Line 上有两个相互交织的 CRC16,上升沿的 CRC 比特组成 odd CRC16,下降沿的 CRC 比特组成 even CRC16。odd CRC16 用于校验该 Data Line 上所有上升沿比特组成的数据,even CRC16 则用于校验该 Data Line 上所有下降沿比特组成的数据。

NOTE:
DDR 模式下使用两个 CRC16 作为校验,可能是为了更可靠的校验,选用 CRC16 而非 CRC32 则可能是出于兼容性设计的考虑。

2.2.7 CRC Status Token

在写数据传输中,eMMC Device 接收到 Host 发送的一个 Data Block 后,会进行 CRC 校验,如果校验成功,eMMC 会在对应的 Data Line 上向 Host 发回一个 Positive CRC status token (010),如果校验失败,则会在对应的 Data Line 上发送一个 Negative CRC status token (101)。

NOTE:
读数据时,Host 接收到 eMMC Device 发送的 Data Block 后,也会进行 CRC 校验,但是不管校验成功或者失败,都不会向 eMMC Device 发送 CRC Status Token。

详细格式如下图所示:

Positive CRC status token

Negative CRC status token

3. eMMC 总线测试过程

当 eMMC Device 处于 SDR 模式时,Host 可以发送 CMD19 命令,触发总线测试过程(Bus testing procedure),测试总线硬件上的连通性。如果 eMMC Device 支持总线测试,那么 eMMC Device 在接收到 CMD19 后,会发回对应的 Response,接着 eMMC Device 会发送一组固定的测试数据给 Host。Host 接收到数据后,检查数据正确与否,即可得知总线是否正确连通。

NOTE: 如果 eMMC Device 不支持总线测试,那么接收到 CMD19 时,不会发回 Response。
总线测试不支持在 DDR 模式下进行。

测试数据如下所示:

NOTE: 总线宽度为 1 时,只发送 DAT0 上的数据,总线宽度为 4 时,则只发送 DAT0-3 上的数据

4. eMMC 总线 Sampling Tuning

由于芯片制造工艺、PCB 走线、电压、温度等因素的影响,数据信号从 eMMC Device 到达 Host 端的时间是存在差异的,Host 接收数据时采样的时间点也需要相应的进行调整。而 Host 端最佳采样时间点,则是通过 Sampling Tuning 流程得到。

NOTE:
不同 eMMC Device 最佳的采样点可能不同,同一 eMMC Device 在不同的环境下运作时的最佳采样点也可能不同。
在 eMMC 标准中,定义了在 HS200 模式下可以进行 Sampling Tuning。

4.1 Sampling Tuning 流程

Sampling Tuning 是用于计算 Host 最佳采样时间点的流程,大致的流程如下:

  1. Host 将采样时间点重置为默认值
  2. Host 向 eMMC Device 发送 Send Tuning Block 命令
  3. eMMC Device 向 Host 发送固定的 Tuning Block 数据
  4. Host 接收到 Tuning Block 并进行校验
  5. Host 修改采样时点,重新从第 2 步开始执行,直到 Host 获取到一个有效采样时间点区间
  6. Host 取有效采样时间点区间的中间值作为采样时间点,并推出 Tuning 流程

NOTE:
上述流程仅仅是一个示例。Tuning 流程执行的时机、频率和具体的步骤是由 Host 端的 eMMC Controller 具体实现而定的。

4.2 Tuning Block 数据

Tuning Block 是专门为了 Tuning 而设计的一组特殊数据。相对于普通的数据,这组特殊数据在传输过程中,会更高概率的出现 high SSO noise、deterministic jitter、ISI、timing errors 等问题。这组数据的具体内容如下所示:

NOTE: 总线宽度为 1 时,只发送 DAT0 上的数据,总线宽度为 4 时,则只发送 DAT0-3 上的数据

5. 参考资料

  1. Embedded Multi-Media Card (e•MMC) Electrical Standard (5.1) [PDF]
  2. SD/MMC Controller, Hard Processor System (HPS) Technical Reference Manual (TRM) [PDF]
  3. CRC wiki [WEB]

原创文章,转发请注明出处。蜗窝科技



Linux MMC framework(2)_host controller driver

$
0
0

1. 前言

本文是Linux MMC framework的第二篇,将从驱动工程师的角度,介绍MMC host controller driver有关的知识,学习并掌握如何在MMC framework的框架下,编写MMC控制器的驱动程序。同时,通过本篇文章,我们会进一步的理解MMC、SD、SDIO等有关的基础知识。

2. MMC host驱动介绍

MMC的host driver,是用于驱动MMC host控制器的程序,位于“drivers/mmc/host”目录。从大的流程上看,编写一个这样的驱动非常简单,只需要三步:

1)调用mmc_alloc_host,分配一个struct mmc_host类型的变量,用于描述某一个具体的MMC host控制器。

2)根据MMC host控制器的硬件特性,填充struct mmc_host变量的各个字段,例如MMC类型、电压范围、操作函数集等等。

3)调用mmc_add_host接口,将正确填充的MMC host注册到MMC core中。

当然,看着简单,一牵涉到实现细节,还是很麻烦的,后面我们会慢慢分析。

注1:分析MMC host driver的时候,Linux kernel中有大把大把的例子(例如drivers/mmc/host/pxamci.c),大家可尽情参考、学习,不必谦虚(这是学习Linux的最佳方法)。

注2:由于MMC host driver牵涉到具体的硬件controller,分析的过程中需要一些具体的硬件辅助理解,本文将以“X Project”所使用Bubblegum-96平台为例,具体的硬件spec可参考[1]。

3. 主要数据结构

3.1 struct mmc_host

MMC core使用struct mmc_host结构抽象具体的MMC host controller,该结构的定义位于“include/linux/mmc/host.h”中,它既可以用来描述MMC控制器所具有的特性、能力(host driver关心的内容),也保存了host driver运行过程中的一些状态、参数(MMC core关心的内容)。需要host driver关心的部分的具体的介绍如下:

parent,一个struct device类型的指针,指向该MMC host的父设备,一般是注册该host的那个platform设备;

class_dev,一个struct device类型的变量,是该MMC host在设备模型中作为一个“设备”的体现。当然,人如其名,该设备从属于某一个class(mmc_host_class);

ops,一个struct mmc_host_ops类型的指针,保存了该MMC host有关的操作函数集,具体可参考3.2小节的介绍;

pwrseq,一个struct mmc_pwrseq类型的指针,保存了该MMC host电源管理有关的操作函数集,具体可参考3.2小节的介绍;

f_min、f_max、f_init,该MMC host支持的时钟频率范围,最小频率、最大频率以及初始频率;

ocr_avail,该MMC host可支持的操作电压范围(具体可参考include/linux/mmc/host.h中MMC_VDD_开头的定义);
注3:OCR(Operating Conditions Register)是MMC/SD/SDIO卡的一个32-bit的寄存器,其中有些bit指明了该卡的操作电压。MMC host在驱动这些卡的时候,需要和Host自身所支持的电压范围匹配之后,才能正常操作,这就是ocr_avail的存在意义。

ocr_avail_sdio、ocr_avail_sd、ocr_avail_mmc,如果MMC host针对SDIO、SD、MMC等不同类型的卡,所支持的电压范围不同的话,需要通过这几个字段特别指定。否则,不需要赋值(初始化为0);

pm_notify,一个struct notifier_block类型的变量,用于支持power management有关的notify实现;

max_current_330、max_current_300、max_current_180,当工作电压分别是3.3v、3v以及1.8v的时候,所支持的最大操作电流(如果MMC host没有特别的限制,可以不赋值);

caps、caps2,指示该MMC host所支持的功能特性,具体可参考3.4小节的介绍;

pm_caps,mmc_pm_flag_t类型的变量,指示该MMC host所支持的电源管理特性;

max_seg_size、max_segs、max_req_size、max_blk_size、max_blk_count、max_busy_timeout,和块设备(如MMC、SD、eMMC等)有关的参数,在古老的磁盘时代,这些参数比较重要。对基于MMC技术的块设备来说,硬件的性能大大提升,这些参数就没有太大的意义了。具体可参考5.2章节有关MMC数据传输的介绍;

lock,一个spin lock,是MMC host driver的私有变量,可用于保护host driver的临界资源;

ios,一个struct mmc_ios类型的变量,用于保存MMC bus的当前配置,具体可参考3.5小节的介绍;

supply,一个struct mmc_supply类型的变量,用于描述MMC系统中的供电信息,具体可参考3.6小节的介绍;

……

private,一个0长度的数组,可以在mmc_alloc_host时指定长度,由host controller driver自行支配。

3.2 struct mmc_host_ops

struct mmc_host_ops抽象并集合了MMC host controller所有的操作函数集,包括:

1)数据传输有关的函数

/*
* It is optional for the host to implement pre_req and post_req in
* order to support double buffering of requests (prepare one
* request while another request is active).
* pre_req() must always be followed by a post_req().
* To undo a call made to pre_req(), call post_req() with
* a nonzero err condition.
*/
void    (*post_req)(struct mmc_host *host, struct mmc_request *req,
                    int err);
void    (*pre_req)(struct mmc_host *host, struct mmc_request *req,
                   bool is_first_req);
void    (*request)(struct mmc_host *host, struct mmc_request *req);

pre_req和post_req是非必需的,host driver可以利用它们实现诸如双buffer之类的高级功能。

数据传输的主题是struct mmc_request类型的指针,具体可参考3.7小节的介绍。

2)总线参数的配置以及卡状态的获取函数

/*
* Avoid calling these three functions too often or in a "fast path",
* since underlaying controller might implement them in an expensive
* and/or slow way.
*
* Also note that these functions might sleep, so don't call them
* in the atomic contexts!
*
* Return values for the get_ro callback should be:
*   0 for a read/write card
*   1 for a read-only card
*   -ENOSYS when not supported (equal to NULL callback)
*   or a negative errno value when something bad happened
*
* Return values for the get_cd callback should be:
*   0 for a absent card
*   1 for a present card
*   -ENOSYS when not supported (equal to NULL callback)
*   or a negative errno value when something bad happened
*/
void    (*set_ios)(struct mmc_host *host, struct mmc_ios *ios);
int     (*get_ro)(struct mmc_host *host);
int     (*get_cd)(struct mmc_host *host);

set_ios用于设置bus的参数(ios,可参考3.5小节的介绍);get_ro可获取card的读写状态(具体可参考上面的注释);get_cd用于检测卡的存在状态。

注4:注释中特别说明了,这几个函数可以sleep,耗时较长,没事别乱用。

3)其它一些非主流函数,都是optional的,用到的时候再去细看即可。

3.3 struct mmc_pwrseq

MMC framework的power sequence是一个比较有意思的功能,它提供一个名称为struct mmc_pwrseq_ops的操作函数集,集合了power on、power off等操作函数,用于控制MMC系统的供电,如下:

struct mmc_pwrseq_ops {
        void (*pre_power_on)(struct mmc_host *host);
        void (*post_power_on)(struct mmc_host *host);
        void (*power_off)(struct mmc_host *host);
        void (*free)(struct mmc_host *host);
};

struct mmc_pwrseq {
        const struct mmc_pwrseq_ops *ops;
};

与此同时,MMC core提供了一个通用的pwrseq的管理模块(drivers/mmc/core/pwrseq.c),以及一些简单的pwrseq策略(如drivers/mmc/core/pwrseq_simple.c、drivers/mmc/core/pwrseq_emmc.c),最终的目的是,通过一些简单的dts配置,即可正确配置MMC的供电,例如:

/* arch/arm/boot/dts/omap3-igep0020.dts */
mmc2_pwrseq: mmc2_pwrseq {
        compatible = "mmc-pwrseq-simple";
        reset-gpios = <&gpio5 11 GPIO_ACTIVE_LOW>,      /* gpio_139 - RESET_N_W */
                      <&gpio5 10 GPIO_ACTIVE_LOW>;      /* gpio_138 - WIFI_PDN */
};

/* arch/arm/boot/dts/rk3288-veyron.dtsi  */
emmc_pwrseq: emmc-pwrseq {
        compatible = "mmc-pwrseq-emmc";
        pinctrl-0 = <&emmc_reset>;
        pinctrl-names = "default";
        reset-gpios = <&gpio2 9 GPIO_ACTIVE_HIGH>;
};

具体的细节,在需要的时候,阅读代码即可,这里不再赘述。

3.4 Host capabilities

通过的caps和caps2两个字段,MMC host driver可以告诉MMC core控制器的一些特性、能力,包括(具体可参考include/linux/mmc/host.h中相关的宏定义,更为细致的使用场景和指南,需要结合实际的硬件,具体分析):

MMC_CAP_4_BIT_DATA,支持4-bit的总线传输;
MMC_CAP_MMC_HIGHSPEED,支持“高速MMC时序”;
MMC_CAP_SD_HIGHSPEED,支持“高速SD时序”;
MMC_CAP_SDIO_IRQ,可以产生SDIO有关的中断;
MMC_CAP_SPI,仅仅支持SPI协议(可参考“drivers/mmc/host/mmc_spi.c”中有关的实现);
MMC_CAP_NEEDS_POLL,表明需要不停的查询卡的插入状态(如果需要支持热拔插的卡,则需要设置该feature);
MMC_CAP_8_BIT_DATA,支持8-bit的总线传输;
MMC_CAP_AGGRESSIVE_PM,支持比较积极的电源管理策略(kernel的注释为“Suspend (e)MMC/SD at idle”);
MMC_CAP_NONREMOVABLE,表明该MMC控制器所连接的卡是不可拆卸的,例如eMMC;
MMC_CAP_WAIT_WHILE_BUSY,表面Host controller在向卡发送命令时,如果卡处于busy状态,是否等待。如果不等待,MMC core在一些流程中(如查询busy状态),需要额外做一些处理;
MMC_CAP_ERASE,表明该MMC控制器允许执行擦除命令;
MMC_CAP_1_8V_DDR,支持工作电压为1.8v的DDR(Double Data Rate)mode[3]
MMC_CAP_1_2V_DDR,支持工作电压为1.2v的DDR(Double Data Rate)mode[3]
MMC_CAP_POWER_OFF_CARD,可以在启动之后,关闭卡的供电(一般只有在SDIO的应用场景中才可能用到,因为SDIO所连接的设备可能是一个独立的设备);
MMC_CAP_BUS_WIDTH_TEST,支持通过CMD14和CMD19进行总线宽度的测试,以便选择一个合适的总线宽度进行通信;
MMC_CAP_UHS_SDR12、MMC_CAP_UHS_SDR25、MMC_CAP_UHS_SDR50、MMC_CAP_UHS_SDR104,它们是SD3.0的速率模式,分别表示支持25MHz、50MHz、100MHz和208MHz的SDR(Single Data Rate,相对[3]中的DDR)模式;
MMC_CAP_UHS_DDR50,它也是SD3.0的速率模式,表示支持50MHz的DDR(Double Data Rate[3])模式;
MMC_CAP_DRIVER_TYPE_A、MMC_CAP_DRIVER_TYPE_C、MMC_CAP_DRIVER_TYPE_D,分别表示支持A/C/D类型的driver strength(驱动能力,具体可参考相应的spec);
MMC_CAP_CMD23,表示该controller支持multiblock transfers(通过CMD23);
MMC_CAP_HW_RESET,支持硬件reset;

MMC_CAP2_FULL_PWR_CYCLE,表示该controller支持从power off到power on的完整的power cycle;
MMC_CAP2_HS200_1_8V_SDR、MMC_CAP2_HS200_1_2V_SDR,HS200是eMMC5.0支持的一种速率模式,200是200MHz的缩写,分别表示支持1.8v和1.2v的SDR模式;
MMC_CAP2_HS200,表示同时支持MMC_CAP2_HS200_1_8V_SDR和MMC_CAP2_HS200_1_2V_SDR;
MMC_CAP2_HC_ERASE_SZ,支持High-capacity erase size;
MMC_CAP2_CD_ACTIVE_HIGH,CD(Card-detect)信号高有效;
MMC_CAP2_RO_ACTIVE_HIGH,RO(Write-protect)信号高有效;
MMC_CAP2_PACKED_RD、MMC_CAP2_PACKED_WR,允许packed read、packed write;
MMC_CAP2_PACKED_CMD,同时支持DMMC_CAP2_PACKED_RD和MMC_CAP2_PACKED_WR;
MMC_CAP2_NO_PRESCAN_POWERUP,在scan之前不要上电;
MMC_CAP2_HS400_1_8V、MMC_CAP2_HS400_1_2V,HS400是eMMC5.0支持的一种速率模式,400是400MHz的缩写,分别表示支持1.8v和1.2v的HS400模式;
MMC_CAP2_HS400,同时支持MMC_CAP2_HS400_1_8V和MMC_CAP2_HS400_1_2V;
MMC_CAP2_HSX00_1_2V,同时支持MMC_CAP2_HS200_1_2V_SDR和MMC_CAP2_HS400_1_2V;
MMC_CAP2_SDIO_IRQ_NOTHREAD,SDIO的IRQ的处理函数,不能在线程里面执行;
MMC_CAP2_NO_WRITE_PROTECT,没有物理的RO管脚,意味着任何时候都是可以读写的;
MMC_CAP2_NO_SDIO,在初始化的时候,不会发送SDIO相关的命令(也就是说不支持SDIO模式)。

3.5 struct mmc_ios

struct mmc_ios中保存了MMC总线当前的配置情况,包括如下信息:

1)clock,时钟频率。

2)vdd,卡的供电电压,通过“1 << vdd”可以得到MMC_VDD_x_x(具体可参考include/linux/mmc/host.h中MMC_VDD_开头的定义),进而得到电压信息。

3)bus_mode,两种信号模式,open-drain(MMC_BUSMODE_OPENDRAIN)和push-pull(MMC_BUSMODE_PUSHPULL),对应不同的高低电平(可参考相应的spec,例如[2])。

4)chip_select,只针对SPI模式,指定片选信号的有效模式,包括没有片选信号(MMC_CS_DONTCARE)、高电平有效(MMC_CS_HIGH)、低电平有效(MMC_CS_LOW)。

5)power_mode,当前的电源状态,包括MMC_POWER_OFF、MMC_POWER_UP、MMC_POWER_ON和MMC_POWER_UNDEFINED。

6)bus_width,总线的宽度,包括1-bit(MMC_BUS_WIDTH_1)、4-bit(MMC_BUS_WIDTH_4)和8-bit(MMC_BUS_WIDTH_8)。

7)timing,符合哪一种总线时序(大多对应某一类MMC规范),包括:

MMC_TIMING_LEGACY,旧的、不再使用的规范;
MMC_TIMING_MMC_HS,High speed MMC规范(具体可参考相应的spec,这里不再详细介绍,下同);
MMC_TIMING_SD_HS,High speed SD;
MMC_TIMING_UHS_SDR12;
MMC_TIMING_UHS_SDR25
MMC_TIMING_UHS_SDR50
MMC_TIMING_UHS_SDR104
MMC_TIMING_UHS_DDR50
MMC_TIMING_MMC_DDR52
MMC_TIMING_MMC_HS200
MMC_TIMING_MMC_HS400

8)signal_voltage,总线信号使用哪一种电压,3.3v(MMC_SIGNAL_VOLTAGE_330)、1.8v(MMC_SIGNAL_VOLTAGE_180)或者1.2v(MMC_SIGNAL_VOLTAGE_120)。

9)drv_type,驱动能力,包括:

MMC_SET_DRIVER_TYPE_B
MMC_SET_DRIVER_TYPE_A
MMC_SET_DRIVER_TYPE_C
MMC_SET_DRIVER_TYPE_D

3.6 struct mmc_supply

struct mmc_supply中保存了两个struct regulator指针(如下),用于控制MMC子系统有关的供电(vmmc和vqmmc)。

struct mmc_supply {
        struct regulator *vmmc;         /* Card power supply */
        struct regulator *vqmmc;        /* Optional Vccq supply */
};

关于vmmc和vqmmc,说明如下:

vmmc是卡的供电电压,一般连接到卡的VDD管脚上。而vqmmc则用于上拉信号线(CMD、CLK和DATA[6])。

通常情况下vqmmc使用和vmmc相同的regulator,同时供电即可。

后来,一些高速卡(例如UHS SD)要求在高速模式下,vmmc为3.3v,vqmmc为1.8v,这就需要两个不同的regulator独立控制。

3.7 struct mmc_request

struct mmc_request封装了一次传输请求,定义如下:

/* include/linux/mmc/core.h */

struct mmc_request {
        struct mmc_command      *sbc;           /* SET_BLOCK_COUNT for multiblock */
        struct mmc_command      *cmd;
        struct mmc_data         *data;
        struct mmc_command      *stop;

        struct completion       completion;
        void                    (*done)(struct mmc_request *);/* completion function */
        struct mmc_host         *host;
};

要理解这个数据结构,需要先了解MMC的总线协议(bus protocol),这里以eMMC[2]为例进行简单的介绍(更为详细的解释,可参考相应的spec以及本站的文章--“eMMC 原理 4 :总线协议[7]”)。

3.7.1 MMC bus protocol

在eMMC的spec中,称总线协议为“message-based MultiMediaCard bus protocol”,这里的message由三种信标(token)组成:

Command,用于启动(或者停止)一次传输,由Host通过CMD line向Card发送;

Response,用于应答上一次的Command,由Card通过CMD line想Host发送;

Data,传输数据,由Host(或者Card)通过DATA lines向Card(或者Host发送)。

以上token除了Command之外,剩余的两个(Response和Data)都是非必需的,也就是说,一次传输可以是:不需要应答、不需要数据传输的Command;需要应答、不需要数据传输的Command;不需要应答、需要数据传输的Command;不需要应答、不需要数据传输的Command。

Command token的格式只有一种(具体可参考[2]中“Command token format”有关的表述),长度为48bits,包括Start bit(0)、Transmitter bit(1, host command)、Content(38bits)、CRC checksum(7bits)、Stop bit(1)。

根据内容的不同,Response token的格式有5中,分别简称为R1/R3/R4/R5/R2,其中R1/R3/R4/R5的长度为48bits,R2为136bits(具体可参考[2]中“Response token format”有关的表述)。

对于包含了Data token的Command,有两种类型:

Sequential commands,发送Start command之后,数据以stream的形式传输,直到Stop command为止。这种方式只支持1-bit总线模式,主要为了兼容旧的技术,一般不使用;

Block-oriented commands,发送Start command之后,数据以block的形式传输(每个block的大小是固定的,且都由CRC保护)。

最后,以block为单位的传输,大体上也分为两类:

在传输开始的时候(Start command),没有指定需要传输的block数目,直到发送Stop command为止。这种方法在spec中称作“Open-ended”;

在传输开始的时候(Start command),指定需要传输的block数据,当达到数据之后,Card会自动停止传输,这样可以省略Stop command。这种方法在spec中称作pre-defined block count。

3.7.2 struct mmc_request

了解MMC bus protocol之后,再来看一次MMC传输请求(struct mmc_request )所包含的内容:

cmd,Start command,为struct mmc_command类型(具体请参考3.7.3中的介绍)的指针,在一次传输的过程中是必须的;

data,传输的数据,为struct mmc_data类型(具体请参考3.7.4中的介绍)的指针,不是必须要的;

stop、sbc,如果需要进行数据传输,根据数据传输的方式(参考3.7.1中的介绍):如果是“Open-ended”,则需要stop命令(stop指针,或者data->stop指针);如果是pre-defined block count,则需要sbc指针(用于发送SET_BLOCK_COUNT--CMD23命令);

completion,一个struct completion变量,用于等待此次传输完成,host controller driver可以根据需要使用;

done,传输完成时的回调,用于通知传输请求的发起者;

host,对应的mmc host controller指针。

3.7.3 struct mmc_command

struct mmc_command结构抽象了一个MMC command,包括如下内容:

/* include/linux/mmc/core.h */

opcode,Command的操作码,用于标识该命令是哪一个命令,具体可参考相应的spec(例如[2]);

arg,一个Command可能会携带参数,具体可参考相应的spec(例如[2]);

resp[4],Command发出后,如果需要应答,结果保存在resp数组中,该数组是32-bit的,因此最多可以保存128bits的应答;

flags,是一个bitmap,保存该命令所期望的应答类型,例如:
MMC_RSP_PRESENT(1 << 0),是否需要应答,如果该bit为0,则表示该命令不需要应答,否则,需要应答;
MMC_RSP_136(1 << 1),如果为1,表示需要136bits的应答;
MMC_RSP_CRC(1 << 2),如果为1,表示需要对该命令进行CRC校验;
等等,具体可参考include/linux/mmc/core.h中“MMC_RSP_”开头的定义;

retries,如果命令发送出错,可以重新发送,该字段向host driver指明最多可重发的次数;

error,如果最终还是出错,host driver需要通过该字段返回错误的原因,kernel定义了一些标准错误,例如ETIMEDOUT、EILSEQ、EINVAL、ENOMEDIUM等,具体含义可参考include/linux/mmc/core.h中注释;

busy_timeout,如果card具有busy检测的功能,该字段指定等待card返回busy状态的超时时间,单位为ms;

data,和该命令对应的struct mmc_data指针;

mrq,和该命令对应的struct mmc_request指针。

3.7.4 struct mmc_data

struct mmc_data结构包含了数据传输有关的内容:

/* include/linux/mmc/core.h */

timeout_ns、timeout_clks,这一笔数据传输的超时时间(单位分别为ns和clks),如果超过这个时间host driver还无法成功发送,则要将状态返回给mmc core;

blksz、blocks,该笔数据包含多少block(blocks),每个block的size多大(blksz),这两个值不会大于struct mmc_host中上报的max_blk_size和max_blk_count;

error,如果数据传输出错,错误值保存在该字段,具体意义和struct mmc_command中的一致;

flags,一个bitmap,指明该笔传说的方向(MMC_DATA_WRITE或者MMC_DATA_READ);

sg,一个struct scatterlist类型的数组,保存了需要传输的数据(可以通过dma_相关的接口,获得相应的物理地址);
sg_len,sg数组的size;
sg_count,通过sg map出来的实际的entry的个数(可能由于物理地址的连续、IOMMU的干涉等,map出来的entry的个数,可能会小于sg的size);
注5:有关scatterlist的介绍,可参考本站另外的文章(TODO)。有关struct mmc_data的使用场景,可参考5.2小节的介绍

host_cookie,host driver的私有数据,怎么用由host driver自行决定。

4. 主要API

第3章花了很大的篇幅介绍了用于抽象MMC host的数据结构----struct mmc_host,并详细说明了和mmc_host相关的mmc request、mmc command、mmc data等结构。基于这些知识,本章将介绍MMC core提供的和struct mmc_host有关的操作函数,主要包括如下几类。

4.1 向MMC host controller driver提供的用于操作struct mmc_host的API

包括:

struct mmc_host *mmc_alloc_host(int extra, struct device *);
int mmc_add_host(struct mmc_host *);
void mmc_remove_host(struct mmc_host *);
void mmc_free_host(struct mmc_host *);
int mmc_of_parse(struct mmc_host *host);
static inline void *mmc_priv(struct mmc_host *host) {
        return (void *)host->private;
}

mmc_alloc_host,动态分配一个struct mmc_host变量。extra是私有数据的大小,可通过host->private指针访问(也可通过mmc_priv接口直接获取)。mmc_free_host执行相反动作。

mmc_add_host,将已初始化好的host变量注册到kernel中。mmc_remove_host执行相反动作。

为了方便,host controller driver可以在dts中定义host的各种特性,然后在代码中调用mmc_of_parse解析并填充到struct mmc_host变量中。dts属性关键字可参考mmc_of_parse的source code(drivers/mmc/core/host.c),并结合第三章的内容自行理解。

int mmc_power_save_host(struct mmc_host *host); 
int mmc_power_restore_host(struct mmc_host *host);

从mmc host的角度进行电源管理,进入/退出power save状态。

void mmc_detect_change(struct mmc_host *, unsigned long delay);

当host driver检测到总线上的设备有变动的话(例如卡的插入和拔出等),需要调用这个接口,让MMC core帮忙做后续的工作,例如检测新插入的卡到底是个什么东东……

另外,可以通过delay参数告诉MMC core延时多久(单位为jiffies)开始处理,通常可以用来对卡的拔插进行去抖动。

void mmc_request_done(struct mmc_host *, struct mmc_request *);

当host driver处理完成一个mmc request之后,需要调用该函数通知MMC core,MMC core会进行一些善后的操作,例如校验结果、调用mmc request的.done回调等等。

static inline void mmc_signal_sdio_irq(struct mmc_host *host)
void sdio_run_irqs(struct mmc_host *host);

对于SDIO类型的总线,这两个函数用于操作SDIO irqs,后面用到的时候再分析。

int mmc_regulator_get_ocrmask(struct regulator *supply);
int mmc_regulator_set_ocr(struct mmc_host *mmc,
                        struct regulator *supply,
                        unsigned short vdd_bit);
int mmc_regulator_set_vqmmc(struct mmc_host *mmc, struct mmc_ios *ios);
int mmc_regulator_get_supply(struct mmc_host *mmc);

regulator有关的辅助函数:

mmc_regulator_get_ocrmask可根据传入的regulator指针,获取该regulator支持的所有电压值,并以此推导出对应的ocr mask(可参考3.1中的介绍)。

mmc_regulator_set_ocr用于设置host controller为某一个操作电压(vdd_bit),该接口会调用regulator framework的API,进行具体的电压切换。

mmc_regulator_set_vqmmc可根据struct mmc_ios信息,自行调用regulator framework的接口,设置vqmmc的电压。

最后,mmc_regulator_get_supply可以帮忙从dts的vmmc、vqmmc属性值中,解析出对应的regulator指针,以便后面使用。

4.2 用于判断MMC host controller所具备的能力的API

比较简单,可结合第3章的介绍理解:

#define mmc_host_is_spi(host)   ((host)->caps & MMC_CAP_SPI)
static inline int mmc_card_is_removable(struct mmc_host *host)
static inline int mmc_card_keep_power(struct mmc_host *host)
static inline int mmc_card_wake_sdio_irq(struct mmc_host *host)
static inline int mmc_host_cmd23(struct mmc_host *host)
static inline int mmc_boot_partition_access(struct mmc_host *host)
static inline int mmc_host_uhs(struct mmc_host *host)
static inline int mmc_host_packed_wr(struct mmc_host *host)
static inline int mmc_card_hs(struct mmc_card *card)
static inline int mmc_card_uhs(struct mmc_card *card)
static inline bool mmc_card_hs200(struct mmc_card *card)
static inline bool mmc_card_ddr52(struct mmc_card *card)
static inline bool mmc_card_hs400(struct mmc_card *card)
static inline void mmc_retune_needed(struct mmc_host *host)
static inline void mmc_retune_recheck(struct mmc_host *host)

5. MMC host驱动的编写步骤

经过上面章节的描述,相信大家对MMC controller driver有了比较深的理解,接下来驱动的编写就是一件水到渠成的事情了。这里简要描述一下驱动编写步骤,也顺便为本文做一个总结。

5.1 struct mmc_host的填充和注册

编写MMC host驱动的所有工作,都是围绕struct mmc_host结构展开的。在对应的platform driver的probe函数中,通过mmc_alloc_host分配一个mmc host后,我们需要根据controller的实际情况,填充对应的字段。

mmc host中大部分和controller能力/特性有关的字段,可以通过dts配置(然后在代码中调用mmc_of_parse自动解析并填充),举例如下(注意其中红色的部分,都是MMC framework的标准字段):

/* arch/arm/boot/dts/exynos5420-peach-pit.dts */

&mmc_1 {
        status = "okay";
        num-slots = <1>;
        non-removable;
        cap-sdio-irq;
        keep-power-in-suspend;
        clock-frequency = <400000000>;
        samsung,dw-mshc-ciu-div = <1>;
        samsung,dw-mshc-sdr-timing = <0 1>;
        samsung,dw-mshc-ddr-timing = <0 2>;
        pinctrl-names = "default";
        pinctrl-0 = <&sd1_clk>, <&sd1_cmd>, <&sd1_int>, <&sd1_bus1>,
                    <&sd1_bus4>, <&sd1_bus8>, <&wifi_en>;
        bus-width = <4>;
        cap-sd-highspeed;
        mmc-pwrseq = <&mmc1_pwrseq>;
        vqmmc-supply = <&buck10_reg>;
};

5.2 数据传输的实现

填充struct mmc_host变量的过程中,工作量最大的,就是对struct mmc_host_ops的实现(毫无疑问!所有MMC host的操作逻辑都封在这里呢!!)。这里简单介绍一下相关的概念,具体的驱动编写步骤,后面文章会结合“X Project”详细描述。

5.2.1 Sectors(扇区)、Blocks(块)以及Segments(段)的理解

我们在3.1小节介绍struct mmc_host的时候,提到了max_seg_size、max_segs、max_req_size、max_blk_size、max_blk_count等参数。这和磁盘设备(块设备)中Sectors、Blocks、Segments等概念有关,下面简单介绍一下:

1)Sectors

Sectors是存储设备访问的基本单位。

对磁盘、NAND等块设备来说,Sector的size是固定的,例如512、2048等。

对存储类的MMC设备来说,按理说也应有固定size的sector。但因为有MMC协议的封装,host驱动以及上面的块设备驱动,不需要关注物理的size。它们需要关注的就是bus上的数据传输单位(具体可参考MMC protocol的介绍[7])。

最后,对那些非存储类的MMC设备来说,完全没有sector的概念了。

2) Blocks

Blocks是数据传输的基本单位,是VFS(虚拟文件系统)抽象出来的概念,是纯软件的概念,和硬件无关。它必须是2的整数倍、不能大于Sectors的单位、不能大于page的长度,一般为512B、2048B或者4096B。

对MMC设备来说,由于协议的封装,淡化了Sector的概念,或者说,MMC设备可以支持一定范围内的任意的Block size。Block size的范围,由两个因素决定:
a)host controller的能力,这反映在struct mmc_host结构的max_blk_size字段上。
b)卡的能力,这可以通过MMC command从卡的CSD(Card-Specific Data)寄存器中读出。

3)Segments[8]

块设备的数据传输,本质上是设备上相邻扇区内存之间的数据传输。通常情况下,为了提升性能,数据传输通过DMA方式。

在磁盘控制器的旧时代,DMA操作都比较简单,每次传输,数据在内存中必须是连续的。现在则不同,很多SOC都支持“分散/聚合”(scatter-gather)DMA操作,这种操作模式下,数据传输可以在多个非连续的内存区域中进行。

对于每个“分散/聚合”DMA操作,块设备驱动需要向控制器发送:
a)初始扇区号和传输的总共扇区数
b)内存区域的描述链表,每个描述都包含一个地址和长度。不同的描述之间,可以在物理上连续,也可以不连续。

控制器来管理整个数据传输,例如:在读操作中,控制器从块设备相邻的扇区上读取数据,然后将数据分散存储在内存的不同区域。

这里的每个内存区域描述(物理连续的一段内存,可以是一个page,也可以是page的一部分),就称作Segment。一个Segment包含多个相邻扇区。

最后,利用“分散/聚合”的DMA操作,一次数据传输可以会涉及多个segments。

理解了Segment的概念之后,max_seg_size和max_segs两个字段就好理解了:

虽然控制器支持“分散/聚合”的DMA操作,但物理硬件总有限制,例如最大的Segment size(也即一个内存描述的最大长度),最多支持的segment个数(max_segs)等。

5.2.2 struct mmc_data中的sg

我们在3.7.4小节介绍struct mmc_data时,提到了scatterlist的概念。结合上面Segment的解释,就很好理解了:

MMC core提交给MMC host driver的数据传输请求,是一个struct scatterlist链表(也即内存区域的描述链表),也可以理解为是一个个的Segment(Segment的个数保存在sg_len变量中了)。

每个Segment是一段物理地址连续的内存区域,所有的Segments对应了MMC设备中连续的Sector(或者说Block,初始扇区号和传输的总共扇区数已经在之前的MMC command中指定了。

host driver在接收到这样的数据传输请求之后,需要调用dma_map_sg将这些Segment映射出来(获得相应的物理地址),以便交给MMC controller传输。

当然,相邻两个Segment的物理地址可能是连续(或者其它原因),map的时候可能会将两个Segment合成一个。因此可供MMC controller传输的内存片可能少于sg_len(具体要看dma_map_sg的返回值,可将结果保存在sg_count中)。

最后,如何实施传输,则要看具体的MMC controller的硬件实现(可能涉及DMA操作),后面文章再详细介绍。

6. 参考文档

[1] SoC_bubblegum96.pdf

[2] JESD84-A44.pdf

[3] DDR mode, https://en.wikipedia.org/wiki/Double_data_rate

[4] http://www.hjreggel.net/cardspeed/cs_sdxc.html

[5] regulator framework,http://www.wowotech.net/tag/regulator

[6] MMC/SD/SDIO介绍

[7] eMMC 原理 4 :总线协议

[8] http://www.ilinuxkernel.com/files/Linux.Generic.Block.Layer.pdf

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

Linux调度器:用户空间接口

$
0
0

一、前言
Linux调度器神秘而充满诱惑,每个Linux工程师都想深入其内部一探究竟。不过中国有一句古话叫做“相由心生”,一个模块精巧的内部逻辑(也就是所谓的“心”)其外延就是简洁而优雅的接口(我称之为“相”)。通过外部接口的定义,其实我们也可以收获百分之六七十的该模块的内部信息。因此,本文主要描述Linux调度器开放给用户空间的接口,希望可以通过用户空间的调度器接口来理解Linux调度器的行为。

 

二、nice函数

nice函数用来修改调用进程的nice value,其接口定义如下:

      #include <unistd.h>
       int nice(int inc);

为了方便说明该接口的作用,我们还是举实际的例子说明。程序调用nice(3),则将当前进程的nice value增加3,这也就是意味着该进程的优先级降低3个level(提升nice value也就是对别人更加nice,自己的优先级就会低)。如果程序调用nice(-5),则将当前进程的nice value减去5,这也就是意味着该进程的优先级提升5个level。当调用错误的时候返回-1,调用成功会稍微有一些歧义。POSIX标准规定了nice函数返回新的nice value,但是linux的系统调用和c库都是采用了操作成功返回0的方式。这样的处理方式使得在调用nice函数的时候无法得到当前的优先级,如果想要得到当前优先级,需要调用getpriority函数,我们在下一小节描述。

虽然说nice函数是用来调整优先级,实际上调整nice value就是调整调度器分配给该进程的CPU时间,具体是如何影响cpu time的呢?我们在后面描述内核代码的时候再详聊。此外,需要注意的是:根据POSIX标准,nice value是一个per process的设定,但是在linux中,nice value没有遵从这个标准,它是per-thread的一个属性。

 

三、getpriority/setpriority函数

从上节的描述中,我们了解到了nice的函数的限制,例如只能修改自己的nice value,无法获取当前的nice value值等,为此我们给出加强版本的nice接口,也就是getpriority/setpriority函数了。getpriority/setpriority函数定义如下:

#include <sys/time.h>
#include  <sys/resource.h>

int getpriority(int which, int who);
int setpriority(int which, int who, int prio);

你说接口增加功能是好事,怎么就把名字也改了呢?为何不是getnice/setnice呢?其实从上节的描述也看出稍许端倪,我们并没有区分调度优先级和nice value这两个值,历史上,首先被使用的是nice value,很快大家觉得这个词不是那么好理解,特别是对于初学者,因此改成优先级(priority)这样的名词可以让用户更好的理解这个API的作用,当然,事实证明这个改动并不是非常理想,我们后面会描述。

getpriority/setpriority功能比较强大,能处理多种请求,不同的请求通过which和who这两个参数来制定。当which等于PRIO_PROCESS的时候,who需要传入一个process id的参数,getpriority将返回指定进程的nice value。当which等于PRIO_PGRP的时候,who需要传入一个process group id的参数,此时getpriority将返回指定进程组中优先级最高的那个(BTW,nice value是最小的)。当which等于PRIO_USER的时候,who需要user id的信息,这时候,getpriority将返回属于该user的所有进程中nice value最小的那个。who等于0说明要get或者set的对象是当前进程(或者当前进程组,或者当前的user)。

setpriority类似与nice,当然功能要强那么一点点,因为它可以接收PRIO_PROCESS,PRIO_PGRP或者PRIO_USER参数用来设定一组进程的nice value。setpriority的返回值和其他函数类似,0表示成功,-1表示操作失败,不过getpriority就稍微有一点绕了。作为linux程序员,我们都知道的nice value是[-20, 19],如果getpriority返回这个范围,那么这里的-1优先级就有点尴尬了,因为一般的linux c库接口函数返回-1表示调用错误,我们是如何区分-1调用错误的返回还是优先级-1的返回值呢?getpriority是少数返回-1也是有可能正确的接口函数:在调用getpriority之前,我们需要首先将errno清零,调用getpriority之后,如果返回-1,我们需要看看errno是否还是保持0值,如果是,那么说明返回的是优先级-1,否则说明发生了错误。

 

四、操作rt priority的接口

传统的类unix内核,调度器是采用round-robin time-sharing的算法:如果有若干个进程是runnable的,那么不着急,大家排排队、吃果果,每个进程分配一个cpu时间片,大家轮流按照分配的时间片来获取cpu资源,所有的时间片用完,那么就重新一轮的分配。在这样的模型下面,间接影响cpu时间片的nice接口函数就够用了。当然,分配了更多的时间片也就是意味着有更高的优先级,因此nice vlaue也被称为进程的优先级。

但是,新的需求层出不穷(人类的欲望是无穷D),特别是实时性方面的需求,因此,POSIX标准(2008版本)增加了实时调度的内容,并且提供了POSIX realtime scheduling API来让用户空间来修改调度策略和调度优先级。这下子有点尴尬了,原来的nice value大家已经习惯称之为进程优先级了,现在真正的进程优先级登场了,怎么区分?为了解决这个问题,我们引入一个新的名词叫做调度策略(scheduling policy)。调度器在运作的时候往往设定一组规则来决定何时,选择哪一个进程进入执行状态,执行多长的时间。那些“规则”就是调度策略。

好的调度策略依赖于对进程的分类,有一类进程是大家都灰常的熟悉了就是普通进程,使用时间片轮转算法的那些进程。当然这类进程还可以细分,例如运算密集型进程(SCHED_BATCH,调度器最好不要太经常的唤醒这种进程),例如idle类进程(SCHED_IDLE),idle类进程优先级非常低,也就是说如果系统有其他事情要处理就去干别的事情(调度其他进程执行),实在没有活干了,再考虑IDLE类型的进程。不论哪一种普通进程,其优先级使用nice value这样一个调度参数来描述就OK了。

除了普通进程,还有一类是严格按照优先级来调度的进程,如果熟悉RTOS的话,对priority-base的调度器应该不会陌生,官大一级压死人,只要优先级高的进程是runnable的,那么优先级低的进程是根本没有机会执行的。这里的优先级才是真正意义的优先级,但是nice value已经被称为进程优先级了,因此这里的优先级被叫做rt priority。rt进程的调度又被细分成两类:SCHED_FIFO和SCHED_RR。这两种调度策略在相同rt priority的时候稍有差别,SCHED_FIFO是谁先到谁先获取cpu资源,并且一直占用,直到主动让出cpu或者退出,相同rt priority的进程才有机会执行。SCHED_RR稍微人性化了一点,相同rt priority的进程有时间片,大家轮流执行。对于实时进程而言,rt priority这个调度参数就描述了全部。

介绍到这里,是时候总结一下了:进程优先级有两个范围,一个是nice value,用前两个小节的API来set或者get。另外一个优先级是rt priority,完全碾压nice value这种优先级,操作rt priority的接口就在这一小节描述。

OK,经过漫长的铺垫过程,我们终于可以介绍realtime process scheduling API了,具体API定义如下:

#include <sched.h>

int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);

int sched_getscheduler(pid_t pid);

int sched_get_priority_max(int policy);--返回指定policy的最大的rt priority
int sched_get_priority_min(int policy);--返回指定policy的最小的rt priority

int sched_setparam(pid_t pid, const struct sched_param *param);
int sched_getparam(pid_t pid, struct sched_param *param);

sched_get_priority_max和sched_get_priority_min分别返回了指定调度策略的最大和最小的rt priority,不同的操作系统实现不同的优先级数量。在linux中,实时进程(SCHED_FIFO和SCHED_RR)的rt priority共计99个level,最小是1,最大是99。对于其他的调度策略,这些函数返回0。

sched_getscheduler函数可以获取指定进程的scheduling policy(如果pid等于0,那么是获取调用进程的调度策略)。sched_setscheduler函数是用来设定指定进程的scheduling policy,对于实时进程,该接口函数还可以设定rt priority。如果设定进程的调度策略是非实时的调度策略的时候(例如SCHED_NORMAL),那么param参数是没有意义的,其sched_priority成员必须设定为0。sched_setparam/sched_getparam非常简单,大家自己看man page好了。

 

五、一统江湖的接口

看起来前面小节描述的API已经够用了,然而,故事并未结束。经过前面关于调度接口的讨论,基本上我们对调度器的行为也已经有了了解:调度器就是按照优先级(指rt priority)来工作,优先级高的永远是优先调度。范围落在[1,99]的rt priority是实时进程,而rt priority等于0的是普通进程。对于普通进程,调度器还要根据nice value(这个也曾经被称为优先级,不要和rt priority弄混了)来进行调整。用户空间的进程可以通过各种前面描述的接口API来修改调度策略、nice value以及rt priority。一切看上去已经完美,CFS类型的调度器处理普通的运算密集形(例如编译内核)和用户交互形的应用(例如vi编辑文件)。如果有应用有实时需求,可以考虑让rt类型的调度器来运筹帷幄。但是,如何混合了一些realtime的应用以及有一些timing要求的应用的时候,SCHED_FIFO和SCHED_RR并不能解决问题,因为在这种调度策略下,高优先级的任务会永远的delay低优先级的任务,如果低优先级的任务有一些timing的需求,这时候,你根本控制不了调度延迟时间。

为了解决上一节中描述的问题,一类新的进程被定义出来,这类进程的优先级比实时进程和普通进程的优先级都要高,这类进行有自己的特点,参考下图:

deadline

这类进程的特点就是每隔固定的周期都会起来干活,需要一定的时间来处理事务。这类进程很牛,一上来就告诉调度器,我可是有点脾气的进程,和其他的那些妖艳的进程不一样的,我每隔一段时间(period)你就得固定分配给我一定的cpu资源(computer time),当然,分配的cpu time必须在该周期内执行完毕,因此就有deadline的概念。为了应对这种需求,3.14内核引入了一类新的进程叫做deadline进程,这类进程的调度策略是SCHED_DEADLINE。调度器对这类进程也会高看一眼,每当一个周期的开始时间到来的时候(也就是该deadline进程被唤醒的时间),调度器要优先处理这个deadline进程对cpu timer的需求,并且在某个指定的deadline时间内调度该进程执行。执行了指定的cpu time后,可以考虑调度走该进行,不过,当下一个周期到来的时候,调度器仍然要奋不顾身的在deadline时间内,再次调度该deadline进程执行。

虽然deadline进程优先级高于其他两类进程,但是用“优先级”来描述这类进程当然是不合理的,应该使用下面的三个参数来描述:

(1)周期时间(上图中的period)

(2)deadline时间(上图中的relative deadline)

(3)一次调度周期内分配多少的cpu时间(上图中的comp. time)

至此,估计您也已经发现,前面描述的接口其实都是不适合设定这些参数的,因此,GNU/linux操作系统中增加了下面的接口API:

#include <sched.h>

int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags);
int sched_getattr(pid_t pid, const struct sched_attr *attr, unsigned int size, unsigned int flags);

attr这个参数的数据类型是struct sched_attr,这个数据结构囊括了一切你想要的关于调度的控制参数:policy,nice value,rt priority,period,deadline等等。用这个接口可以完成所有前面几个小节描述API能完成的任务,唯一的不好的地方就是这个接口是linux特有的,不是posix标准,是否应用这个接口就是见仁见智了。更细节的知识这里就不描述了,大家还是参考man page好了。

 

六、其他

上面描述的接口API都是和调度器参数相关,其实Linux调度器还有两类接口。一个是sched_getaffinity和sched_setaffinity,用于操作一个线程的CPU affinity。另外一个接口是sched_yield,该接口可以让出CPU资源,让Linux调度器选择一个合适的线程执行。这些接口很简单,大家仔细学习就OK了。

 

参考文档:

1、POSIX标准2008

2、linux下的各种man page

3、linux 4.4.6内核源代码

 

原创文章,转发请注明出处。蜗窝科技

玩转BLE(3)_使用微信蓝牙精简协议伪造记步数据

$
0
0

1. 前言

在物联网时代,有一个问题肯定会让人头疼(现在已经初露端倪了):

物联网中的IOT设备有两个主要特点:
1)简单小巧(不具备复杂的人机交互接口,需要手机等终端设备辅助完成配置、控制等功能)。
2)数量和种类繁多(消费者面对的可是数量众多的不同厂家、不同类型的设备)。

基于这两个特点,手机等终端设备一般通过APP(或APK)对IOT设备进行控制,不同厂家的不同设备,通常需要不同的APP/APK。于是出现了这样的结果:
如果你家里有一个小米yeelight的床头灯,你需要安装一个yeelight的APP/APK;
如果你手上戴一个百度智能手环,你需要安装一个百度智能手环APP/APK;
如果你跑步时戴一个xxx运动蓝牙耳机,你需要安装一个xxx运动蓝牙耳机APP/APK;
如果你要骑摩拜单车,你需要安装一个摩拜单车APP/APK;
如果你也行骑一下优拜单车,你需要安装一个优拜单车APP/APK;
……(要列的话,我觉得永远都列不完,大家可以想象一下,这是不是一个灾难??)

既然有问题,肯定就有人试图解决,于是微信IoT、阿里小智、亚马逊IoT等等,互联网大佬们就各显神通了。最终谁能称霸天下,现在还不得而知,本文也不去讨论这些。

作为蓝牙BLE的介绍文章,本文将以微信IoT的“微信蓝牙精简协议”为例,通过“把一个蓝牙适配器模拟成微信计步器”,分别从BLE技术(怎样注册一个GATT service)和微信IoT(微信物联网平台的思路和想法)两个角度,窥一窥IoT江湖的冰山一角(权当开阔眼界了)。

2. 微信蓝牙精简协议简介

微信蓝牙精简协议的思路很简单(具体可参考“http://iot.weixin.qq.com/wiki/new/index.html?page=4-3“),如下图:

微信蓝牙精简协议-框图

图片1 微信蓝牙精简协议框架

总结来说,就是:

通过微信(可以运行在手机、平板、电脑、等硬件上面)统一和IoT设备交互;

交互的介质是BLE协议(可以摇身变为其它无线协议);

交互的过程可由服务器协助完成(服务器可选择微信服务器或者第三方服务器)。

虽然思路简单,背后却有深深的“不怀好意”,因为:

1)IoT设备想要和微信(或者微信背后的服务器)交互,就必须遵守微信定义的协议;

2)IoT设备通过微信和服务的交互(无论是第三方服务器,还是微信服务器),都必须遵守微信定义的协议,并经由微信服务器转发。

基于上面两点,物联网中最重要的两个环节:设备和数据,都掌握在微信的手中了。恐怖!!

到目前为止,“微信蓝牙精简协议”有一个比较成熟的应用实例:微信计步器,其工作流程为:

1)在带有计步功能的BLE设备上,按照协议规定的方式,基于BLE GATT,实现相应的Profile、Service、Attribute。

2)在微信公众平台,为该设备申请一个唯一的设备ID,并和设备的MAC地址一起,在公众平台上注册、授权。

3)在微信的微信运动小程序中,扫描并添加该设备,即可通过BLE读取设备的计步信息。

4)微信运动读取计步信息后,可以进行后续的动作(例如在朋友圈刷排名等)。

以上步骤,后面章节将会使用一个蓝牙适配器(模拟蓝牙计步器),配合自己的手机微信,进行演示说明。

3. 将蓝牙适配器模拟成一个计步器

3.1 环境准备

后续实验基于如下的硬件、软件环境(其它环境也okay,不过我没有测试)。

运行Ubuntu为例的PC、笔记本(或者开发板),包含蓝牙4.0(及以上)功能(自带或者使用蓝牙适配器);

Ubuntu中安装有较新版本的Bluez协议栈及工具集,以及其它必须的软禁包(具体步骤略,碰到问题的时候可以一点点解决);

具有BLE功能的手机(上面有可以正常使用的微信);

3.2 搭建Golang开发环境

在包含Bluez(及相关工具集)的Linux OS中,使用Bluez的工具,可以完成大部分的BLE实验,例如:

hcitool、bluetoothctl等工具,可以进行BLE设备的扫描、连接、配对、广播等操作;

hcitool可以发送HCI command,设置BLE的广播数据;

gatttool可以在GATT层面,完成GATT profile的连接、service attribute的读写等操作;

等等。

但有一个功能点,不是很容易实现,就是如何自定义一个GATT profile(要知道,BLE 90%以上的功能都是通过GATT实现的,如果做到这一点,学习BLE就非常方便了)。当然,BLE不复杂,我们可以直接使用BLuez提供的API,自行开发。如果是产品开发,无可厚非,但是如果仅仅只做个实验,这样大张旗鼓的就不划算了,最好能有一些开源的工具。确实有,我搜集、尝试过几种工具:

1)使用bluez的测试代码(bluez-5.37/test/example-gatt-server),是一个python脚本。这个家伙对环境依赖度比较高,很难用,最终没有用起来。

2)一个由nodejs脚本写的工具(https://github.com/luluxie/weixin-iot)。基本功能还好,不过本人对js不太熟,就算了。

3)一个由Go语言写的工具(https://github.com/paypal/gatt)。功能OK,关键还是新奇的Go语言,果断使用~~

这就是搭建Golang开发环境的原因。至于步骤,很简单(以Ubuntu为例):

1)安装Go语言
sudo apt install golang

2)创建GOPATH环境变量
mkdir ~/gopath
export GOPATH=$HOME/gopath    #为了方便,可以放到~/.profile中

3.3 代码准备

在github上,从https://github.com/paypal/gatt中clone一个仓库,clone的仓库地址为https://github.com/wowotech/gatt。clone后使用go get命令,下载代码到本地:

vim@ubuntu:~$ go get github.com/wowotech/gatt

下载后代码的位置为:

vim@ubuntu:~$ ls $GOPATH/src/github.com/wowotech/

gatt

然后,手动将代码中所有的“github.com/paypal”类型的import修改为“github.com/wowotech”(fuck Go!!)

cd $GOPATH/src/github.com/wowotech/gatt

find . -name "*.go" | xargs sed -i 's/paypal/wowotech/g'

提交记录如下:

https://github.com/wowotech/gatt/commit/1f1c23e94cb7e1b54078a568a60cf1aaef7de195

3.4 编译并运行一个示例文件.

(略)。

3.5 支持“微信蓝牙精简协议”

参考examples/server.go以及蓝牙精简协议的定义[1],新建weixin.go,增加微信蓝牙精简协议。最终的文件如下(代码很简单,熟悉BLE GATT协议的同学,很容易看懂,就不过多解释了):

https://github.com/wowotech/gatt/commit/942e7480ad1664dabacdb5561fa98a831621f7de

注1:可以通过代码中如下的定义修改计步的步数(想刷爆微信运动朋友圈的同学注意了,嘿嘿!!):

+ // steps little endian

+ // 01(steps) 10 27 00(0x002710 = 10000)

+ // http://iot.weixin.qq.com/wiki/new/index.html?page=4-3

+ steps := []byte{ 0x01, 0x5c, 0x74, 0x01 }

3.6 编译、运行、测试

编译:

vim@ubuntu:~/gopath/src/github.com/wowotech/gatt/examples$ go build weixin.go

运行:

vim@ubuntu:~/gopath/src/github.com/wowotech/gatt/examples$ sudo ./weixin

[sudo] password for vim:

2017/03/04 01:03:13 dev: hci0 up
2017/03/04 01:03:14 dev: hci0 down
2017/03/04 01:03:14 dev: hci0 opened
State: PoweredOn
2017/03/04 01:03:14 BD Addr: 5C:F3:70:6A:BA:27
2017/03/04 01:03:14 Generating attribute table:
2017/03/04 01:03:14 handle type props secure pvt value
2017/03/04 01:03:14 0x0001 0x2800 0x02 0x00 *gatt.Service [ E7 FE ]
2017/03/04 01:03:14 0x0002 0x2803 0x32 0x00 *gatt.Characteristic [ 32 03 00 A1 FE ]
2017/03/04 01:03:14 0x0003 0xfea1 0x32 0x00 *gatt.Characteristic [ ]
2017/03/04 01:03:14 0x0004 0x2902 0x0E 0x00 *gatt.Descriptor [ 00 00 ]
2017/03/04 01:03:14 0x0005 0x2803 0x3E 0x00 *gatt.Characteristic [ 3E 06 00 A2 FE ]
2017/03/04 01:03:14 0x0006 0xfea2 0x3E 0x00 *gatt.Characteristic [ ]
2017/03/04 01:03:14 0x0007 0x2902 0x0E 0x00 *gatt.Descriptor [ 00 00 ]
2017/03/04 01:03:14 0x0008 0x2803 0x02 0x00 *gatt.Characteristic [ 02 09 00 C9 FE ]
2017/03/04 01:03:14 0x0009 0xfec9 0x02 0x00 *gatt.Characteristic [ ]

然后在手机下载AirSyncDebugger2.3.0.apk(http://iot.weixin.qq.com/wiki/new/index.html?page=4-3的底部)测试精简协议是否okay:

打开APK---->精简协议---->计步器测试

测试界面如下(Android平台):

wx-蓝牙精简协议-AirSyncDebugger

4. 在微信公众平台注册并授权设备

第2章成功模拟出来一个遵守“微信蓝牙精简协议”的计步器之后,我们需要登录微信的公众平台(可以使用测试帐号),注册一个产品类别,并授权该设备,之后就可以在微信运动界面使用这个设备了。

4.1 登录“微信公众平台接口测试帐号”

打开下面链接:

http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login

点击登录按钮,用自己微信的扫一扫登录,登录后的界面如下:

wx-iot-登录界面

注意图中红色涂抹的三个信息(要记录下来,后面有用):

微信号:
appID:
appsecret:

4.2 开启设备功能接口

将上面的登录界面往下拉,找到“功能服务”,“设备功能”,点击“开启”,开启设备功能接口,如下图:

wx-iot-设备功能接口

4.3 添加产品

设备功能接口开启后,会出现“设置”按钮,点击进入下一个界面,进行设备功能管理。在该界面,点击“添加产品”按钮,为我们的计步器设备添加一个产品类别:

wx-iot-添加产品

按照提示填入相关的信息(需要注意红色圈出的地方):

wx-iot-基础资料登记1

wx-iot-基础资料登记2

点击“下一步”进入“产品能力登记”界面:

wx-iot-产品能力登记

最后,点击“添加”按钮,将产品添加进去。添加成功后,进入如下的界面(注意其中红色涂抹的那一串数字,是产品的ID,记下来,后面有用):

wx-iot-product_id

4.4 授权设备

4.4.1 获取access_token

根据上面得到的appID和appsecret,在浏览器里面输入如下指令,获取访问微信公众平台的token(令牌):

https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=xxxxxxxxxx&secret=xxxxxxxx

注意红色部分要修改为实际内容(具体可参考4.1章节获取的信息)。

浏览器会返回如下内容:

{"access_token":"xxxxxxxxxxxxxxxxxxx","expires_in":7200}

有两个字段,access_token(记录下来,后面有用)和有效时间(7200小时,很长了)。

4.4.2 为设备生成一个唯一ID(device_id)及二维码(通过微信扫描即可添加设备)

在浏览器输入如下指令:

https://api.weixin.qq.com/device/getqrcode?access_token=xxxxxxxxxxxx&product_id=xxxxx

其中access_token为4.4.1中获取的,product_id为4.3中获取的。

浏览器的返回如下:

{"base_resp":{"errcode":0,"errmsg":"ok"},"deviceid":"xxxxxx","qrticket":"http:\/\/we.qq.com\/d\/AQArGYez06UDOcqKNe1jqWeeLhiF7fIKebEP5hiT"}

deviceid为新生成的唯一ID,记录下来,后面有用。

qrticket是设备的二维码,随便找一个二维码生成的网站(例如http://cli.im/),就可得到图片二维码,如下(注意需要将浏览器返回的转义字符改回正常):

wx-iot-二维码获取

4.4.3 设备鉴权

这一步要借助微信公众平台的debug页面了,单纯的浏览器无法搞定。打开如下的debug地址:

http://mp.weixin.qq.com/debug/

找到如下界面:

wx-iot-设备授权

body输入的内容如下:

{
    "device_num":"1",
    "device_list":[ 
     {
        "id":" xxxxxxxx",
        "mac":"5CF3706ABA27", 
        "connect_protocol":"3",
        "auth_key":"",
        "close_strategy":"1", 
        "conn_strategy":"5",
        "crypt_method":"0",
        "auth_ver":"0",
        "manu_mac_pos":"-1",
        "ser_mac_pos":"-2",
        "ble_simple_protocol": "1"
    }
    ],
    "op_type":"0",
    "product_id": "xxxxx"
}

注意上面红色的字段,id是4.4.2中获得的deviceid,mac是蓝牙设备的物理地址,product_id是4.3中获得产品ID的。

点击“检查问题”,看到如下的成功信息,说明授权成功了:

wx-iot-授权成功

5. 使用手机微信绑定设备

使用微信扫一扫,扫描刚才获得的二维码,点击“绑定设备”,即可添加设备,如下:

wx-扫一扫绑定设备

绑定成功后,设备显示未连接,如下:

wx-绑定设备未连接

接下来要连接设备。对iOS来说,在设置界面无法主动连接BLE设备,必须有应用程序自行连接,这里以微信运动为例,搜索并连接刚才授权的那个设备,即可看到计步数据的更新。步骤如下面图片(不再解释了,大家可以自己试试):

wx-微信运动-设置

wx-微信运动-添加数据来源

wx-微信运动-搜索并添加设备

6. 参考文档

[1] 微信蓝牙精简协议,http://iot.weixin.qq.com/wiki/new/index.html?page=4-3

[2] https://github.com/paypal/gatt

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

Linux调度器:进程优先级

$
0
0

一、前言

本文主要描述的是进程优先级这个概念。从用户空间来看,进程优先级就是nice value和scheduling priority,对应到内核,有静态优先级、realtime优先级、归一化优先级和动态优先级等概念,我们希望能在第二章将这些相关的概念描述清楚。为了加深理解,在第三章我们给出了几个典型数据流过程的分析。

 

二、overview

1、蓝图

priority

2、用户空间的视角

在用户空间,进程优先级有两种含义:nice value和scheduling priority。对于普通进程而言,进程优先级就是nice value,从-20(优先级最高)~19(优先级最低),通过修改nice value可以改变普通进程获取cpu资源的比例。随着实时需求的提出,进程又被赋予了另外一种属性scheduling priority,而这些进程被称为实时进程。实时进程的优先级的范围可以通过sched_get_priority_min和sched_get_priority_max,对于linux而言,实时进程的scheduling priority的范围是1(优先级最低)~99(优先级最高)。当然,普通进程也有scheduling priority,被设定为0。

3、内核中的实现

内核中,task struct中有若干和进程优先级有个的成员,如下:

struct task_struct {
......
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;
......
    unsigned int policy;
......
}

policy成员记录了该线程的调度策略,而其他的成员表示了各种类型的优先级,下面的小节我们会一一描述。

4、静态优先级

task struct中的static_prio成员。我们称之静态优先级,其特点如下:

(1)值越小,进程优先级越高

(2)0 – 99用于real-time processes(没有实际的意义),100 – 139用于普通进程

(3)缺省值是 120

(4)用户空间可以通过nice()或者setpriority对该值进行修改。通过getpriority可以获取该值。

(5)新创建的进程会继承父进程的static priority。

静态优先级是所有相关优先级的计算的起点,要么继承自父进程,要么用户空间自行设定。一旦修改了静态优先级,那么normal priority和动态优先级都需要重新计算。

5、实时优先级

task struct中的rt_priority成员表示该线程的实时优先级,也就是从用户空间的视角来看的scheduling priority。0是普通进程,1~99是实时进程,99的优先级最高。

6、归一化优先级

task struct中的normal_prio成员。我们称之归一化优先级(normalized priority),它是根据静态优先级、scheduling priority和调度策略来计算得到,代码如下:

static inline int normal_prio(struct task_struct *p)
{
    int prio;

    if (task_has_dl_policy(p))
        prio = MAX_DL_PRIO-1;
    else if (task_has_rt_policy(p))
        prio = MAX_RT_PRIO-1 - p->rt_priority;
    else
        prio = __normal_prio(p);
    return prio;
}

这里我们先聊聊归一化(Normalization)这个看起来稍微有点晦涩的术语。如果你做过音视频定点算法的优化,应该对这个词不陌生。不同的定点数据有不同的表示,有Q31的,有Q15,这些数据的小数点的位置不同,无法进行比较、加减等操作,因此需要归一化,全部转换成某个特定的数据格式(其实就是确定小数点的位置)。在数学上,1米和1mm在进行操作的时候也需要归一化,全部转换成同一个量纲就OK了。对于这里的优先级,调度器需要综合考虑各种因素,例如调度策略,nice value、scheduling priority等,把这些factor全部考虑进来,归一化成一个数轴上的number,以此来表示其优先级,这就是normalized priority。对于一个线程,其normalized priority的number越小,其优先级越大。

调度策略是deadline的进程比RT进程和normal进程的优先级还要高,因此它的归一化优先级是负数:-1。如果采用实时调度策略,那么该线程的normalized priority和rt_priority相关。task struct中的rt_priority成员是用户空间视角的实时优先级(scheduling priority),MAX_RT_PRIO-1是99,MAX_RT_PRIO-1 - p->rt_priority则翻转了实时进程的scheduling priority,最高优先级是0,最低是98。顺便说一句,normalized priority是99的情况是没有意义的。对于普通进程,normalized priority就是其静态优先级。

7、动态优先级

task struct中的prio成员表示了该线程的动态优先级,也就是调度器在进行调度时候使用的那个优先级。动态优先级在运行时可以被修改,例如在处理优先级翻转问题的时候,系统可能会临时调升一个普通进程的优先级。一般设定动态优先级的代码是这样的:p->prio = effective_prio(p),具体计算动态优先级的代码如下:

static int effective_prio(struct task_struct *p)
{
    p->normal_prio = normal_prio(p);
    if (!rt_prio(p->prio))
        return p->normal_prio;
    return p->prio;
}

rt_prio是一个根据当前优先级来确定是否是实时进程的函数,包括两种情况,一种情况是该进程是实时进程,调度策略是SCHED_FIFO或者SCHED_RR。另外一种情况是人为的将该进程提升到RT priority的区域(例如在使用优先级继承的方法解决系统中优先级翻转问题的时候)。在这两种情况下,我们都不改变其动态优先级,即effective_prio返回当前动态优先级p->prio。其他情况,进程的动态优先级跟随归一化的优先级。

 

三、典型数据流程分析

1、用户空间设定nice value

用户空间设定nice value的操作,在内核中主要是set_user_nice函数实现的,无论是sys_nice或者sys_setpriority,在参数检查和权限检查之后都会调用set_user_nice函数,完成具体的设定。代码如下:

void set_user_nice(struct task_struct *p, long nice)
{
    int old_prio, delta, queued;
    unsigned long flags;
    struct rq *rq; 
    rq = task_rq_lock(p, &flags);
    if (task_has_dl_policy(p) || task_has_rt_policy(p)) {-----------(1)
        p->static_prio = NICE_TO_PRIO(nice);
        goto out_unlock;
    }
    queued = task_on_rq_queued(p);-------------------(2)
    if (queued)
        dequeue_task(rq, p, DEQUEUE_SAVE);

    p->static_prio = NICE_TO_PRIO(nice);----------------(3)
    set_load_weight(p);
    old_prio = p->prio;
    p->prio = effective_prio(p);
    delta = p->prio - old_prio;

    if (queued) {
        enqueue_task(rq, p, ENQUEUE_RESTORE);------------(2)
        if (delta < 0 || (delta > 0 && task_running(rq, p)))------------(4)
            resched_curr(rq);
    }
out_unlock:
    task_rq_unlock(rq, p, &flags);
}

(1)如果是实时进程或者deadline类型的进程,那么nice value其实是没有什么实际意义的,不过我们还是设定其静态优先级,当然,这样的设定其实不会起到什么作用的,也不会实际改变调度器行为,因此直接返回,没有dequeue和enqueue的动作。

(2)在step中已经处理了调度策略是RT类和DEADLINE类的进程,因此,执行到这里,只可能是普通进程了,使用CFS算法。如果该task在run queue上(queued 等于true),那么由于我们修改了nice value,调度器需要重新审视当前runqueue中的task。因此,我们需要将该task从rq中摘下,在重新计算优先级之后,再次插入该runqueue对应的runable task的红黑树中。

(3)最核心的代码就是p->static_prio = NICE_TO_PRIO(nice);这一句了,其他的都是side effect。比如说load weight。当cpu一刻不停的运算的时候,其load是100%,没有机会调度到idle进程休息一下。当系统中没有实时进程或者deadline进程的时候,所有的runnable的进程一起来瓜分cpu资源,以此不同的进程分享一个特定比例的cpu资源,我们称之load weight。不同的nice value对应不同的cpu load weight,因此,当更改nice value的时候,也必须通过set_load_weight来更新该进程的cpu load weight。除了load weight,该线程的动态优先级也需要更新,这是通过p->prio = effective_prio(p);来完成的。

(4)delta 记录了新旧线程的动态优先级的差值,当调试了该线程的优先级(delta < 0),那么有可能产生一个调度点,因此,调用resched_curr,给当前正在运行的task做一个标记,以便在返回用户空间的时候进行调度。此外,如果修改当前running状态的task的动态优先级,那么调降(delta > 0)意味着该进程有可能需要让出cpu,因此也需要resched_curr标记当前running状态的task需要reschedule。

2、进程缺省的调度策略和调度参数

我们先思考这样的一个问题:在用户空间设定调度策略和调度参数之前,一个线程的default scheduling policy是什么呢?这需要追溯到fork的时候(具体代码在sched_fork函数中),这个和task struct中sched_reset_on_fork设定相关。如果没有设定这个flag,那么说明在fork的时候,子进程跟随父进程的调度策略,如果设定了这个flag,则说明子进程的调度策略和调度参数不能继承自父进程,而是需要设定为default。代码片段如下:

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{

……
    p->prio = current->normal_prio; -------------------(1)
    if (unlikely(p->sched_reset_on_fork)) {
        if (task_has_dl_policy(p) || task_has_rt_policy(p)) {----------(2)
            p->policy = SCHED_NORMAL;
            p->static_prio = NICE_TO_PRIO(0);
            p->rt_priority = 0;
        } else if (PRIO_TO_NICE(p->static_prio) < 0)
            p->static_prio = NICE_TO_PRIO(0);

        p->prio = p->normal_prio = __normal_prio(p); ------------(3)
        set_load_weight(p); 
        p->sched_reset_on_fork = 0;
    }

……

}

(1)sched_fork只是fork过程中的一个片段,在fork一开始,dup_task_struct已经复制了一个和父进程完全一个的进程描述符(task struct),因此,如果没有步骤2中的重置,那么子进程是跟随父进程的调度策略和调度参数(各种优先级),当然,有时候为了解决PI问题而临时调升父进程的动态优先级,在fork的时候不宜传递到子进程中,因此这里重置了动态优先级。

(2)缺省的调度策略是SCHED_NORMAL,静态优先级等于120(也就是说nice value等于0),rt priority等于0(普通进程)。不管父进程如何,即便是deadline的进程,其fork的子进程也需要恢复到缺省参数。

(3)既然调度策略和静态优先级已经修改了,那么也需要更新动态优先级和归一化优先级。此外,load weight也需要更新。一旦子进程中恢复到了缺省的调度策略和优先级,那么sched_reset_on_fork这个flag已经完成了历史使命,可以clear掉了。

OK,至此,我们了解了在fork过程中对调度策略和调度参数的处理,这里还是要追加一个问题:为何不一切继承父进程的调度策略和参数呢?为何要在fork的时候reset to default呢?在linux中,对于每一个进程,我们都会进行资源限制。例如对于那些实时进程,如果它持续消耗cpu资源而没有发起一次可以引起阻塞的系统调用,那么我们猜测这个realtime进程跑飞了,从而锁住了系统。对于这种情况,我们要进行干预,因此引入了RLIMIT_RTTIME这个per-process的资源限制项。但是,如果用户空间的realtime进程通过fork其实也可以绕开RLIMIT_RTTIME这个限制,从而肆意的攫取cpu资源。然而,机智的内核开发人员早已经看穿了这一切,为了防止实时进程“泄露”到其子进程中,sched_reset_on_fork这个flag被提出来。

3、用户空间设定调度策略和调度参数

通过sched_setparam接口函数可以修改rt priority的调度参数,而通过sched_setscheduler功能会更强一些,不但可以设定rt priority,还可以设定调度策略。而sched_setattr是一个集大成之接口,可以设定一个线程的调度策略以及该调度策略下的调度参数。当然,对于内核,这些接口都通过__sched_setscheduler这个内核函数来完成对指定线程调度策略和调度参数的修改。

__sched_setscheduler分成两个部分,首先进行安全性检查和参数检查,其次进行具体的设定。

我们先看看安全性检查。如果用户空间可以自由的修改调度策略和调度优先级,那么世界就乱套了,每个进程可能都想把自己的调度策略和优先级提升上去,从而获取足够的CPU 资源。因此用户空间设定调度策略和调度参数要遵守一定的规则:如果没有CAP_SYS_NICE的能力,那么基本上该线程能被允许的操作只是降级而已。例如从SCHED_FIFO修改成SCHED_NORMAL,异或不修改scheduling policy,而是降低静态优先级(nice value)或者实时优先级(scheduling priority)。这里例外的是SCHED_DEADLINE的设定,按理说如果进程本身的调度策略就是SCHED_DEADLINE,那么应该允许“优先级”降低的操作(这里用优先级不是那么合适,其实就是减小run time,或者加大period,这样可以放松对cpu资源的获取),但是目前的4.4.6内核不允许(也许以后版本的内核会允许)。此外,如果没有CAP_SYS_NICE的能力,那么设定调度策略和调度参数的操作只能是限于属于同一个登录用户的线程。如果拥有CAP_SYS_NICE的能力,那么就没有那么多限制了,可以从普通进程提升成实时进程(修改policy),也可以提升静态优先级或者实时优先级。

具体的修改比较简单,是通过__setscheduler_params函数完成,其实也就是是根据sched_attr中的参数设定到task struct相关成员中,大家可以自行阅读代码进行理解。

参考文档:

1、linux下的各种man page

2、linux 4.4.6内核源代码

原创文章,转发请注明出处。蜗窝科技

中断上下文中调度会怎样?

$
0
0

一、前言

每一个Linux驱动工程师都知道这样一个准则:在中断上下文中不能睡眠。但是为什么interrupt context中不能调用导致睡眠的kernel API呢?如果驱动这么做会导致什么样的后果呢?这就是本文探讨的主题。为了理解这个主题,我们设计了一些非常简单的驱动程序和用户空间的程序,实际做实验观察实验效果,最后给出了结果和分析。

BTW,本文的实验在X86 64bit + 标准4.4内核上完成。

 

二、测试程序

1、cst驱动模块

我们首先准备一个能够在中断上下文中睡眠的驱动程序,在这里我称之Context schedule test module(后文简称cst模块)。这个驱动程序类似潜伏在内核中的“捣蛋鬼”,每隔1秒随机命中一个进程,然后引发调度。首先准备一个Makefile,代码如下:

KERNELSRC ?= /home/xxxx/work/linux-4.4.6

default:
        $(MAKE) -C $(KERNELSRC) M=$$PWD

clean:
        $(MAKE) -C $(KERNELSRC) M=$$PWD clean

按理说代码中的xxxx应该是我的名字,如果你愿意在你的环境中测试,可以修改成你的名字,当然,最重要的是需要某个版本的内核代码。在内核升级文档中,我已经编译了/home/xxxx/work/linux-4.4.6目录下的内核,并把我的计算机升级到4.4.6的内核上,如果你愿意可以按照那份文档进行升级,否则可能会有一些版本问题需要处理。除了Makefile之外,还需要一个Kbuild文件:

obj-m := cst.o

当然,最重要的是cst模块的源代码:

#include
#include
#include
#include

#define DRIVER_DESC "context schedule test driver"

static struct timer_list cst_timer;

static void cst_timer_handler (unsigned long data)
{
        struct task_struct *p = current;

        pr_info("=====in timer handler=====\n");
        pr_info("cst shoot %16s [%x] task:\n", p->comm, preempt_count());
        mod_timer(&cst_timer, jiffies + HZ);
        schedule();
}

static int __init cst_init(void)
{
        init_timer(&cst_timer);
        cst_timer.function = cst_timer_handler;
        cst_timer.expires = jiffies + HZ;
        add_timer(&cst_timer);

        pr_info(DRIVER_DESC " : init on cpu:%d\n", smp_processor_id());
        return 0;
}
module_init(cst_init);

static void __exit cst_exit(void)
{
        del_timer_sync(&cst_timer);
        pr_info(DRIVER_DESC " : exit\n");
}
module_exit(cst_exit);

MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_AUTHOR("linuxer ");
MODULE_LICENSE("GPL");

代码非常的简单,无需多说,直接make就可以编译得到cst.ko的内核模块了。

2、用户空间测试程序

为了更方便的测试,我们需要准备一个“受害者”,代码如下:

#include
#include

int main(int argc, char **argv)
{
    int i = 0;

    while (1) {
        sqrt (rand ());

        if ((i % 0xffffff) == 0)
            printf ("=\n");

        if ((i % 0xfffffff) == 0)
            printf ("haha......still alive\n");

        i++;
    }

    return 0;
}

这段代码也很简单:不断的产生一个随机数,并运算其平方根,在使得的时候,向用户输出一些字符,表明自己的状态。当程序执行起来的时候,大部分时间在用户态(运算),偶尔进入内核态(printf)。这个进程并不知道在内核态有一个cst的模块,每隔一秒就发射一只休眠之箭,可能命中用户态,也可能命中内核态,看运气吧,但是无论怎样,该进程被射中之后都会进入睡眠。

 

三、执行测试并观察结果

1、先把用户空间的测试程序跑起来

要想测试导弹(呵呵~~我们的cst模块就是一个捣蛋) 的性能,必须要有靶机或者靶舰。当然也可以不用“靶机”程序,只不过捣蛋鬼cst总是命中swapper进程,有点无趣,因此这里需要把我们用户空间的那个测试程序跑起来,让CPU先活跃起来。

需要注意的是,在多核cpu上,我们需要多跑几个“靶机”进程才能让系统不会always进入idle状态。例如我的T450是4核cpu,因此我需要运行4个靶机程序才能让系统中的4个cpu core都燥起来。可以通过下面的命令确认:

ps –eo comm,psr | grep cst

BTW,靶机程序是cst_test。通过上面的命令,可以看到系统中运行了四个cst_test进程,分别在4个cpu上。

2、插入内核模块

靶机已经就绪,是时候发射捣蛋了,命令如下:

sudo insmod ./cst.ko

一旦插入了cst内核模块,捣蛋鬼就开始运作了,每隔1秒钟发射一次,总有一个倒霉蛋被命中,被调度。当然,在我们的测试中,一般总是cst_test这个进程被命中。

3、观察结果

一切准备就绪,是时候搬个小板凳坐下来看好戏了。当然,我们需要一个观察的工具,输入如下命令:

sudo tail –f /var/log/messages

在上面的cst模块中,输出并没有直接到控制台,因此我们需要通过内核日志来看看cst的运行情况。

 

四、结果和分析

1、结果

很奇怪,一切都是正常的,系统没有死,cst模块也运行正常,cst_test进程也始终保持alive状态,不断的运行在无聊的平方根、打印着无聊的字符串。唯一异常的是日志,每隔1秒钟就会dump stack一次。

2、分析

当cst模块命中cst_test进程,无论是userspace还是kernel space,都会在内核栈上保存中断发生那一点的上下文,唯一不同是如果发生在userspace,那么发生中断那一刻,内核栈是空的,而如果在kernel space,内核栈上已经有了cst_test通过系统调用进入内核的现场以及该系统调用各个函数的stack frame,当中断发生的时候,在当前内核栈的栈顶上继续压入了中断现场,之后就是中断处理的各个函数的stack frame,最后是cst_timer_handler的stack frame,由于调用了schedule函数,cst_test进程的现场被继续压入内核栈,调度器决定下一个调度的进程。

cst_test进程虽然被调度了,但是仍然在runqueue中,调度器一定会在适当的时机调度cst_test进程重新进入执行态,这时候恢复其执行就OK了,cpu执行cst_timer_handler函数schedule之后的代码,继续未完的中断上下文的执行,然后从内核栈恢复中断现场,一切又按照原路返回了。

当然,这里的测试看起来一切OK,但这并不是说可以自由的在中断上下文中调用导致睡眠的内核API,因为我们这里给出了一个简单的例子,实际上也有可能导致系统死锁。例如在内核态持有锁的时候被中断,然后发生调度。有兴趣的同学可以自己修改上面的代码实验这种情况。

3、why

最后还是回到这个具体的技术问题:为什么interrupt context中不能调用导致睡眠的kernel API?

我的看法是这样的:调度器是每一个OS的必备组件,在编码阶段之前,我们往往要制定下我们的设计概念。对于Linux 调度器,它的目标就是调度一个线程,一个线程就是调度实体(暂不考虑group sched)。中断上下文是不是调度实体呢?当然不是,它没有专属的task struct,内核无从调度。这是调度器设计者的决定,这样的决定让调度器设计起来简洁而美丽。

基于上面的设计概念,中断上下文(hard irq和softirq context)并不参与调度(暂不考虑中断线程化),它们是异步事件的处理机制,目标就是尽快完成处理,返回现场。因此,所有中断上下文的优先级都是高于进程上下文的,也就是说,对于用户进程(无论内核态还是用户态)或者内核线程,除非disable了CPU的本地中断,否则一旦中断发生,它们是没有任何能力阻挡中断上下文抢占当前进程上下文的执行的。

因此,Linux kernel的设计者制定了规则:

1、中断上下文不是调度实体

2、中断上下文的优先级高于进程上下文

而在中断上下文中调度毫无疑问会打破规则,因此不能在硬中断、软中断环境中调用阻塞函数。不过,在linux调度器的具体实现的时候,检测到了在中断上下文中调度schedule函数也并没有强制linux进入panic,可能是linux的开发者认为一个好的内核调度器无论如何也尽自己最大的努力让系统运行下去吧。但是,在厂商自己提供的内核中,往往修改调度器行为,在中断上下文中检测到调度就直接panic了,对于内核开发者而言,这样更好,可以尽早的发现问题。

 

原创文章,转发请注明出处。蜗窝科技

进程切换分析(2):TLB处理

$
0
0

一、前言

进程切换是一个复杂的过程,本文不准备详细描述整个进程切换的方方面面,而是关注进程切换中一个小小的知识点:TLB的处理。为了能够讲清楚这个问题,我们在第二章描述在单CPU场景下一些和TLB相关的细节,第三章推进到多核场景,至此,理论部分结束。在第二章和第三章,我们从基本的逻辑角度出发,并不拘泥于特定的CPU和特定的OS,这里需要大家对基本的TLB的组织原理有所了解,具体可以参考本站的《TLB操作》一文。再好的逻辑也需要体现在HW block和SW block的设计中,在第四章,我们给出了linux4.4.6内核在ARM64平台上的TLB代码处理细节(在描述tlb lazy mode的时候引入部分x86架构的代码),希望能通过具体的代码和实际的CPU硬件行为加深大家对原理的理解。

 

二、单核场景的工作原理

1、block diagram

我们先看看在单核场景下,和进程切换相关的逻辑block示意图:

tlb

CPU上运行了若干的用户空间的进程和内核线程,为了加快性能,CPU中往往设计了TLB和Cache这样的HW block。Cache为了更快的访问main memory中的数据和指令,而TLB是为了更快的进行地址翻译而将部分的页表内容缓存到了Translation lookasid buffer中,避免了从main memory访问页表的过程。

假如不做任何的处理,那么在进程A切换到进程B的时候,TLB和Cache中同时存在了A和B进程的数据。对于kernel space其实无所谓,因为所有的进程都是共享的,但是对于A和B进程,它们各种有自己的独立的用户地址空间,也就是说,同样的一个虚拟地址X,在A的地址空间中可以被翻译成Pa,而在B地址空间中会被翻译成Pb,如果在地址翻译过程中,TLB中同时存在A和B进程的数据,那么旧的A地址空间的缓存项会影响B进程地址空间的翻译,因此,在进程切换的时候,需要有tlb的操作,以便清除旧进程的影响,具体怎样做呢?我们下面一一讨论。

2、绝对没有问题,但是性能不佳的方案

当系统发生进程切换,从进程A切换到进程B,从而导致地址空间也从A切换到B,这时候,我们可以认为在A进程执行过程中,所有TLB和Cache的数据都是for A进程的,一旦切换到B,整个地址空间都不一样了,因此需要全部flush掉(注意:我这里使用了linux内核的术语,flush就是意味着将TLB或者cache中的条目设置为无效,对于一个ARM平台上的嵌入式工程师,一般我们会更习惯使用invalidate这个术语,不管怎样,在本文中,flush等于invalidate)。

这种方案当然没有问题,当进程B被切入执行的时候,其面对的CPU是一个干干净净,从头开始的硬件环境,TLB和Cache中不会有任何的残留的A进程的数据来影响当前B进程的执行。当然,稍微有一点遗憾的就是在B进程开始执行的时候,TLB和Cache都是冰冷的(空空如也),因此,B进程刚开始执行的时候,TLB miss和Cache miss都非常严重,从而导致了性能的下降。

3、如何提高TLB的性能?

对一个模块的优化往往需要对该模块的特性进行更细致的分析、归类,上一节,我们采用进程地址空间这样的术语,其实它可以被进一步细分为内核地址空间和用户地址空间。对于所有的进程(包括内核线程),内核地址空间是一样的,因此对于这部分地址翻译,无论进程如何切换,内核地址空间转换到物理地址的关系是永远不变的,其实在进程A切换到B的时候,不需要flush掉,因为B进程也可以继续使用这部分的TLB内容(上图中,橘色的block)。对于用户地址空间,各个进程都有自己独立的地址空间,在进程A切换到B的时候,TLB中的和A进程相关的entry(上图中,青色的block)对于B是完全没有任何意义的,需要flush掉。

在这样的思路指导下,我们其实需要区分global和local(其实就是process-specific的意思)这两种类型的地址翻译,因此,在页表描述符中往往有一个bit来标识该地址翻译是global还是local的,同样的,在TLB中,这个标识global还是local的flag也会被缓存起来。有了这样的设计之后,我们可以根据不同的场景而flush all或者只是flush local tlb entry。

4、特殊情况的考量

我们考虑下面的场景:进程A切换到内核线程K之后,其实地址空间根本没有必要切换,线程K能访问的就是内核空间的那些地址,而这些地址也是和进程A共享的。既然没有切换地址空间,那么也就不需要flush 那些进程特定的tlb entry了,当从K切换会A进程后,那么所有TLB的数据都是有效的,从大大降低了tlb miss。此外,对于多线程环境,切换可能发生在一个进程中的两个线程,这时候,线程在同样的地址空间,也根本不需要flush tlb。

4、进一步提升TLB的性能

还有可能进一步提升TLB的性能吗?有没有可能根本不flush TLB?

当然可以,不过这需要我们在设计TLB block的时候需要识别process specific的tlb entry,也就是说,TLB block需要感知到各个进程的地址空间。为了完成这样的设计,我们需要标识不同的address space,这里有一个术语叫做ASID(address space ID)。原来TLB查找是通过虚拟地址VA来判断是否TLB hit。有了ASID的支持后,TLB hit的判断标准修改为(虚拟地址+ASID),ASID是每一个进程分配一个,标识自己的进程地址空间。TLB block如何知道一个tlb entry的ASID呢?一般会来自CPU的系统寄存器(对于ARM64平台,它来自TTBRx_EL1寄存器),这样在TLB block在缓存(VA-PA-Global flag)的同时,也就把当前的ASID缓存在了对应的TLB entry中,这样一个TLB entry中包括了(VA-PA-Global flag-ASID)。

有了ASID的支持后,A进程切换到B进程再也不需要flush tlb了,因为A进程执行时候缓存在TLB中的残留A地址空间相关的entry不会影响到B进程,虽然A和B可能有相同的VA,但是ASID保证了硬件可以区分A和B进程地址空间。

 

三、多核的TLB操作

1、block diagram

完成单核场景下的分析之后,我们一起来看看多核的情况。进程切换相关的TLB逻辑block示意图如下:

tlb-mp

在多核系统中,进程切换的时候,TLB的操作要复杂一些,主要原因有两点:其一是各个cpu core有各自的TLB,因此TLB的操作可以分成两类,一类是flush all,即将所有cpu core上的tlb flush掉,还有一类操作是flush local tlb,即仅仅flush本cpu core的tlb。另外一个原因是进程可以调度到任何一个cpu core上执行(当然具体和cpu affinity的设定相关),从而导致task处处留情(在各个cpu上留有残余的tlb entry)。

2、TLB操作的基本思考

根据上一节的描述,我们了解到地址翻译有global(各个进程共享)和local(进程特定的)的概念,因而tlb entry也有global和local的区分。如果不区分这两个概念,那么进程切换的时候,直接flush该cpu上的所有残余。这样,当进程A切出的时候,留给下一个进程B一个清爽的tlb,而当进程A在其他cpu上再次调度的时候,它面临的也是一个全空的TLB(其他cpu的tlb不会影响)。当然,如果区分global 和local,那么tlb操作也基本类似,只不过进程切换的时候,不是flush该cpu上的所有tlb entry,而是flush所有的tlb local entry就OK了。

对local tlb entry还可以进一步细分,那就是了ASID(address space ID)或者PCID(process context ID)的概念了(global tlb entry不区分ASID)。如果支持ASID(或者PCID)的话,tlb操作变得简单一些,或者说我们没有必要执行tlb操作了,因为在TLB搜索的时候已经可以区分各个task上下文了,这样,各个cpu中残留的tlb不会影响其他任务的执行。在单核系统中,这样的操作可以获取很好的性能。比如A---B--->A这样的场景中,如果TLB足够大,可以容纳2个task的tlb entry(现代cpu一般也可以做到这一点),那么A再次切回的时候,TLB是hot的,大大提升了性能。

不过,对于多核系统,这种情况有一点点的麻烦,其实也就是传说中的TLB shootdown带来的性能问题。在多核系统中,如果cpu支持PCID并且在进程切换的时候不flush tlb,那么系统中各个cpu中的tlb entry则保留各种task的tlb entry,当在某个cpu上,一个进程被销毁,或者修改了自己的页表(也就是修改了VA PA映射关系)的时候,我们必须将该task的相关tlb entry从系统中清除出去。这时候,你不仅仅需要flush本cpu上对应的TLB entry,还需要shootdown其他cpu上的和该task相关的tlb残余。而这个动作一般是通过IPI实现(例如X86),从而引入了开销。此外PCID的分配和管理也会带来额外的开销,因此,OS是否支持PCID(或者ASID)是由各个arch代码自己决定(对于linux而言,x86不支持,而ARM平台是支持的)。

 

四、进程切换中的tlb操作代码分析

1、tlb lazy mode

在context_switch中有这样的一段代码:

if (!mm) {
    next->active_mm = oldmm;
    atomic_inc(&oldmm->mm_count);
    enter_lazy_tlb(oldmm, next);
} else
    switch_mm(oldmm, mm, next);

这段代码的意思就是如果要切入的next task是一个内核线程(next->mm == NULL )的话,那么可以通过enter_lazy_tlb函数标记本cpu上的next task进入lazy TLB mode。由于ARM64平台上的enter_lazy_tlb函数是空函数,因此我们采用X86来描述lazy TLB mode。

当然,我们需要一些准备工作,毕竟对于熟悉ARM平台的嵌入式工程师而言,x86多少有点陌生。

到目前,我们还都是从逻辑角度来描述TLB操作,但是在实际中,进程切换中的tlb操作是HW完成还是SW完成呢?不同的处理器思路是不一样的(具体原因未知),有的处理器是HW完成,例如X86,在加载cr3寄存器进行地址空间切换的时候,hw会自动操作tlb。而有的处理是需要软件参与完成tlb操作,例如ARM系列的处理器,在切换TTBR寄存器的时候,HW没有tlb动作,需要SW完成tlb操作。因此,x86平台上,在进程切换的时候,软件不需要显示的调用tlb flush函数,在switch_mm函数中会用next task中的mm->pgd加载CR3寄存器,这时候load cr3的动作会导致本cpu中的local tlb entry被全部flush掉。

在x86支持PCID(X86术语,相当与ARM的ASID)的情况下会怎样呢?也会在load cr3的时候flush掉所有的本地CPU上的 local tlb entry吗?其实在linux中,由于TLB shootdown,普通的linux并不支持PCID(KVM中会使用,但是不在本文考虑范围内),因此,对于x86的进程地址空间切换,它就是会有flush local tlb entry这样的side effect。

另外有一点是ARM64和x86不同的地方:ARM64支持在一个cpu core执行tlb flush的指令,例如tlbi vmalle1is,将inner shareablity domain中的所有cpu core的tlb全部flush掉。而x86不能,如果想要flush掉系统中多有cpu core的tlb,只能是通过IPI通知到其他cpu进行处理。

好的,至此,所有预备知识都已经ready了,我们进入tlb lazy mode这个主题。虽然进程切换伴随tlb flush操作,但是某些场景亦可避免。在下面的场景,我们可以不flush tlb(我们仍然采用A--->B task的场景来描述):

(1)如果要切入的next task B是内核线程,那么我们也暂时不需要flush TLB,因为内核线程不会访问usersapce,而那些进程A残留的TLB entry也不会影响内核线程的执行,毕竟B没有自己的用户地址空间,而且和A共享内核地址空间。

(2)如果A和B在一个地址空间中(一个进程中的两个线程),那么我们也暂时不需要flush TLB。

除了进程切换,还有其他的TLB flush场景。我们先看一个通用的TLB flush场景,如下图所示:

lazy

一个4核系统中,A0 A1和A2 task属于同一个进程地址空间,CPU_0和CPU_2上分别运行了A0和A2 task,CPU_1有点特殊,它正在运行一个内核线程,但是该内核线程正在借用A1 task的地址空间,CPU_3上运行不相关的B task。

当A0 task修改了自己的地址翻译,那么它不能只是flush CPU_0的tlb,还需要通知到CPU_1和CPU_2,因为这两个CPU上当前active的地址空间和CPU_0是一样的。由于A1 task的修改,CPU_1和CPU_2上的这些缓存的TLB entry已经失效了,需要flush。同理,可以推广到更多的CPU上,也就是说,在某个CPUx上运行的task修改了地址映射关系,那么tlb flush需要传递到所有相关的CPU中(当前的mm等于CPUx的current mm)。在多核系统中,这样的通过IPI来传递TLB flush的消息会随着cpu core的增加而增加,有没有办法减少那些没有必要的TLB flush呢?当然有,也就是上图中的A1 task场景,这也就是传说中的lazy tlb mode。

我先回头看看代码。在代码中,如果next task是内核线程,我们并不会执行switch_mm(该函数会引起tlb flush的动作),而是调用enter_lazy_tlb进入lazy tlb mode。在x86架构下,代码如下:

static inline void enter_lazy_tlb(struct mm_struct *mm, struct task_struct *tsk)
{
#ifdef CONFIG_SMP
    if (this_cpu_read(cpu_tlbstate.state) == TLBSTATE_OK)
        this_cpu_write(cpu_tlbstate.state, TLBSTATE_LAZY);
#endif
}

在x86架构下,进入lazy tlb mode也就是在该cpu的cpu_tlbstate变量中设定TLBSTATE_LAZY的状态就OK了。因此,进入lazy mode的时候,也就不需要调用switch_mm来切换进程地址空间,也就不会执行flush tlb这样毫无意义的动作了。 enter_lazy_tlb并不操作硬件,只要记录该cpu的软件状态就OK了。

切换之后,内核线程进入执行状态,CPU_1的TLB残留进程A的entry,这对于内核线程的执行没有影响,但是当其他CPU发送IPI要求flush TLB的时候呢?按理说应该立刻flush tlb,但是在lazy tlb mode下,我们可以不执行flush tlb操作。这样问题来了:什么时候flush掉残留的A进程的tlb entry呢?答案是在下一次进程切换中。因为一旦内核线程被schedule out,并且切入一个新的进程C,那么在switch_mm,切入到C进程地址空间的时候,所有之前的残留都会被清除掉(因为有load cr3的动作)。因此,在执行内核线程的时候,我们可以推迟tlb invalidate的请求。也就是说,当收到ipi中断要求进行该mm的tlb invalidate的动作的时候,我们暂时没有必要执行了,只需要记录状态就OK了。

2、ARM64中如何管理ASID?

和x86不同的是:ARM64支持了ASID(类似x86的PCID),难道ARM64解决了TLB Shootdown的问题?其实我也在思考这个问题,但是还没有想明白。很显然,在ARM64中,我们不需要通过IPI来进行所有cpu core的TLB flush动作,ARM64在指令集层面支持shareable domain中所有PEs上的TLB flush动作,也许是这样的指令让TLB flush的开销也没有那么大,那么就可以选择支持ASID,在进程切换的时候不需要进行任何的TLB操作,同时,由于不需要IPI来传递TLB flush,那么也就没有特别的处理lazy tlb mode了。

既然linux中,ARM64选择支持ASID,那么它就要直面ASID的分配和管理问题了。硬件支持的ASID有一定限制,它的编址空间是8个或者16个bit,最大256或者65535个ID。当ASID溢出之后如何处理呢?这就需要一些软件的控制来协调处理。 我们用硬件支持上限为256个ASID的情景来描述这个基本的思路:当系统中各个cpu的TLB中的asid合起来不大于256个的时候,系统正常运行,一旦超过256的上限后,我们将全部TLB flush掉,并重新分配ASID,每达到256上限,都需要flush tlb并重新分配HW ASID。具体分配ASID代码如下:

static u64 new_context(struct mm_struct *mm, unsigned int cpu)
{
    static u32 cur_idx = 1;
    u64 asid = atomic64_read(&mm->context.id);
    u64 generation = atomic64_read(&asid_generation);

    if (asid != 0) {-------------------------(1)
        u64 newasid = generation | (asid & ~ASID_MASK); 
        if (check_update_reserved_asid(asid, newasid))
            return newasid; 
        asid &= ~ASID_MASK;
        if (!__test_and_set_bit(asid, asid_map))
            return newasid;
    }


    asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, cur_idx);---(2)
    if (asid != NUM_USER_ASIDS)
        goto set_asid;

    generation = atomic64_add_return_relaxed(ASID_FIRST_VERSION,----(3)
                         &asid_generation);
    flush_context(cpu);

    asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, 1); ------(4)

set_asid:
    __set_bit(asid, asid_map);
    cur_idx = asid;
    return asid | generation;
}

(1)在创建新的进程的时候会分配一个新的mm,其software asid(mm->context.id)初始化为0。如果asid不等于0那么说明这个mm之前就已经分配过software asid(generation+hw asid)了,那么new context不过就是将software asid中的旧的generation更新为当前的generation而已。

(2)如果asid等于0,说明我们的确是需要分配一个新的HW asid,这时候首先要找一个空闲的HW asid,如果能够找到(jump to set_asid),那么直接返回software asid(当前generation+新分配的hw asid)。

(3)如果找不到一个空闲的HW asid,说明HW asid已经用光了,这是只能提升generation了。这时候,多有cpu上的所有的old generation需要被flush掉,因为系统已经准备进入new generation了。顺便一提的是这里generation变量已经被赋值为new generation了。

(4)在flush_context函数中,控制HW asid的asid_map已经被全部清零了,因此,这里进行的是new generation中HW asid的分配。

3、进程切换过程中ARM64的tlb操作以及ASID的处理

代码位于arch/arm64/mm/context.c中的check_and_switch_context:

void check_and_switch_context(struct mm_struct *mm, unsigned int cpu)
{
    unsigned long flags;
    u64 asid;

    asid = atomic64_read(&mm->context.id); -------------(1)

    if (!((asid ^ atomic64_read(&asid_generation)) >> asid_bits) ------(2)
        && atomic64_xchg_relaxed(&per_cpu(active_asids, cpu), asid))
        goto switch_mm_fastpath;

    raw_spin_lock_irqsave(&cpu_asid_lock, flags); 
    asid = atomic64_read(&mm->context.id);
    if ((asid ^ atomic64_read(&asid_generation)) >> asid_bits) { ------(3)
        asid = new_context(mm, cpu);
        atomic64_set(&mm->context.id, asid);
    }

    if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending)) ------(4)
        local_flush_tlb_all();

    atomic64_set(&per_cpu(active_asids, cpu), asid);
    raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);

switch_mm_fastpath:
    cpu_switch_mm(mm->pgd, mm);
}

看到这些代码的时候,你一定很抓狂:本来期望支持ASID的情况下,进程切换不需要TLB flush的操作了吗?怎么会有那么多代码?呵呵~~实际上理想很美好,现实很骨干,代码中嵌入太多管理asid的内容了。

(1)现在准备切入mm变量指向的地址空间,首先通过内存描述符获取该地址空间的ID(software asid)。需要说明的是这个ID并不是HW asid,实际上mm->context.id是64个bit,其中低16 bit对应HW 的ASID(ARM64支持8bit或者16bit的ASID,但是这里假设当前系统的ASID是16bit)。其余的bit都是软件扩展的,我们称之generation。

(2)arm64支持ASID的概念,理论上进程切换不需要TLB的操作,不过由于HW asid的编址空间有限,因此我们扩展了64 bit的software asid,其中一部分对应HW asid,另外一部分被称为asid generation。asid generation从ASID_FIRST_VERSION开始,每当HW asid溢出后,asid generation会累加。asid_bits就是硬件支持的ASID的bit数目,8或者16,通过ID_AA64MMFR0_EL1寄存器可以获得该具体的bit数目。

当要切入的mm的software asid仍然处于当前这一批次(generation)的ASID的时候,切换中不需要任何的TLB操作,可以直接调用cpu_switch_mm进行地址空间的切换,当然,也会顺便设定active_asids这个percpu变量。

(3)如果要切入的进程和当前的asid generation不一致,那么说明该地址空间需要一个新的software asid了,更准确的说是需要推进到new generation了。因此这里调用new_context分配一个新的context ID,并设定到mm->context.id中。

(4)各个cpu在切入新一代的asid空间的时候会调用local_flush_tlb_all将本地tlb flush掉。

 

参考文献:

1、64-ia-32-architectures-software-developer-manual-325462.pdf

2、DDI0487A_e_armv8_arm.pdf

3、Linux 4.4.6内核源代码

 

原创文章,转发请注明出处。蜗窝科技

蓝牙协议分析(10)_BLE安全机制之LE Encryption

$
0
0

1. 前言

前面文章介绍了两种BLE的安全机制:白名单[4]和LL privacy[3]。说实话,在这危机四伏的年代,这两种“捂着脸讲话(其它人不知道是谁在讲话,因而不能插话、不能假传圣旨,但讲话的内容却听得一清二楚)”的方法,实在是小儿科。对于物联网的应用场景来说,要做到安全,就必须对传输的数据进行加密,这就是LE Encryption要完成的事情(当然,只针对面向连接的数据),具体请参考本文的介绍。

2. 基本概念

从字面理解,Encryption是一个名词,意思是“加密术”,因此LE Encryption就是“BLE所使用的加密技术”的意思。了解加密概念的同学应该都知道,通信过程中的加密无非就是如下简单的流程:

数据发送方在需要发送数据的时候,按照一定的加密算法,将数据加密;

数据接收方在接收到数据的时候,按照等同的解密算法,将数据解密。

因此,对LE Encryption来说,需要考虑的事情无非就两条:

1)加密/解密的事情,需要在协议的哪个层次去做?

2)使用什么样的加密/解密算法?

对问题1来说,很好回答:无论是从安全的角度,还是从通信效率的角度,都应该由链路层(LL,Link Layer[1])在发送/接收数据的时候,完成加密/解密动作(具体可参考)。而问题2呢,到底要使用什么的算法,这是蓝牙标准化组织的事情,我们在本文只需要了解最终的结论即可(具体可参考)。

3. packet的加密/解密过程

LE加密/解密的过程如下面图片1所示:

LE加密解密

图片1 LE加密/解密过程

Master/Slave的LE Host会保存一个LTK(Long Term Key,至少128bits),对BLE用户(或者应用)来说,这个Key是所有加密/解密动作源头;

每当为某个LE连接启动加密传输的时候,Master和Slave的LL会协商生成一个128bits的随机数SKD(Session Key Diversifier,128bits),并以它为输入,以LTK为key,通过Encryption Engine加密生成SessionKey;

每当有明文数据包需要发送的时候,需要对明文进行加密。加密的过程,是以明文数据包为输入,以SessionKey为Key,同样通过Encryption Engine加密生成密文数据包;

同样,每当收到密文数据包的时候,需要对密文解密。解密的过程,是以密文数据包为输入,以SessionKey为Key,同样通过Encryption Engine解密生成明文数据包。

理解了加密/解密的过程之后,问题随之而来:

1)LTK是怎么来的?

2)SKD是个什么东西?怎么来的?

3)Encryption Engine又什么东西呢?

对于问题1,需要由SMP解答(也就是我们常说的配对过程),具体可参考后续的文章。问题3比较单纯,就是一个由Bluetooth specifiction所规定的加密算法,后面会单独写一篇文章介绍。而问题2,则涉及到LE Encryption的操作流程,具体可参考后面第4章的介绍。

4. Encryption Procedure

LE Encryption的过程主要由Link Layer控制(具体可参考“BLUETOOTH SPECIFICATION Version 4.2 [Vol 6, Part B] 5.1.3 Encryption Procedure”):当连接建立之后,Link Layer可以应Host的请求,使能对数据包的Encryption操作,过程如下(具体可参考“BLUETOOTH SPECIFICATION Version 4.2 [Vol 6, Part D]  6.6 START ENCRYPTION”):

LE encryption

图片2 Start Encryption

1)Host A发送LE Start Encryption HCI命令,请求Link Layer启动加密。该命令的格式如下:

Command OCF Command parameters Return Parameters
HCI_LE_Start_Encryption 0x0019 Connection_Handle
Random_Number
Encrypted_Diversifier
Long_Term_Key
 

Connection_Handle,连接句柄;
Random_Number和Encrypted_Diversifier分别简称为Rand和EDIV(Rand是一个64bits的随机数,EDIV是一个16bits的Diversifier),它们在LE Legacy Pairing的过程中,用于在多个LTK标识某一个具体的LTK。而在新的LE Secure Connections Pairing过程中,则不再使用(赋值为0即可)。关于LE的配对过程,可参考后面SMP的分析文章,这里不再详细描述;
Long_Term_Key,保存在Host段的加密key。

2)LL A收到Host的加密请求之后,会向LL B发送LL_ENC_REQ PDU以请求加密,该PDU的格式为:

Rand (8 octets) EDIV (2 octets) SKDm (8 octets) IVm (4 octets)

Rand和EDIV就是上面的Random_Number和Encrypted_Diversifier;
SKDm(session key diversifier ),是一个64bits的随机数,用于和SKDs一起,生成本次加密的SessionKey;
IVm(initialization vector ),一个32bits的随机数,和IVs一起组成64bits的IV,后面Encryption Engine会使用。

3)LL B收到LL_ENC_REQ PDU之后,会向Host B发送LE Long Term Key Request HCI Event,该Event的格式为:

Event Event code Event Parameters
LE Long Term Key Request 0x3E Subevent_Code
Connection_Handle
Random_Number
Encryption_Diversifier

Subevent_Code为0x05;
Connection_Handle,连接句柄;
Random_Number和Encrypted_Diversifier就是LL_ENC_REQ PDU中的Rand和EDIV,最初是由Host A通过LE Start Encryption命令发送过来的。

4)如果Host B能提供LTK,则通过LE Long Term Key Request Reply HCI命令,将LTK提供给LL B,该命令的格式为:

Command OCF Command parameters Return Parameters
HCI_LE_Long_Term_Key_
Request_Reply
0x001A Connection_Handle
Long_Term_Key
status
Connection_Handle

5)LL B收到LTK之后,会向LL A回应一个LL_ENC_ RSP PDU,该PDU的格式为:

SKDs (8 octets) IVs (4 octets)

SKDs和LL A的SDKm共同组成128bits的SKD;
IVs和LL A的IVm共同组成64bits的IV。

6)LL A收到LL_ENC_ RSP PDU之后,可以向LL B发送LL_START_ENC_REQ PDU,开启Encryption,LL B则回应LL_START_ENC_RSP PDU,这两个PDU均不携带任何的参数。

加密start之后,双方就可以安全的通信了。当然,LE encryption还提供了其它诸如暂停加密(LL_PAUSE_ENC_REQ/LL_PAUSE_ENC_RSP)、重启加密等过程,比较简单,这里就不详细介绍了,具体可参考BLE Spec[2]中的相关描述。

5. 参考文档

[1] 蓝牙协议分析(3)_蓝牙低功耗(BLE)协议栈介绍

[2] Core_v4.2.pdf

[3] 蓝牙协议分析(9)_BLE安全机制之LL Privacy

[4] 蓝牙协议分析(8)_BLE安全机制之白名单

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net


Linux DMA Engine framework(1)_概述

$
0
0

1. 前言

前面文章介绍“Linux MMC framework”的时候,涉及到了MMC数据传输,进而不可避免地遭遇了DMA(Direct Memory Access)。因而,择日不如撞日,就开几篇文章介绍Linux的DMA Engine framework吧。

本文是DMA Engine framework分析文章的第一篇,主要介绍DMA controller的概念、术语(从硬件的角度,大部分翻译自kernel的document[1])。之后,会分别从Provider(DMA controller驱动)和Consumer(其它驱动怎么使用DMA传输数据)两个角度,介绍Linux DMA engine有关的技术细节。

2. DMA Engine硬件介绍

DMA是Direct Memory Access的缩写,顾名思义,就是绕开CPU直接访问memory的意思。在计算机中,相比CPU,memory和外设的速度是非常慢的,因而在memory和memory(或者memory和设备)之间搬运数据,非常浪费CPU的时间,造成CPU无法及时处理一些实时事件。因此,工程师们就设计出来一种专门用来搬运数据的器件----DMA控制器,协助CPU进行数据搬运,如下图所示:

dma

图片1 DMA示意图

思路很简单,因而大多数的DMA controller都有类似的设计原则,归纳如下[1]

注1:得益于类似的设计原则,Linux kernel才有机会使用一套framework去抽象DMA engine有关的功能。

2.1 DMA channels

一个DMA controller可以“同时”进行的DMA传输的个数是有限的,这称作DMA channels。当然,这里的channel,只是一个逻辑概念,因为:

鉴于总线访问的冲突,以及内存一致性的考量,从物理的角度看,不大可能会同时进行两个(及以上)的DMA传输。因而DMA channel不太可能是物理上独立的通道;

很多时候,DMA channels是DMA controller为了方便,抽象出来的概念,让consumer以为独占了一个channel,实际上所有channel的DMA传输请求都会在DMA controller中进行仲裁,进而串行传输;

因此,软件也可以基于controller提供的channel(我们称为“物理”channel),自行抽象更多的“逻辑”channel,软件会管理这些逻辑channel上的传输请求。实际上很多平台都这样做了,在DMA Engine framework中,不会区分这两种channel(本质上没区别)。

2.2 DMA request lines

由图片1的介绍可知,DMA传输是由CPU发起的:CPU会告诉DMA控制器,帮忙将xxx地方的数据搬到xxx地方。CPU发完指令之后,就当甩手掌柜了。而DMA控制器,除了负责怎么搬之外,还要决定一件非常重要的事情(特别是有外部设备参与的数据传输):

何时可以开始数据搬运?

因为,CPU发起DMA传输的时候,并不知道当前是否具备传输条件,例如source设备是否有数据、dest设备的FIFO是否空闲等等。那谁知道是否可以传输呢?设备!因此,需要DMA传输的设备和DMA控制器之间,会有几条物理的连接线(称作DMA request,DRQ),用于通知DMA控制器可以开始传输了。

这就是DMA request lines的由来,通常来说,每一个数据收发的节点(称作endpoint),和DMA controller之间,就有一条DMA request line(memory设备除外)。

最后总结:

DMA channel是Provider(提供传输服务),DMA request line是Consumer(消费传输服务)。在一个系统中DMA request line的数量通常比DMA channel的数量多,因为并不是每个request line在每一时刻都需要传输。

2.3 传输参数

在最简单的DMA传输中,只需为DMA controller提供一个参数----transfer size,它就可以欢快的工作了:

在每一个时钟周期,DMA controller将1byte的数据从一个buffer搬到另一个buffer,直到搬完“transfer size”个bytes即可停止。

不过这在现实世界中往往不能满足需求,因为有些设备可能需要在一个时钟周期中,传输指定bit的数据,例如:

memory之间传输数据的时候,希望能以总线的最大宽度为单位(32-bit、64-bit等),以提升数据传输的效率;
而在音频设备中,需要每次写入精确的16-bit或者24-bit的数据;
等等。

因此,为了满足这些多样的需求,我们需要为DMA controller提供一个额外的参数----transfer width

另外,当传输的源或者目的地是memory的时候,为了提高效率,DMA controller不愿意每一次传输都访问memory,而是在内部开一个buffer,将数据缓存在自己buffer中:

memory是源的时候,一次从memory读出一批数据,保存在自己的buffer中,然后再一点点(以时钟为节拍),传输到目的地;
memory是目的地的时候,先将源的数据传输到自己的buffer中,当累计一定量的数据之后,再一次性的写入memory。

这种场景下,DMA控制器内部可缓存的数据量的大小,称作burst size----另一个参数。

2.4 scatter-gather

我们在“Linux MMC framework(2)_host controller driver”中已经提过scatter的概念,DMA传输时也有类似概念:

一般情况下,DMA传输一般只能处理在物理上连续的buffer。但在有些场景下,我们需要将一些非连续的buffer拷贝到一个连续buffer中(这样的操作称作scatter gather,挺形象的)。

对于这种非连续的传输,大多时候都是通过软件,将传输分成多个连续的小块(chunk)。但为了提高传输效率(特别是在图像、视频等场景中),有些DMA controller从硬件上支持了这种操作。
注2:具体怎么支持,和硬件实现有关,这里不再多说(只需要知道有这个事情即可,编写DMA controller驱动的时候,自然会知道怎么做)。

3. 总结

本文简单的介绍了DMA传输有关的概念、术语,接下来将会通过下面两篇文章,介绍Linux DMA engine有关的实现细节:

Linux DMA Engine framework(2)_provider

Linux DMA Engine framework(3)_consumer

4. 参考文档

[1] Documentation/dmaengine/provider.txt

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

系统休眠(System Suspend)和设备中断处理

$
0
0

一、设备IRQ的suspend和resume

本小节主要解决这样一个问题:在系统休眠过程中,如何suspend设备中断(IRQ)?在从休眠中唤醒的过程中,如何resume设备IRQ?

一般而言,在系统suspend过程的后期,各个设备的IRQ (interrupt request line)会被disable掉。具体的时间点是在各个设备的late suspend阶段之后。代码如下(删除了部分无关代码):

static int suspend_enter(suspend_state_t state, bool *wakeup)

{……

error = dpm_suspend_late(PMSG_SUSPEND);-----late suspend阶段

error = platform_suspend_prepare_late(state);

下面的代码中会disable各个设备的irq

error = dpm_suspend_noirq(PMSG_SUSPEND);----进入noirq的阶段

error = platform_suspend_prepare_noirq(state);

……

}

在dpm_suspend_noirq函数中,会针对系统中的每一个device,依次调用device_suspend_noirq来执行该设备noirq情况下的suspend callback函数,当然,在此之前会调用suspend_device_irqs函数来disable所有设备的irq。

之所以这么做,其思路是这样的:在各个设备驱动完成了late suspend之后,按理说这些已经被suspend的设备不应该再触发中断了。如果还有一些设备没有被正确的suspend,那么我们最好的策略是mask该设备的irq,从而阻止中断的递交。此外,在过去的代码中(指interrupt handler),我们对设备共享IRQ的情况处理的不是很好,存在这样的问题:在共享IRQ的设备们完成suspend之后,如果有中断触发,这时候设备驱动的interrupt handler并没有准备好。在有些场景下,interrupt handler会访问已经suspend设备的IO地址空间,从而导致不可预知的issue。这些issue很难debug,因此,我们引入了suspend_device_irqs()以及设备noirq阶段的callback函数。

系统resume过程中,在各个设备的early resume过程之前,各个设备的IRQ会被重新打开,具体代码如下(删除了部分无关代码):

static int suspend_enter(suspend_state_t state, bool *wakeup)

{……

platform_resume_noirq(state);----首先执行noirq阶段的resume

dpm_resume_noirq(PMSG_RESUME);------在这里会恢复irq,然后进入early resume阶段

platform_resume_early(state);

dpm_resume_early(PMSG_RESUME);

……}

在dpm_resume_noirq函数中,会调用各个设备驱动的noirq callback,在此之后,调用resume_device_irqs函数,完成各个设备irq的enable。

 

二、关于IRQF_NO_SUSPEND Flag

当然,有些中断需要在整个系统的suspend-resume过程中(包括在noirq阶段,包括将nonboot CPU推送到offline状态以及系统resume后,将其重新设置为online的阶段)保持能够触发的状态。一个简单的例子就是timer中断,此外IPI以及一些特殊目的设备中断也需要如此。

在中断申请的时候,IRQF_NO_SUSPEND flag可以用来告知IRQ subsystem,这个中断就是上一段文字中描述的那种中断:需要在系统的suspend-resume过程中保持enable状态。有了这个flag,suspend_device_irqs并不会disable该IRQ,从而让该中断在随后的suspend和resume过程中,保持中断开启。当然,这并不能保证该中断可以将系统唤醒。如果想要达到唤醒的目的,请调用enable_irq_wake。

需要注意的是:IRQF_NO_SUSPEND flag影响使用该IRQ的所有外设(一个IRQ可以被多个外设共享,不过ARM中不会这么用)。如果一个IRQ被多个外设共享,并且各个外设都注册了对应的interrupt handler,如果其一在申请中断的时候使用了IRQF_NO_SUSPEND flag,那么在系统suspend的时候(指suspend_device_irqs之后,按理说各个IRQ已经被disable了),所有该IRQ上的各个设备的interrupt handler都可以被正常的被触发执行,即便是有些设备在调用request_irq(或者其他中断注册函数)的时候没有设定IRQF_NO_SUSPEND flag。正因为如此,我们应该尽可能的避免同时使用IRQF_NO_SUSPEND 和IRQF_SHARED这两个flag。

 

三、系统中断唤醒接口:enable_irq_wake() 和 disable_irq_wake()

有些中断可以将系统从睡眠状态中唤醒,我们称之“可以唤醒系统的中断”,当然,“可以唤醒系统的中断”需要配置才能启动唤醒系统这样的功能。这样的中断一般在工作状态的时候就是作为普通I/O interrupt出现,只要在准备使能唤醒系统功能的时候,才会发起一些特别的配置和设定。

这样的配置和设定有可能是和硬件系统(例如SOC)上的信号处理逻辑相关的,我们可以考虑下面的HW block图:

irq-suspend

外设的中断信号被送到“通用的中断信号处理模块”和“特定中断信号接收模块”。正常工作的时候,我们会turn on“通用的中断信号处理模块”的处理逻辑,而turn off“特定中断信号接收模块” 的处理逻辑。但是,在系统进入睡眠状态的时候,有可能“通用的中断信号处理模块”已经off了,这时候,我们需要启动“特定中断信号接收模块”来接收中断信号,从而让系统suspend-resume模块(它往往是suspend状态时候唯一能够工作的HW block了)可以正常的被该中断信号唤醒。一旦唤醒,我们最好是turn off“特定中断信号接收模块”,让外设的中断处理回到正常的工作模式,同时,也避免了系统suspend-resume模块收到不必要的干扰。

IRQ子系统提供了两个接口函数来完成这个功能:enable_irq_wake()函数用来打开该外设中断线通往系统电源管理模块(也就是上面的suspend-resume模块)之路,另外一个接口是disable_irq_wake(),用来关闭该外设中断线通往系统电源管理模块路径上的各种HW block。

调用了enable_irq_wake会影响系统suspend过程中的suspend_device_irqs处理,代码如下:

static bool suspend_device_irq(struct irq_desc *desc)

{

……

if (irqd_is_wakeup_set(&desc->irq_data)) {

irqd_set(&desc->irq_data, IRQD_WAKEUP_ARMED);

return true;

}

省略Disable 中断的代码

}

也就是说,一旦调用enable_irq_wake设定了该设备的中断作为系统suspend的唤醒源,那么在该外设的中断不会被disable,只是被标记一个IRQD_WAKEUP_ARMED的标记。对于那些不是wakeup source的中断,在suspend_device_irq 函数中会标记IRQS_SUSPENDED并disable该设备的irq。在系统唤醒过程中(resume_device_irqs),被diable的中断会重新enable。

当然,如果在suspend的过程中发生了某些事件(例如wakeup source产生了有效信号),从而导致本次suspend abort,那么这个abort事件也会通知到PM core模块。事件并不需要被立刻通知到PM core模块,一般而言,suspend thread会在某些点上去检查pending的wakeup event。

在系统suspend的过程中,每一个来自wakeup source的中断都会终止suspend过程或者将系统唤醒(如果系统已经进入suspend状态)。但是,在执行了suspend_device_irqs之后,普通的中断被屏蔽了,这时候,即便HW触发了中断信号也无法执行其interrupt handler。作为wakeup source的IRQ会怎样呢?虽然它的中断没有被mask掉,但是其interrupt handler也不会执行(这时候的HW Signal只是用来唤醒系统)。唯一有机会执行的interrupt handler是那些标记IRQF_NO_SUSPEND flag的IRQ,因为它们的中断始终是enable的。当然,这些中断不应该调用enable_irq_wake进行唤醒源的设定。

 

四、Interrupts and Suspend-to-Idle

Suspend-to-idle (也被称为"freeze" 状态)是一个相对比较新的系统电源管理状态,相关代码如下:

static int suspend_enter(suspend_state_t state, bool *wakeup)

{

……

各个设备的late suspend阶段

各个设备的noirq suspend阶段

if (state == PM_SUSPEND_FREEZE) {

freeze_enter();

goto Platform_wake;

}

……

}

Freeze和suspend的前面的操作基本是一样的:首先冻结系统中的进程,然后是suspend系统中的形形色色的device,不一样的地方在noirq suspend完成之后,freeze不会disable那些non-BSP的处理器和syscore suspend阶段,而是调用freeze_enter函数,把所有的处理器推送到idle状态。这时候,任何的enable的中断都可以将系统唤醒。而这也就意味着那些标记IRQF_NO_SUSPEND(其IRQ没有在suspend_device_irqs过程中被mask掉)是有能力将处理器从idle状态中唤醒(不过,需要注意的是:这种信号并不会触发一个系统唤醒信号),而普通中断由于其IRQ被disable了,因此无法唤醒idle状态中的处理器。

那些能够唤醒系统的wakeup interrupt呢?由于其中断没有被mask掉,因此也可以将系统从suspend-to-idle状态中唤醒。整个过程和将系统从suspend状态中唤醒一样,唯一不同的是:将系统从freeze状态唤醒走的中断处理路径,而将系统从suspend状态唤醒走的唤醒处理路径,需要电源管理HW BLOCK中特别的中断处理逻辑的参与。

 

五、IRQF_NO_SUSPEND 标志和enable_irq_wake函数不能同时使用

针对一个设备,在申请中断的时候使用IRQF_NO_SUSPEND flag,又同时调用enable_irq_wake设定唤醒源是不合理的,主要原因如下:

1、如果IRQ没有共享,使用IRQF_NO_SUSPEND flag说明你想要在整个系统的suspend-resume过程中(包括suspend_device_irqs之后的阶段)保持中断打开以便正常的调用其interrupt handler。而调用enable_irq_wake函数则说明你想要将该设备的irq信号设定为中断源,因此并不期望调用其interrupt handler。而这两个需求明显是互斥的。

2、IRQF_NO_SUSPEND 标志和enable_irq_wake函数都不是针对一个interrupt handler的,而是针对该IRQ上的所有注册的handler的。在一个IRQ上共享唤醒源以及no suspend中断源是比较荒谬的。

不过,在非常特殊的场合下,一个IRQ可以被设定为wakeup source,同时也设定IRQF_NO_SUSPEND 标志。为了代码逻辑正确,该设备的驱动代码需要满足一些特别的需求。

 

参考文献

1、内核文档power/suspend-and-interrupts.txt

 

原创文章,转发请注明出处。蜗窝科技

Linux DMA Engine framework(2)_功能介绍及解接口分析

$
0
0

1. 前言

从我们的直观感受来说,DMA并不是一个复杂的东西,要做的事情也很单纯直白。因此Linux kernel对它的抽象和实现,也应该简洁、易懂才是。不过现实却不甚乐观(个人感觉),Linux kernel dmaengine framework的实现,真有点晦涩的感觉。为什么会这样呢?

如果一个软件模块比较复杂、晦涩,要么是设计者的功力不够,要么是需求使然。当然,我们不敢对Linux kernel的那些大神们有丝毫怀疑和不敬,只能从需求上下功夫了:难道Linux kernel中的driver对DMA的使用上,有一些超出了我们日常的认知范围?

要回答这些问题并不难,将dmaengine framework为consumers提供的功能和API梳理一遍就可以了,这就是本文的目的。当然,也可以借助这个过程,加深对DMA的理解,以便在编写那些需要DMA传输的driver的时候,可以更游刃有余。

2. Slave-DMA API和Async TX API

从方向上来说,DMA传输可以分为4类:memory到memory、memory到device、device到memory以及device到device。Linux kernel作为CPU的代理人,从它的视角看,外设都是slave,因此称这些有device参与的传输(MEM2DEV、DEV2MEM、DEV2DEV)为Slave-DMA传输。而另一种memory到memory的传输,被称为Async TX。

为什么强调这种差别呢?因为Linux为了方便基于DMA的memcpy、memset等操作,在dma engine之上,封装了一层更为简洁的API(如下面图片1所示),这种API就是Async TX API(以async_开头,例如async_memcpy、async_memset、async_xor等)。

dma_api

图片1 DMA Engine API示意图

最后,因为memory到memory的DMA传输有了比较简洁的API,没必要直接使用dma engine提供的API,最后就导致dma engine所提供的API就特指为Slave-DMA API(把mem2mem剔除了)。

本文主要介绍dma engine为consumers提供的功能和API,因此就不再涉及Async TX API了(具体可参考本站后续的文章。

注1:Slave-DMA中的“slave”,指的是参与DMA传输的设备。而对应的,“master”就是指DMA controller自身。一定要明白“slave”的概念,才能更好的理解kernel dma engine中有关的术语和逻辑。

3. dma engine的使用步骤

注2:本文大部分内容翻译自kernel document[1],喜欢读英语的读者可以自行参考。

对设备驱动的编写者来说,要基于dma engine提供的Slave-DMA API进行DMA传输的话,需要如下的操作步骤:

1)申请一个DMA channel。

2)根据设备(slave)的特性,配置DMA channel的参数。

3)要进行DMA传输的时候,获取一个用于识别本次传输(transaction)的描述符(descriptor)。

4)将本次传输(transaction)提交给dma engine并启动传输。

5)等待传输(transaction)结束。

然后,重复3~5即可。

上面5个步骤,除了3有点不好理解外,其它的都比较直观易懂,具体可参考后面的介绍。

3.1 申请DMA channel

任何consumer(文档[1]中称作client,也可称作slave driver,意思都差不多,不再特意区分)在开始DMA传输之前,都要申请一个DMA channel(有关DMA channel的概念,请参考[2]中的介绍)。

DMA channel(在kernel中由“struct dma_chan”数据结构表示)由provider(或者是DMA controller)提供,被consumer(或者client)使用。对consumer来说,不需要关心该数据结构的具体内容(我们会在dmaengine provider的介绍中在详细介绍)。

consumer可以通过如下的API申请DMA channel:

struct dma_chan *dma_request_chan(struct device *dev, const char *name);

该接口会返回绑定在指定设备(dev)上名称为name的dma channel。dma engine的provider和consumer可以使用device tree、ACPI或者struct dma_slave_map类型的match table提供这种绑定关系,具体可参考XXXX章节的介绍。

最后,申请得到的dma channel可以在不需要使用的时候通过下面的API释放掉:

void dma_release_channel(struct dma_chan *chan);

3.2 配置DMA channel的参数

driver申请到一个为自己使用的DMA channel之后,需要根据自身的实际情况,以及DMA controller的能力,对该channel进行一些配置。可配置的内容由struct dma_slave_config数据结构表示(具体可参考4.1小节的介绍)。driver将它们填充到一个struct dma_slave_config变量中后,可以调用如下API将这些信息告诉给DMA controller:

int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)

 

3.3 获取传输描述(tx descriptor)

DMA传输属于异步传输,在启动传输之前,slave driver需要将此次传输的一些信息(例如src/dst的buffer、传输的方向等)提交给dma engine(本质上是dma controller driver),dma engine确认okay后,返回一个描述符(由struct dma_async_tx_descriptor抽象)。此后,slave driver就可以以该描述符为单位,控制并跟踪此次传输。

struct dma_async_tx_descriptor数据结构可参考4.2小节的介绍。根据传输模式的不同,slave driver可以使用下面三个API获取传输描述符(具体可参考Documentation/dmaengine/client.txt[1]中的说明):

struct dma_async_tx_descriptor *dmaengine_prep_slave_sg(
        struct dma_chan *chan, struct scatterlist *sgl,
        unsigned int sg_len, enum dma_data_direction direction,
        unsigned long flags);

struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic(
        struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len,
        size_t period_len, enum dma_data_direction direction);

struct dma_async_tx_descriptor *dmaengine_prep_interleaved_dma(
        struct dma_chan *chan, struct dma_interleaved_template *xt,
        unsigned long flags);

dmaengine_prep_slave_sg用于在“scatter gather buffers”列表和总线设备之间进行DMA传输,参数如下:
注3:有关scatterlist 我们在[3][2]中有提及,后续会有专门的文章介绍它,这里暂且按下不表。

chan,本次传输所使用的dma channel。

sgl,要传输的“scatter gather buffers”数组的地址;
sg_len,“scatter gather buffers”数组的长度。

direction,数据传输的方向,具体可参考enum dma_data_direction (include/linux/dma-direction.h)的定义。

flags,可用于向dma controller driver传递一些额外的信息,包括(具体可参考enum dma_ctrl_flags中以DMA_PREP_开头的定义):
DMA_PREP_INTERRUPT,告诉DMA controller driver,本次传输完成后,产生一个中断,并调用client提供的回调函数(可在该函数返回后,通过设置struct dma_async_tx_descriptor指针中的相关字段,提供回调函数,具体可参考4.2小节的介绍);
DMA_PREP_FENCE,告诉DMA controller driver,后续的传输,依赖本次传输的结果(这样controller driver就会小心的组织多个dma传输之间的顺序);
DMA_PREP_PQ_DISABLE_P、DMA_PREP_PQ_DISABLE_Q、DMA_PREP_CONTINUE,PQ有关的操作,TODO。

dmaengine_prep_dma_cyclic常用于音频等场景中,在进行一定长度的dma传输(buf_addr&buf_len)的过程中,每传输一定的byte(period_len),就会调用一次传输完成的回调函数,参数包括:

chan,本次传输所使用的dma channel。

buf_addr、buf_len,传输的buffer地址和长度。

period_len,每隔多久(单位为byte)调用一次回调函数。需要注意的是,buf_len应该是period_len的整数倍。

direction,数据传输的方向。

dmaengine_prep_interleaved_dma可进行不连续的、交叉的DMA传输,通常用在图像处理、显示等场景中,具体可参考struct dma_interleaved_template结构的定义和解释(这里不再详细介绍,需要用到的时候,再去学习也okay)。

3.4 启动传输

通过3.3章节介绍的API获取传输描述符之后,client driver可以通过dmaengine_submit接口将该描述符放到传输队列上,然后调用dma_async_issue_pending接口,启动传输。

dmaengine_submit的原型如下:

dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)

参数为传输描述符指针,返回一个唯一识别该描述符的cookie,用于后续的跟踪、监控。

dma_async_issue_pending的原型如下:

void dma_async_issue_pending(struct dma_chan *chan);

参数为dma channel,无返回值。

注4:由上面两个API的特征可知,kernel dma engine鼓励client driver一次提交多个传输,然后由kernel(或者dma controller driver)统一完成这些传输。

3.5 等待传输结束

传输请求被提交之后,client driver可以通过回调函数获取传输完成的消息,当然,也可以通过dma_async_is_tx_complete等API,测试传输是否完成。不再详细说明了。

最后,如果等不及了,也可以使用dmaengine_pause、dmaengine_resume、dmaengine_terminate_xxx等API,暂停、终止传输,具体请参考kernel document[1]以及source code。

4. 重要数据结构说明

4.1 struct dma_slave_config

中包含了完成一次DMA传输所需要的所有可能的参数,其定义如下:

/* include/linux/dmaengine.h */

struct dma_slave_config {
        enum dma_transfer_direction direction;
        phys_addr_t src_addr;
        phys_addr_t dst_addr;
        enum dma_slave_buswidth src_addr_width;
        enum dma_slave_buswidth dst_addr_width;
        u32 src_maxburst;
        u32 dst_maxburst;
        bool device_fc;
        unsigned int slave_id;
};

direction,指明传输的方向,包括(具体可参考enum dma_transfer_direction的定义和注释):
    DMA_MEM_TO_MEM,memory到memory的传输;
    DMA_MEM_TO_DEV,memory到设备的传输;
    DMA_DEV_TO_MEM,设备到memory的传输;
    DMA_DEV_TO_DEV,设备到设备的传输。
    注5:controller不一定支持所有的DMA传输方向,具体要看provider的实现。
    注6:参考第2章的介绍,MEM to MEM的传输,一般不会直接使用dma engine提供的API。

src_addr,传输方向是dev2mem或者dev2dev时,读取数据的位置(通常是固定的FIFO地址)。对mem2dev类型的channel,不需配置该参数(每次传输的时候会指定);
dst_addr,传输方向是mem2dev或者dev2dev时,写入数据的位置(通常是固定的FIFO地址)。对dev2mem类型的channel,不需配置该参数(每次传输的时候会指定);
src_addr_width、dst_addr_width,src/dst地址的宽度,包括1、2、3、4、8、16、32、64(bytes)等(具体可参考enum dma_slave_buswidth 的定义)。

src_maxburst、dst_maxburst,src/dst最大可传输的burst size(可参考[2]中有关burst size的介绍),单位是src_addr_width/dst_addr_width(注意,不是byte)。

device_fc,当外设是Flow Controller(流控制器)的时候,需要将该字段设置为true。CPU中有关DMA和外部设备之间连接方式的设计中,决定DMA传输是否结束的模块,称作flow controller,DMA controller或者外部设备,都可以作为flow controller,具体要看外设和DMA controller的设计原理、信号连接方式等,不在详细说明(感兴趣的同学可参考[4]中的介绍)。

slave_id,外部设备通过slave_id告诉dma controller自己是谁(一般和某个request line对应)。很多dma controller并不区分slave,只要给它src、dst、len等信息,它就可以进行传输,因此slave_id可以忽略。而有些controller,必须清晰地知道此次传输的对象是哪个外设,就必须要提供slave_id了(至于怎么提供,可dma controller的硬件以及驱动有关,要具体场景具体对待)。

4.2 struct dma_async_tx_descriptor

传输描述符用于描述一次DMA传输(类似于一个文件句柄)。client driver将自己的传输请求通过3.3中介绍的API提交给dma controller driver后,controller driver会返回给client driver一个描述符。

client driver获取描述符后,可以以它为单位,进行后续的操作(启动传输、等待传输完成、等等)。也可以将自己的回调函数通过描述符提供给controller driver。

传输描述符的定义如下:

struct dma_async_tx_descriptor {
         dma_cookie_t cookie;
         enum dma_ctrl_flags flags; /* not a 'long' to pack with cookie */
         dma_addr_t phys;
         struct dma_chan *chan;
         dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx);
         int (*desc_free)(struct dma_async_tx_descriptor *tx);
         dma_async_tx_callback callback;
         void *callback_param;
         struct dmaengine_unmap_data *unmap;
#ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH
         struct dma_async_tx_descriptor *next;
         struct dma_async_tx_descriptor *parent;
         spinlock_t lock;
#endif
};

cookie,一个整型数,用于追踪本次传输。一般情况下,dma controller driver会在内部维护一个递增的number,每当client获取传输描述的时候(参考3.3中的介绍),都会将该number赋予cookie,然后加一。
注7:有关cookie的使用场景,我们会在后续的文章中再详细介绍。

flags, DMA_CTRL_开头的标记,包括:
DMA_CTRL_REUSE,表明这个描述符可以被重复使用,直到它被清除或者释放;
DMA_CTRL_ACK,如果该flag为0,表明暂时不能被重复使用。

phys,该描述符的物理地址??不太懂!

chan,对应的dma channel。

tx_submit,controller driver提供的回调函数,用于把改描述符提交到待传输列表。通常由dma engine调用,client driver不会直接和该接口打交道。

desc_free,用于释放该描述符的回调函数,由controller driver提供,dma engine调用,client driver不会直接和该接口打交道。

callback、callback_param,传输完成的回调函数(及其参数),由client driver提供。

后面其它参数,client driver不需要关心,暂不描述了。

5. 参考文档

[1] Documentation/dmaengine/client.txt

[2] Linux DMA Engine framework(1)_概述

[3] Linux MMC framework(2)_host controller driver

[4] https://forums.xilinx.com/xlnx/attachments/xlnx/ELINUX/10658/1/drivers-session4-dma-4public.pdf

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

Linux的时钟

$
0
0

一、前言

时钟或者钟表(clock)是一种计时工具,每个人都至少有一块,可能在你的手机里,也可能佩戴在你的手腕上。如果Linux也是一个普通人的话,那么她的手腕上应该有十几块手表,包括:CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_PROCESS_CPUTIME_ID、CLOCK_THREAD_CPUTIME_ID、CLOCK_MONOTONIC_RAW、CLOCK_REALTIME_COARSE、CLOCK_MONOTONIC_COARSE、CLOCK_BOOTTIME、CLOCK_REALTIME_ALARM、CLOCK_BOOTTIME_ALARM、CLOCK_TAI。本文主要就是介绍Linux内核中的形形色色的“钟表”。

 

二、理解Linux中各种clock分类的基础

既然本文讲Linux中的计时工具,那么我们首先面对的就是“什么是时间?”,这个问题实在是太难回答了,因此我们这里就不正面回答了,我们只是从几个侧面来窥探时间的特性,而时间的本质就留给物理学家和哲学家思考吧。

1、如何度量时间

时间往往是和变化相关,因此人们往往喜欢使用有固定周期变化规律的运动行为来定义时间,于是人们把地球围绕太阳旋转一周的时间分成24份,每一份定义为一个小时,而一个小时被平均分成3600份,每一份就是1秒。然而,地球的运动周期不是那么稳定,怎么办?多测量几个,平均一下嘛。

虽然通过天体的运动定义了秒这样的基本的时间度量单位,但是,要想精确的表示时间,我们依赖一种有稳定的周期变化的现象。上一节我们说过了:地球围绕太阳运转不是一个稳定的周期现象,因此每次观察到的周期不是固定的(当然都大约是24小时的样子),用它来定义秒多少显得不是那么精准。科学家们发现铯133原子在能量跃迁时候辐射的电磁波的振荡频率非常的稳定(不要问我这是什么原理,我也不知道),因此被用来定义时间的基本单位:秒(或者称之为原子秒)。

2、Epoch

定义了时间单位,等于时间轴上有了刻度,虽然这条代表时间的直线我们不知道从何开始,最终去向何方,我们终归是可以把一个时间点映射到这条直线上了。甚至如果定义了原点,那么我们可以用一个数字(到原点的距离)来表示时间。

如果说定义时间的度量单位是技术活,那么定义时间轴的原点则完全是一个习惯问题。拿出你的手表,上面可以读出2017年5月10,23时17分28秒07毫秒……作为一个地球人,你选择了耶稣诞辰日做原点,讲真,这弱爆了。作为linuxer,你应该拥有这样的一块手表,从这个手表上只能看到一个从当前时间点到linux epoch的秒数和毫秒数。Linux epoch定义为1970-01-01 00:00:00 +0000 (UTC),后面的这个UTC非常非常重要,我们后面会描述。

除了wall time,linux系统中也需要了解系统自启动以来过去了多少的时间,这时候,我们可以把钟表的epoch调整成系统的启动时间点,这时候获取系统启动时间就很容易了,直接看这块钟表的读数即可。

3、时间调整

记得小的时候,每隔一段时间,老爸的手表总会慢上一分钟左右的时间,也是他总是在7点钟,新闻联播之前等待那校时的最后一响。一听到“刚才最后一响是北京时间7点整”中那最后“滴”的一声,老爸也把自己的手表调整成为7点整。对于linux系统,这个操作类似clock_set接口函数。

类似老爸机械表的时间调整,linux的时间也需要调整,机械表的发条和齿轮结构没有那么精准,计算机的晶振亦然。前面讲了,UTC的计时是基于原子钟的,但是来到Linux内核这个场景,我们难道要为我们的计算机安装一个原子钟来计时吗?当然可以,如果你足够有钱的话。我们一般人的计算机还是基于系统中的本地振荡器来计时的,虽然精度不理想,但是短时间内你也不会有太多的感觉。当然,人们往往是向往更精确的计时(有些场合也需要),因此就有了时间同步的概念(例如NTP(Network Time Protocol))。

所谓时间同步其实就是用一个精准的时间来调整本地的时间,具体的调整方式有两种,一种就是直接设定当前时间值,另外一种是采用了润物细无声的形式,对本地振荡器的输出进行矫正。第一种方法会导致时间轴上的时间会向前或者向后的跳跃,无法保证时间的连续性和单调性。第二种方法是对时间轴缓慢的调整(而不是直接设定),从而保证了连续性和单调性。

4、闰秒(leap second)

通过原子秒延展出来的时间轴就是TAI(International Atomic Time)clock。这块“表”不管日出、日落,机械的按照ce原子定义的那个秒在推进时间。冷冰冰的TAI clock虽然精准,但是对人类而言是不友好的,毕竟人还是生活在这颗蓝色星球上。而那些基于地球自转,公转周期的时间(例如GMT)虽然符合人类习惯,但是又不够精确。在这样的背景下,UTC(Coordinated Universal Time)被提出来了,它是TAI clock的基因(使用原子秒),但是又会适当的调整(leap second),满足人类生产和生活的需要。

OK,至此,我们了解了TAI和UTC两块表的情况,这两块表的发条是一样的,按照同样的时间滴答(tick,精准的根据原子频率定义的那个秒)来推动钟表的秒针的转动,唯一不同的是,UTC clock有一个调节器,在适当的时间,可以把秒针向前或者向后调整一秒。

TAI clock和UTC clock在1972年进行了对准(相差10秒),此后就各自独立运行了。在大部分的时间里,UTC clock跟随TAI clock,除了在适当的时间点,realtime clock会进行leap second的补偿。从1972年到2017年,已经有了27次leap second,因此TAI clock的读数已经比realtime clock(UTC时间)快了37秒。换句话说,TAI和UTC两块表其实可以抽象成一个时间轴,只不过它们之间有一个固定的偏移。在1972年,它们之间的offset是10秒,经过多年的运转,到了2017年,offset累计到37秒,让我静静等待下一个leap second到了的时刻吧。

5、计时范围

有一类特殊的clock称作秒表,启动后开始计时,中间可以暂停,可以恢复。我们可以通过这样的秒表来记录一个人睡眠的时间,当进入睡眠状态的时候,按下start按键开始计时,一旦醒来则按下stop,暂停计时。linux中也有这样的计时工具,用来计算一个进程或者线程的执行时间。

6、时间精度

时间是连续的吗?你眼中的世界是连续的吗?看到窗外清风吹拂的树叶的时候,你感觉每一个树叶的形态都被你捕捉到了。然而,未必,你看急速前进的汽车的轮胎的时候,感觉车轮是倒转的。为什么?其实这仅仅是因为我们的眼睛大约是每秒15~20帧的速度在采样这个世界,你看到的世界是离散的。算了,扯远了,我们姑且认为时间的连续的,但是Linux中的时间记录却不是连续的,我们可以用下面的图片表示:

time-sample

系统在每个tick到来的时候都会更新系统时间(到linux epoch的秒以及纳秒值记录),当然,也有其他场景进行系统时间的更新,这里就不赘述了。因此,对于linux的时间而言,它是一些离散值,是一些时间采样点的值而已。当用户请求时间服务的时候,例如获取当前时间(上图中的红线),那么最近的那个Tick对应的时间采样点值再加上一个当前时间点到上一个tick的delta值就精准的定位了当前时间。不过,有些场合下,时间精度没有那么重要,直接获取上一个tick的时间值也基本是OK的,不需要校准那个delta也能满足需求。而且粗粒度的clock会带来performance的优势。

7、睡觉的时候时间会停止运作吗?

在现实世界提出这个问题会稍显可笑,鲁迅同学有一句名言:时间永是流逝,街市依旧太平。但是对于Linux系统中的clock,这个就有现实的意义了。比如说clock的一个重要的派生功能是创建timer(也就是说timer总是基于一个特定的clock运作)。在一个5秒的timer超期之前,系统先进入了suspend或者关机状态,这时候,5秒时间到达的时候,一般的timer都不会触发,因为底层的clock可能是基于一个free running counter的,在suspend或者关机状态的时候,这个HW counter都不再运作了,你如何期盼它能唤醒系统,来执行timer expired handler?但是用户还是有这方面的实际需求的,最简单的就是关机闹铃。怎么办?这就需要一个特别的clock,能够在suspend或者关机的时候,仍然可以运作,推动timer到期触发。

 

三、Linux下的各种clock总结

在linux系统中定义了如下的clock id:

#define CLOCK_REALTIME            0
#define CLOCK_MONOTONIC            1
#define CLOCK_PROCESS_CPUTIME_ID    2
#define CLOCK_THREAD_CPUTIME_ID        3
#define CLOCK_MONOTONIC_RAW        4
#define CLOCK_REALTIME_COARSE        5
#define CLOCK_MONOTONIC_COARSE        6
#define CLOCK_BOOTTIME            7
#define CLOCK_REALTIME_ALARM        8
#define CLOCK_BOOTTIME_ALARM        9
#define CLOCK_SGI_CYCLE            10    /* Hardware specific */
#define CLOCK_TAI            11

CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID这两个clock是专门用来计算进程或者线程的执行时间的(用于性能剖析),一旦进程(线程)被切换出去,那么该进程(线程)的clock就会停下来。因此,这两种的clock都是per-process或者per-thread的,而其他的clock都是系统级别的。

根据上面一章的各种分类因素,我们可以将其他clock总结整理如下:

  leap second? clock set? clock tunning? original point resolution active in suspend?
realtime yes yes yes Linux epoch ns no
monotonic yes no yes Linux epoch ns no
monotonic raw yes no no Linux epoch ns no
realtime coarse yes yes yes Linux epoch tick no
monotonic coarse yes no yes Linux epoch tick no
boot time yes no yes machine start ns no
realtime alarm yes yes yes Linux epoch ns yes
boottime alarm yes no yes machine start ns yes
tai no no no Linux epoch ns no

 

 原创文章,转发请注明出处。蜗窝科技

Linux DMA Engine framework(3)_dma controller驱动

$
0
0

1. 前言

本文将从provider的角度,介绍怎样在linux kernel dmaengine的框架下,编写dma controller驱动。

2. dma controller驱动的软件框架

设备驱动的本质是描述并抽象硬件,然后为consumer提供操作硬件的友好接口。dma controller驱动也不例外,它要做的事情无外乎是:

1)抽象并控制DMA控制器。

2)管理DMA channel(可以是物理channel,也可以是虚拟channel,具体可参考[1]中的介绍),并向client driver提供友好、易用的接口。

3)以DMA channel为操作对象,响应client driver(consumer)的传输请求,并控制DMA controller,执行传输。

当然,按照惯例,为了统一提供给consumer的API(参考[2]),并减少DMA controller driver的开发难度(从论述题变为填空题),dmaengine framework提供了一套controller driver的开发框架,主要思路是(参考图片1):

dma驱动框架

图片1 DMA驱动框架

1)使用struct dma_device抽象DMA controller,controller driver只要填充该结构中必要的字段,就可以完成dma controller的驱动开发。

2)使用struct dma_chan(图片1中的DCn)抽象物理的DMA channel(图片1中的CHn),物理channel和controller所能提供的通道数一一对应。

3)基于物理的DMA channel,使用struct virt_dma_cha抽象出虚拟的dma channel(图片1中的VCx)。多个虚拟channel可以共享一个物理channel,并在这个物理channel上进行分时传输。

4)基于这些数据结构,提供一些便于controller driver开发的API,供driver使用。

上面三个数据结构的描述,可参考第3章的介绍。然后,我们会在第4章介绍相关的API、controller driver的开发思路和步骤以及dmaengine中和controller driver有关的重要流程。

3. 主要数据结构描述

3.1 struct dma_device

用于抽象dma controller的struct dma_device是一个庞杂的数据结构(具体可参考include/linux/dmaengine.h中的代码),不过真正需要dma controller driver关心的内容却不是很多,主要包括:

注1:为了加快对dmaengine framework的理解和掌握,这里只描述一些简单的应用场景,更复杂的场景,只有等到有需求的时候,再更深入的理解。

channels,一个链表头,用于保存该controller支持的所有dma channel(struct dma_chan,具体可参考3.2小节)。在初始化的时候,dma controller driver首先要调用INIT_LIST_HEAD初始化它,然后调用list_add_tail将所有的channel添加到该链表头中。

cap_mask,一个bitmap,用于指示该dma controller所具备的能力(可以进行什么样的DMA传输),例如(具体可参考enum dma_transaction_type的定义):
    DMA_MEMCPY,可进行memory copy;
    DMA_MEMSET,可进行memory set;
    DMA_SG,可进行scatter list传输;
    DMA_CYCLIC,可进行cyclic类[2]的传输;
    DMA_INTERLEAVE,可进行交叉传输[2]
    等等,等等(各种奇奇怪怪的传输类型,不看不知道,一看吓一跳!!)。
另外,该bitmap的定义,需要和后面device_prep_dma_xxx形式的回调函数对应(bitmap中支持某个传输类型,就必须提供该类型对应的回调函数)。

src_addr_widths,一个bitmap,表示该controller支持哪些宽度的src类型,包括1、2、3、4、8、16、32、64(bytes)等(具体可参考enum dma_slave_buswidth 的定义)。
dst_addr_widths,一个bitmap,表示该controller支持哪些宽度的dst类型,包括1、2、3、4、8、16、32、64(bytes)等(具体可参考enum dma_slave_buswidth 的定义)。

directions,一个bitmap,表示该controller支持哪些传输方向,包括DMA_MEM_TO_MEM、DMA_MEM_TO_DEV、DMA_DEV_TO_MEM、DMA_DEV_TO_DEV,具体可参考enum dma_transfer_direction的定义和注释,以及[2]中相关的说明。

max_burst,支持的最大的burst传输的size。有关burst传输的概念可参考[1]。

descriptor_reuse,指示该controller的传输描述可否可重复使用(client driver可只获取一次传输描述,然后进行多次传输)。

device_alloc_chan_resources/device_free_chan_resources,client driver申请/释放[2] dma channel的时候,dmaengine会调用dma controller driver相应的alloc/free回调函数,以准备相应的资源。具体要准备哪些资源,则需要dma controller driver根据硬件的实际情况,自行决定(这就是dmaengine framework的流氓之处,呵呵~)。

device_prep_dma_xxx,同理,client driver通过dmaengine_prep_xxx API获取传输描述符的时候,damengine则会直接回调dma controller driver相应的device_prep_dma_xxx接口。至于要在这些回调函数中做什么事情,dma controller driver自己决定就是了(真懒啊!)。

device_config,client driver调用dmaengine_slave_config[2]配置dma channel的时候,dmaengine会调用该回调函数,交给dma controller driver处理。

device_pause/device_resume/device_terminate_all,同理,client driver调用dmaengine_pause、dmaengine_resume、dmaengine_terminate_xxx等API的时候,dmaengine会调用相应的回调函数。

device_issue_pending,client driver调用dma_async_issue_pending启动传输的时候,会调用调用该回调函数。

总结:dmaengine对dma controller的抽象和封装,只是薄薄的一层:仅封装出来一些回调函数,由dma controller driver实现,被client driver调用,dmaengine本身没有太多的操作逻辑。

3.2 struct dma_chan

struct dma_chan用于抽象dma channel,其内容为:

struct dma_chan {
        struct dma_device *device;
        dma_cookie_t cookie;
        dma_cookie_t completed_cookie;

        /* sysfs */
        int chan_id;
        struct dma_chan_dev *dev;

        struct list_head device_node;
        struct dma_chan_percpu __percpu *local;
        int client_count;
        int table_count;

        /* DMA router */
        struct dma_router *router;
        void *route_data;

        void *private;
};

需要dma controller driver关心的字段包括:

device,指向该channel所在的dma controller。

cookie,client driver以该channel为操作对象获取传输描述符时,dma controller driver返回给client的最后一个cookie。

completed_cookie,在这个channel上最后一次完成的传输的cookie。dma controller driver可以在传输完成时调用辅助函数dma_cookie_complete设置它的value。

device_node,链表node,用于将该channel添加到dma_device的channel列表中。

router、route_data,TODO。

3.3 struct virt_dma_cha

struct virt_dma_chan用于抽象一个虚拟的dma channel,多个虚拟channel可以共用一个物理channel,并由软件调度多个传输请求,将多个虚拟channel的传输串行地在物理channel上完成。该数据结构的定义如下:

/* drivers/dma/virt-dma.h */

struct virt_dma_desc {
        struct dma_async_tx_descriptor tx;
        /* protected by vc.lock */
        struct list_head node;
};

struct virt_dma_chan {
        struct dma_chan chan;
        struct tasklet_struct task;
        void (*desc_free)(struct virt_dma_desc *);

        spinlock_t lock;

        /* protected by vc.lock */
        struct list_head desc_allocated;
        struct list_head desc_submitted;
        struct list_head desc_issued;
        struct list_head desc_completed;

        struct virt_dma_desc *cyclic;

};

chan,一个struct dma_chan类型的变量,用于和client driver打交道(屏蔽物理channel和虚拟channel的差异)。

task,一个tasklet,用于等待该虚拟channel上传输的完成(由于是虚拟channel,传输完成与否只能由软件判断)。

desc_allocated、desc_submitted、desc_issued、desc_completed,四个链表头,用于保存不同状态的虚拟channel描述符(struct virt_dma_desc,仅仅对struct dma_async_tx_descriptor[2]做了一个简单的封装)。

4. dmaengine向dma controller driver提供的API汇整

damengine直接向dma controller driver提供的API并不多(大部分的逻辑交互都位于struct dma_device结构的回调函数中),主要包括:

1)struct dma_device变量的注册和注销接口

/* include/linux/dmaengine.h */
int dma_async_device_register(struct dma_device *device);
void dma_async_device_unregister(struct dma_device *device);

dma controller driver准备好struct dma_device变量后,可以调用dma_async_device_register将它(controller)注册到kernel中。该接口会对device指针进行一系列的检查,然后对其做进一步的初始化,最后会放在一个名称为dma_device_list的全局链表上,以便后面使用。

dma_async_device_unregister,注销接口。

2)cookie有关的辅助接口,位于“drivers/dma/dmaengine.h”中,包括

static inline void dma_cookie_init(struct dma_chan *chan)
static inline dma_cookie_t dma_cookie_assign(struct dma_async_tx_descriptor *tx)
static inline void dma_cookie_complete(struct dma_async_tx_descriptor *tx)
static inline enum dma_status dma_cookie_status(struct dma_chan *chan,
        dma_cookie_t cookie, struct dma_tx_state *state)

由于cookie有关的操作,有很多共性,dmaengine就提供了一些通用实现:

void dma_cookie_init,初始化dma channel中的cookie、completed_cookie字段。

dma_cookie_assign,为指针的传输描述(tx)分配一个cookie。

dma_cookie_complete,当某一个传输(tx)完成的时候,可以调用该接口,更新该传输所对应channel的completed_cookie字段。

dma_cookie_status,获取指定channel(chan)上指定cookie的传输状态。

3)依赖处理接口

void dma_run_dependencies(struct dma_async_tx_descriptor *tx);

由前面的描述可知,client可以同时提交多个具有依赖关系的dma传输。因此当某个传输结束的时候,dma controller driver需要检查是否有依赖该传输的传输,如果有,则传输之。这个检查并传输的过程,可以借助该接口进行(dma controller driver只需调用即可,省很多事)。

4)device tree有关的辅助接口

extern struct dma_chan *of_dma_simple_xlate(struct of_phandle_args *dma_spec,
                struct of_dma *ofdma);
extern struct dma_chan *of_dma_xlate_by_chan_id(struct of_phandle_args *dma_spec,
                struct of_dma *ofdma);

上面两个接口可用于将client device node中有关dma的字段解析出来,并获取对应的dma channel。后面实际开发的时候会举例说明。

5)虚拟dma channel有关的API

后面会有专门的文章介绍虚拟dma,这里不再介绍。

5. 编写一个dma controller driver的方法和步骤

上面啰嗦了这么多,相信大家还是似懂非懂(很正常,我也是,dmaengine framework特点就是框架简单,细节复杂)。到底怎么在dmaengine的框架下编写dma controller驱动呢?现在看来,只靠这篇文章,可能达不到目的了,这里先罗列一下基本步骤,后续我们会结合实际的开发过程,进一步的理解和掌握。

编写一个dma controller driver的基本步骤包括(不考虑虚拟channel的情况):

1)定义一个struct dma_device变量,并根据实际的硬件情况,填充其中的关键字段。

2)根据controller支持的channel个数,为每个channel定义一个struct dma_chan变量,进行必要的初始化后,将每个channel都添加到struct dma_device变量的channels链表中。

3)根据硬件特性,实现struct dma_device变量中必要的回调函数(device_alloc_chan_resources/device_free_chan_resources、device_prep_dma_xxx、device_config、device_issue_pending等等)。

4)调用dma_async_device_register将struct dma_device变量注册到kernel中。

5)当client driver申请dma channel时(例如通过device tree中的dma节点获取),dmaengine core会调用dma controller driver的device_alloc_chan_resources函数,controller driver需要在这个接口中奖该channel的资源准备好。

6)当client driver配置某个dma channel时,dmaengine core会调用dma controller driver的device_config函数,controller driver需要在这个函数中将client想配置的内容准备好,以便进行后续的传输。

7)client driver开始一个传输之前,会把传输的信息通过dmaengine_prep_slave_xxx接口交给controller driver,controller driver需要在对应的device_prep_dma_xxx回调中,将这些要传输的内容准备好,并返回给client driver一个传输描述符。

8)然后,client driver会调用dmaengine_submit将该传输提交给controller driver,此时dmaengine会调用controller driver为每个传输描述符所提供的tx_submit回调函数,controller driver需要在这个函数中将描述符挂到该channel对应的传输队列中。

9)client driver开始传输时,会调用dma_async_issue_pending,controller driver需要在对应的回调函数(device_issue_pending)中,依次将队列上所有的传输请求提交给硬件。

10)等等。

6. 参考文档

[1] Linux DMA Engine framework(1)_概述

[2] Linux DMA Engine framework(2)_功能介绍及解接口分析

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

为什么会有文件系统(二)

$
0
0

距我将全套盗墓笔记成功保存在8MB空间里已经过去了19天58分钟32秒,我渐渐发觉更高、更快、更强的绝不限于奥运精神,也充分体现了人类贪婪的本质,无尽的需求催生出这光怪陆离的大千世界。

就在今天下午,我得到一个通知,要么继续使用连续的存储空间,但是只能有4MB,要么去使用不连续的存储空间,总量可以仍然是8MB,那一刻,我的内心反而是平静的,因为我知道,这就是现实,一个不够优秀的系统是无法满足各种刁钻的需求的,并且我并不想丢掉一半的盗墓笔记,所以我必须使用不连续的存储空间,一个不算坏的消息是,就算是不连续,但是每块最小也有2048字节,并且连续的存储空间是2048字节对齐的,还有什么好说的,撸起袖子加油干,这很2017。

当时我的脑海中,浮现出了星空的图像,天顶中每颗闪烁的星代表的就是一段文字,我要怎么将它们串在一起呢?我想,首先要解决的是识别问题,即眼前的这颗星属于哪本书?是的,我需要星的索引信息,每条索引信息对应着一段可存储的空间,记录空间在硬盘中的偏移,长度,内容是属于哪本书,对应内容在书内的偏移,这样通过索引信息就可以在硬盘中找到存储着的盗墓笔记的片段了,于是有了如下的设计,

book_name用来存储书名,hd_ofs存储这段存储空间在硬盘中的偏移,file_ofs存储这段存储空间存储的内容在书中的偏移,chunk_len存储这段存储空间的长度,看起来是能工作的,那么这样的设计够不够好呢,答案显然是需要拿出工匠精神再来打磨一下了。

book_name,这里看起来很糟糕,如果书名很长则无法存储完整,如果书名很短则浪费了存储空间,这里真的需要存储一个书名吗?按照我的需求,盗墓笔记全套是8本书,那么第一本书,我这里记录1即可,依次则是2,3,4,...,我只需要数字就可以进行区分,于是新的设计出现了


但是,新的问题又出现了,我能够通过一个个的index对象找到数据块,但是我该如何找到这些index对象呢?由于每个index对象占用12字节,那么将index搓堆存在一个只存储index的数据块内,那么一个块能存170个index,就像下面这样

很好,现在有了一个index块,那么170个index最多只能映射(170 * 2048)字节(340KB)的内容,可我要存储的盗墓笔记不止这么点内容,所以还需要更多的index块

很好,现在有了更多的index块,我能通过index找到想要看的内容,但是index块也是不连续的,我要如何找到index块在哪里呢?其实,我对之前每个数据块填充170个index对象已经感觉难受了,因为170个index对象只使用了2040字节,这样一个数据块就有8字节的浪费,如果这8字节用来存储另一个index块在硬盘中的偏移位置,那么index块之间就能串联在一起,而我要做的就是找到那个入口


经过了两顿烧烤的谈判,我终于赢得了硬盘第1024个数据块的永久使用权,于是第1024数据块就成为了串起整部盗墓笔记的那个入口

(未完待续)

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

linux内核中的GPIO系统之(4):pinctrl驱动的理解和总结

$
0
0

1. 前言

本站之前的三篇文章[1][2][3]介绍了pin controller(对应的pin controller subsystem)、gpio controller(对应的GPIO subsystem)有关的基本概念,包括pin multiplexing、pin configuration等等。本文将基于这些文章,单纯地从pin controller driver的角度(屏蔽掉pinctrl core的实现细节),理解pinctrl subsystem的设计思想,并掌握pinctrl驱动的移植和实现方法。

2. pin controller的概念和软件抽象

相信每一个嵌入式从业人员,都知道“pin(管脚)”是什么东西(就不赘述了)。由于SoC系统越来越复杂、集成度越来越高,SoC中pin的数量也越来越多、功能也越来越复杂,这就对如何管理、使用这些pins提出了挑战。因此,用于管理这些pins的硬件模块(pin controller)就出现了。相应地,linux kernel也出现了对应的驱动(pin controller driver)。

Kernel pinctrl core使用struct pinctrl_desc抽象一个pin controller,该结构的定义如下(先贴在这里,后面会围绕这个抽象一步步展开):

/* include/linux/pinctrl/pinctrl.h */
struct pinctrl_desc {
        const char *name;
        const struct pinctrl_pin_desc *pins;
        unsigned int npins;
        const struct pinctrl_ops *pctlops;
        const struct pinmux_ops *pmxops;
        const struct pinconf_ops *confops;
        struct module *owner;
#ifdef CONFIG_GENERIC_PINCONF
        unsigned int num_custom_params;
        const struct pinconf_generic_params *custom_params;
        const struct pin_config_item *custom_conf_items;
#endif
};

注1:本文后续的描述基于本站“X Project”所使用的kernel版本[4]
注2:本文很多的表述(特别是例子),都是引用kernel的document[5](写的很好,可以耐心看看)。

2.1 Pin

kernel的pin controller子系统要想管理好系统的pin资源,第一个要搞明白的问题就是:系统中到底有多少个pin?用软件语言来表述就是:要把系统中所有的pin描述出来,并建立索引。这由上面struct pinctrl_desc结构中pins和npins来完成。

对pinctrl core来说,它只关心系统中有多少个pin,并使用自然数为这些pin编号,后续的操作,都是以这些编号为操作对象。至于编号怎样和具体的pin对应上,完全是pinctrl driver自己的事情。

因此,pinctrl driver需要根据实际情况,将系统中所有的pin组织成一个struct pinctrl_pin_desc类型的数组,该类型的定义为:

/**
* struct pinctrl_pin_desc - boards/machines provide information on their
  * pins, pads or other muxable units in this struct
  * @number: unique pin number from the global pin number space
  * @name: a name for this pin
  * @drv_data: driver-defined per-pin data. pinctrl core does not touch this
  */
struct pinctrl_pin_desc {
        unsigned number;
        const char *name;
        void *drv_data;
};

number和name完全由driver自己决定,不过要遵循有利于代码编写、有利于理解等原则。另外,为了便于driver的编写,可以在drv_data中保存driver的私有数据结构(可以包含相关的寄存器偏移等信息)。

注3:[5]中有个例子,大家可以参考理解。

2.2 Pin groups

在SoC系统中,有时需要将很多pin组合在一起,以实现特定的功能,例如SPI接口、I2C接口等。因此pin controller需要以group为单位,访问、控制多个pin,这就是pin groups。相应地,pin controller subsystem需要提供一些机制,来获取系统中到底有多少groups、每个groups包含哪些pins、等等。

因此,pinctrl core在struct pinctrl_ops中抽象出三个回调函数,用来获取pin groups相关信息,如下:

struct pinctrl_ops {
        int (*get_groups_count) (struct pinctrl_dev *pctldev);
        const char *(*get_group_name) (struct pinctrl_dev *pctldev,
                                        unsigned selector);
        int (*get_group_pins) (struct pinctrl_dev *pctldev,
                               unsigned selector,
                               const unsigned **pins,
                               unsigned *num_pins);
        void (*pin_dbg_show) (struct pinctrl_dev *pctldev, struct seq_file *s,
                          unsigned offset);
        int (*dt_node_to_map) (struct pinctrl_dev *pctldev,
                               struct device_node *np_config,
                               struct pinctrl_map **map, unsigned *num_maps);
        void (*dt_free_map) (struct pinctrl_dev *pctldev,
                             struct pinctrl_map *map, unsigned num_maps);
};

get_groups_count,获取系统中pin groups的个数,后续的操作,将以相应的索引为单位(类似数组的下标,个数为数组的大小)。

get_group_name,获取指定group(由索引selector指定)的名称。

get_group_pins,获取指定group的所有pins(由索引selector指定),结果保存在pins(指针数组)和num_pins(指针)中。

注4:dt_node_to_map用于将device tree中的pin state信息转换为pin map,具体可参考后面第3章、第4章的介绍。

当然,最终的group信息是由pinctrl driver提供的,至于driver怎么组织这些group,那是driver自己的事情了,[4]中有一个例子,大家可以参考(在编写pinctrl driver的时候直接copy然后rename即可)。  

2.3. Pin configuration(对象是pin或者pin group)

2.1和2.2中介绍了pinctrl subsystem中的操作对象(pin or pin group)以及抽象方法。嵌入式系统的工程师都知道,SoC中的管脚有些属性可以配置,例如上拉、下拉、高阻、驱动能力等。pinctrl subsystem使用pin configuration来封装这些功能,具体体现在struct pinconf_ops数据结构中,如下:

struct pinconf_ops {
#ifdef CONFIG_GENERIC_PINCONF
         bool is_generic;
#endif
        int (*pin_config_get) (struct pinctrl_dev *pctldev,
                               unsigned pin,
                               unsigned long *config);
        int (*pin_config_set) (struct pinctrl_dev *pctldev,
                               unsigned pin,
                                unsigned long *configs,
                                unsigned num_configs);
        int (*pin_config_group_get) (struct pinctrl_dev *pctldev,
                                      unsigned selector,
                                      unsigned long *config);
        int (*pin_config_group_set) (struct pinctrl_dev *pctldev,
                                      unsigned selector,
                                      unsigned long *configs,
                                     unsigned num_configs);
        int (*pin_config_dbg_parse_modify) (struct pinctrl_dev *pctldev,
                                            const char *arg,
                                            unsigned long *config);
        void (*pin_config_dbg_show) (struct pinctrl_dev *pctldev,
                                      struct seq_file *s,
                                      unsigned offset);
        void (*pin_config_group_dbg_show) (struct pinctrl_dev *pctldev,
                                            struct seq_file *s,
                                            unsigned selector);
        void (*pin_config_config_dbg_show) (struct pinctrl_dev *pctldev,
                                             struct seq_file *s,
                                             unsigned long config);
}; 

pin_config_get,获取指定pin(管脚的编号,由2.1中pin的注册信息获得)当前配置,保存在config指针中(配置的具体含义,只有pinctrl driver自己知道,下同)。

pin_config_set,设置指定pin的配置(可以同时配置多个config,具体意义要由相应pinctrl driver解释)。

pin_config_group_get、pin_config_group_set,获取或者设置指定pin group的配置项。

剩下的是一些debug用的api,不再说明(用得着的时候,再研究也不迟)。

关于pinconf,有一点一定要想明白:

kernel pinctrl subsystem并不关心configuration的具体内容是什么,它只提供pin configuration get/set的通用机制,至于get到的东西,以及set的东西,到底是什么,是pinctrl driver自己的事情。后面结合pin map和pin state的介绍(3.2小节),就能更好地理解这种设计了。

2.4. Pin multiplexing(对象是pin或者pin group)

为了照顾不同类型的产品、不同的应用场景,SoC中的很多管脚可以配置为不同的功能,例如A2和B5两个管脚,既可以当作普通的GPIO使用,又可以配置为I2C0的的SCL和SDA,也可以配置为UART5的TX和RX,这称作管脚的复用(pin multiplexing,简称为pinmux)。kernel pinctrl subsystem使用struct pinmux_ops来抽象pinmux有关的操作,如下:

struct pinmux_ops {
         int (*request) (struct pinctrl_dev *pctldev, unsigned offset);
        int (*free) (struct pinctrl_dev *pctldev, unsigned offset);
        int (*get_functions_count) (struct pinctrl_dev *pctldev);
        const char *(*get_function_name) (struct pinctrl_dev *pctldev,
                                           unsigned selector);
        int (*get_function_groups) (struct pinctrl_dev *pctldev,
                                  unsigned selector,
                                  const char * const **groups,
                                  unsigned *num_groups);
        int (*set_mux) (struct pinctrl_dev *pctldev, unsigned func_selector,
                        unsigned group_selector);
        int (*gpio_request_enable) (struct pinctrl_dev *pctldev,
                                    struct pinctrl_gpio_range *range,
                                     unsigned offset);
        void (*gpio_disable_free) (struct pinctrl_dev *pctldev,
                                   struct pinctrl_gpio_range *range,
                                    unsigned offset);
        int (*gpio_set_direction) (struct pinctrl_dev *pctldev,
                                   struct pinctrl_gpio_range *range,
                                    unsigned offset,
                                   bool input);
        bool strict;
};

注5:function的概念:

  • 为了管理SoC的管脚复用,pinctrl subsystem抽象出function的概念,用来表示I2C0、UART5等功能。pin(或者pin group)所对应的function一经确定,它(们)的管脚复用状态也就确定了
  • 和2.2中描述的pin group类似,pinctrl core不关心function的具体形态,只要求pinctrl driver将SoC的所有可能的function枚举出来(格式自行定义,不过需要有编号、名称等内容,可参考[5]中的例子),并注册给pinctrl core。后续pinctrl core将会通过function的索引,访问、控制相应的function
  • 另外,有一点大家应该很熟悉:在SoC的设计中,同一个function(如I2C0),可能可以map到不同的pin(或者pin group)上

理解了function的概念之后,struct pinmux_ops中的API就简单了:

get_functions_count,获取系统中function的个数。

get_function_name,获取指定function的名称。

get_function_groups,获取指定function所占用的pin group(可以有多个)。

set_mux,将指定的pin group(group_selector)设置为指定的function(func_selector)。

request,检查某个pin是否已作它用,用于管脚复用时的互斥(避免多个功能同时使用某个pin而不知道,导致奇怪的错误)。

free,request的反操作。

gpio_request_enable、gpio_disable_free、gpio_set_direction,gpio有关的操作,等到gpio有关的文章中再说明。

strict,为true时,说明该pin controller不允许某个pin作为gpio和其它功能同时使用。

3. pinctrl subsystem的控制逻辑

第2章以struct pinctrl_desc为引子,介绍了pinctrl subsystem中有关pin controller的概念抽象,包括pin、pin group、pinconf、pinmux、pinmux function、等等,相当于从provider的角度理解pinctrl subsystem。那么,问题来了,怎么使用pinctrl subsystem提供的功能控制管脚的配置以及功能复用呢?这看似需要由consumer(例如各个外设的驱动)自行处理,实际上却不尽然:

1)前面讲了,由于pinctrl subsystem的特殊性,对于pin configuration以及pin multiplexing而言,要怎么配置、怎么复用,只有pinctrl driver自己知道。同理,各个consumer也是云里雾里,啥都搞不清楚(想象各位编写设备驱动需要用到pinctrl的时候的心情吧!)。

2)那这样的配置有道理吗?有!记得我们在[6]中提到过,对一个确定的产品来说,某个设备所使用的pinctrl功能(function)、以及所对应的pin(或者pin group)、还有这些pin(或者pin group)的属性配置,基本上在产品设计的时候就确定好了,consumer没必要(也不想)关心技术细节。因此pinctrl driver就要多做一些事情,帮助consumer厘清pin有关资源的使用情况,并在这些设备需要使用的时候(例如probe时),一声令下,将资源准备好。

3)因此,pinctrl subsystem的设计理念就是:不需要consumer关心pin controller的技术细节,只需要在特定的时候向pinctrl driver发出一些简单的指令,告诉pinctrl driver自己的需求即可(例如我在运行时需要使用这样一组配置,在休眠时使用那样一组配置)。

4)最后,需求的细节(例如需要使用哪些pin、配置为什么功能、等等),要怎么确定呢?一般是通过machine的配置文件、具体版型的device tree等,告诉pinctrl subsystem,以便在需要的时候使用。

下面小节我们将会根据这些思路,进行更为详细的分析。

3.1 pin state

根据前面的描述,pinctrl driver抽象出来了一些离散的对象:pin(pin group)、function、configuration,并实现了这些对象的控制和配置方式。然后我们回到某一个具体的device上(如SPI2):

该device在某一状态下(如工作状态、休眠状态、等等),所使用的pin(pin group)、pin(pin group)的function和configuration,是唯一确定的。

把上面的话颠倒过来说,就是:

pin(pin group)以及相应的function和configuration的组合,可以确定一个设备的一个“状态”。

这个状态在pinctrl subsystem中就称作pin state。而pinctrl driver和具体板型有关的部分,需要负责枚举该板型下所有device(当然,特指那些需要pin资源的device)的所有可能的状态,并详细定义这些状态需要使用的pin(或pin group),以及这些pin(或pin group)需要配置为哪种function、哪种配置项。这些状态确定之后,consumer(device driver)就好办了,直接发号施令就行了:

喂,pinctrl subsystem,帮忙将我的xxx state激活。

pinctrl subsystem接收到指令后,找到该state的相关信息(pin、function和configuration),并调用pinctrl driver提供的相应API(参考第2章中struct pinctrl_desc有关的内容),控制pin controller即可。

3.2 pin map

在pinctrl subsystem中,pin state有关的信息是通过pin map收集,相关的数据结构如下:

/* include/linux/pinctrl/machine.h */

struct pinctrl_map {
        const char *dev_name;
        const char *name;
        enum pinctrl_map_type type;
        const char *ctrl_dev_name;
        union {
                struct pinctrl_map_mux mux;
                 struct pinctrl_map_configs configs;
        } data;
};

dev_name,device的名称。

name,pin state的名称。

ctrl_dev_name,pin controller device的名字。

type,该map的类型,包括PIN_MAP_TYPE_MUX_GROUP(配置管脚复用)、PIN_MAP_TYPE_CONFIGS_PIN(配置pin)、PIN_MAP_TYPE_CONFIGS_GROUP(配置pin group)、PIN_MAP_TYPE_DUMMY_STATE(不需要任何配置,仅仅为了表示state的存在。

data,该map需要用到的数据项,是一个联合体,如果map的类型是PIN_MAP_TYPE_CONFIGS_GROUP,则为struct pinctrl_map_mux类型的变量;如果map的类型是PIN_MAP_TYPE_CONFIGS_PIN或者PIN_MAP_TYPE_CONFIGS_GROUP,则为struct pinctrl_map_configs类型的变量。

struct pinctrl_map_mux的定义如下:

struct pinctrl_map_mux {
         const char *group;
        const char *function;
};

group,group的名字,指明该map所涉及的pin group。

function,function的名字,表示该map需要将group配置为哪种function。

struct pinctrl_map_configs的定义如下:

struct pinctrl_map_configs {
        const char *group_or_pin;
        unsigned long *configs;
        unsigned num_configs;
};    

group_or_pin,pin或者pin group的名字。

configs,configuration数组,指明要将该group_or_pin配置成“神马样子”。

num_configs,配置项的个数。

注6:讲到这里,应该理解为什么2.3小结中struct pinconf_ops中的api,都不知道configuration到底是什么东西了吧?因为都是pinctrl driver自己安排好的,自产自销,外人(pinctrl subsystem以及consumers)没必要理解!

最后,某一个device的某一种pin state,可以由多个不同类型的map entry组合而成,举例如下[5]

static struct pinctrl_map mapping[] __initdata = {
        PIN_MAP_MUX_GROUP("foo-i2c.0", PINCTRL_STATE_DEFAULT, "pinctrl-foo", "i2c0", "i2c0"),
        PIN_MAP_CONFIGS_GROUP("foo-i2c.0", PINCTRL_STATE_DEFAULT, "pinctrl-foo", "i2c0", i2c_grp_configs),
        PIN_MAP_CONFIGS_PIN("foo-i2c.0", PINCTRL_STATE_DEFAULT, "pinctrl-foo", "i2c0scl", i2c_pin_configs),
        PIN_MAP_CONFIGS_PIN("foo-i2c.0", PINCTRL_STATE_DEFAULT, "pinctrl-foo", "i2c0sda", i2c_pin_configs),
};

这是一个mapping数组,包含4个map entry,定义了"foo-i2c.0"设备的一个pin state(PINCTRL_STATE_DEFAULT,"default"),该state由一个PIN_MAP_TYPE_MUX_GROUP entry、一个PIN_MAP_TYPE_CONFIGS_GROUP entry以及两个PIN_MAP_TYPE_CONFIGS_PIN entry组成(这些entry的具体含义,大家可参考[5]以及相应的source code理解,这里不再详细说明)。

3.3 通过dts生成pin map

在旧时代,kernel的bsp工程师需要在machine有关的代码中,静态的定义pin map数组(类似于3.2小节中的例子),这一个非常繁琐且不易维护的过程。不过当kernel引入device tree之后,事情就简单了很多:

pinctrl driver确定了pin map各个字段的格式之后,就可以在dts文件中维护pin state以及相应的mapping table。pinctrl core在初始化的时候,会读取并解析dts,并生成pin map。

而各个consumer,可以在自己的dts node中,直接引用pinctrl driver定义的pin state,并在设备驱动的相应的位置,调用pinctrl subsystem提供的API,active或者deactive这些state。

至于dts中pin map描述的格式是什么,则完全由pinctrl driver自己决定,因为,最终的解析工作(dts to map)也是它自己做的(具体可参考后面第4章的介绍)。

4. pinctrl subsystem的整体流程

通过前面几章的分析,我们对pinctrl subsystem有了一个比较全面的认识,这里以pinctrl整个使用流程为例,简单的总结一下。

1)pinctrl driver根据pin controller的实际情况,实现struct pinctrl_desc(包括pin/pin group的抽象,function的抽象,pinconf、pinmux的operation API实现,dt_node_to_map的实现,等等),并注册到kernel中。

2)pinctrl driver在pin controller的dts node中,根据自己定义的格式,描述每个device的所有pin state。大致的形式如下(具体可参考kernel中的代码,照葫芦总能画出来瓢~~~):

        pinctrl_xxx {                               /* the dts node for pin controller */
                ...
                xxx_state_xxx: xxx_xxx {  /* dts node for xxx device's "xxx state" */
                        xxx_pinmux {             /* pinmux entry */
                                xxx = xxxx;
                                xxx = xxxxxxx;
                                ...
                        };
                        xxx_pinconf {            /* pinconf entry */
                                xxx = xxxx;
                                xxx = xxxxxxx;
                                ...
                        };
                        xxx_pinconf {
                                xxx = xxxx;
                                xxx = xxxxxxx;
                                ...
                        };
                       ...
               };
               ...
        };

3)相应的consumer driver可以在自己的dts node中,引用pinctrl driver所定义的pin state,例如:

               xxx_device: xxx@xxxxxxxx {
                        compatible = "xxx,xxxx";
                        ...
                        pinctrl-names = "default";
                        pinctrl-0 = <&xxx_state_xxx>;
                        ...
                };

4)consumer driver在需要的时候,可以调用pinctrl_get/devm_pinctrl_get接口,获得一个pinctrl handle(struct pinctrl类型的指针)。pinctrl subsystem在pinctrl get的过程中,解析consumer device的dts node,找到相应的pin state,进行调用pinctrl driver提供的dt_node_to_map API,解析pin state并转换为pin map。以driver probe时为例,调用过程如下(大家可以自己去看代码):

probe
    devm_pinctrl_get or pinctrl_get
        create_pinctrl(drivers/pinctrl/core.c)
            pinctrl_dt_to_map(drivers/pinctrl/devicetree.c)
                dt_to_map_one_config
                    pctlops->dt_node_to_map

5)consumer获得pinctrl handle之后,可以调用pinctrl subsystem提供的API(例如pinctrl_select_state),使自己的某个pin state生效。pinctrl subsystem进而调用pinctrl driver提供的各种回调函数,配置pin controller的硬件。
注7:具体细节就不再描述了,后续开发pinctrl driver的时候,可以再着重说明。

5. 参考文档

[1] linux内核中的GPIO系统之(1):软件框架

[2] linux内核中的GPIO系统之(2):pin control subsystem

[3] Linux内核中的GPIO系统之(3):pin controller driver代码分析

[4] https://github.com/wowotechX/linux

[5] Documentation/pinctrl.txt

[6] X-006-UBOOT-pinctrl driver移植(Bubblegum-96平台)

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net


CMA模块学习笔记

$
0
0

前言

本文是近期学习CMA模块的一个学习笔记,方便日后遗忘的时候,回来查询以便迅速恢复上下文。

学习的基本方法是这样的:一开始,我自己先提出了若干的问题,然后带着这些问题查看网上的资料,代码,最后整理形成这样以问题为导向的index,顺便也向笨叔叔致敬。笨叔叔写了一本书叫做《奔跑吧Linux内核》,采用了问答的方式描述了4.x Linux内核中的进程管理、内存管理,同步和中断子系统。7月将和大家见面,敬请期待。

阅读本文最好手边有一份linux source code,我使用的是4.4.6版本。

 

一、什么是CMA

CMA,Contiguous Memory Allocator,是内存管理子系统中的一个模块,负责物理地址连续的内存分配。一般系统会在启动过程中,从整个memory中配置一段连续内存用于CMA,然后内核其他的模块可以通过CMA的接口API进行连续内存的分配。CMA的核心并不是设计精巧的算法来管理地址连续的内存块,实际上它的底层还是依赖内核伙伴系统这样的内存管理机制,或者说CMA是处于需要连续内存块的其他内核模块(例如DMA mapping framework)和内存管理模块之间的一个中间层模块,主要功能包括:

1、解析DTS或者命令行中的参数,确定CMA内存的区域,这样的区域我们定义为CMA area。

2、提供cma_alloc和cma_release两个接口函数用于分配和释放CMA pages

3、记录和跟踪CMA area中各个pages的状态

4、调用伙伴系统接口,进行真正的内存分配。

 

二、内核中为何建立CMA模块?

Linux内核中已经提供了各种内存分配的接口,为何还有建立CMA这种连续内存分配的机制呢?

我们先来看看内核哪些模块有物理地址连续的需求。huge page模块需要物理地址连续是显而易见的。大家都熟悉的处理器(不要太古老),例如ARM64,其内存管理单元都可以支持多个页面大小(4k、64K、2M或者更大的page size),但在大多数CPU架构上,Linux内核总是倾向使用最小的page size,即4K page size。Page size大于4K的page统称为“huge page”。对于一个2M的huge page,MMU会把一个连续的2M的虚拟地址mapping到连续的、2M的物理地址上去,当然,这2M size的物理地址段必须是由512个地址连续的4k page frame组成。

当然,更多的连续内存的分配需求来自形形色色的驱动。例如现在大家的手机都有视频功能,camer功能,这类驱动都需要非常大块的内存,而且有DMA用来进行外设和大块内存之间的数据交换。对于嵌入式设备,一般不会有IOMMU,而且DMA也不具备scatter-getter功能,这时候,驱动分配的大块内存(DMA buffer)必须是物理地址连续的。

顺便说一句,huge page的连续内存需求和驱动DMA buffer还是有不同的,例如在对齐要求上,一个2M的huge page,其底层的2M 的物理页面的首地址需要对齐在2M上,一般而言,DMA buffer不会有这么高的对齐要求。因此,我们这里讲的CMA主要是为设备驱动准备的,huge page相关的内容不在本文中描述。

我们来一个实际的例子吧:我的手机,像素是1300W的,一个像素需要3B,那么拍摄一幅图片需要的内存大概是1300W x 3B = 26MB。通过内存管理系统分配26M的内存,压力可是不小。当然,在系统启动之处,伙伴系统中的大块内存比较大,也许分配26M不算什么,但是随着系统的运行,内存不断的分配、释放,大块内存不断的裂解,再裂解,这时候,内存碎片化导致分配地址连续的大块内存变得不是那么的容易了,怎么办?作为驱动工程师,我们有两个选择:其一是在启动时分配用于视频采集的DMA buffer,另外一个方案是当实际使用camer设备的时候分配DMA buffer。前者的选择是可靠的,但它有一个缺点,即当照相机不使用时(大多数时间内camera其实都是空闲的),预留的那些DMA BUFFER的内存实际上是浪费了(特别在内存配置不大的系统上更是如此)。后一种选择不会浪费内存,但是不可靠,随着内存碎片化,大的、连续的内存分配变得越来越困难,一旦内存分配失败,camera功能就会缺失,估计用户不会答应。

这就是驱动工程师面临的困境,为了解决这个问题,各个驱动各出奇招,但是都不能非常完美的解决问题。最终来自Michal Nazarewicz的CMA补丁将可以把各个驱动工程师的烦恼“一洗了之”。对于CMA 内存,当前驱动没有分配使用的时候,这些memory可以内核的被其他的模块使用(当然有一定的要求),而当驱动分配CMA内存后,那些被其他模块使用的内存需要吐出来,形成物理地址连续的大块内存,给具体的驱动来使用。

 

三、CMA模块的蓝图是怎样的?

 cma

了解一个模块,先不要深入细节,我们先远远的看看CMA在整个系统中的位置。虽然用于解决驱动的内存分配问题,但是驱动并不会直接调用CMA模块的接口,而是通过DMA mapping framework来间接使用CMA的服务。一开始,CMA area的概念是全局的,通过内核配置参数和命令行参数,内核可以定位到Global CMA area在内存中的起始地址和大小(注:这里的Global的意思是针对所有的driver而言的)。并在初始化的时候,调用dma_contiguous_reserve函数,将指定的memory region保留给Global CMA area使用。人性是贪婪的,驱动亦然,很快,有些驱动想吃独食,不愿意和其他驱动共享CMA,因此出现两种CMA area:Global CMA area给大家共享,而per device CMA可以给指定的一个或者几个驱动使用。这时候,命令行参数不是那么合适了,因此引入了device tree中的reserved memory node的概念。当然,为了兼容,内核仍然支持CMA的command line参数。

 

三、CMA模块如何管理和配置CMA area?

在CMA模块中,struct cma数据结构用来抽象一个CMA area,具体定义如下:

struct cma {
    unsigned long   base_pfn;
    unsigned long   count;
    unsigned long   *bitmap;
    unsigned int order_per_bit; /* Order of pages represented by one bit */
    struct mutex    lock;
};

cma模块使用bitmap来管理其内存的分配,0表示free,1表示已经分配。具体内存管理的单位和struct cma中的order_per_bit成员相关,如果order_per_bit等于0,表示按照一个一个page来分配和释放,如果order_per_bit等于1,表示按照2个page组成的block来分配和释放,以此类推。struct cma中的bitmap成员就是管理该cma area内存的bit map。count成员说明了该cma area内存有多少个page。它和order_per_bit一起决定了bitmap指针指向内存的大小。base_pfn定义了该CMA area的其实page frame number,base_pfn和count一起定义了该CMA area在内存在的位置。

我们前面说过了,CMA模块需要管理若干个CMA area,有gloal的,有per device的,代码如下:

struct cma cma_areas[MAX_CMA_AREAS];

每一个struct cma抽象了一个CMA area,标识了一个物理地址连续的memory area。调用cma_alloc分配的连续内存就是从CMA area中获得的。具体有多少个CMA area是编译时决定了,而具体要配置多少个CMA area是和系统设计相关,你可以为特定的驱动准备一个CMA area,也可以只建立一个通用的CMA area,供多个驱动使用(本文重点描述这个共用的CMA area)。

房子建好了,但是还空着,要想金屋藏娇,还需要一个CMA配置过程。配置CMA内存区有两种方法,一种是通过dts的reserved memory,另外一种是通过command line参数和内核配置参数。

device tree中可以包含reserved-memory node,在该节点的child node中,可以定义各种保留内存的信息。compatible属性是shared-dma-pool的那个节点是专门用于建立 global CMA area的,而其他的child node都是for per device CMA area的。

Global CMA area的初始化可以参考定义如下:

RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);

具体的setup过程倒是比较简单,从device tree中可以获取该memory range的起始地址和大小,调用cma_init_reserved_mem函数即可以注册一个CMA area。需要补充说明的是:CMA对应的reserved memory节点必须有reusable属性,不能有no-map的属性。具体reusable属性的reserved memory有这样的特性,即在驱动不使用这些内存的时候,OS可以使用这些内存(当然有限制条件),而当驱动从这个CMA area分配memory的时候,OS可以reclaim这些内存,让驱动可以使用它。no-map属性和地址映射相关,如果没有no-map属性,那么OS会为这段memory创建地址映射,象其他普通内存一样。但是有no-map属性的往往是专用于某个设备驱动,在驱动中会进行io remap,如果OS已经对这段地址进行了mapping,而驱动又一次mapping,这样就有不同的虚拟地址mapping到同一个物理地址上去,在某些ARCH上(ARMv6之后的cpu),会造成不可预知的后果。而CMA这个场景,reserved memory必须要mapping好,这样才能用于其他内存分配场景,例如page cache。

per device CMA area的注册过程和各自具体的驱动相关,但是最终会dma_declare_contiguous这个接口函数,为一个指定的设备而注册CMA area,这里就不详述了。

通过命令行参数也可以建立cma area。我们可以通过cma=nn[MG]@[start[MG][-end[MG]]]这样命令行参数来指明Global CMA area在整个物理内存中的位置。在初始化过程中,内核会解析这些命令行参数,获取CMA area的位置(起始地址,大小),并调用cma_declare_contiguous接口函数向CMA模块进行注册(当然,和device tree传参类似,最终也是调用cma_init_reserved_mem接口函数)。除了命令行参数,通过内核配置(CMA_SIZE_MBYTES和CMA_SIZE_PERCENTAGE)也可以确定CMA area的参数。

 

四、memblock、CMA和伙伴系统的初始化顺序是怎样的?

套用一句广告词:CMA并不进行内存管理,它只是”内存管理机制“的搬运工。也就是说,CMA area的内存最终还是要并入伙伴系统进行管理。在这样大方向的指导下,CMA模块的初始化必须要在适当的时机,以适当的方式插入到内存管理(包括memblock和伙伴系统)初始化过程中。

内存管理子系统进行初始化的时候,首先是memblock掌控全局的,这时候需要确定整个系统的的内存布局,简单说就是了解整个memory的分布情况,那些是memory block是memory type,那些memory block是reserved type。毫无疑问,CMA area对应的当然是reserved type。最先进行的是memory type的内存块的建立,可以参考如下代码:

setup_arch--->setup_machine_fdt--->early_init_dt_scan--->early_init_dt_scan_nodes--->memblock_add

随后会建立reserved type的memory block,可以参考如下代码:

setup_arch--->arm64_memblock_init--->early_init_fdt_scan_reserved_mem--->__fdt_scan_reserved_mem--->memblock_reserve

完成上面的初始化之后,memblock模块已经通过device tree构建了整个系统的内存全貌:哪些是普通内存区域,哪些是保留内存区域。对于那些reserved memory,我们还需要进行初始化,代码如下:

setup_arch--->arm64_memblock_init--->early_init_fdt_scan_reserved_mem--->fdt_init_reserved_mem--->__reserved_mem_init_node

上面的代码会scan内核中的一个特定的section(还记得前面RESERVEDMEM_OF_DECLARE的定义吗?),如果匹配就会调用相应的初始化函数,而对于Global CMA area而言,这个初始化函数就是rmem_cma_setup。当然,如果有需要,具体的驱动也可以定义自己的CMA area,初始化的思路都是一样的。

至此,通过device tree,所有的内核模块要保留的内存都已经搞清楚了(不仅仅是CMA保留内存),是时候通过命令行参数保留CMA内存了,具体的调用如下:

setup_arch--->arm64_memblock_init--->dma_contiguous_reserve

实际上,在构建CMA area上,device tree的功能已经完全碾压命令行参数,因此dma_contiguous_reserve有可能没有实际的作用。如果没有通过命令行或者内核配置文件来定义Global CMA area,那么这个函数调用当然不会起什么作用,如果device tree已经设定了Global CMA area,那么其实dma_contiguous_reserve也不会真正reserve memory(device tree优先级高于命令行)。

如果有配置命令行参数,而且device tree并没有设定Global CMA area,那么dma_contiguous_reserve才会真正有作用。那么根据配置参数可以有两种场景:一种是CMA area是固定位置的,即参数给出了确定的起始地址和大小,这种情况比较简单,直接调用memblock_reserve就OK了,另外一种情况是动态分配的,这时候,需要调用memblock的内存分配接口memblock_alloc_range来为CMA area分配内存。

memblock始终是初始化阶段的内存管理模块,最终我们还是要转向伙伴系统,具体的代码如下:

start_kernel--->mm_init--->mem_init--->free_all_bootmem--->free_low_memory_core_early--->__free_memory_core

在上面的过程中,free memory被释放到伙伴系统中,而reserved memory不会进入伙伴系统,对于CMA area,我们之前说过,最终被由伙伴系统管理,因此,在初始化的过程中,CMA area的内存会全部导入伙伴系统(方便其他应用可以通过伙伴系统分配内存)。具体代码如下:

core_initcall(cma_init_reserved_areas);

至此,所有的CMA area的内存进入伙伴系统。

 

五、CMA是如何工作的?

1、准备知识

如果想要了解CMA是如何运作的,你可能需要知道一点点关于migrate types和pageblocks的知识。当从伙伴系统请求内存的时候,我们需要提供了一个gfp_mask的参数。它有很多的功能,不过在CMA这个场景,它用来指定请求页面的迁移类型(migrate type)。migrate type有很多中,其中有一个是MIGRATE_MOVABLE类型,被标记为MIGRATE_MOVABLE的page说明该页面上的数据是可以迁移的。也就是说,如果需要,我们可以分配一个新的page,copy数据到这个new page上去,释放这个page。而完成这样的操作对系统没有任何的影响。我们来举一个简单的例子:对于内核中的data section,其对应的page不是是movable的,因为一旦移动数据,那么内核模块就无法访问那些页面上的全局变量了。而对于page cache这样的页面,其实是可以搬移的,只要让指针指向新的page就OK了。

伙伴系统不会跟踪每一个page frame的迁移类型,实际上它是按照pageblock为单位进行管理的,memory zone中会有一个bitmap,指明该zone中每一个pageblock的migrate type。在处理内存分配请求的时候,一般会首先从和请求相同migrate type(gfp_mask)的pageblocks中分配页面。如果分配不成功,不同migrate type的pageblocks中也会考虑,甚至可能改变pageblock的migrate type。这意味着一个non-movable页面请求也可以从migrate type是movable的pageblock中分配。这一点CMA是不能接受的,所以我们引入了一个新的migrate type:MIGRATE_CMA。这种迁移类型具有一个重要性质:只有可移动的页面可以从MIGRATE_CMA的pageblock中分配。

2、初始化CMA area

static int __init cma_activate_area(struct cma *cma)
{
    int bitmap_size = BITS_TO_LONGS(cma_bitmap_maxno(cma)) * sizeof(long);
    unsigned long base_pfn = cma->base_pfn, pfn = base_pfn;
    unsigned i = cma->count >> pageblock_order;
    struct zone *zone; -----------------------------(1)

    cma->bitmap = kzalloc(bitmap_size, GFP_KERNEL); ----分配内存

    zone = page_zone(pfn_to_page(pfn)); ---找到page对应的memory zone

    do {--------------------------(2)
        unsigned j;

        base_pfn = pfn;
        for (j = pageblock_nr_pages; j; --j, pfn++) {-------------(3)
            if (page_zone(pfn_to_page(pfn)) != zone)
                goto err;
        }
        init_cma_reserved_pageblock(pfn_to_page(base_pfn));----------(4)
    } while (--i);

    mutex_init(&cma->lock);

    return 0;

err:
    kfree(cma->bitmap);
    cma->count = 0;
    return -EINVAL;
}

(1)CMA area有一个bitmap来管理各个page的状态,这里bitmap_size给出了bitmap需要多少的内存。i变量表示该CMA area有多少个pageblock。

(2)遍历该CMA area中的所有的pageblock。

(3)确保CMA area中的所有page都是在一个memory zone内,同时累加了pfn,从而得到下一个pageblock的初始page frame number。

(4)将该pageblock导入到伙伴系统,并且将migrate type设定为MIGRATE_CMA。

2、分配连续内存

cma_alloc用来从指定的CMA area上分配count个连续的page frame,按照align对齐。具体的代码就不再分析了,比较简单,实际上就是从bitmap上搜索free page的过程,一旦搜索到,就调用alloc_contig_range向伙伴系统申请内存。需要注意的是,CMA内存分配过程是一个比较“重”的操作,可能涉及页面迁移、页面回收等操作,因此不适合用于atomic context。

3、释放连续内存

分配连续内存的逆过程,除了bitmap的操作之外,最重要的就是调用free_contig_range,将指定的pages返回伙伴系统。

 

参考文献:

LWN上的若干和CMA相关的文档,包括:

1、A deep dive into CMA

2、A reworked contiguous memory allocator

3、CMA and ARM

4、Contiguous memory allocation for drivers

 

原创文章,转发请注明出处。蜗窝科技

蜗窝微信群问题整理(1)

$
0
0

前言:蜗窝微信群开张了,这个群是为那些愿意慢下来,仔细研究内核技术、愿意为了搞清楚内核代码逻辑而废寝忘食的工程师准备的,在这个群里,大家讨论了一些技术问题,当然,也有一些问题没有解决和答案。鉴于微信群的特点,我还是把大家讨论的技术整理了一下,分享出来,希望能够对其他工程师有所帮助。

 

问题一:请问sysconf(_SC_CLK_TCK)拿到的HZ是不是jiffies的频率呢?如果是为何我内核配置的HZ是250,但是这个函数返回的是100?这是为什么?

1、userspace需要知道内核中的HZ的配置吗?

其实用户空间应该不需要关心内核HZ的设定,所以那个接口其实是没意义,毕竟这是内核的特性,也许内核是tickless的呢?。用户空间和时间相关接口用纳秒就好,不要用tick值表示时间,否则用户空间还要转换成纳秒,而且需要知道内核HZ,封装性不好。

2、既然不需要,那么这个接口是干什么的?

这个函数返回的是USER_TICK而不是真是内核的HZ,因此虽然你的内核配置是250,但是sysconf(_SC_CLK_TCK)依然返回100。

虽然新的程序不建议使用这个接口,但是市面上还有众多的老的程序还是依赖这些接口的,因此,为了兼容的原因,userspace和kernel还是还是需要交互tick的,内核采用的办法是这样的:HZ是内核自己使用,定义USER_TICK用于和userspace接口,内核在返回用户空间的时候会转换成USER_TICK。

3、怎么实现的?

用户空间和内核空间有很多交互的形态,例如大家最熟悉的就是系统调用。不过我们这里sysconf(_SC_CLK_TCK)函数并没有引发系统调用,她使用的是auxiliary vector。

一个程序的二进制文件在被加载的过程中(代码位于linux/fs/binfmt_elf.c), 会通过若干NEW_AUX_ENT来定义auxiliary vector table,具体和这个场景的代码如下:

NEW_AUX_ENT(AT_CLKTCK, CLOCKS_PER_SEC);

# define USER_HZ    100        /* some user interfaces are */
# define CLOCKS_PER_SEC    (USER_HZ)       /* in "ticks" like times() */

而在程序中,可以通过调用getauxval(AT_CLKTCK) 来或者这个tick就是固定为100。当然,还可以用sysconf(_SC_CLK_TCK),这个函数其实也是辗转调用了getauxval(AT_CLKTCK) 来获取user tick。

4、如何使用

主要的使用场景包括:

(1)times函数计算进程时间。这个函数返回tick为单位的数值

(2)用户空间的proc接口有需要使用USER_TICK来转换。

 

 

问题二:想问大神们一个问题,关于linux kernel workqueue的,版本是3.6.18.
我碰到一个问题,我们的系统情况是这样的:

1、音频驱动使用了中断线程化处理

2、interrupt thread中接收数据并放入kfifo,然后queue work通知worker线程来处理kfifo中的数据。

3、用户空间操作alsa设备的进程读取数据并播放。注意,该进程是rt进程,rt priority是60.

4、双核配置,其中音频中断送达其中一个CPU

刚开始,我们使用system_wq这个系统自带的workqueue,但是会出现音频来不及处理的情况,用create_wq创建了per cpu的wq,问题有改善,但是仍然会有声音断断续续的问题,最后改成了alloc_workqueue unbond来创建wq,经过反复测试,问题就没有再出现了。按理说调用create_wq创建per cpu的workqueue应该有很好的through表现,反倒是调用unbond才能解决问题,这有点奇怪,请问大家有什么看法?

分析如下:

1、我们假设中断送达cpu0,那么如果不出意外的话,interrupt thread应该是也是在cpu0,并且优先级是50,低于用户空间的进程。

2、如果使用per cpu的workqueue的话,那么在interrupt thread中queue work的话,那么该work线程也是在cpu 0执行,work线程是普通进程。

3、整个数据处理过程是:interrupt thread ---》 workqueue  -----》 用户空间RT进程

4、如果数据处理大部分位于interrupt thread 和workqueue中,那么整个数据处理是失衡的,大部分压在在中断送达的那个CPU0上

5、改用unbound workqueue的话,在interrupt thread中queue的work不是固定绑定在cpu 0上执行,因此数据处理比较流畅。

6、主要的数据处理在worker线程中,但是它的优先级最低。受两端夹击,最终导致kfifo满从而丢失了音频数据。

 

问题三:ps和top的优先级为何看起来不是很一样?ps的rtprio是正数,而TOP中的实时进程是负数?

通过ps –eo class,rtprio,ni,comm命令看到的是实时进程的调度优先级(rtprio)和普通进程的nice value(ni)。这个比较符合内核工程师的习惯:

1、 如果是实时进程,那么rtprio是1~99,如果不是,那么显示“-“

2、 如果是普通进程,那么rtprio显示“-“,ni显示-20~19

通过top看到的两个和优先级相关的域是PR和NI,分别表示进程的动态优先级(task_struct中的prio成员)和nice value。

我们先看看普通进程的场景,对于CFS之前的内核(2.6.23),PR显示的是基于静态优先级NI的动态优先级,当一个进程开始运行的时候,PR=NI+20,一旦运行起来,动态优先级会在一个范围内摆动,具体的摆动范围是和进程状态相关。例如如果该进程sleep较长的时间,那么它应该多占用一些CPU,因此其动态优先级PR会降低(实际的优先级是升高的)。相关,如果一个进程占用太多的CPU时间,那么其动态优先级PR值会降升高(实际动态优先级是降低的),因此该进程更容易被其他进程抢占。

在引入CFS之后,由于调度器算法发生了变化,进程睡眠时间比较久也没有什么“折扣“了,一切都是按照最大公平的原则来调度进程,因此,这时候PR固定等于NI加20。

上面说的是普通进程,对于RT进程,PN应该展示的是基于(task_struct中的rt_priority成员)的动态优先级。具体在内核中如何运算大家可以自行查阅代码,但是来到用户空间(proc/pid/stat),这个动态优先级则是-100~-1。其中-100是最高优先级。

在top程序中的逻辑是:

if (unlikely(-99 > p->priority) || unlikely(999 < p->priority)) {

f = " rt"; --输出rt这两个字符

} else

MKCOL((int)p->priority);--输出实际的优先级

也就是说当设定为最高优先级的时候(-100,对于rtprio是99),显示的就只有rt两个字符,其他时候会显示出一个负值。内核态的50对应用户空间的-51。

 

问题四:小弟最近在看,经典rcu的实现(linux2.6.23),其中对于每个cpu度过一个静默期的判断有疑问。代码如下:

void rcu_check_callbacks(int cpu, int user)
{
    if (user || -------------------A
        (idle_cpu(cpu) && !in_softirq() && -------B
                hardirq_count() <= (1 << HARDIRQ_SHIFT))) {
        rcu_qsctr_inc(cpu);
        rcu_bh_qsctr_inc(cpu);
    } else if (!in_softirq())
        rcu_bh_qsctr_inc(cpu);
    tasklet_schedule(&per_cpu(rcu_tasklet, cpu));
}

 

代码A处比较好理解,如果timer命中用户空间,那么说明至少发生了一次进程切换,当然要标记本CPU的QS状态,但是B处的判断怎么理解?

回答:对于经典RCU来说,标记本CPU的QS状态有两个条件:

1、经历一次用户态/内核态切换 ,也就是有进程切换

2、当前任务为IDLE进程,说明有其他任务切换到idle进程的动作

代码中A的判断调试针对上面的case 1,B条件对应上面的case 2,不过,在case2,我们需要排除一些其他的场景。

对于B条件,为什么要加上软中断和硬中断计数,其实也比较好理解:因为中断可以随意的打断任何任务,当然也包含IDLE任务。如果在TICK处理之前,已经有中断嵌套了,那么在上一次嵌套中断里面,很可能进入了RCU读端临界区,这种情况下,当前TICK显示不能经过静止状态。

 

问题五:各位大神,我在看lock dep . 什么是 hardirq safe lock? 还有什么是 hard irq unsafe lock?看不明白,为何 hard irq safe lock  -> hard irq unsafe lock 是不允许的?

回答:

1、所谓hardirq safe 就是在关中断的时候拿的锁,一般用在中断上下文和进程上下文同步的场景。

2、hardirq unsafe 的锁就是lockdep检测到没有在关中断的时候拿到的锁,所以unsafe,而获取该锁是需要关中断的。

3、如果允许hardirq safe -> hardirq unsafe, 那么可能会造成死锁

4、一个实际的例子:考虑一个情况,进程在拿到锁B (unsafe)的时候被中断,中断里先拿锁A (safe) 在去拿锁B (unsafe) 这个时候就会死锁

5、这个规则就是说在中断中用到的锁,在进程上下文中如果想要用,必须关中断。

X-023-KERNEL-Linux pinctrl driver的移植

$
0
0

1. 前言

本文是“linux内核中的GPIO系统之(4):pinctrl驱动的理解和总结”的一个实例,结合”X Project”的开发过程,介绍pinctrl driver的移植步骤,进而加深对pinctrl framework的理解。

注1:本文后续的描述,kernel基于本站“X Project”所使用的kernel版本[4],硬件基于 ”X Project”所使用的“Bubbugum-96”平台。

2. pinctrl driver的移植步骤

2.1 添加pinctrl driver在驱动模型中的框架

在linux kernel中,pin controller也是一个普通的platform device,因此需要基于platform设备的框架(可参考[1])添加自身的驱动模型。对于这些单纯的设备,可以直接用一些标准化的接口来完成,如下(都是轻车熟路的东西,不再解释了):

---->drivers/pinctrl/pinctrl-owl.c

/*============================================================================
*                                                               platform driver
*==========================================================================*/
static const struct of_device_id owl_pinctrl_of_match[] = {
        {
                .compatible = "actions,s900-pinctrl",
        },
        {
        },
};
MODULE_DEVICE_TABLE(of, owl_pinctrl_of_match);

static int __init owl_pinctrl_probe(struct platform_device *pdev)

        return 0;          
}

static void __exit owl_pinctrl_remove(struct platform_device *pdev)
{
}

static struct platform_driver owl_pinctrl_platform_driver = {
        .probe          = owl_pinctrl_probe,
        .remove         = owl_pinctrl_remove,
        .driver         = {
                .name   = "owl_pinctrl",
                .of_match_table = owl_pinctrl_of_match,
        },
};
module_platform_driver(owl_pinctrl_platform_driver);

MODULE_ALIAS("platform driver: owl_pinctrl");
MODULE_DESCRIPTION("pinctrl driver for owl seria soc(s900, etc.)");
MODULE_AUTHOR("wowo<wowo@wowotech.net>");
MODULE_LICENSE("GPL v2");

(另外,需要编辑drivers/pinctrl中的Kconfig和Makefile文件,加入我们新的pinctrl driver,并更改”X Project”的kernel配置项,加入该driver的编译,具体步骤略,代码可参考如下的patch:https://github.com/wowotechX/linux/commit/45ca29b3680db7b6d62735439a0ef93ab9797537,

https://github.com/wowotechX/linux/commit/749f114564765025d907618d7306bc668c0a0a93)。

2.2 添加pinctrl driver自身的框架

参考“Documentation/pinctrl.txt[2]"中的示例,创建一个struct pinctrl_desc变量,并在probe中调用pinctrl_register注册到kernel中。

另外,为了在同一个driver中支持多个soc,可以将struct pinctrl_desc变量的指针保存在每个soc的match table中,并在probe中借助of_device_get_match_data将其获取出来(这是device tree时代的惯用伎俩,大家可以多使用),如下黄色背景所示:

static const struct of_device_id owl_pinctrl_of_match[] = {
        {
                 .compatible = "actions,s900-pinctrl",
                .data = &s900_desc,
        },
        {
        },
};

...

static int __init owl_pinctrl_probe(struct platform_device *pdev)
{
        ...

        desc = of_device_get_match_data(dev);
        if (desc == NULL) {
                 dev_err(dev, "device get match data failed\n");
                 return -ENODEV;
        }

        pctl = pinctrl_register(desc, dev, NULL);
        ...
}

可以直接从Documentation/pinctrl.txt中copy过来,将foo换成自己平台的代号(如这里的s900),然后在owl_pinctrl_probe中调用pinctrl_register注册即可。

以上代码可参考如下的patch:https://github.com/wowotechX/linux/commit/9ce89fe7e5d68f2d84dab39af0665acfc3b62751

2.3 定义管脚(pin)

driver框架确定了之后,就简单了,根据[3]中的介绍,一个一个的添加各种软件抽象就行了。例如可以根据自己平台的实际情况,定义管脚。

注2:这里以“Bubbugum-96”平台为例,硬件有关的说明可参考之前的文章。另外,为了说明pinctrl driver的编写过程,这里简化一下,暂时只关心UART5有关的pin,其它的后面用的时候再加就行了。

根据原理图,uart5有关的pin脚包括(好东西,足够我们把pinctrl的原理展示完了):

GPIOA25/UART5_RX/SENS0_VSYNC/PWM2(A7)
GPIOA27/UART5_TX/SENS0_HSYNC/PWM2(D8)
GPIOA28/UART2_RX/UART5_RX/SD0_D0(C5)
GPIOA29/UART2_TX/UART5_TX/SD0_D1(B6)
GPIOF6/UART5_RX/UART3_RTSB(G2)
GPIOF7/UART5_TX/UART3_CTSB(G1)

名字,好办,就用A7、D8之类的就行了,编号怎么办呢?

先随便定吧(后面可能会根据寄存器的特性调整),直接用字母(转成数字,A是0,B是1,等等)乘以10加数字(数字好像是从1开始,那就数字减一),即:

A7 = (1 – 1)* 10 + (7 – 1)= 6
D8 =  (4 – 1) * 10 + (8 – 1) = 37
C5 = (3 – 1) * 10 + (5 - 1) = 24
B6 = (2 – 1) * 10 + (6 – 1) = 15
G2 = (7 – 1) * 10 + (2 – 1) = 61
G1 = (7 – 1) * 10 + (1 – 1) =60

最终定义出来的管脚如下:

static const struct pinctrl_pin_desc s900_pins[] = {
        PINCTRL_PIN(6, "A7"),
        PINCTRL_PIN(15, "B6"),
        PINCTRL_PIN(24, "C5"),
        PINCTRL_PIN(37, "D8"),
        PINCTRL_PIN(60, "G1"),
        PINCTRL_PIN(61, "G2"),
};

2.4 定义pin group

暂时不考虑GPIO,上面6个pin可以分解出如下groups(麻雀虽小五脏俱全啊!不过简单起见,这里只关注UART5有关的group,因为其它group可能会涉及更多的pin):

uart5_0_group,包括A7和D8两个pin,由bubblegum96的spec[4]可知,当MFP_CTL1 寄存器(0xE01B0044)的bit28:26和bit25:23分别为001(UART5_RX)和100(UART5_TX)时,该group生效;

uart5_1_group,包括C5和B6两个pin,由bubblegum96的spec[4]可知,当MFP_CTL2 寄存器(0xE01B0048)的bit19:17和bit16:14分别为101(UART5_RX)和101(UART5_TX)时,该group生效;

uart5_2_group,包括G2和G1两个pin,由bubblegum96的spec[4]可知,当MFP_CTL2 寄存器(0xE01B0048)的bit21和bit20分别为1(UART5_RX)和1(UART5_TX)时,该group生效。

最后参考[2]中的例子,定义并注册struct pinctrl_ops变量,具体可参考如下patch:https://github.com/wowotechX/linux/commit/478f3fcf2fc950daa6fb85eb302f943a427f0878

2.5 Pin configuration的设计

参考[4]中“5.7.3.24 PAD_PULLCTL0 ”~“5.7.3.35 PAD_SR2 ”章节有关pin configuration的描述,发现s900的pinconf有点特殊,是以特定的功能(function,例如UART0_RX)为单位进行控制的,也就是说,对某一个功能来说,可能映射到不同的管脚上,但对它的配置,都是由相同的寄存器控制的。

基于这样的硬件框架,怎么去抽象pinconf的operations,值得思考。不过由于本文的例子(uart5)没有可配置的选项,所以我们就先略过这里。后面有机会再补回来吧(不影响我们对pinctrl的整体理解)。

2.6 Pin multiplexing的设计

同理,参考[2]中的示例,首先抽象出Pin multiplexing中的functions,本文的例子中,暂时只有一个:uart5,定义如下:

/* drivers/pinctrl/pinctrl-owl.c */

static const char * const uart5_groups[] = {
        "uart5_0_grp", "uart5_1_grp", "uart5_2_grp"
};

static const struct owl_pmx_func s900_functions[] = {
        {
                .name = "uart5",
                .groups = uart5_groups,
                .num_groups = ARRAY_SIZE(uart5_groups),
         },
};

该function可以映射到三个不同的group中:"uart5_0_grp", "uart5_1_grp", "uart5_2_grp"(它们的名字要和2.4中定义的groups name相同)。

然后,定义一个struct pinmux_ops变量,并实现相应的回调函数,如下:

static struct pinmux_ops s900_pmxops = {
        .get_functions_count = s900_get_functions_count,
        .get_function_name = s900_get_fname,
        .get_function_groups = s900_get_groups,
        .set_mux = s900_set_mux,
        .strict = true,
};

上面get_functions_count 、get_function_name以及get_function_groups三个函数比较简单,这里重点提一下set_mux,它的原型如下:

int (*set_mux) (struct pinctrl_dev *pctldev, unsigned func_selector,
                 unsigned group_selector);

有两个参数:func_selector,用于指定function的number(就是s900_functions中的数组index);group_selector,用于指定group的number(就是2.4中介绍的s900_groups的数组index)。pinctrl driver拿到这两个参数之后,怎么去配置寄存器使某个group的某个function生效呢?要根据具体的pin controller的硬件情况来定。以本文作为示例的s900为例,该SoC的Pin multiplexing的设置,并不需要function和group的一一对应,以uart5 function的group0为例,参考2.4中的介绍:

只要把MFP_CTL1 寄存器(0xE01B0044)的bit28:26和bit25:23分别设置为001(UART5_RX)和100(UART5_TX),就可以使uart5的该group生效。

也就是说,s900 SoC可以单独以group为单位进行pinmux配置。那么我们在代码中可以把相应的寄存器配置信息和groups的定义绑定,在pinctrl core调用.set_mux回调函数的时候,忽略 func_selector,直接通过group_selector获取相应的寄存器信息后,配置寄存器即可,如下:

+struct owl_pmx_reginfo {
+       const unsigned *offset;
+       const unsigned *mask;
+       const unsigned *val;
+
+       unsigned num_entries;
+};
+
  struct owl_pinctrl_group {
        const char *name;
         const unsigned *pins;
        unsigned num_pins;
+
+       const struct owl_pmx_reginfo *reginfo;
+};

......

static const struct owl_pinctrl_group s900_groups[] = {
        {
                 .name = "uart5_0_grp",
                .pins = s900_uart5_0_pins,
                .num_pins = ARRAY_SIZE(s900_uart5_0_pins),
+               .reginfo = &s900_uart5_0_reginfo,
        },
        {
                .name = "uart5_1_grp",
                 .pins = s900_uart5_1_pins,
                .num_pins = ARRAY_SIZE(s900_uart5_1_pins),
+               .reginfo = &s900_uart5_1_reginfo,
        },
        {
                .name = "uart5_2_grp",
                .pins = s900_uart5_2_pins,
                 .num_pins = ARRAY_SIZE(s900_uart5_2_pins),
+               .reginfo = &s900_uart5_2_reginfo,
        },
  };

...

+static int s900_set_mux(struct pinctrl_dev *pctldev, unsigned selector,
+                       unsigned group)
+{
+       int i;
+       uint32_t val;
+
+       struct owl_pinctrl_priv *priv = pctldev->driver_data;
+
+       const struct owl_pmx_reginfo *reginfo;
+
+       reginfo = s900_groups[gourp].reginfo;
+
+       for (i = 0; i < reginfo->num_entries; i++) {
+               val = readl(priv->mem_base + reginfo->offset[i]);
+               val &= ~reginfo->mask[i];
+               val |= reginfo->val[i];
+               writel(val, priv->mem_base + reginfo->offset[i]);

+       }
+
+       return 0;
+}

以上代码比较直接、简单,又和具体的硬件平台有关,我就不详细解释了,大家在编写自己平台的pinctrl driver时,可以具体情况具体对待。

以上可参考如下patch:https://github.com/wowotechX/linux/commit/52d6b8337a31267fd9b158d5f1f9d9590a16f92d

2.7 dt_node_to_map的设计

上面的pinmux设计完成之后,我们需要提供一种机制,让consumer(这里为serial driver)可以方便的使用。这里介绍利用device tree的方法----步骤有三:

1)定义pinctrl device的dts node,并包含一个包含uart5 function的子节点(节点的格式可以自行决定,因为是自己解析),例如:

--- a/arch/arm64/boot/dts/actions/s900-bubblegum.dts
+++ b/arch/arm64/boot/dts/actions/s900-bubblegum.dts
@@ -39,9 +39,25 @@
                clock-frequency = <24000000>;
        };

+       pinctrl@0xe01b0000 {
+               compatible = "actions,s900-pinctrl";
+               reg = <0 0="" 0xe01b0000="" 0x550="">;
+
+               uart5_state_default: uart5_default {
+                       pinmux {
+                               function = "uart5";
+                               group = "uart5_0_grp";
+                       };
+                       /* others, TODO */
+               };
+       };

这里子节点的名字为uart5_default(别名为uart5_state_default,以便consumer device的dts node引用),代表了一个pin state(相关概念可参考[3]中的介绍);

简单期间,该state里面包含一个entry----pinmux,该entry有两个关键字,function和group,都是字符串类型的。

2)在pinctrl driver中,实现.dt_node_to_map和.dt_free_map回调函数,其中.dt_node_to_map用于将dts node中的信息转换为pin map,示例如下:

+static int s900_dt_node_to_map(struct pinctrl_dev *pctldev,
+                              struct device_node *np_config,
+                              struct pinctrl_map **map,
+                              unsigned *num_maps)
+{
+       int ret, child_cnt;
+
+       const char *function;
+       const char *group;
+
+       struct device *dev = pctldev->dev;
+       struct device_node *np;
+
+       dev_dbg(dev, "%s\n", __func__);
+
+       *map = NULL;
+       *num_maps = 0;
+
+       child_cnt = of_get_child_count(np_config);
+       dev_dbg(dev, "child_cnt %d\n", child_cnt);
+
+       if (child_cnt == 0)
+               return 0;
+
+       *map = kzalloc(sizeof(struct pinctrl_map) * child_cnt, GFP_KERNEL);
+       if (*map == NULL) {
+               dev_dbg(dev, "malloc failed\n");
+               return -ENOMEM;
+       }
+
+       for_each_child_of_node(np_config, np) {
+               ret = of_property_read_string(np, "function", &function);
+               if (ret != 0)
+                       continue;
+
+               ret = of_property_read_string(np, "group", &group);
+               if (ret != 0)
+                       continue;
+
+               dev_dbg(dev, "got a pinmux entry: %s-%s\n", function, group);
+
+               (*map)[*num_maps].type = PIN_MAP_TYPE_MUX_GROUP;
+               (*map)[*num_maps].data.mux.function = function;
+               (*map)[*num_maps].data.mux.group = group;
+               (*num_maps)++;
+       }
+
+       return 0;
+}

这个例子比较简单:

参数主要包括:np_config,dts节点指针,对应上面的“uart5_state_default”;map,pinctrl map指针的指针,需要pinctrl driver申请空间;num_maps,map个数的指针,driver可以修改该指针的值,告诉上层软件最终的map个数。

得到dts node指针之后,我们可以遍历该node下的所有子节点,并检查对应的子节点下是否有有效的function和group字符串,如果有,则代表找到了一个有效的entry,将相应的信息(function name和group name)保存在maps数组中的一个entry即可。

3)在对应的consumer的dts node中,引用上述的pinctrl state,如下:

        serial5: serial@e012a000 {
                compatible = "actions,s900-serial";
                reg = <0 0="" 0xe012a000="" 0x2000="">;  /* UART5_BASE */
                interrupts = ;
+
+               pinctrl-names = "default";
+               pinctrl-0 = <&uart5_state_default>;
        };

上面pinctrl-names指定为"defualt",这样可以在设备probe的时候由kernel自行获取相应的pinctrl state并使之生效。

然后在pinctrl-0字段中填入pinctrl state的地址:&uart5_state_default。

最后serial driver在probe的时候,将会按照[3]中介绍过程(probe-->devm_pinctrl_get or pinctrl_get-->create_pinctrl-->pinctrl_dt_to_map-->dt_to_map_one_config-->pctlops->dt_node_to_map-->s900_dt_node_to_map),将该设备对应的pinctrl state保存下来,并通过pinctrl_lookup_state查找名称为"default“的state,调用pinctrl_select_state,使其生效。

以上代码可参考如下patch:https://github.com/wowotechX/linux/commit/7c7b7b948b6f4eb25ef7926c89eb1199e07a7bc9

3. 总结

经过上面的步骤,我们开发了一个简单的pinctrl driver,并通过consumer的dts node将其绑定。这样consumer driver在probe的时候,会自动调用pinctrl subsystem提供的API,获取一个default state,并使其生效。

当然,本文所描述的pinctrl driver只算一个简单的demo(只有uart5有关的管脚),实际的项目开发过程中要比这复杂的多,但万变不离其宗,仅仅是代码量和编程技巧的差异而已。

4. 参考文档

[1] Linux设备模型(8)_platform设备

[2] Documentation/pinctrl.txt

[3] linux内核中的GPIO系统之(4):pinctrl驱动的理解和结

[4] https://github.com/96boards/documentation/blob/master/ConsumerEdition/Bubblegum-96/HardwareDocs/SoC_bubblegum96.pdf

[5] https://github.com/96boards/documentation/blob/master/ConsumerEdition/Bubblegum-96/HardwareDocs/Schematics_Bubblegum96.pdf

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

Dynamic DMA mapping Guide

$
0
0

一、前言

这是一篇指导驱动工程师如何使用DMA API的文档,为了方便理解,文档中给出了伪代码的例程。另外一篇文档dma-api.txt给出了相关API的简明描述,有兴趣也可以看看那一篇,这两份文档在DMA API的描述方面是一致的。

 

二、从CPU角度看到的地址和从DMA控制器看到的地址有什么不同?

在DMA API中涉及好几个地址的概念(物理地址、虚拟地址和总线地址),正确的理解这些地址是非常重要的。

内核通常使用的地址是虚拟地址。我们调用kmalloc()、vmalloc()或者类似的接口返回的地址都是虚拟地址,保存在"void *"的变量中。

虚拟内存系统(TLB、页表等)将虚拟地址(程序角度)翻译成物理地址(CPU角度),物理地址保存在“phys_addr_t”或“resource_size_t”的变量中。对于一个硬件设备上的寄存器等设备资源,内核是按照物理地址来管理的。通过/proc/iomem,你可以看到这些和设备IO 相关的物理地址。当然,驱动并不能直接使用这些物理地址,必须首先通过ioremap()接口将这些物理地址映射到内核虚拟地址空间上去。

I/O设备使用第三种地址:“总线地址”。如果设备在MMIO地址空间中有若干的寄存器,或者该设备足够的智能,它可以通过DMA执行读写系统内存的操作,这些情况下,设备使用的地址就是总线地址。在某些系统中,总线地址与CPU物理地址相同,但一般来说它们不是。iommus和host bridge可以在物理地址和总线地址之间进行映射。

从设备的角度来看,DMA控制器使用总线地址空间,不过可能仅限于总线空间的一个子集。例如:即便是一个系统支持64位地址内存和64 位地址的PCI bar,但是DMA可以不使用全部的64 bit地址,通过IOMMU的映射,PCI设备上的DMA可以只使用32位DMA地址。

我们用下面这样的系统结构来说明各种地址的概念:

address

在PCI设备枚举(初始化)过程中,内核了解了所有的IO device及其对应的MMIO地址空间(MMIO是物理地址空间的子集),并且也了解了是PCI主桥设备将这些PCI device和系统连接在一起。PCI设备会有BAR(base address register),表示自己在PCI总线上的地址,CPU并不能通过总线地址A(位于BAR范围内)直接访问总线上的PCI设备,PCI host bridge会在MMIO(即物理地址)和总线地址之间进行mapping。因此,对于CPU,它实际上是可以通过B地址(位于MMIO地址空间)访问PCI设备(反正PCI host bridge会进行翻译)。地址B的信息保存在struct resource变量中,并可以通过/proc/iomem开放给用户空间。对于驱动程序,它往往是通过ioremap()把物理地址B映射成虚拟地址C,这时候,驱动程序就可以通过ioread32(C)来访问PCI总线上的地址A了。

如果PCI设备支持DMA,那么在驱动中我们可以通过kmalloc或者其他类似接口分配一个DMA buffer,并且返回了虚拟地址X,MMU将X地址映射成了物理地址Y,从而定位了DMA buffer在系统内存中的位置。因此,驱动可以通过访问地址X来操作DMA buffer,但是PCI 设备并不能通过X地址来访问DMA buffer,因为MMU对设备不可见,而且系统内存所在的系统总线和PCI总线属于不同的地址空间。

在一些简单的系统中,设备可以通过DMA直接访问物理地址Y,但是在大多数的系统中,有一个IOMMU的硬件block用来将DMA可访问的总线地址翻译成物理地址,也就是把上图中的地址Z翻译成Y。理解了这些底层硬件,你也就知道类似dma_map_single这样的DMA API是在做什么了。驱动在调用dma_map_single这样的接口函数的时候会传递一个虚拟地址X,在这个函数中会设定IOMMU的页表,将地址X映射到Z,并且将返回z这个总线地址。驱动可以把Z这个总线地址设定到设备上的DMA相关的寄存器中。这样,当设备发起对地址Z开始的DMA操作的时候,IOMMU可以进行地址映射,并将DMA操作定位到Y地址开始的DMA buffer。

根据上面的描述我们可以得出这样的结论:Linux可以使用动态DMA 映射(dynamic DMA mapping)的方法,当然,这需要一些来自驱动的协助。所谓动态DMA 映射是指只有在使用的时候,才建立DMA buffer虚拟地址到总线地址的映射,一旦DMA传输完毕,就将之前建立的映射关系销毁。

虽然上面的例子使用IOMMU为例描述,不过本文随后描述的API也可以在没有IOMMU硬件的平台上运行。

顺便说明一点:DMA API适用于各种CPU arch,各种总线类型,DMA mapping framework已经屏蔽了底层硬件的细节。对于驱动工程师而言,你应该使用通用的DMA API(例如dma_map_*() 接口函数),而不是和特定总线相关的API(例如pci_map_*() 接口函数)。

驱动想要使用DMA mapping framework的API,需要首先包含相关头文件:

#include

这个头文件中定义了dma_addr_t这种数据类型,而这种类型的变量可以保存任何有效的DMA地址,不管是什么总线,什么样的CPU arch。驱动调用了DMA API之后,返回的DMA地址(总线地址)就是这种类型的。

 

三、什么样的系统内存可以被DMA控制器访问到?

既然驱动想要使用DMA mapping framework提供的接口,我们首先需要知道的就是是否所有的系统内存都是可以调用DMA API进行mapping?还是只有一部分?那么这些可以DMA控制器访问系统内存有什么特点?关于这一点,一直以来有一些不成文的规则,在本文中我们看看是否能够将其全部记录下来。

如果驱动是通过伙伴系统的接口(例如__get_free_page*())或者类似kmalloc() or kmem_cache_alloc()这样的通用内存分配的接口来分配DMA buffer,那么这些接口函数返回的虚拟地址可以直接用于DMA mapping接口API,并通过DMA操作在外设和dma buffer中交换数据。

使用vmalloc() 分配的DMA buffer可以直接使用吗?最好不要这样,虽然强行使用也没有问题,但是终究是比较麻烦。首先,vmalloc分配的page frame是不连续的,如果底层硬件需要物理内存连续,那么vmalloc分配的内存不能满足硬件要求。即便是底层DMA硬件支持scatter-gather,vmalloc分配出来的内存仍然存在其他问题。我们知道vmalloc分配的虚拟地址和对应的物理地址没有线性关系(kmalloc或者__get_free_page*这样的接口,其返回的虚拟地址和物理地址有一个固定偏移的关系),而在做DMA mapping的时候,需要知道物理地址,有线性关系的虚拟地址很容易可以获取其物理地址,但是对于vmalloc分配的虚拟地址,我们需要遍历页表才可以找到其物理地址。

在驱动中定义的全局变量可以用于DMA吗?如果编译到内核,那么全局变量位于内核的数据段或者bss段。在内核初始化的时候,会建立kernel image mapping,因此全局变量所占据的内存都是连续的,并且VA和PA是有固定偏移的线性关系,因此可以用于DMA操作。不过,在定义这些全局变量的DMA buffer的时候,我们要小心的进行cacheline的对齐,并且要处理CPU和DMA controller之间的操作同步,以避免cache coherence问题。

如果驱动编译成模块会怎么样呢?这时候,驱动中的全局定义的DMA buffer不在内核的线性映射区域,其虚拟地址是在模块加载的时候,通过vmalloc分配,因此这时候如果DMA buffer如果大于一个page frame,那么实际上我们也是无法保证其底层物理地址的连续性,也无法保证VA和PA的线性关系,这一点和编译到内核是不同的。

通过kmap接口返回的内存可以做DMA buffer吗?也不行,其原理类似vmalloc,这里就不赘述了。

块设备使用的I/O buffer和网络设备收发数据的buffer是如何确保其内存是可以进行DMA操作的呢?块设备I/O子系统和

网络子系统在分配buffer的时候会确保这一点的。

 

四、DMA寻址限制

你的设备有DMA寻址限制吗?不同的硬件平台有不同的配置方式,有的平台没有限制,外设可以访问系统内存的每一个Byte,有些则不可以。例如:系统总线有32个bit,而你的设备通过DMA只能驱动低24位地址,在这种情况下,外设在发起DMA操作的时候,只能访问16M以下的系统内存。如果设备有DMA寻址的限制,那么驱动需要将这个限制通知到内核。如果驱动不通知内核,那么内核缺省情况下认为外设的DMA可以访问所有的系统总线的32 bit地址线。对于64 bit平台,情况类似,不再赘述。

是否有DMA寻址限制是和硬件设计相关,有时候标准总线协议也会规定这一点。例如:PCI-X规范规定,所有的PCI-X设备必须要支持64 bit的寻址。

如果有寻址限制,那么在该外设驱动的probe函数中,你需要询问内核,看看是否有DMA controller可以支持这个外设的寻址限制。虽然有缺省的寻址限制的设定,不过最好还是在probe函数中进行相关处理,至少这说明你已经为你的外设考虑过寻址限制这事了。

一旦确定了设备DMA寻址限制之后,我们可以通过下面的接口进行设定:

int dma_set_mask_and_coherent(struct device *dev, u64 mask);

根据DMA buffer的特性,DMA操作有两种:一种是streaming,DMA buffer是一次性的,用完就算。这种DMA buffer需要自己考虑cache一致性。另外一种是DMA buffer是cache coherent的,软件实现上比较简单,更重要的是这种DMA buffer往往是静态的、长时间存在的。不同类型的DMA操作可能有有不同的寻址限制,也可能相同。如果相同,我们可以用上面这个接口设定streaming和coherent两种DMA 操作的地址掩码。如果不同,可以下面的接口进行设定:

int dma_set_mask(struct device *dev, u64 mask);

int dma_set_coherent_mask(struct device *dev, u64 mask);

前者是设定streaming类型的DMA地址掩码,后者是设定coherent类型的DMA地址掩码。为了更好的理解这些接口,我们聊聊参数和返回值。dev指向该设备的struct device对象,一般来说,这个struct device对象应该是嵌入在bus-specific 的实例中,例如对于PCI设备,有一个struct pci_dev的实例与之对应,而在这里需要传入的dev参数则可以通过&pdev->dev得到(pdev指向struct pci_dev的实例)。mask表示你的设备支持的地址线信息。如果调用这些接口返回0,则说明一切OK,从该设备到指定mask的内存的DMA操作是可以被系统支持的(包括DMA controller、bus layer等)。如果返回值非0,那么说明这样的DMA寻址是不能正确完成的,如果强行这么做将会产生不可预知的后果。驱动必须检测返回值,如果不行,那么建议修改mask或者不使用DMA。也就是说,对上面接口调用失败后,你有三个选择:

1、用另外的mask

2、不使用DMA模式,采用普通I/O模式

3、忽略这个设备的存在,不对其进行初始化

一个可以寻址32 bit的设备,其初始化的示例代码如下:

if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
    dev_warn(dev, "mydev: No suitable DMA available\n");
    goto ignore_this_device;
}

另一个常见的场景是有64位寻址能力的设备。一般来说我们会首先尝试设定64位的地址掩码,但是这时候有可能会失败,从而将掩码降低为32位。内核之所以会在设定64位掩码的时候失败,这并不是因为平台不能进行64位寻址,而仅仅是因为32位寻址比64位寻址效率更高。例如,SPARC64 平台上,PCI SAC寻址比DAC寻址性能更好。

下面的代码描述了如何确定streaming类型DMA的地址掩码:

int using_dac;

if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {
    using_dac = 1;
} else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) {
    using_dac = 0;
} else {
    dev_warn(dev, "mydev: No suitable DMA available\n");
    goto ignore_this_device;
}

设定coherent 类型的DMA地址掩码也是类似的,不再赘述。需要说明的是:coherent地址掩码总是等于或者小于streaming地址掩码,因此,一般来说,我们只要设定了streaming地址掩码成功了,那么使用同样的掩码或者小一些的掩码来设定coherent地址掩码总是会成功,因此这时候我们一般就不检查dma_set_coherent_mask的返回值了,当然,有些设备很奇怪,只能使用coherent DMA,那么这种情况下,驱动需要检查dma_set_coherent_mask的返回值。

 

五、两种类型的DMA mapping

1、一致性DMA映射(Consistent DMA mappings )

Consistent DMA mapping有下面两种特点:

(1)持续使用该DMA buffer(不是一次性的),因此Consistent DMA总是在初始化的时候进行map,在shutdown的时候unmap。

(2)CPU和DMA controller在发起对DMA buffer的并行访问的时候不需要考虑cache的影响,也就是说不需要软件进行cache操作,CPU和DMA controller都可以看到对方对DMA buffer的更新。实际上一致性DMA映射中的那个Consistent实际上可以称为coherent,即cache coherent。

缺省情况下,coherent mask被设定为低32 bit(0xFFFFFFFF),即便缺省值是OK了,我们也建议你通过接口在驱动中设定coherent mask。

一般使用Consistent DMA mapping的场景包括:

(1)网卡驱动和网卡DMA控制器往往是通过一些内存中的描述符(形成环或者链)进行交互,这些保存描述符的memory一般采用Consistent DMA mapping。

(2)SCSI硬件适配器上的DMA可以主存中的一些数据结构(mailbox command)进行交互,这些保存mailbox command的memory一般采用Consistent DMA mapping。

(3)有些外设有能力执行主存上的固件代码(microcode),这些保存microcode的主存一般采用Consistent DMA mapping。

上面的这些例子有同样的特性:CPU对memory的修改可以立刻被device感知到,反之亦然。一致性映射可以保证这一点。

需要注意的是:一致性的DMA映射并不意味着不需要memory barrier这样的工具来保证memory order,CPU有可能为了性能而重排对consistent memory上内存访问指令。例如:如果在DMA consistent memory上有两个word,分别是word0和word1,对于device一侧,必须保证word0先更新,然后才有对word1的更新,那么你需要这样写代码:

       desc->word0 = address;
        wmb();
        desc->word1 = DESC_VALID;

只有这样才能保证在所有的平台上,给设备驱动可以正常的工作。

此外,在有些平台上,修改了DMA Consistent buffer后,你的驱动可能需要flush write buffer,以便让device侧感知到memory的变化。这个动作类似在PCI桥中的flush write buffer的动作。

2、流式DMA映射(streaming DMA mapping)

流式DMA映射是一次性的,一般是需要进行DMA传输的时候才进行mapping,一旦DMA传输完成,就立刻ummap(除非你使用dma_sync_*的接口,下面会描述)。并且硬件可以为顺序化访问进行优化。

这里的streaming可以被认为是asynchronous,或者是不属于coherent memory范围的。

一般使用streaming DMA mapping的场景包括:

(1)网卡进行数据传输使用的DMA buffer

(2)文件系统中的各种数据buffer,这些buffer中的数据最终到读写到SCSI设备上去,一般而言,驱动会接受这些buffer,然后进行streaming DMA mapping,之后和SCSI设备上的DMA进行交互。

设计streaming DMA mapping这样的接口是为了充分优化硬件的性能,为了打到这个目标,在使用这些接口的时候,你必须清清楚楚的知道调用接口会发生什么。

无论哪种类型的DMA映射都有对齐的限制,这些限制来自底层的总线,当然也有可能是某些总线上的设备有这样的限制。此外,如果系统中的cache并不是DMA coherent的,而且底层的DMA buffer不合其他数据共享cacheline,这样的系统将工作的更好。

 

六、如何使用coherent DMA mapping的接口?

1、分配并映射dma buffer

为了分配并映射一个较大(page大小或者类似)的coherent DMA memory,你需要调用下面的接口:

   dma_addr_t dma_handle;

    cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

DMA操作总是会涉及具体设备上的DMA controller,而dev参数就是执行该设备的struct device对象的。size参数指明了你想要分配的DMA Buffer的大小,byte为单位。dma_alloc_coherent这个接口也可以在中断上下文调用,当然,gfp参数要传递GFP_ATOMIC标记,gfp是内存分配的flag,dma_alloc_coherent仅仅是透传该flag到内存管理模块。

需要注意的是dma_alloc_coherent分配的内存的起始地址和size都是对齐在page上(类似__get_free_pages的感觉,当然__get_free_pages接受的size参数是page order),如果你的驱动不需要那么大的DMA buffer,那么可以选择dma_pool接口,下面会进一步描述。

如果传入非空的dev参数,即使驱动调用了掩码设置接口函数设定了DMA mask,说明该设备可以访问大于32-bit地址空间的地址,一致性DMA映射的接口函数也一般会默认的返回一个32-bit可寻址的DMA buffer地址。要知道dma mask和coherent dma mask是不同的,除非驱动显示的调用dma_set_coherent_mask()接口来修改coherent dma mask,例如大于大于32-bit地址,dma_alloc_coherent接口函数才会返回大于32-bit地址空间的地址。dma pool接口也是如此。

dma_alloc_coherent函数返回两个值,一个是从CPU角度访问DMA buffer的虚拟地址,另外一个是从设备(DMA controller)角度看到的bus address:dma_handle,驱动可以将这个bus address传递给HW。

即便是请求的DMA buffer的大小小于PAGE SIZE,dma_alloc_coherent返回的cpu虚拟地址和DMA总线地址都保证对齐在最小的PAGE_SIZE上,这个特性确保了分配的DMA buffer有这样的特性:如果page size是64K,即便是驱动分配一个小于或者等于64K的dma buffer,那么DMA buffer不会越过64K的边界。

2、umap并释放dma buffer

当驱动需要umap并释放dma buffer的时候,需要调用下面的接口:

dma_free_coherent(dev, size, cpu_addr, dma_handle);

这个接口函数的dev、size参数上面已经描述过了,而cpu_addr和dma_handle这两个参数就是dma_alloc_coherent() 接口的那两个地址返回值。需要强调的一点就是:和dma_alloc_coherent不同,dma_free_coherent不能在中断上下文中调用。(因为在有些平台上,free DMA的操作会引发TLB维护的操作(从而引发cpu core之间的通信),如果关闭了IRQ会锁死在SMP IPI 的代码中)。

3、dma pool

如果你的驱动需非常多的小的dma buffer,那么dma pool是最适合你的机制。这个概念类似kmem_cache,__get_free_pages往往获取的是连续的page frame,而kmem_cache是批发了一大批page frame,然后自己“零售”。dma pool就是通过dma_alloc_coherent接口获取大块一致性的DMA内存,然后驱动可以调用dma_pool_alloc从那个大块DMA内存中分一个小块的dma buffer供自己使用。具体接口描述就不说了,大家可以自行阅读。

 

七、DMA操作方向

由于下面的章节会用到DMA操作方向这个概念,因此我们先简单的描述一下,DMA操作方向定义如下:

DMA_BIDIRECTIONAL
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_NONE

如果你知道的话,你应该尽可能的提供准确的DMA操作方向。

DMA_TO_DEVICE表示“从内存(dma buffer)到设备”,而 DMA_FROM_DEVICE表示“从设备到内存(dma buffer)”,上面的这些字符定义了数据在DMA操作中的移动方向。

虽然我们强烈要求驱动在知道DMA传输方向的适合,精确的指明是DMA_TO_DEVICE或者DMA_FROM_DEVICE,然而,如果你确实是不知道具体的操作方向,那么设定为DMA_BIDIRECTIONAL也是可以的,表示DMA操作可以执行任何一个方向的的数据搬移。你的平台需要保证这一点可以让DMA正常工作,当然,这也有可能会引入一些性能上的额外开销。

DMA_NONE主要是用于调试。在驱动知道精确的DMA方向之前,可以把它保存在DMA控制数据结构中,在dma方向设定有问题的适合,你可以跟踪dma方向的设置情况,以便定位问题所在。

除了潜在的平台相关的性能优化之外,精确地指定DMA操作方向还有另外一个优点就是方便调试。有些平台实际上在创建DMA mapping的时候,页表(指将bus地址映射到物理地址的页表)中有一个写权限布尔值,这个值非常类似于用户程序地址空间中的页保护。当DMA控制器硬件检测到违反权限设置时(这时候dma buffer设定的是MA_TO_DEVICE类型,实际上DMA controller只能是读dma buffer),这样的平台可以将错误写入内核日志,从而方便了debug。

只有streaming mappings才会指明DMA操作方向,一致性DMA映射隐含的DMA操作方向是DMA_BIDIRECTIONAL。我们举一个streaming mappings的例子:在网卡驱动中,如果要发送数据,那么在map/umap的时候需要指明DMA_TO_DEVICE的操作方向,而在接受数据包的时候,map/umap需要指明DMA操作方向是DMA_FROM_DEVICE。

 

八、如何使用streaming DMA mapping的接口?

streaming DMA mapping的接口函数可以在中断上下文中调用。streaming DMA mapping有两个版本的接口函数,一个是用来map/umap单个的dma buffer,另外一个是用来map/umap形成scatterlist的多个dma buffer。

1、map/umap单个的dma buffer

map单个的dma buffer的示例如下:

struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
void *addr = buffer->ptr;
size_t size = buffer->len;

dma_handle = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
    goto map_error_handling;
}

umap单个的dma buffer可以使用下面的接口:

dma_unmap_single(dev, dma_handle, size, direction);

当调用dma_map_single()返回错误的时候,你应当调用dma_mapping_error()来处理错误。虽然并不是所有的DMA mapping实现都支持dma_mapping_error这个接口(调用dma_mapping_error函数实际上会调用底层dma_map_ops操作函数集中的mapping_error成员函数),但是调用它来进行出错处理仍然是一个好的做法。这样做的好处是可以确保DMA mapping代码在所有DMA实现中都能正常工作,而不需要依赖底层实现的细节。没有检查错误就使用返回的地址可能会导致程序失败,可能会产生kernel panic或者悄悄的损坏你有用的数据。下面列举了一些不正确的方法来检查DMA mapping错误,之所以是错误的方法是因为这些代码对底层的DMA实现进行了假设。顺便说的是虽然这里是使用dma_map_single作为示例,实际上也是适用于dma_map_page()的。

错误示例一:

dma_addr_t dma_handle;

dma_handle = dma_map_single(dev, addr, size, direction);
if ((dma_handle & 0xffff != 0) || (dma_handle >= 0x1000000)) {
    goto map_error;
}

错误示例二:

dma_addr_t dma_handle;

dma_handle = dma_map_single(dev, addr, size, direction);
if (dma_handle == DMA_ERROR_CODE) {
    goto map_error;
}

当DMA传输完成的时候,程序应该调用dma_unmap_single()函数umap dma buffer。例如:在DMA完成传输后会通过中断通知CPU,而在interrupt handler中可以调用dma_unmap_single()函数。dma_map_single函数在进行DMA mapping的时候使用的是CPU指针(虚拟地址),这样就导致该函数有一个弊端:不能使用HIGHMEM memory进行mapping。鉴于此,map/unmap接口提供了另外一个类似的接口,这个接口不使用CPU指针,而是使用page和page offset来进行DMA mapping:

struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
struct page *page = buffer->page;
unsigned long offset = buffer->offset;
size_t size = buffer->len;

dma_handle = dma_map_page(dev, page, offset, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
    goto map_error_handling;
}

...

dma_unmap_page(dev, dma_handle, size, direction);

在上面的代码中,offset表示一个指定page内的页内偏移(以Byte为单位)。和dma_map_single接口函数一样,调用dma_map_page()返回错误后需要调用dma_mapping_error() 来进行错误处理,上面都已经描述了,这里不再赘述。当DMA传输完成的时候,程序应该调用dma_unmap_page()函数umap dma buffer。例如:在DMA完成传输后会通过中断通知CPU,而在interrupt handler中可以调用dma_unmap_page()函数。

2、map/umap多个形成scatterlist的dma buffer

在scatterlist的情况下,你要映射的对象是分散的若干段DMA buffer,示例代码如下:

int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist *sg;

for_each_sg(sglist, sg, count, i) {
    hw_address[i] = sg_dma_address(sg);
    hw_len[i] = sg_dma_len(sg);
}

上面的代码中nents说明了sglist中条目的数量(即map多少段dma buffer)。

具体DMA映射的实现是自由的,它可以把scatterlist 中的若干段连续的DMA buffer映射成一个大块的,连续的bus address region。例如:如果DMA mapping是以PAGE_SIZE为粒度进行映射,那么那些分散的一块块的dma buffer可以被映射到一个对齐在PAGE_SIZE,然后各个dma buffer依次收尾相接的一个大的总线地址区域上。这样做的好处就是对于那些不支持(或者支持有限)scatter-gather 的DMA controller,仍然可以通过mapping来实现。dma_map_sg调用识别的时候返回0,当调用成功的时候,返回成功mapping的数目。

一旦调用成功,你需要调用for_each_sg来遍历所有成功映射的mappings(这个数目可能会小于nents)并且使用sg_dma_address() 和 sg_dma_len() 这两个宏来得到mapping后的dma地址和长度。

umap多个形成scatterlist的dma buffer是通过下面的接口实现的:

dma_unmap_sg(dev, sglist, nents, direction);

再次强调,调用dma_unmap_sg的时候要确保DMA操作已经完成。另外,传递给dma_unmap_sg的nents参数需要等于传递给dma_map_sg的nents参数,而不是该函数返回的count。

由于DMA地址空间是共享资源,每一次dma_map_{single,sg}() 的调用都需要有其对应的dma_unmap_{single,sg}(),如果你总是分配dma地址资源而不回收,那么系统将会由于DMA address被用尽而陷入不可用的状态。

3、sync操作

如果你需要多次访问同一个streaming DMA buffer,并且在DMA传输之间读写DMA Buffer上的数据,这时候你需要小心进行DMA buffer的sync操作,以便CPU和设备(DMA controller)可以看到最新的、正确的数据。

首先用dma_map_{single,sg}()进行映射,在完成DMA传输之后,用:

dma_sync_single_for_cpu(dev, dma_handle, size, direction);

或者:

dma_sync_sg_for_cpu(dev, sglist, nents, direction);

   来完成sync的操作,以便CPU可以看到最新的数据。

如果,CPU操作了DMA buffer的数据,然后你又想把控制权交给设备上的DMA 控制器,让DMA controller访问DMA buffer,这时候,在真正让HW(指DMA控制器)去访问DMA buffer之前,你需要调用:

dma_sync_single_for_device(dev, dma_handle, size, direction);

或者:

dma_sync_sg_for_device(dev, sglist, nents, direction);

以便device(也就是设备上的DMA控制器)可以看到cpu更新后的数据。此外,需要强调的是:传递给dma_sync_sg_for_cpu() 和 dma_sync_sg_for_device()的ents参数需要等于传递给dma_map_sg的nents参数,而不是该函数返回的count。

在完成最后依次DMA传输之后,你需要调用DMA unmap函数dma_unmap_{single,sg}()。如果在第一次dma_map_*() 调用和dma_unmap_*()之间,你从来都没有碰过DMA buffer中的数据,那么你根本不需要调用dma_sync_*() 这样的sync操作。

下面的例子给出了一个sync操作的示例:

my_card_setup_receive_buffer(struct my_card *cp, char *buffer, int len)
{
    dma_addr_t mapping;

    mapping = dma_map_single(cp->dev, buffer, len, DMA_FROM_DEVICE);
    if (dma_mapping_error(cp->dev, mapping)) {
        goto map_error_handling;
    }

    cp->rx_buf = buffer;
    cp->rx_len = len;
    cp->rx_dma = mapping;

    give_rx_buf_to_card(cp);
}

...

my_card_interrupt_handler(int irq, void *devid, struct pt_regs *regs)
{
    struct my_card *cp = devid;

    ...
    if (read_card_status(cp) == RX_BUF_TRANSFERRED) {
        struct my_card_header *hp;

HW已经完成了传输,在cpu访问buffer之前,cpu需要先sync一下,以便看到最新的数据。
        dma_sync_single_for_cpu(&cp->dev, cp->rx_dma,
                    cp->rx_len,
                    DMA_FROM_DEVICE);

sync之后就可以安全的读dma buffer了
        hp = (struct my_card_header *) cp->rx_buf;
        if (header_is_ok(hp)) {
            dma_unmap_single(&cp->dev, cp->rx_dma, cp->rx_len,
                     DMA_FROM_DEVICE);
            pass_to_upper_layers(cp->rx_buf);
            make_and_setup_new_rx_buf(cp);
        } else {
            give_rx_buf_to_card(cp);
        }
    }
}

当使用了这套DMA mapping接口后,驱动不应该再使用virt_to_bus() 这个接口了,当然bus_to_virt()也不行。不过,如果你的驱动使用了这些接口怎么办呢?其实这套新的DMA mapping接口没有和virt_to_bus、bus_to_virt()一一对应的接口,因此,为了让你的程序能工作,你需要对驱动程序进行小小的修改:你必须要保存从dma_alloc_coherent()、dma_pool_alloc()以及dma_map_single()接口函数返回的dma address(对于dma_map_sg()这个接口,dma地址保存在scatterlist 中,当然这需要硬件支持dynamic DMA mapping ),并把这个dma address保存在驱动的数据结构中,并且同时/或者保存在硬件的寄存器中。

所有的驱动代码都需要迁移到DMA mapping framework的接口函数上来。目前内核已经计划完全移除virt_to_bus() 和bus_to_virt() 这两个函数,因为它们已经过时了。有些平台由于不能正确的支持virt_to_bus() 和bus_to_virt(),因此根本就没有提供这两个接口。


九、错误处理

DMA地址空间在某些CPU架构上是有限的,因此分配并mapping可能会产生错误,我们可以通过下面的方法来判定是否发生了错误:

(1)检查是否dma_alloc_coherent() 返回了NULL或者dma_map_sg 返回0

(2)检查dma_map_single和dma_map_page返回了dma address(通过dma_mapping_error函数)

   dma_addr_t dma_handle;

    dma_handle = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_handle)) {
        goto map_error_handling;
    }

(3)当在mapping多个page的时候,如果中间发生了mapping error,那么需要对那些已经mapped的page进行unmap的操作。下面的示例代码用dma_map_single函数,对于dma_map_page也一样适用。

示例代码一:

dma_addr_t dma_handle1;
dma_addr_t dma_handle2;

dma_handle1 = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle1)) {
    goto map_error_handling1;
}
dma_handle2 = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle2)) {
    goto map_error_handling2;
}

...

map_error_handling2:
    dma_unmap_single(dma_handle1);
map_error_handling1:

示例代码二(如果我们在循环中mapping dma buffer,当在中间出错的时候,一样要unmap所有已经映射的dma buffer):

dma_addr_t dma_addr;
dma_addr_t array[DMA_BUFFERS];
int save_index = 0;

for (i = 0; i < DMA_BUFFERS; i++) {

    ...

    dma_addr = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_addr)) {
        goto map_error_handling;
    }
    array[i].dma_addr = dma_addr;
    save_index++;
}

...

map_error_handling:

for (i = 0; i < save_index; i++) {

    ...

    dma_unmap_single(array[i].dma_addr);
}

如果在网卡驱动的tx回调函数(例如ndo_start_xmit)中出现了DMA mapping失败,那么驱动必须调用dev_kfree_skb() 来是否socket buffer并返回NETDEV_TX_OK 。这表示这个socket buffer由于错误而丢弃掉了。

如果在SCSI driver的queue command回调函数中出现了DMA mapping失败,那么驱动必须返回SCSI_MLQUEUE_HOST_BUSY 。这意味着SCSI子系统稍后会再次重传该command给driver。

 

十、优化数据结构

在很多的平台上,dma_unmap_{single,page}()其实什么也没有做,是空函数。因此,跟踪映射的dma address及其长度基本上就是浪费内存空间。为了方便驱动工程师编写代码方便,我们提供了几个实用工具(宏定义),如果没有它们,驱动程序中将充分ifdef或者类似的一些“work around”。下面我们并不是一个个的介绍这些宏定义,而是给出一些示例代码,驱动工程师可以照葫芦画瓢。

1、DEFINE_DMA_UNMAP_{ADDR,LEN}。在DMA buffer数据结构中使用这个宏定义,具体例子如下:

before:

    struct ring_state {
        struct sk_buff *skb;
        dma_addr_t mapping;
        __u32 len;
    };

   after:

    struct ring_state {
        struct sk_buff *skb;
        DEFINE_DMA_UNMAP_ADDR(mapping);
        DEFINE_DMA_UNMAP_LEN(len);
    };

 

根据CONFIG_NEED_DMA_MAP_STATE的配置不同,DEFINE_DMA_UNMAP_{ADDR,LEN}可能是定义相关的dma address和长度的成员,也可能是空。

2、dma_unmap_{addr,len}_set()。使用该宏定义来赋值,具体例子如下:

before:

    ringp->mapping = FOO;
    ringp->len = BAR;

   after:

    dma_unmap_addr_set(ringp, mapping, FOO);
    dma_unmap_len_set(ringp, len, BAR);

3、dma_unmap_{addr,len}(),使用该宏来访问变量。

before:

    dma_unmap_single(dev, ringp->mapping, ringp->len,
             DMA_FROM_DEVICE);

   after:

    dma_unmap_single(dev,
             dma_unmap_addr(ringp, mapping),
             dma_unmap_len(ringp, len),
             DMA_FROM_DEVICE);

上面的这些代码基本是不需要解释你就会明白的了。另外,我们对于dma address和len是分开处理的,因为在有些实现中,unmaping的操作仅仅需要dma address信息就够了。

 

十一、平台移植需要注意的问题

如果你仅仅是驱动工程师,并不负责将linux迁移到某个cpu arch上去,那么后面的内容其实你可以忽略掉了。

1、Struct scatterlist的需求

如果cpu arch支持IOMMU(包括软件模拟的IOMMU),那么你需要打开CONFIG_NEED_SG_DMA_LENGTH 这个内核选项。

2、ARCH_DMA_MINALIGN

CPU体系结构相关的代码必须要要保证kmalloc分配的buffer是DMA-safe的(kmalloc分配的buffer也是有可能用于DMA buffer),驱动和内核子系统的正确运行都是依赖这个条件的。如果一个cpu arch不是全面支持DMA-coherent的(例如硬件并不保证cpu cache中的数据等于main memory中的数据),那么必须定义ARCH_DMA_MINALIGN。而通过这个宏定义,kmalloc分配的buffer可以保证对齐在ARCH_DMA_MINALIGN上,从而保证了kmalloc分配的DMA Buffer不会和其他的buffer共享一个cacheline。想要了解具体的实例可以参考arch/arm/include/asm/cache.h。

另外,请注意:ARCH_DMA_MINALIGN 是DMA buffer的对齐约束,你不需要担心CPU ARCH的数据对齐约束(例如,有些CPU arch邀请有些数据对象需要64-bit对齐)。

 

十二、后记

如果没有来自广大人民群众的反馈和建议,这份文档(包括DMA API本身)可能会显得过时,陈旧。

此外,对这份文档有帮助的人如下(没有按照什么特别的顺序):

Russell King
Leo Dagum
Ralf Baechle
Grant Grundler
Jay Estabrook
Thomas Sailer
Andrea Arcangeli
Jens Axboe
David Mosberger-Tang davidm@hpl.hp.com

 

备注:本文基本上是内核文档DMA-API-HOWTO.txt的翻译,如果有兴趣可以参考原文。


原创翻译文章,转发请注明出处。蜗窝科技

X-024-OHTHERS-在windows平台下使用libusb

$
0
0

1. 前言

话说我们“X Project”的第一个任务就是通过USB将主机上的Image文件下载到开发板的Ram中执行(参考[1]中有关的内容),为此我们在host中porting了一个简单的应用程序(称作DFU[2]),负责和开发板ROM中的代码交流,下载并执行Image文件。为了方便,该应用程序使用libusb[3]进行USB有关的操作。

libusb不止使用起来简单,还有一个极大的优点,就是“跨平台”的特性。我们之前的例子[4]都是在Linux平台下操作的,最近由于win10内置了Ubuntu,Linux平台有关的开发工作,基本上都可以在这里完成了,因此就不需要费时、费神地切换到纯Linux环境下工作了。

不过呢,Win10的Ubuntu好是好,但没法像纯Linux系统那样支持USB设备,DFU有关的工作就无法在这里正常工作,因此就发挥libusb的特性,把“X Project” DFU[2]有关的代码在Windows下跑起来,也算感受一下“跨平台”的魅力。具体步骤如下。

2. 步骤

1)安装MinGW[6]

参考[5]中的介绍,libusb在Windows下可以在MinGW环境下编译、使用,因此我们可以按照[7]中的步骤,在Windows中下载并安装MinGW。

2) 编译libusb、dfu,并运行dfu(具体可参考[1]中有关的文章)

发现没有USB的驱动程序(见下面的log):

cd /d/work/xprj/build && make libusb && make dfu

cd /d/work/xprj/tools/dfu && ./dfu.exe bubblegum 0
board_bubblegum_init
board bubblegum
address 0x0
filename (null)
need_run 0
bubblegum_init
b96_init_usb
b96_init_device
libusb: info [windows_get_device_list] The following device has no driver: '\\.\USB#VID_10D6&PID_10D6#5&A3E6D0F&0&3'
libusb: info [windows_get_device_list] libusb will not be able to access it.
Error: cannot open device 10d6:10d6
b96_init_device failed
board->init failed!

3)通过Zadig[8]安装USB的通用驱动

参考[5]中的说明,要在Windows访问USB设备,需要相应的驱动程序,例如WinUSB等,可以使用Zadig[8]工具辅助安装:

https://github.com/libusb/libusb/wiki/Windows#How_to_use_libusb_on_Windows

Driver Installation

If your target device is not HID, you must install a driver before you can communicate with it using libusb. Currently, this means installing one of Microsoft's WinUSB, libusb-win32 or libusbK drivers. Two options are available:

  • Recommended: Use the most recent version of Zadig, an Automated Driver Installer GUI application for WinUSB, libusb-win32 and libusbK...
  • Alternatively, if you are only interested in WinUSB, you can download the WinUSB driver files and customize the inf file for your device.

http://zadig.akeo.ie/

Zadig
is a Windows application that installs generic USB drivers, such as WinUSB, libusb-win32/libusb0.sys or libusbK, to help you access USB devices.

Zadig的使用极其简单,通过USB ID确定需要安装驱动的USB设备,然后再右边的列表中选择安装的驱动类型(这里以WinUSB为例),点击“Install Driver”即可,如下图所示:

image

4)再次运行dfu工具,已经和开发板对上暗号了,成功!!

$ /d/work/xprj/build/../tools/dfu/dfu bubblegum 0xe406b200 /d/work/xprj/build/../tools/actions/splboot.bin 1
board_bubblegum_init
board bubblegum
address 0xe406b200
filename d:/work/xprj/build/../tools/actions/splboot.bin
need_run 1
bubblegum_init
b96_init_usb
b96_init_device
bDescriptorType: 1
bNumConfigurations: 1
iManufacturer: 0
bNumInterfaces: 1
Info: cannot detach kernel driver: LIBUSB_ERROR_NOT_SUPPORTED
Configuiration: 1
Handler: 009AB270
bubblegum_upload, filename d:/work/xprj/build/../tools/actions/splboot.bin, addr 0xe406b200
writeBinaryFile
writeBinaryFileSeek
CBW: 55 53 42 43 00 00 00 00 83 43 00 00 00 00 10 05 00 b2 06 e4 83 43 00 00 00 00 00 00 00 00 00
Bulk transferred 17283 bytes
readCSW
CSW:55 53 42 53 00 00 00 00 00 00 00 00 00
bubblegum_run, addr 0xe406b200
unknownCMD07
CBW: 55 53 42 43 00 00 00 00 00 00 00 00 00 00 00 10 00 b2 06 e4 00 00 00 00 00 00 00 00 00 00 00
Transffered: 31
readCSW
CSW:55 53 42 53 00 00 00 00 00 00 00 00 00
bubblegum_exit
b96_uninit_device
b96_uninit_usb

3. 参考文档

[1] 【任务1】启动过程-Boot from USB

[2] xprj dfu, https://github.com/wowotechX/tools/tree/master/dfu

[3] libusb, http://libusb.info/

[4] X-010-UBOOT-使用booti命令启动kernel(Bubblegum-96平台)

[5] https://github.com/libusb/libusb/wiki/Windows#How_to_use_libusb_on_Windows

[6] MinGW, www.mingw.org/

[7] Windows系统结合MinGW搭建软件开发环境

[8] zadig, http://zadig.akeo.ie/

[9] X-002-HW-S900芯片boot from USB有关的硬件描述

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

Viewing all 218 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>