2.10 Linux 串口、CF卡、MTD、I2C驱动分析

来源:互联网 发布:孚盟软件 编辑:程序博客网 时间:2024/06/08 11:21

本文以风河linux代码为例,解析Linux 串口、CF卡、flash MTD、I2C驱动的具体实现。

1、串口驱动

1.1、原理简介

系统A 的xlr732cpu提供了两个串口,我们使用了其中一个串口作为控制台。串口虽然简单,但是作用是非常重要的。在内核调试的初期一切都未正常,只能向串口直接输出调试信息来调试;在系统的启动阶段也只能向串口打印启动信息。

在Linux系统中,串口驱动的架构如下:

1

1.2、直接向串口打印

在内核调试的初始阶段,一切都是混沌,我们可以使用直接向串口输出数据的方法来调试。

2

1.3、串口驱动的注册

直接向串口输出数据是在系统初始阶段干的事情。如果要把串口当成可以输入输出的交互接口,串口必须要向系统注册成标准的串口设备。

上面串口层次结构图中串口的实现有很多层,但是用户串口驱动层来看,注册一个串口驱动需要调用的接口就两个:

  • 1、uart_register_driver() //注册uart串口驱动
  • 2、uart_add_one_port() //增加实体串口

1.4、8250串口驱动的注册

wr3.0的串口驱动代码在drivers\serial\8250.c文件当中,我们分析其具体实现,它是利用了“platform”总线的device、driver架构来调用uart_register_driver()、uart_add_one_port(),以实现串口的驱动注册。

3
4
5

上面说到实际的 “serial8250” platform device在arch\mips\rmi\ptr\setup.c中定义,我们看看其具体的实现:

6
7

再看看“serial8250” platform driver的probe函数的实现:

8
9
10
11
12

1.5、串口的数据收发

从上往下整个串口的收发层次比较复杂,我们只看8250串口驱动内的数据收发关键函数。receive_chars()、transmit_chars()。

13
14
15
16
17

1.6、Console串口的注册

串口不但被注册成了uart字符驱动,还注册成了console。Console的作用就是使用printk输入内核打印时,会打印到每个注册console上。串口console功能和字符设备功能是独立的。

18
19
20

2、CF卡驱动

2.1、原理简介

系统A 的local bus cs4上挂接了一个cf卡,我们的cf卡使用的是ide接口,在linux系统下被注册成了标准的ide块设备。可以通过命令查看:

21

在linux系统中,ide块设备驱动的架构如下:

22

我们分析wr.14的cf卡ide驱动代码,看看其实现原理。Ide驱动也是采取linux通用的驱动模型:bus、device、driver。

2.2、Ide bus注册

系统首先注册一个“ide”总线,以供ide driver和ide device向上挂载。

23

在dirvers\ide\ide.c中可以看到“ide”总线注册的过程:

24
25

ide_bus_type数据结构中需要关注的是“.match”函数ide_bus_match(),其是用来匹配driver和device的。

26

ide_bus_match()固定的返回1,说明“ide”总线对所有的driver和device进行适配。

另外需要注意的是ide_init()中调用的init_ide_data()和probe_for_hwifs()函数。init_ide_data()函数初始化设备数组ide_hwifs[];probe_for_hwifs()函数把各个ide设备的参数填进ide设备数组ide_hwifs[]中,系统会根据ide_hwifs[]来逐个注册ide设备。后面详细描述其实现。

2.3、Ide driver注册

我们可以看到内核中引用ide_bus_type的驱动有idedisk_driver、ide_cdrom_driver、idefloppy_driver、idescsi_driver、idetape_driver,但是我们cf卡只使用其中的disk驱动,所以我们只分析idedisk_driver。

在drivers\ide\ide-disk.c中,我们可以看到idedisk_driver的注册过程:

27
28

驱动注册完成后,可以在“ide”总线的driver目录中看到:

29

idedisk_driver数据结构中需要关注的是 “.gen_driver/.probe”函数ide_disk_probe(),该函数的目的是在有ide device插入并且适配idedisk_driver驱动,就调用probe函数执行初始化和注册块设备。

30
31

2.4、Ide device注册

有了“ide”总线,有了“ide-disk”驱动,还需要向系统添加我们的ide设备。

