0%

Makefile 特殊符号

- @ + $ $$

- 符号 (连字符)

任何命令行的任何非零退出状态都被忽略,忽略当前命令行执行时所遇到的错误。
make 在执行命令的时候,如果遇到 error,会退出执行。加上减号的目的是即便此命令行执行出错,那么也不要管,继续执行 make。
通常删除或者创建文件时,遇到文件不存在或者已经创建。如果希望忽略掉错误,继续执行,就可以在命令行前面添加 -

1
2
3
.PHONY : clean
clean :
-rm $(objects)

@ 符号 (at 符号)

通常 makefile 会将其执行的命令行在执行前输出到屏幕上。如果将 @ 添加到命令行前,这个命令将不被 make 回显出来,即不显示命令本身而只显示结果

1
2
all :
@echo "Make"

+ 符号 (加号)

使用加号修饰符让命令始终执行。命令行执行时不受到 make 的 -n -t -q 三个参数的影响,忽略这三个参数。
如果 make 执行时,使用 -n 或 –just-print。该参数显示命令,不会执行命令。这个功能有利于调试 Makefile,方便查看执行的命令形式和顺序。

$ 符号 (美元符号)

美元符号 $,扩展打开 makefile 中定义的变量。

$$ 符号

$$ 符号,扩展打开 makefile 中定义的 shell 变量。

gdb查看内存数据

格式:

1
x/nfu

说明:

  • x 是 examine 的缩写,意思是检查
  • n表示要显示的内存单元的个数
  • f表示显示方式
  • u表示一个地址单元的长度:

显示方式 f 取值

1
2
3
4
5
6
7
8
9
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
i 指令地址格式
c 按字符格式显示变量。
f 按浮点数格式显示变量。

地址单元长度 u 取值

1
2
3
4
b表示单字节,
h表示双字节,
w表示四字节,
g表示八字节

example

1
2
x/50xw 0x40451400

50是数量,x是16进制,w是四字节

Examining Memory

You can use the command x (for “examine”) to examine memory in any of several formats, independently of your program’s data types.

1
2
3
x/nfu addr
x addr
x

Use the x command to examine memory.

n, f, and u are all optional parameters that specify how much memory to display and how to format it; addr is an expression giving the address where you want to start displaying memory. If you use defaults for nfu, you need not type the slash ‘/’. Several commands set convenient defaults for addr.

n, the repeat count

The repeat count is a decimal integer; the default is 1. 
It specifies how much memory (counting by units u) to display. 
If a negative number is specified, memory is examined backward from addr.

f, the display format

The display format is one of the formats used by print 
(‘x’, ‘d’, ‘u’, ‘o’, ‘t’, ‘a’, ‘c’, ‘f’, ‘s’), 
‘i’ (for machine instructions) and ‘m’ (for displaying memory tags). 
The default is ‘x’ (hexadecimal) initially. 
The default changes each time you use either x or print.

u, the unit size

The unit size is any of

1
2
3
4
5
6
7
8
9
10
11
b
Bytes.

h
Halfwords (two bytes).

w
Words (four bytes). This is the initial default.

g
Giant words (eight bytes).
  • Each time you specify a unit size with x, that size becomes the default unit the next time you use x.
  • For the ‘i’ format, the unit size is ignored and is normally not written.
  • For the ‘s’ format, the unit size defaults to ‘b’, unless it is explicitly given.
  • Use x /hs to display 16-bit char strings and x /ws to display 32-bit strings. The next use of x /s will again display 8-bit strings.
  • Note that the results depend on the programming language of the current compilation unit.
  • If the language is C, the ‘s’ modifier will use the UTF-16 encoding while ‘w’ will use UTF-32. The encoding is set by the programming language and cannot be altered.

modify memory

In order to set the variable g, use

1
(gdb) set var g=4

GDB allows more implicit conversions in assignments than C; you can freely store an integer value into a pointer variable or vice versa,
and you can convert any structure to any other structure that is the same length or shorter.

To store values into arbitrary places in memory, use the ‘{…}’ construct to generate a value of specified type at a specified address (see Expressions).
For example, {int}0x83040 refers to memory location 0x83040 as an integer (which implies a certain size and representation in memory), and

1
set {int}0x83040 = 4

stores the value 4 into that memory location.

This should work for any valid pointer, and can be cast to any appropriate data type.

1
set *((int *) 0xbfbb0000) = 20

e.g.

1
2
3
4
5
6
7
8
9
10
11
12

set *(unsigned char *)<memaddr> = <value> ; write 1 byte
set *(unsigned short *)<memaddr> = <value> ; write 2 bytes
set *(unsigned int *)<memaddr> = <value> ; write 4 bytes
set *(unsigned long long *)<memaddr> = <value> ; write 8 bytes

or

set *(char *)<memaddr> = <value> ; write 1 byte
set *(short *)<memaddr> = <value> ; write 2 bytes
set *(int *)<memaddr> = <value> ; write 4 bytes
set *(long long *)<memaddr> = <value> ; write 8 bytes

or use

1
set *0xbfbb0000=20

change memory locations directly.

inline 内联函数

有些函数短小精悍而且调用频繁,调用开销大,经常保护现场,恢复现场,算下来性价比不高。这时就可以将这个函数声明为内联函数。编译器在编译过程中遇到内联函数,像宏一样,将内联函数直接在调用处展开,这样就可以减少函数调用的开支:直接执行内联函数展开的代码,不用再保存现场和恢复现场

内联函数和宏

内联函数和宏的功能差不多,为什么不直接定义一个宏定义?而是定义一个内联函数?

存在即合理,内联函数在 C 语言中有广泛运用,自然有其原因。与宏相比,内联函数有以下优势:

  • 参数类型检查

        内联函数随有宏的展开特性,但其本质仍是函数,在编译过程中,编译器仍可以对其进行参数检查,而宏不具备这个功能。
    
  • 便于调试

    ​ 函数支持的调试功能有断点、单步等,内联函数同样支持。

  • 返回值

    ​ 内联函数有返回值,返回一个结果给调用者。这个优势是相对 ANSI C 说的,因为现在宏也可以有返回值和类型了,如前面使用语句表达式的宏。

  • 接口封装

    ​ 有些内联函数可以用来封装一个接口,而宏不具备这个功能。

内联函数不足

内联函数并不是完美无瑕的,也存在不足。

  • 内联函数会增大函数体积。
    如果一个文件中多次调用内联函数,多次展开,那整个程序的体积就会变大,在一定程度上会减低程序的执行效率。
  • 减低代码复用性。
    函数的作用之一就是提高代码的复用性。我们将一些代码封装成函数,进行模块化编程,可以减轻软件开发的工作量,而内联函数往往又减低函数的复用性。

编译器对内联函数的处理

我们通过 inline关键字将一个函数声明为内联函数,但编译器不一定会对这个内联函数在进行展开。编译器也要根据实际情况进行评估。除了检测用户定义的内联函数是否含有指针、循环、递归,还会在函数执行效率和函数调用开销之间进行权衡。

从程序员角度出发,是否展开主要考虑如下因素:

  • 函数体积大小
  • 函数体内无指针赋值、递归、循环等语句
  • 调用频繁

当我们认为一个函数体积小,而且被大量调用,应该做内联展开。

属性声明:noinline

明确告诉编译器不展开内联函数

属性声明:always_inline