入口在drivers\ide\ide-generic.c中的ide_generic_init:

32

继续调用ideprobe_init()函数:

33

需要关注ideprobe_init()->hwif_init()->init_irq()->ide_init_queue()函数,ide_init_queue()中初始化了ide块设备的队列,真正的数据读写都是在这里实现的。

34

2.5、用户Ide设备的注册

从上面ide驱动的架构分析可以看到,ide总线、ide驱动、ide块设备的注册都是标准接口,关于块设备操作ide命令接口也是标准的。用户新添加ide设备,只需要向ide设备数组ide_hwifs[]中添加本设备的io基地址和中断,系统就会自动的将你的ide设备添加上。

我们看到wr1.4中的cf卡驱动drivers\ide\mips\ide-cfcard.c中的核心实现就是把本设备添加到ide_hwifs[]中:

35
36

2.6、Wr3.0中Ide device的注册

以上几小节描述的都是wr1.4内核中的ide驱动架构,在wr3.0中ide驱动架构有稍许修改。关于ide bus和ide driver的注册没有改变,关于ide device的注册方法有变动。在wr1.4中,用户把所有ide设备的参数加到ide_hwifs[]数组中,由系统统一注册成ide device;而在wr3.0中是每个ide设备独立注册的。

我们来研究wr3.0 cf卡驱动的实现过程,在drivers\ide\mips\phoenix_ide.c中:

37
38

可以看到用户ide驱动向“/sys/bus/platform”总线注册了一个驱动和一个设备。我们看看platform_bus_type的“.match”函数的实现。

39
40

看到只要驱动和设备的名称一致,就认为适配。所以platform_bus_type总线只是简单提供了一个bus、driver、device的架构,只要driver、device的名字一致就启动适配。

phoenix_ide_init_module()函数最终的目的调用到了phoenix_ide_driver的“.probe” phoenix_ide_probe()函数。

41

可以看到phoenix_ide_probe()的目的就是根据用户参数初始化一个hw_regs_t数据结构,再根据这个数据结构向系统注册ide设备。调用过程为ide_host_add() ->ide_host_register() ->hwif_register_devices() ->device_register()。

3、I2C驱动

3.1、原理简介

系统A 的cpu xlr732自带两条i2c总线:一条用来挂接eeprom,内部存储网口mac地址等信息;一条作为ipmi总线和bmc通讯。
I2c总线在linux下被注册成i2c_adapter,并且被 “i2c-dev”驱动用字符驱动的形式呈现给用户。可以通过命令查看:

42

在讲解linux i2c驱动架构之前,先熟悉几个概念:

  • i2c_adapter:对应一个物理上的适配器;
  • i2c_algorithm:是对适配器的支持的算法。就是通信协议,用来驱动i2c_adapter;
  • i2c_client:对应一个物理上的设备;
  • i2c_driver:设备驱动,用来驱动i2c_client;

在linux系统中,i2c设备驱动的架构如下:

43

i2c驱动也遵循一般linux驱动的bus、driver、device架构:

  • 首先驱动会注册一个i2c总线i2c_bus_type,即“/sys/bus/i2c”;
  • i2c总线上的driver为i2c_driver类型,即“/sys/bus/i2c/drivers”下的驱动;
  • i2c总线上的device为i2c_adapter类型,即i2c的物理适配器(也是i2c物理总线),对应 “/sys/class/i2c-adapter/”下的设备。
  • i2c总线上另一种device为i2c_client类型,物理上对应的是i2c总线上的设备,每一个i2c_client需要绑定i2c_adapter。

I2c驱动中driver和device的适配方法有两种:

  • 一种是Adapter方式(LEGACY),用于i2c_driver和i2c_adapter的适配,调用i2c_driver的driver->attach_adapter()函数来进行适配和初始化;
  • 另一种是Probe方式(new style),用于i2c_driver和i2c_client的适配,使用i2c_bus_type总线的“.match”函数i2c_device_match()来进行适配,适配成功调用i2c_driver的driver->probe()函数来进行初始化。

我们来分析wr3.0内核中i2c驱动的具体实现:

3.2、I2c bus注册

在drivers\i2c\i2c-core.c文件i2c_init()函数中注册“/sys/bus/i2c/”总线:

44
45