明确告诉编译器展开内联函数

内联函数定义在头文件中

在Linux 内核中,有大量的内联函数被定义在头文件中,而且经常使用 static修饰。

Q: 为什么内联函数要定义在头文件中?

A: 因为它是一个内联函数,可以像宏一样展开,任何像使用这个内联函数的源文件,都不必亲自去定义一遍,直接包含这个头文件即可,即像宏定义一样使用。

Q:为什么要加static修饰

inline 定义的内联函数不一定会展开,当一个工程中多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。使用 static 关键字修饰,则可以将这个函数的作用域限制在各自的文件内,避免重定义错误的发生。

像素颜色

用红绿蓝三颜色来表示,可以用24位数据来表示红绿蓝,也可以用16位等等格式,比如:

  • bpp:bits per pixel,每个像素用多少位来表示
  • 24bpp:实际上会用到32位,其中8位未使用,其余24位中分别用8位表示红(R)、绿(G)、蓝(B) 。24位使用32位来存储方便寻址。
  • 16bpp:有rbg565,rgb555
    • rgb565:用5位表示红、6位表示绿、5位表示蓝。人眼对绿色较敏感,能分辨出其中的细微差别
    • rgb555:16位数据中用5位表示红、5位表示绿、5位表示蓝,浪费一位

注:有些LCD控制器可以设置红绿蓝三原色的位置,比如24位的数据里,可能是RGB888,也可能是BGR888

Framebuffer

存储像素数据的一块特殊内存,显存

  • 也有芯片手册称为 GRAM, G指图形

对于应用工程师

使用LCD只需要掌握三点:

  1. 颜色格式:16bpp/24bpp
  2. Framebuffer基地址
  3. LCD屏幕分辨率,根据分辨率才能找到像素点在显存的任意位置

应用工程师将数据写入Framebuffer即可,LCD controler (LCD控制器)会帮助更新屏幕上像素的颜色。

对于驱动工程师

对LCD的理解要深入硬件,比如要回答这几个问题:

  • Framebuffer在哪里?

LCD里面还是LCD外面

  • 谁把Framebuffer中的数据发给LCD?

LCD controler,驱动工程师很大一部分工作既是设置初始化LCD controler

统一的硬件模型

LCD接口众多,但硬件模型一致,原理一致

MCU常用的8080接口LCD模组

内存,LCD控制器,LCD屏幕组合成一个LCM模组,单片机F103直接跟模组通信

F103一般通过以下信号线跟LCM模组通信

  • CS 片选线
  • RD / WD 读写控制线
  • DataBus 数据总线
  • Data / cmd 控制引脚,决定DataBus上传输的是数据还是地址等其他信息

MPU常用的TFT RGB接口

只有LCD屏幕在外面,LCD控制器位于ARM芯片内部,可外接显存。LCD控制器通过RGB三组线以及其他控制信号线对LCD屏幕进行控制

  • DCLK 移动一个像素点
  • HSYNC 从最右跳到下一行
  • VSYNC 从最后一个跳到第一个
  • DE 决定是否接受RGB数据,在像素点跳转时禁用RGB数据

RGB三组线

  • 对于使用真彩色的LCD控制器,RGB引脚上的数据直接来自自Framebuffer;

  • 对于使用调色板的LCD控制器,Framebuffer中的数据只是用来取出调色板中的颜色,调色板中的数据会被放到RGB引脚上去。

编写Framebuffer框架

分配显存时,不可以使用kmalloc函数。

显存要保证物理地址连续,kamlloc函数分配的内存可以保证虚拟地址的联系,但在物理地址上不一定是连续的。

Framebuffer框架分为上下两层:

  • fbmem.c:承上启下
    • 实现、注册file_operations结构体
    • 把APP的调用向下转发到具体的硬件驱动程序
  • xxx_fb.c:硬件相关的驱动程序
    • 实现、注册fb_info结构体
    • 实现硬件操作

Framebuffer核心:

  • 分配fb_info

    • framebuffer_alloc
  • 设置fb_info

    • var
    • fbops
    • 硬件相关操作
  • 注册fb_info

    • register_framebuffer

imx6ull LCD 控制器

控制器模块

  • 硬件框架
  • 数据传输与处理
  • 时序控制

数据处理过程

  1. 从显存读数据 32bit * n
  2. 判断是否交换
  3. 使用哪种RGB格式 RGB555, RGB565, RGB888
  4. 设置时序,用于发送数据
  5. RGB 数据格式 跟LCD屏幕匹配

例如:RGB 888 - > 16bpp的LCD ,在8位中只传五位

LCD控制器寄存器简介

查看任何芯片的LCD控制器寄存器时,记住几个要点:

① 怎么把LCD的信息告诉LCD控制器:即分辨率、行列时序、像素时钟等;
② 怎么把显存地址、像素格式告诉LCD控制器。

内核中的LCD驱动程序

如何确定内存LCD驱动程序

在已经编译好的内核中 drivers/video/fbdev目录下,有哪些.o文件,对应的.c文件。

编程_LCD驱动程序框架_使用设备树

Linux驱动程序 = 驱动程序框架 + 硬件编程。

驱动程序框架核心就是:

  • 分配fb_info
  • 设置fb_info
  • 注册fb_info
  • 硬件相关的设置

硬件相关的设置

  • 引脚设置
  • 时钟设置
  • LCD控制器设置

入口函数注册platform_driver

设备树结点:

1
2
3
4
framebuffer-mylcd {
compatible = "huahui,lcd_drv";
};

编写probe函数

  • 驱动程序框架核心 fb_info
  • 硬件相关的设置

引脚配置

主要使用pinctrl子系统把引脚配置为LCD功能,对于背光引脚等使用GPIO子系统的函数控制它的输出电平。

设备树结点

1
2
3
pinctrl-names = "default";
pinctrl-0 = <&pmylcd_pinctrl>;
backlight-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>;

时钟配置

通过芯片手册,查看需要使能那些时钟

设备树结点

1
2
3
clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
<&clks IMX6UL_CLK_LCDIF_APB>;
clock-names = "pix", "axi";

配置LCD控制器

在设备树里指定LCD参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
framebuffer-mylcd {
compatible = "100ask,lcd_drv";
pinctrl-names = "default";
pinctrl-0 = <&mylcd_pinctrl>;
backlight-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>;

clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
<&clks IMX6UL_CLK_LCDIF_APB>;
clock-names = "pix", "axi";

display = <&display0>;

display0: display {
bits-per-pixel = <24>;
bus-width = <24>;

display-timings {
native-mode = <&timing0>;

timing0: timing0_1024x768 {
clock-frequency = <50000000>;
hactive = <1024>;
vactive = <600>;
hfront-porch = <160>;
hback-porch = <140>;
hsync-len = <20>;
vback-porch = <20>;
vfront-porch = <12>;
vsync-len = <3>;

hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <0>;
};

};
};
};

从设备树获得参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int ret;
u32 width;
u32 bits_per_pixel;
struct display_timings *timings = NULL;

/* get display node from device tree , pdev->dev.of_node : from father node*/
display_np = of_parse_phandle(pdev->dev.of_node, "display", 0);

/* get common info 通用属性 */
ret = of_property_read_u32(display_np, "bus-width", &width);
ret = of_property_read_u32(display_np, "bits-per-pixel",
&bits_per_pixel);

/* get timings from device tree */
timings = of_get_display_timings(display_np);

使用参数配置LCD控制器

根据芯片手册,一个一个设置寄存器:

  • Framebuffer地址设置
  • Framebuffer中数据格式设置
  • LCD时序参数设置
  • LCD引脚极性设置

注意:硬件参数,例如lcd控制器物理地址等,最好在设备树中指定,而不是写在代码中

调试LCD驱动程序

要做的事情

  • 去除内核自带的驱动程序

  • 加入我们编写的驱动程序、设备树文件

  • 重新编译内核、设备树

  • 上机测试:使用编译出来的内核、设备树启动板子

单Buffer的缺点与改进方法

单buffer的缺点

  • 如果APP速度很慢,可以看到它在LCD上缓慢绘制图案

  • 即使APP速度很高,LCD控制器不断从Framebuffer中读取数据来显示,而APP不断把数据写入Framebuffer

    • 假设APP想把LCD显示为整屏幕的蓝色、红色

    • 很大几率出现这种情况:

      • LCD控制器读取Framebuffer数据,读到一半时,在LCD上显示了半屏幕的蓝色

      • 这是APP非常高效地把整个Framebuffer的数据都改为了红色

      • LCD控制器继续读取数据,于是LCD上就会显示半屏幕蓝色、半屏幕红色

      • 人眼就会感觉到屏幕闪烁、撕裂

使用多Buffer来改进

上述两个缺点的根源是一致的:Framebuffer中的数据还没准备好整帧数据,就被LCD控制器使用了。

  • 使用双buffer甚至多buffer可以解决这个问题。
1
2
3
4
5
6
* 假设有2个Framebuffer:FB0、FB1
* LCD控制器正在读取FB0
* APP写FB1
* 写好FB1后,让LCD控制器切换到FB1
* APP写FB0
* 写好FB0后,让LCD控制器切换到FB0

具体流程:

  • 驱动:分配多个buffer
1
2
3
4
5
fb_info->fix.smem_len = SZ_32M;
fbi->screen_base = dma_alloc_writecombine(fbi->device, //fbi->screen_base虚拟地址
fbi->fix.smem_len,
(dma_addr_t *)&fbi->fix.smem_start, //fix.smem_start物理地址
GFP_DMA | GFP_KERNEL);
  • 驱动:保存buffer信息
1
2
fb_info->fix.smem_len  // 含有总buffer大小 
fb_info->var // 含有单个buffer信息

fb_info固定信息 fix:显存起始地址,大小

可变信息 var:x / y 分辨率

1
2
3
4
5
6
/*一般情况下*/ /*分配了多个buffer,在y轴上叠加*/
yres_virtual = yres *n; xres_virtual = xres;
/* 但驱动程序一开始并不使能多buffer
* 即yres_virtual = yres;
* 需要 set : yres_virtual = yres *n;
*/
  • APP : 读取buffer信息
1
2
3
4
5
6
ioctl(fd_fb, FBIOGET_FSCREENINFO, &fix);
ioctl(fd_fb, FBIOGET_VSCREENINFO, &var);

// 计算是否支持多buffer,有多少个buffer
screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
nBuffers = fix.smem_len / screen_size;
  • APP:使能多buffer
1
2
var.yres_virtual = nBuffers * var.yres;
ioctl(fd_fb, FBIOPUT_VSCREENINFO, &var);
  • APP : 写buffer
1
2
3
4
5
6
7
fb_base = (unsigned char *)mmap(NULL , fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);

/* get buffer */
pNextBuffer = fb_base + nNextBuffer * screen_size;

/* set buffer */
lcd_draw_screen(pNextBuffer, colors[i]);
  • APP : 切换buffer
1
2
3
/* switch buffer */
var.yoffset = nNextBuffer * var.yres;
ioctl(fd_fb, FBIOPAN_DISPLAY, &var);
  • 驱动:切换buffer
1
2
3
4
5
// fbmem.c
fb_ioctl
do_fb_ioctl
fb_pan_display(info, &var);
err = info->fbops->fb_pan_display(var, info) // 调用硬件相关的函数

U-BOOT DM驱动模型

Before driver model

  • U-Boot has 10 useful design principles (e.g. small, fast, simple, portable)
    • Huge community, over 1000 boards supported by the end of 2011
    • But Ad-hoc driver model started to bite
  • Drivers were invoked through direct C calls
    • i2c_read() is implemented by whichever driver is compiled in
    • CONFIG option select which I2C driver to use, clock speed, bus number, etc.
  • Hard to scale
    • Multiple I2C drivers must be munged into a single driver
    • Or an ad-hoc framework created to handle this requirements
  • Configuration becoming unwieldy
    • 6000 CONFIG options at its peak
    • Kconfig conversion helps, but that’s still a lot of options

dm-u-boot.pdf

U-Boot Mini Summit talk on driver model at ELCE 2014

Why driver model?

  • Device init and access is ad-hoc
    – scsi_init(), mmc_init(), nand_init()
  • Many subsystems only allow one driver
    – But I have USB2 and USB3!
  • Communication between subsystems is tricky
    – How does an I2C expander or PMIC provide a GPIO?
  • Hard to answer simple questions
    – How many GPIOs? What is my serial console?
  • Board file functions provide the glue
    – What GPIO provides my MMC card detect?

Architecture 体系结构

DM (Driver Model)是 U-Boot 中的驱动框架。

  • udevice 描述具体的某一个硬件设备。

    • Instance of a driver
    • Created from some platform-specific information bound to a driver
  • driver 是与这个设备匹配的驱动。

    • Code to talk to a peripheral type (e.g. Ethernet switch, I2C controller, LCD)
  • uclass 是同一类设备的抽象,提供管理同一类设备的抽象接口。

    • A way of grouping devices which operate the same way
  • Device tree and hierarchy

    • 设备树和层次结构
  • Memory allocation

  • Sequence numbers

    • 序列号

像 Kernel 中的驱动三要素 device 、bus 、driver 一样,DM 也有自己的三要素:udevice、uclass、driver。以 serial 驱动为例:

Device tree configuration

Device sequence

Automatic memory allocation

Architecture 2

  • Binding and probing
    • Bing creates the device but does not touch hardware
    • Probing activates the device ready for use
  • Avoid private data structures
    • Everything out in the open
  • SPL support
    • fdtgrep
    • Simple malloc()
    • Drop device removal code, warnings, etc.

Driver model benefits (en)

  • Consistent view of devices regardless of their type
  • Multiple driver can be used with the same subsystem
    • Drivers can be created which use others driver for their transport layer
  • The lifecycle of a device is clear and consistent
  • Devices can be bound automatically
    • Then probed automatically when used
  • Supports device tree for configuration
    • Thus sharing this with Linux and potentially other projects
    • Avoids recreating the same information again in a different format

Driver model benefits (zh)

  • 一致的设备视图,无论其类型如何
  • 同一个子系统可以使用多个驱动程序
    • 可以创建使用其他驱动程序作为传输层的驱动程序
  • 设备生命周期清晰一致
  • 设备可自动绑定
    • 然后使用时自动探测
  • 支持设备树配置
    • 因此与 Linux 和潜在的其他项目共享这个
    • 避免以不同的格式重新创建相同的信息