3.3、I2c device注册(i2c_adapter)

I2c总线适配器,i2c_adapter类型device的注册,使用drivers\i2c\i2c-core.c文件中的i2c_add_adapter ()函数。

i2c_adapter类型的device的注册并没有指定bus为i2c_bus_type,而是指定class为i2c_adapter_class,所以i2c_adapter类型的device要和i2c_bus_type上挂载的驱动适配,需要手工遍历i2c_bus_type上挂载的驱动,调用driver->attach_adapter()函数来进行适配和初始化。

46
47
48
49

3.4、I2c device注册(i2c_client)

I2c总线上挂载i2c设备,i2c_client类型device的注册,使用drivers\i2c\i2c-core.c文件中的i2c_attach_client ()函数。

i2c_client类型的device的注册到了i2c_bus_type总线上,所以i2c_driver和i2c_client的适配,使用i2c_bus_type总线的“.match”函数i2c_device_match()来进行适配,适配成功调用i2c_driver的driver->probe()函数来进行初始化。

50
51

3.5、I2c driver注册

i2c_driver类型driver的注册,使用drivers\i2c\i2c-core.c文件中的i2c_register_driver ()函数。

52
53

i2c_bus_type总线的“.match”函数i2c_device_match() :

54

i2c_bus_type总线的“.probe”函数i2c_device_probe () :

55

3.6、I2c驱动的数据读写

我们来分析一下I2c驱动的数据读写。

I2c驱动的数据读写是站在i2c_client基础之上的,普通的数据读写使用i2c_master_recv()和i2c_master_send()对i2c_client进行操作。基于smbus的数据读写使用i2c_smbus_read_byte()、i2c_smbus_write_byte()等一系列函数对i2c_client进行操作。

如果没有注册i2c_client,只注册了i2c_adapter,可以新建一个匿名的i2c_client来绑定i2c_adapter,以实现I2c的读写,i2c_adapter的字符设备i2c-dev.c就是这样干的。

drivers\i2c\i2c-core.c文件中可以看到i2c内核读写的实现过程:

56
57

i2c_master_recv()和i2c_master_send()最后都是调用i2c_transfer()来对i2c_client绑定的i2c_adapter进行数据传输。

58
59

可以看到i2c_transfer()进一步调用到i2c_adapter的i2c_algorithm->master_xfer()函数。i2c_adapter的i2c_algorithm是i2c适配器的支持的算法,即是i2c适配器是哪种类型的适配器,怎么样操作i2c适配器的控制寄存器来发起i2c数据读写。

i2c_algorithm在i2c_adapter使用i2c_add_adapter()注册前初始化。例如我们使用的palm类型algorithm的i2c_adapter的注册,drivers\i2c\algos\ i2c-algo-palm.c文件中可以看到其实现过程:

60
61

palm_algo中的“.master_xfer”函数palm_xfer()会调用到palm_tx()、palm_tx() —> palm_write()、palm_read()—> algo_data->write()、algo_data->read()—> adap->algo_data->write()、adap->algo_data-> read ()。

可见最终调用到了i2c_adapter的algo_data->write()和algo_data-> read ()函数,这两个函数在drivers\i2c\ busses\i2c-bk3220.c文件中定义,这两个函数提供了对i2c适配器的控制寄存器的基本读写函数。

62
63

3.7、I2c client的字符设备呈现

向系统注册成功i2c_client类型的设备,可以在内核态通过i2c_master_recv()和i2c_master_send()对i2c_client进行读写操作。但是如果想在用户态能够对i2c_client类型的设备进行读写访问的话,还需要建立字符设备,把i2c_master_recv()和i2c_master_send()操作封装成字符设备的cdev->file_operations->read()和cdev->file_operations->write()操作。

下面是一个实例,怎么样把i2c_client类型的设备封装成字符设备:

64
65
66
67

3.8、I2c adapter的字符设备呈现

上面看到读写i2c数据需要通过i2c_client来操作。但是如果没有注册i2c_client,只注册了i2c_adapter,怎么来启动读写呢?

可以新建一个匿名的i2c_client来绑定i2c_adapter,以实现I2c的读写。i2c-dev.c就是这样干的,其目的是把i2c_adapter封装成字符设备呈现给用户态使用。drivers\i2c\i2c-dev.c文件中可以看到其实现过程:

68
69
70
71
72

4、Flash MTD驱动

4.1、原理简介

系统A cpu上挂载了一个bootrom和一个flash芯片,bootrom芯片用来存储基本bootrom,flash芯片用来存储扩展bootrom和lbp。
其中flash芯片在linux下面被注册成了基于mtd的块设备和字符设备:

73

在linux系统中,flash mtd设备驱动的架构如下:

74

Flash驱动的注册定义在drivers\mtd\maps\mrp_flash.c中,其本身并不复杂,我们通过分析其过程来研究wr3.0 linux mtd驱动原理。

4.2、用户flash设备注册

75
76

4.3、探测flash的mtd_info信息

do_map_probe()函数用来探测生成flash的mtd_info信息。参数为:探测方法名和flash的地址信息map_info。函数定义在drivers\mtd\chips\chipreg.c中:

77
78
79

我们可以看到,通过驱动名来查找探测驱动,其实质就是在chip_drvs_list链表中查找探测驱动。而探测驱动的注册函数register_mtd_chip_driver()就是将驱动加入到chip_drvs_list链表中。

我们查看有谁来调用register_mtd_chip_driver()来判断系统有哪些探测驱动,可以看到有cfi、jedec、map_absent、map_ram、map_rom等探测驱动。

80

最常用的就是cfi探测,我们下面只分析cfi的探测方法。

4.4、CFI接口flash信息探测

cfi全称为Common Flash Memory Interface,多种不同类型flash芯片都支持,是一种通用flash信息查询接口。

一般进入cfi查询模式的命令为向地址0x55写数据0x98:

81

Cfi提供的信息中包括了:

  • 1、 芯片使用的读、写、擦除的命令cmdset;即使用哪种风格的命令组合来启动读、写、擦除;
  • 2、 芯片的时序参数;
  • 3、 芯片的大小;

等等信息,如下图:

82
83
84

cfi接口的flash探测驱动定义在drivers\mtd\chips\cfi_probe.c中:

85

Cfi探测驱动的“.probe”函数为cfi_probe():

86
87
88

.5、AMD 命令集的flash操作方法

从上节cfi读出的信息中,有两个字段可以表示flash的操作命令集id,即flash读、写、擦除的方法。

89

对命令集id的解析如下:

90

可以看到flash的命令集有很多种,但是我们现在应用的是amd命令集,我们具体介绍这一种。

91
92
93
94
95

4.6、添加flash分区

探测完flash芯片的mtd_info信息以后,回到用户init_mrp6600flash()函数中,下一步调用add_mtd_partitions()函数来增加flash的分区。定义在drivers\mtd\mtdpart.c中:

96
97
98
99
100

我们可以看到添加分区add_mtd_partitions()函数最终调用到了使用mtd_notifiers链表中的函数来进行实际的分区注册。
而向mtd_notifiers链表中注册是使用register_mtd_user():

101

通过对register_mtd_user()函数调用关系的分析,可以可以看到mtd_blk、mtdchar、mtdoops三种注册了自己的flash分区注册函数。我们只使用了mtd块设备和字符设备,所以我们只分析这两种的实现。

102

4.7、添加flash分区的mtd块设备

Mtd块设备添加分区操作,定义在drivers\mtd\mtdblock.c中 :

103
104
105
106
107

4.8、mtd块设备的读写

块设备的读写函数实现在读写队列中,mtdblock的块设备读写队列在drivers\mtd\mtd_blkdevs.c中初始化:
108
109
110
111
112
113
114

Mtdblock的readsect()和writesect()函数,定义在drivers\mtd\mtdblock.c中 :

115
116
117
118
119
120
121
122
123
124

4.9、添加flash分区的mtd字符设备

Mtd字符设备添加分区操作,定义在drivers\mtd\mtdchar.c中 :

125
126
127

我们可以看到mtd字符设备关于flash分区添加的钩子函数中,并没有做什么实际的事情。有关mtd字符设备的实现核心全都在mtdchar创建的mtd字符设备中。

4.10、mtd字符设备的操作函数

Mtd字符设备操作,定义在drivers\mtd\mtdchar.c中 :

128
129
130
131
132
133
134
135
136
137