DM Limitations

  • A driver can be in only one uclass
    • Multi-function devices must use separate child devices
  • Uses flattened device tree (FDT,即 device tree, dts)
    • Driver model uses the device tree offset
    • Overlays and other mutations are not supported

Comparisons with Linux

  • Classes

  • Buses

  • Binding and probing

  • Memory allocation

  • Relocation and SPL //重定位 & SPL

  • device visibility

  • Locking

  • Data structure size

    • Core structure sizes are moderate
      • struct uclass
      • struct udevice
      • struct driver
  • automatic memory allocation

A few examples

Example 1: Requesting GPIOs

Example 2: Enabling power

重要数据结构

udevice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* struct udevice - An instance of a driver
*
* This holds information about a device, which is a driver bound to a
* particular port or peripheral (essentially a driver instance).
*
* A device will come into existence through a 'bind' call, either due to
* a U_BOOT_DEVICE() macro (in which case platdata is non-NULL) or a node
* in the device tree (in which case of_offset is >= 0). In the latter case
* we translate the device tree information into platdata in a function
* implemented by the driver ofdata_to_platdata method (called just before the
* probe method if the device has a device tree node.
*
* All three of platdata, priv and uclass_priv can be allocated by the
* driver, or you can use the auto_alloc_size members of struct driver and
* struct uclass_driver to have driver model do this automatically.
*
* @driver: The driver used by this device
* @name: Name of device, typically the FDT node name
* @platdata: Configuration data for this device
* @parent_platdata: The parent bus's configuration data for this device
* @uclass_platdata: The uclass's configuration data for this device
* @node: Reference to device tree node for this device
* @driver_data: Driver data word for the entry that matched this device with
* its driver
* @parent: Parent of this device, or NULL for the top level device
* @priv: Private data for this device
* @uclass: Pointer to uclass for this device
* @uclass_priv: The uclass's private data for this device
* @parent_priv: The parent's private data for this device
* @uclass_node: Used by uclass to link its devices
* @child_head: List of children of this device
* @sibling_node: Next device in list of all devices
* @flags: Flags for this device DM_FLAG_...
* @req_seq: Requested sequence number for this device (-1 = any)
* @seq: Allocated sequence number for this device (-1 = none). This is set up
* when the device is probed and will be unique within the device's uclass.
* @devres_head: List of memory allocations associated with this device.
* When CONFIG_DEVRES is enabled, devm_kmalloc() and friends will
* add to this list. Memory so-allocated will be freed
* automatically when the device is removed / unbound
*/
struct udevice {
const struct driver *driver;
const char *name;
void *platdata;
void *parent_platdata;
void *uclass_platdata;
ofnode node;
ulong driver_data;
struct udevice *parent;
void *priv;
struct uclass *uclass;
void *uclass_priv;
void *parent_priv;
struct list_head uclass_node;
struct list_head child_head;
struct list_head sibling_node;
uint32_t flags;
int req_seq;
int seq;
#ifdef CONFIG_DEVRES
struct list_head devres_head;
#endif
};

有三种途径生成一个 udevice:

  1. dts 设备节点

  2. UBOOTDEVICE(__name) 宏申明

  3. 调用 ‘bind’ API, device_bind_xxx

Device Model Start-up sequence

DM 启动顺序, core/root.c

  • dm_init_and_scan():
  • dm_init()
    • Creates an empty list of devices and uclasses
    • Binds and probes a root device
  • dm_scan_platdata()
    • Scans available platform data looking devices to be created
    • Platform data may only be used when memory constrains prohibit device tree
  • dm_scan_fdt()
    • Scan device tree and bind driver to nodes to create devices

根据当前 U-Boot 的编程哲学,基本大部分设备都是通过 dts 来描述,还有少部分设备因为特殊原因,可以通过 U_BOOT_DEVICE(_name) 宏申明。

匹配过程

在UBoot DM 初始化阶段(initfdm 和 initrdm),通过调用 dm_init_and_scan(boolpre_reloc_only) 根据名称 (UBOOT_DEVICE 中和 driver 的 name,或者 dts 和 driver 的 compatible) 匹配到对应的 driver,

然后调用 device_bind_common 函数生成 udevice,udevice 会和 driver 绑定,

并根据 driver 中的uclass id 找到对应的 uclass driver,并生成相应的 uclass, 并把该设备挂到 uclass 的设备节点之下。

最后调用 driver 的 bind 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| -> dm_init_and_scan
| -> dm_init
| -> INIT_LIST_HEAD //创建 root 节点
| -> device_bind_by_name()
| -> lists_driver_lookup_name //lists_driver_lookup_name("root_driver"), This function returns a pointer a driver given its name.
| -> ll_entry_start //Point to first entry of linker-generated array
| -> ll_entry_count //Return the number of elements in linker-generated array
| -> for { strcmp } //通过名字来遍历,得到匹配的 driver
| -> device_bind_common
| -> uclass_get
| -> uclass_find //Find uclass by uclass_id
| -> list_add_tail //put dev into parent's successor list
| -> uclass_bind_device
| -> device_probe
| ->
| -> dm_scan_platdata
| -> dm_extended_scan_fdt
| -> dm_scan_other

还有部分特殊的驱动,他们并不存在实际意义上的设备,比如 MMC 子系统中的 mmcblk 驱动,

该驱动主要是把所有的 mmc 设备注册到更上一层的 blk 子系统中,向 blk 层提供操作 mmc 设备的 blkops,向下通过mmc uclass 提供的统一接口控制 mmc 设备。显然,这个驱动位于抽象层,它不和具体的硬件设备直接交互,并不适合用一个 dts(dts 是用来描述具体的硬件信息的) 节点或者 UBOOTDEVICE(_name) 宏来为这个驱动显示的申明设备。这种情形下一般通过主动调用 device_bind_xxx 系列 API 来完成驱动和设备已经更上一层 uclass 之间的 bind。

A:生成 udevice。
B:绑定 udevice 和 driver。
C:把设备挂到 uclass 的dev_head 链表下。
D:调用设备驱动的 bind 接口。

uclass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* struct uclass - a U-Boot drive class, collecting together similar drivers
*
* A uclass provides an interface to a particular function, which is
* implemented by one or more drivers. Every driver belongs to a uclass even
* if it is the only driver in that uclass. An example uclass is GPIO, which
* provides the ability to change read inputs, set and clear outputs, etc.
* There may be drivers for on-chip SoC GPIO banks, I2C GPIO expanders and
* PMIC IO lines, all made available in a unified way through the uclass.
*
* @priv: Private data for this uclass
* @uc_drv: The driver for the uclass itself, not to be confused with a
* 'struct driver'
* @dev_head: List of devices in this uclass (devices are attached to their
* uclass when their bind method is called)
* @sibling_node: Next uclass in the linked list of uclasses
*/
struct uclass {
void *priv;
struct uclass_driver *uc_drv;
struct list_head dev_head;
struct list_head sibling_node;
};

这里主要的成员是 uclassdriver 和 devhead 链表。

  • dev_head 是一个链表头, 用来链接该类下的所有设备。可以通过 uclass_foreach_dev(dev,uc) 遍历该class 下的所有设备。
  • uclass_driver 是针对某一类设备提供的通用操作接口,然后通过 udevice->driver->ops 操作到具体的硬件设备。
    uclassdriver 通过 UCLASSDRIVER(name) 宏申明, 在 device_bind_common 中根据 设备对应的驱动 driver 中的 uclass id 找到 uclassdriver,并生成相应的 uclass, 并把设备挂到该 uclass 的设备节点 dev_head 下。

以 pwm backlight 为例:

通过 UBOOTDRIVER 的 id 可以看出,该设备(pwm backlight)驱动属于 UCLASSPANELBACKLIGHT 类。

这里定义了 backlight 的 UCLASS_DRIVER。该 uclass driver 提供了 backlight_enable(struct udevice*dev) 和 backlight_set_brightness(struct udevice*dev,intpercent) 两个通用的 API 供应用调用,可以看到他们都需要传递对应设备的 udevice ,然后通过 backlight_get_ops(dev) 拿到对该设备的操作接口。

#define backlight_get_ops(dev) ((struct backlight_ops *)(dev)->driver->ops)

driver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* struct driver - A driver for a feature or peripheral
*
* This holds methods for setting up a new device, and also removing it.
* The device needs information to set itself up - this is provided either
* by platdata or a device tree node (which we find by looking up
* matching compatible strings with of_match).
*
* Drivers all belong to a uclass, representing a class of devices of the
* same type. Common elements of the drivers can be implemented in the uclass,
* or the uclass can provide a consistent interface to the drivers within
* it.
*
* @name: Device name
* @id: Identifies the uclass we belong to
* @of_match: List of compatible strings to match, and any identifying data
* for each.
* @bind: Called to bind a device to its driver
* @probe: Called to probe a device, i.e. activate it
* @remove: Called to remove a device, i.e. de-activate it
* @unbind: Called to unbind a device from its driver
* @ofdata_to_platdata: Called before probe to decode device tree data
* @child_post_bind: Called after a new child has been bound
* @child_pre_probe: Called before a child device is probed. The device has
* memory allocated but it has not yet been probed.
* @child_post_remove: Called after a child device is removed. The device
* has memory allocated but its device_remove() method has been called.
* @priv_auto_alloc_size: If non-zero this is the size of the private data
* to be allocated in the device's ->priv pointer. If zero, then the driver
* is responsible for allocating any data required.
* @platdata_auto_alloc_size: If non-zero this is the size of the
* platform data to be allocated in the device's ->platdata pointer.
* This is typically only useful for device-tree-aware drivers (those with
* an of_match), since drivers which use platdata will have the data
* provided in the U_BOOT_DEVICE() instantiation.
* @per_child_auto_alloc_size: Each device can hold private data owned by
* its parent. If required this will be automatically allocated if this
* value is non-zero.
* @per_child_platdata_auto_alloc_size: A bus likes to store information about
* its children. If non-zero this is the size of this data, to be allocated
* in the child's parent_platdata pointer.
* @ops: Driver-specific operations. This is typically a list of function
* pointers defined by the driver, to implement driver functions required by
* the uclass.
* @flags: driver flags - see DM_FLAGS_...
*/
struct driver {
char *name;
enum uclass_id id;
const struct udevice_id *of_match;
int (*bind)(struct udevice *dev);
int (*probe)(struct udevice *dev);
int (*remove)(struct udevice *dev);
int (*unbind)(struct udevice *dev);
int (*ofdata_to_platdata)(struct udevice *dev);
int (*child_post_bind)(struct udevice *dev);
int (*child_pre_probe)(struct udevice *dev);
int (*child_post_remove)(struct udevice *dev);
int priv_auto_alloc_size;
int platdata_auto_alloc_size;
int per_child_auto_alloc_size;
int per_child_platdata_auto_alloc_size;
const void *ops; /* driver-specific operations */
uint32_t flags;
};

通过 UBOOTDRIVER(__name) 宏声明。如果 driver 实现了 bind 接口,该bind 将在 device_bind_common 中 device 和 driver 匹配上后被调用, 而且在 device_bind_common 中会完成 udevice 和 driver 的绑定。

driver 一般都有对应的 probe 接口,通过 device_probe(structudevice*dev) 调用,需要注意的是driver 的 bind 接口调用的比 probe 接口早, 大部分在 dm_init_and_scan 中就被调用了。

driver 一般会提供 ops 操作接口,供上一层调用。

需要说明的是,driver 一般都不需要把自己注册到 uclass 中,而是在 device_bind _common 阶段实现driver 、uclass、device 三者的对接,然后 uclass 层通过 udevice->driver->ops 获取对应 driver 的操作接口。

设备驱动的使用

一般应用层的代码要使用某个设备的时候,首先需要通过 uclass_get_device_xxx 系列 API 拿到该设备的 udevice, 然后通过该设备的 uclass 提供的 API 操作该设备。

uclass_get_device_xxx 拿到该设备的 udevice 后会调用该设备的 probe 接口。

以前面提到的 pwm backlight 为例:

1
2
3
4
5
6
7
/**
* drivers/video/simple_panel.c
*/
struct udevice *bldev;
uclass_get_device_by_phandle(UCLASS_PANEL_BACKLIGHT, dev, "backlight", &bldev);
backlight_enable(bldev);
backlight_set_brightness(bldev, percent);

dma-buf

dma-buf的出现是为了解决各驱动之间buffer共享的问题,因此它本质上是buffer和file的结合,即它既是一块物理连续的buffer,
也是一个linux file。buffer是内容,file是媒介,通过file这个媒介来实现buffer的共享。

一个典型的dma-buf的应用框架如下:

通常,分配buffer的模块称为exportor,使用该buffer的模块称为importoruser

一个最简单的dma-buf驱动包含以下元素:

  1. dma_buf_ops
  2. DEFINE_DMA_BUF_EXPORT_INFO
  3. dma_buf_export()

dma-buf 不仅仅只能用于DMA硬件访问

dma-buf本质是buffer和file的结合,任然是一块buffer,不仅能用于DMA硬件访问,也同样适应CPU软件访问,这也是dma-buf在内核中
广受欢迎的一个重要原因。经过他的API都带有dma字样。

dma-buf既能分配物理连续的buffer,也可以是离散的buffer

分配那种beffer最终取决与exportor驱动采用何种方式来分配buffer。例如:采用内核中最常见的kmalloc()函数来分配dma-buf,这块buffer
自然就是物理连续的。

CPU Access

从 linux-3.4 开始,dma-buf 引入了 CPU 操作接口,使得开发人员可以在内核空间里直接使用 CPU 来访问 dma-buf 的物理内存。

如下 dma-buf API 实现了 CPU 在内核空间对 dma-buf 内存的访问:

  • dma_buf_kmap()
  • dma_buf_kmap_atomic()
  • dma_buf_vmap()

(它们的反向操作分别对应各自的 unmap 接口)
通过 dma_buf_kmap() / dma_buf_vmap() 操作,就可以把实际的物理内存,映射到 kernel 空间,并转化成 CPU 可以连续访问的虚拟地址
方便后续软件直接读写这块物理内存。因此,无论这块 buffer 在物理上是否连续,在经过 kmap / vmap 映射后的虚拟地址一定是连续的。

上述的3个接口分别和 linux 内存管理子系统(MM)中的 kmap()、 kmap_atomic()vmap() 函数一一对应,三者的区别如下:
|函数| 说明|
|–|–|
|kmap()| 一次只能映射1个page,可能会睡眠,只能在进程上下文中调用|
|kmap_atomic()| 一次只能映射1个page,不会睡眠,可在中断上下文中调用|
|vmap()| 一次可以映射多个pages,且这些pages物理上可以不连续,只能在进程上下文中调用|

从 linux-4.19 开始,dma_buf_kmap_atomic() 不再被支持。
dma_buf_ops 中的 map / map_atomic 接口名,其实原本就叫 kmap / kmap_atomic,只是后来发现与 highmem.h 中的宏定义重名了,
了避免开发人员在自己的驱动中引用 highmem.h 而带来的命名冲突问题,于是去掉了前面的“k”字。

DMA Access

dma-buf 允许CPU 在 kernel 空间访问 dma-buf 物理内存,但通常这种操作方法在内核中出现的频率并不高,因为 dma-buf 设计之初
就是为满足那些大内存访问需求的硬件而设计的,如GPU/DPU。在这种场景下,如果使用CPU直接去访问 memory,那么性能会大大降低。
因此,dma-buf 在内核中出现频率最高的还是

  • dma_buf_attach()
  • dma_buf_map_attachment()

这两个接口是dma-buf提供给DMA硬件访问的主要API,而且两者有严格的调用顺序,必须先attach,再map_attachment,因为后者的参数
是由前者提供的,所以通常这两个接口形影不离。
两个 API 相对应的反向操作接口为: dma_buf_dettach()dma_buf_unmap_attachment()

sg_table

sg_table 是 dma-buf 供 DMA 硬件访问的终极目标,也是 DMA 硬件访问离散 memory 的唯一途径。

sg_table 本质上是由一块块单个物理连续的 buffer 所组成的链表,但是这个链表整体上看却是离散的,
因此它可以很好的描述从高端内存上分配出的离散 buffer。当然,它同样可以用来描述从低端内存上分配出的物理连续 buffer。
如下图所示:

sg_table代表着整个链表,而它的每一个链表项则由scatterlist来表示。因此,1个scatterlist也就对应着一块物理连续的 buffer。
通过如下接口来获取一个scatterlist对应 buffer 的物理地址和长度:

  • sg_dma_address(sgl)
  • sg_dma_len(sgl)

有了 buffer 的物理地址和长度,可以将这两个参数配置到 DMA 硬件寄存器中,这样就可以实现 DMA 硬件对这一小块 buffer 的访问。
如果需要访问整块离散 buffer ,可通过使用 for 循环,不断的解析scatterlist,不断的配置 DMA 硬件寄存器。

对于现代多媒体硬件来说,IOMMU 的出现,解决了程序员编写 for 循环的烦恼。因为在 for 循环中,每次配置完 DMA 硬件寄存器后,
都需要等待本次 DMA 传输完毕,然后才能进行下一次循环,这大大降低了软件的执行效率。而 IOMMU 的功能就是用来解析 sg_table 的,
它会将 sg_table 内部一个个离散的小 buffer 映射到自己内部的设备地址空间,使得这整块 buffer 在自己内部的设备地址空间上是连续的
这样,在访问离散 buffer 的时候,只需要将 IOMMU 映射后的设备地址(与 MMU 映射后的 CPU 虚拟地址不是同一概念)和整块 buffer 的
size 配置到 DMA 硬件寄存器中即可,中途无需再多次配置,便完成了 DMA 硬件对整块离散 buffer 的访问,大大的提高了软件的效率。

dma_buf_attach()

该函数实际上是dma-buf attach device的缩写,用于建立一个devicedma-buf的链接关系,这个连接关系被存放在新创建的
dma_buf_attachment对象中,供后续调用dma_buf_map_attachment()使用。

该函数对应dma_buf_ops中的回调接口,如果device对后续的map_attachment操作没有特殊要求,可以不实现。

dma_buf_map_attachment()

该函数实际上是dma-buf map attachment into sg_table的缩写,主要完成2件事:

  • 生成 sg_table
  • 同步 Cache

选择返回sg_table而不是物理地址,是为了兼容所有DMA硬件(带或不带IOMMU),因为sg_table既可以表示连续物理内存,也可以表示
非连续物理内存。

同步Cache是为了防止该buffer事先被CPU填充过,数据暂存在Cache中而非DDR上,导致DMA访问的不是最新的有效数据。
通过刷cache避免此类问题。同样的,在DMA访问内存结束后,需要将Cache设置为无效(no-Cache),以便后续CPU直接从DDR上读取数据。
通常使用如下流式DMA映射接口来完成Cache的同步:

  • dma_map_single() / dma_unmap_single()
  • dma_map_page() / dma_unmap_page()
  • dma_map_sg() / dma_unmap_sg()

dma_buf_map_attachment() 对应 dma_buf_ops 中的 map_dma_buf 回调接口,
该回调接口(包括 unmap_dma_buf 在内)被强制要求实现。

延伸:dma_buf_ops中部分回调被要求强制实现。

为什么要attach操作

同一个 dma-buf 可能会被多个 DMA 硬件访问,而每个 DMA 硬件可能会因为自身硬件能力的限制,对这块 buffer 有自己特殊的要求。
比如硬件 A 的寻址能力只有0x0 ~ 0x10000000,而硬件 B 的寻址能力为 0x0 ~ 0x80000000,那么在分配 dma-buf 的物理内存时,
就必须以硬件 A 的能力为标准进行分配,这样硬件 A 和 B 都可以访问这段内存。
否则,如果只满足 B 的需求,那么 A 可能就无法访问超出 0x10000000 地址以外的内存空间,道理其实类似于木桶理论。
因此,attach 操作可以让 exporter 驱动根据不同的 device 硬件能力,来分配最合适的物理内存。

通过设置 device->dma_params 参数,来告知 exporter 驱动该 DMA 硬件的能力限制。

何时分配内存

既可以在 export 阶段分配,也可以在 map_attachment 阶段分配,甚至可以在两个阶段都分配,这通常由 DMA 硬件能力来决定。

首先,驱动人员需要统计当前系统中都有哪些 DMA 硬件要访问 dma-buf;
然后,根据不同的 DMA 硬件能力,来决定在何时以及如何分配物理内存。

通常的策略如下(假设只有 A、B 两个硬件需要访问 dma-buf ):

  • 如果硬件 A 和 B 的寻址空间有交集,则在 export 阶段进行内存分配,分配时以 A / B 的交集为准;
  • 如果硬件 A 和 B 的寻址空间没有交集,则只能在 map attachment 阶段分配内存。

对于第二种策略,因为 A 和 B 的寻址空间没有交集(即完全独立),所以它们实际上是无法实现内存共享的。
此时的解决办法是: A 和 B 在 map attachment 阶段,都分配各自的物理内存,然后通过 CPU 或 通用DMA 硬件,
将 A 的 buffer 内容拷贝到 B 的 buffer 中去,以此来间接的实现 buffer “共享”。

另外还有一种策略,就是不管三七二十一,先在 export 阶段分配好内存,然后在首次 map attachment 阶段
通过 dma_buf->attachments 链表,与所有 device 的能力进行一一比对,如果满足条件则直接返回 sg_table;
如果不满足条件,则重新分配符合所有 device 要求的物理内存,再返回新的 sg_table。

总结

  1. sg_table 是 DMA 硬件操作的关键;
  2. attach 的目的是为了让后续 map attachment 操作更灵活;
  3. map attachment 主要完成两件事:生成 sg_table 和 Cache 同步;
  4. DMA 的硬件能力决定了 dma-buf 物理内存的分配时机;

在user space 访问 dma-buf

user space 访问 dma-buf 也属于 CPU Access 的一种。

mmap

为了方便应用程序能直接在用户空间读写 dma-buf 的内存,dma_buf_ops为我们提供了一个mmap回调接口,
可以把 dma-buf 的物理内存直接映射到用户空间,这样应用程序就可以像访问普通文件那样访问 dma-buf 的物理内存了。

在 Linux 设备驱动中,大多数驱动的 mmap 操作接口都是通过调用remap_pfn_range()函数来实现的,dma-buf 也不例外

除了dma_buf_ops提供的 mmap 回调接口外,dma-buf 还为我们提供了dma_buf_mmap()内核 API,
使得我们可以在其他设备驱动中就地取材,直接引用 dma-buf 的 mmap 实现,以此来间接的实现设备驱动的 mmap 文件操作接口

file

dma-buf 本质上是 buffer 与 file 的结合,既然与file有关系,就涉及到fd

fd

如下内核 API 实现了 dma-buf 与 fd 之间的相互转换:

  • dma_buf_fd():dma-buf –> new fd
  • dma_buf_get():fd –> dma-buf

通常使用方法如下:

1
2
fd = dma_buf_fd(dmabuf);
dmabuf = dma_buf_get(fd);

get / put

只要是文件,内部都会有一个引用计数(f_count)。当dma_buf_export()函数创建dma-buf时,该引用计数被初始化为1;当这个引用计数为0时,则会自动触发
dma_buf_opsrelease回调接口,并释放dma-buf对象。

linux内核中操作file引用计数的常用函数为fget()fput(),而dma-buf又在此基础上进行了封装,如下:

  • get_dma_buf()
  • dma_buf_get()
  • dma_buf_put()

其中区别如下:

函数 区别
get_dma_buf() 仅引用计数加1
dma_buf_get() 引用计数加1,并将 fd 转换成 dma_buf 指针
dma_buf_put() 引用计数减1
dma_buf_fd() 引用计数不变,仅创建 fd

release

通常 release 回调接口用来释放 dma-buf 所对应的物理 buffer。
凡是所有和该 dma-buf 相关的私有数据也都应该在这里被 free 掉。

前面说过,只有当 dma-buf 的引用计数递减到0时,才会触发 release 回调接口。因此

  • 如果不想正在使用的 buffer 被突然释放,请提前 get;
  • 如果想在 kernel space 释放 buffer,请使劲 put;
  • 如果想从 user space 释放 buffer,请尝试 close;

这就是为什么在内核设备驱动中,我们会看到那么多 dma-buf get 和 put 的身影

如果没有任何程序来修改该 dma-buf 的引用计数,自始自终都保持为1,会无法执行 release 接口
这会导致 buffer 无法被释放,造成内存泄漏

跨进程 fd

做 Linux 应用开发的同事都知道,fd 属于进程资源,它的作用域只在单个进程空间范围内有效,即同样的 fd 值,
在进程 A 和 进程 B 中所指向的文件是不同的。因此 fd 是不能在多个进程之间共享的,
也就是说 dma_buf_fd() 与 dma_buf_get() 只能是在同一进程中调用。

fd 并不是完全不能在多进程中共享,而是需要采用特殊的方式进行传递。
在 linux 系统中,最常用的做法就是通过 socket 来实现 fd 的传递。而在 Android 系统中,则是通过 Binder 来实现的。
需要注意的是,传递后 fd 的值可能会发生变化,但是它们所指向的文件都是同一文件。
总之,有了 Binder,dma_buf_fd() 和 dma_buf_get() 就可以不用严格限制在同一进程中使用了。

总结

  • 为什么需要fd?

    1. 方便应用程序直接在 user space 访问该 buffer,通过 mmap;

    2. 方便该 buffer 在各个驱动模块之间流转,而无需拷贝;

    3. 降低了各驱动之间的耦合度。

  • 如何实现 fd 跨进程共享? Binder!

  • get / put 将影响 dma-buf 的内存释放

Cache 一致性

dma-buf 有以下接口用于 Cache 同步:

  • begin_cpu_access
  • end_cpu_access

CPU 与 DMA 访问 DDR 之间的区别:

CPU 在访问内存时是要经过 Cache 的,而 DMA 外设则是直接和 DDR 打交道,因此这就存在 Cache 一致性的问题了
即 Cache 里面的数据是否和 DDR 里面的数据保持一致。
比如 DMA 外设早已将 DDR 中的数据改写了,而 CPU 却浑然不知,仍然在访问 Cache 里面暂存的旧数据。

所以 Cache 一致性问题,只有在 CPU 参与访问的情况下才会发生。
如果一个 dma-buf 自始自终都只被一个硬件访问(要么CPU,要么DMA),那么 Cache 一致性问题就不会存在。

当然,如果一个 dma-buf 所对应的物理内存本身就是 Uncache 的(也叫一致性内存),
或者说该 buffer 在被分配时是以 coherent 方式分配的,
这种情况下,CPU 是不经过 cache 而直接访问 DDR 的,自然 Cache 一致性问题也就不存在了。

为什么需要 begin / end 操作?

dma-buf使用流式 DMA 映射接口来实现 Cache 同步操作。这类接口的特点就是 Cache 同步只是一次性的,
即在 dma map 的时候执行一次 Cache Flush 操作,在 dma unmap 的时候执行一次 Cache Invalidate 操作,
而这中间的过程是不保证 Cache 和 DDR 上数据的一致性的

因此如果 CPU 在 dma map 和 unmap 之间又去访问了这块内存,
那么有可能 CPU 访问到的数据就只是暂存在 Cache 中的旧数据,这就带来了问题。

那么什么情况下会出现 CPU 在 dma map 和 unmap 期间又去访问这块内存呢?
一般不会出现 DMA 硬件正在传输过程中突然 CPU 发起访问的情况,
而更多的是在 DMA 硬件发起传输之前,或 DMA 硬件传输完成之后,
并且仍然处于 dma map 和 unmap 操作之间的时候,CPU 对这段内存发起了访问。

针对这种情况,就需要在 CPU 访问内存前,先将 DDR 数据同步到 Cache 中(Invalidate);
在 CPU 访问结束后,将 Cache 中的数据回写到 DDR 上(Flush),以便 DMA 能获取到 CPU 更新后的数据。
这也就是 dma-buf 给我们预留 {begin,end}_cpu_access 的原因。

Kernel API

dma-buf 为我们提供了如下内核 API,用来在 dma map 期间发起 CPU 访问操作:

  • dma_buf_begin_cpu_access()
  • dma_buf_end_cpu_access()

它们分别对应 dma_buf_ops 中的 begin_cpu_accessend_cpu_access 回调接口。

通常在驱动设计时, begin_cpu_access / end_cpu_access 使用如下流式 DMA 接口来实现 Cache 同步:

  • dma_sync_single_for_cpu() / dma_sync_single_for_device()
  • dma_sync_sg_for_cpu() / dma_sync_sg_for_device()

CPU 访问内存之前,通过调用 dma_sync_{single,sg}_for_cpu()Invalidate Cache
这样 CPU 在后续访问时才能重新从 DDR 上加载最新的数据到 Cache 上。

CPU 访问内存结束之后,通过调用 dma_sync_{single,sg}_for_device()Flush Cache,将 Cache 中的数据全部回写到 DDR 上,
这样后续 DMA 才能访问到正确的有效数据。

User API

考虑到 mmap() 操作,dma-buf 也为我们提供了 Userspace 的同步接口,通过 DMA_BUF_IOCTL_SYNC ioctl() 来实现。
该 cmd 需要一个 struct dma_buf_sync 参数,用于表明当前是 begin 还是 end 操作,是 read 还是 write 操作。

总结

  • 只有在 DMA map/unmap 期间 CPU 又要访问内存的时候,才有必要使用 begin / end 操作;
  • { begin,end }_cpu_access 实际是 dma_sync()* 接口的封装,目的是要 invalidate 或 flush cache;
  • Usespace 通过 DMA_BUF_IOCTL_SYNC 来触发 begin / end 操作;

container_of 宏

Linux 内核第一宏。主要作用:

根据结构体某一成员的地址,获取这个结构体的首地址。

主要原理:

用结构体成员的地址,减去该成员在结构体内的偏移,即可得到该结构体的首地址。

1
2
3
4
5
6
7
8
9
10
11
12
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })

container_of 宏三个参数:

  • type: 结构体类型
  • member: 结构体内的成员
  • ptr: 结构体内成员member的地址

container_of 宏实现分析

offsetof 宏

其功能是获得成员MEMBER在TYPE结构中的偏移量

结构体作为一个复合类型数据,里面可包含多个变量。当我们定义一个结构体时,编译器要为其在内
存中分配空间。根据每个成员的数据类型和字节对齐方式,编译器会按照结构体中各个成员的顺序,在
内存中分配一片连续的空间来存储他们。

一个结构体数据类型,在同一编译环境下,各个成员相对于结构体首地址的偏移是固定不变的。
当结构体的首地址为0时,结构体中各个成员的地址在数值上等同于结构体各成员相对于结构体首地址的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct student {
int age;
int num;
int math;
};
int main(void)
{
printf("&age = %p\n", &((struct student*)0)->age);
printf("&age = %p\n", &((struct student*)0)->num);
printf("&age = %p\n", &((struct student*)0)->math);
return 0;
}
````
在上面程序中,将数字0通过强制类型转换,转换为一个指向结构体类型student的常量指针
然后分别打印这个变量指针指向的各个成员地址。其运行结果如下:
```c
&age = 00000000
&num = 00000004
&math = 00000008

因为常量指针的值为0,即可以看作结构体首地址为0,
所以结构体每个成员变量的地址即该成员相对于结构体首地址的偏移
这正是offsetof宏的功能。

1
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

根据运算符优先级分析:

  • (TYPE *)0: 将0地址强制转化为一个指向TYPE类型的结构体常量指针
  • (TYPE *)0)->MEMBER: 通过常量指针,访问成员MEMBER
  • &((TYPE *)0)->MEMBER: 获取MEMBER成员的地址,地址值即为MEMBER成员在TYPE结构中的偏移量
  • (size_t) &((TYPE *)0)->MEMBER: 将地址值强制转化为size_t类型的整形数

const typeof(((type *)0)->member) * __mptr = (ptr);

结构体中的成员数据可以是任意数据类型,为了让这个宏兼容各种数据类型,定义了一个临时指针变量__mptr,用来存储结构体成员MEMBER的地址,即存储宏中参数ptr的值。

必须保证__mptrptr的指针类型一样,因此使用typeof关键字,用来获取结构体成员MEMBER的数据类型。

typeof 是 GNU C 新增的一个关键字,用来获取数据类型。

内核链表的操作常用的二个宏list_for_each_entrylist_for_each_entry_safe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* list_for_each_entry - iterate over list of given type
* @pos: the type * to use as a loop cursor.
* @head: the head for your list.
* @member: the name of the list_head within the struct.
*/
#define list_for_each_entry(pos, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member); \
!list_entry_is_head(pos, head, member); \
pos = list_next_entry(pos, member))

/**
* list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
* @pos: the type * to use as a loop cursor.
* @n: another type * to use as temporary storage
* @head: the head for your list.
* @member: the name of the list_head within the struct.
*/
#define list_for_each_entry_safe(pos, n, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member), \
n = list_next_entry(pos, member); \
!list_entry_is_head(pos, head, member); \
pos = n, n = list_next_entry(n, member))

list_for_each_entry宏是一个for循环语句,for循环的第一个参数就是让(head)->next指向member成员所在数据结构的指针,也就是将pos初始化为链表头指向的第一个实体链表成员,for的第三句话通过pos->member.next指针遍历整个实体链表,当pos->member.next再次指向我们的链表头的时候跳出for循环。整个过程没有对链表头进行遍历(不需要被遍历),所以使用list_for_each_entry遍历链表必须从链表头开始。 因此可以看出,list_for_each_entry的功能就是遍历以head为链表头的实体链表,对实体链表中的数据结构进行处理;

list_for_each_entry_safe用指针n对链表的下一个数据结构进行了临时存储,所以如果在遍历链表的时候需要做删除链表中的当前项操作时,用list_for_each_entry_safe可以安全的删除,而不会影响接下来的遍历过程(用n指针可以继续完成接下来的遍历, 而list_for_each_entry则无法继续遍历,删除后会导致无法继续遍历)。

“error: invalid path”

Summary

The Git cloning of repository succeeds on a Linux client but fails on a Windows client with an “invalid path” error.

Solution

Depending on the filename, configuring Git to ignore NTFS naming may workaround the issue.

1
git config --global core.protectNTFS false

Turning off protectNTFS will stop Git from complaining about files that have a base name that is reserved but will not prevent an error if the filename is one of the reserved names.

git reset

首先解析以下这三个相关的状态和概念:

  1. HEAD:可以描述为当前分支最后一个提交。即本地的信息中的当前版本。

  2. Index:在工作副本修改之后执行过git add操作的版本文件,可以commit了的。

  3. Working Copy:工作副本是你正在修改,但是没有执行任何git操作的文件。

总结

代码修改,还没做任何操作的时候就是 Working Copy,

git add * 操作之后就是Index,

git commit 之后就是HEAD。如果代码修改了之后进行git add 操作,然后git commit,那么所有三者(HEAD,INDEX(STAGING),WORKING COPY)都是相同的状态,内容相同。

–soft

–soft(更改HEAD)(恢复git commit的操作)

软重置。仅改变当前head指针

–soft参数,Git只是单纯的把本地HEAD更改到你指定的版本,仅HEAD的定义发生了变化。

Working Copy 和Index的修改得以保留。

因为只回退了commit的信息。如果还要提交,可直接commit即可。

–hard(更改三者)

–hard 参数将当前 HEAD, INDEX(STAGING), WORKING COPY 全部改变。

不仅工作区修改的代码会还原,暂存区的信息也会丢弃。

–mixed(default)

恢复git add的操作,包含恢复git commit的操作

–mixed是reset的默认参数,不指定任何参数时默认使用。

移动head指针,改变暂存区内容,但不会改变工作区

总结

git reset用于在进行git addgit commit操作后,但还未进行git push操作时进行版本管理。

1、soft: 重置git commit

2、mixed: 重置git commit 和 git add

3、hard: 重置git commit 和 git add 和工作副本的修改。