分类目录归档:技术文章

SPI协议及其工作原理浅析

一、概述.

SPI, Serial Perripheral Interface, 串行外围设备接口, 是 Motorola 公司推出的一种同步串行接口技术. SPI 总线在物理上是通过接在外围设备微控制器(PICmicro) 上面的微处理控制单元 (MCU) 上叫作同步串行端口(Synchronous Serial Port) 的模块(Module)来实现的, 它允许 MCU 以全双工的同步串行方式, 与各种外围设备进行高速数据通信.

SPI主要应用在 EEPROM, Flash, 实时时钟(RTC), 数模转换器(ADC), 数字信号处理器(DSP) 以及数字信号解码器之间. 它在芯片中只占用四根管脚 (Pin) 用来控制以及数据传输, 节约了芯片的 pin 数目, 同时为 PCB 在布局上节省了空间. 正是出于这种简单易用的特性, 现在越来越多的芯片上都集成了 SPI技术.

二、 特点

1. 采用主-从模式(Master-Slave) 的控制方式

SPI规定了两个SPI设备之间通信必须由主设备 (Master) 来控制次设备 (Slave). 一个Master 设备可以通过提供Clock以及对Slave设备进行片选(Slave Select)来控制多个Slave设备,SPI协议还规定Slave设备的Clock由Master设备通过SCK管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock, 没有 Clock 则 Slave 设备不能正常工作.

2. 采用同步方式(Synchronous)传输数据

Master 设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样, 来保证数据在两个设备之间是同步传输的.

3. 数据交换(Data Exchanges)

SPI 设备间的数据传输之所以又被称为数据交换, 是因为 SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个 "发送者(Transmitter)" 或者 "接收者(Receiver)". 在每个 Clock 周期内, SPI 设备都会发送并接收一个 bit 大小的数据, 相当于该设备有一个 bit 大小的数据被交换了.

一个 Slave 设备要想能够接收到 Master 发过来的控制信号, 必须在此之前能够被 Master 设备进行访问 (Access). 所以, Master 设备必须首先通过 SS/CS pin 对 Slave 设备进行片选, 把想要访问的 Slave 设备选上.

在数据传输的过程中,  每次接收到的数据必须在下一次数据传输之前被采样. 如果之前接收到的数据没有被读取, 那么这些已经接收完成的数据将有可能会被丢弃,  导致 SPI 物理模块最终失效. 因此, 在程序中一般都会在 SPI 传输完数据后, 去读取 SPI 设备里的数据, 即使这些数据(Dummy Data)在我们的程序里是无用的.

三、 工作机制

 1. 概述

上图只是对 SPI 设备间通信的一个简单的描述, 下面就来解释一下图中所示的几个组件(Module):

SSPBUF, Synchronous Serial Port Buffer, 泛指 SPI 设备里面的内部缓冲区, 一般在物理上是以 FIFO 的形式, 保存传输过程中的临时数据;

SSPSR, Synchronous Serial Port Register, 泛指 SPI 设备里面的移位寄存器(Shift Regitser), 它的作用是根据设置好的数据位宽(bit-width) 把数据移入或者移出 SSPBUF;

Controller, 泛指 SPI 设备里面的控制寄存器, 可以通过配置它们来设置 SPI 总线的传输模式.

通常情况下, 我们只需要对上图所描述的四个管脚(pin) 进行编程即可控制整个 SPI 设备之间的数据通信:

SCK, Serial Clock, 主要的作用是 Master 设备往 Slave 设备传输时钟信号, 控制数据交换的时机以及速率;

SS/CS, Slave Select/Chip Select, 用于 Master 设备片选 Slave 设备, 使被选中的 Slave 设备能够被 Master 设备所访问;

SDO/MOSI, Serial Data Output/Master Out Slave In, 在 Master 上面也被称为 Tx-Channel, 作为数据的出口, 主要用于 SPI 设备发送数据;

SDI/MISO, Serial Data Input/Master In Slave Out, 在 Master 上面也被称为 Rx-Channel, 作为数据的入口, 主要用于SPI 设备接收数据;

SPI 设备在进行通信的过程中, Master 设备和 Slave 设备之间会产生一个数据链路回环(Data Loop), 就像上图所画的那样, 通过 SDO 和 SDI 管脚, SSPSR 控制数据移入移出 SSPBUF, Controller 确定 SPI 总线的通信模式, SCK 传输时钟信号.


2. Timing.

上图通过 Master 设备与 Slave 设备之间交换1 Byte 数据来说明 SPI 协议的工作机制.

首先,  在这里解释一下两个概念:

CPOL: 时钟极性, 表示 SPI 在空闲时, 时钟信号是高电平还是低电平. 若 CPOL 被设为 1, 那么该设备在空闲时 SCK 管脚下的时钟信号为高电平. 当 CPOL 被设为 0 时则正好相反.

CPOL = 0: SCK idle phase is low; 

CPOL = 1: SCK idle phase is high;

CPHA: 时钟相位, 表示 SPI 设备是在 SCK 管脚上的时钟信号变为上升沿时触发数据采样, 还是在时钟信号变为下降沿时触发数据采样. 若 CPHA 被设置为 1, 则 SPI 设备在时钟信号变为下降沿时触发数据采样, 在上升沿时发送数据. 当 CPHA 被设为 0 时也正好相反.

CPHA = 0: Output data at negedge of clock while receiving data at posedge of clock;

CPHA = 1: Output data at posedge of clock while receiving data at negedge of clock;

上图里的 "Mode 1, 1" 说明了本例所使用的 SPI 数据传输模式被设置成 CPOL = 1, CPHA = 1. 这样, 在一个 Clock 周期内, 每个单独的 SPI 设备都能以全双工(Full-Duplex) 的方式, 同时发送和接收 1 bit 数据, 即相当于交换了 1 bit 大小的数据. 如果 SPI 总线的 Channel-Width 被设置成 Byte, 表示 SPI 总线上每次数据传输的最小单位为 Byte, 那么挂载在该 SPI 总线的设备每次数据传输的过程至少需要 8 个 Clock 周期(忽略设备的物理延迟). 因此, SPI 总线的频率越快, Clock 周期越短, 则 SPI 设备间数据交换的速率就越快.

3.SSPSR.

SSPSR 是 SPI 设备内部的移位寄存器(Shift Register). 它的主要作用是根据 SPI 时钟信号状态, 往 SSPBUF 里移入或者移出数据, 每次移动的数据大小由 Bus-Width 以及 Channel-Width 所决定.

Bus-Width 的作用是指定地址总线到 Master 设备之间数据传输的单位.

例如, 我们想要往 Master 设备里面的 SSPBUF 写入 16 Byte 大小的数据: 首先, 给 Master 设备的配置寄存器设置 Bus-Width 为 Byte; 然后往 Master 设备的 Tx-Data 移位寄存器在地址总线的入口写入数据, 每次写入 1 Byte 大小的数据(使用 writeb 函数); 写完 1 Byte 数据之后, Master 设备里面的 Tx-Data 移位寄存器会自动把从地址总线传来的1 Byte 数据移入 SSPBUF 里; 上述动作一共需要重复执行 16 次.

Channel-Width 的作用是指定 Master 设备与 Slave 设备之间数据传输的单位. 与 Bus-Width 相似,  Master 设备内部的移位寄存器会依据 Channel-Width 自动地把数据从 Master-SSPBUF 里通过 Master-SDO 管脚搬运到 Slave 设备里的 Slave-SDI 引脚, Slave-SSPSR 再把每次接收的数据移入 Slave-SSPBUF里.

通常情况下, Bus-Width 总是会大于或等于 Channel-Width, 这样能保证不会出现因 Master 与 Slave 之间数据交换的频率比地址总线与 Master 之间的数据交换频率要快, 导致 SSPBUF 里面存放的数据为无效数据这样的情况.


4. SSPBUF.

我们知道, 在每个时钟周期内, Master 与 Slave 之间交换的数据其实都是 SPI 内部移位寄存器从 SSPBUF 里面拷贝的. 我们可以通过往 SSPBUF 对应的寄存器 (Tx-Data / Rx-Data register) 里读写数据, 间接地操控 SPI 设备内部的 SSPBUF.

例如, 在发送数据之前, 我们应该先往 Master 的 Tx-Data 寄存器写入将要发送出去的数据, 这些数据会被 Master-SSPSR 移位寄存器根据 Bus-Width 自动移入 Master-SSPBUF 里, 然后这些数据又会被 Master-SSPSR 根据 Channel-Width 从 Master-SSPBUF 中移出, 通过 Master-SDO  管脚传给 Slave-SDI 管脚,  Slave-SSPSR 则把从  Slave-SDI 接收到的数据移入 Slave-SSPBUF 里.  与此同时, Slave-SSPBUF 里面的数据根据每次接收数据的大小(Channel-Width), 通过 Slave-SDO 发往 Master-SDI, Master-SSPSR 再把从 Master-SDI 接收的数据移入 Master-SSPBUF.在单次数据传输完成之后, 用户程序可以通过从 Master 设备的 Rx-Data 寄存器读取 Master 设备数据交换得到的数据.

5. Controller.

Master 设备里面的 Controller 主要通过时钟信号(Clock Signal)以及片选信号(Slave Select Signal)来控制 Slave 设备. Slave 设备会一直等待, 直到接收到 Master 设备发过来的片选信号, 然后根据时钟信号来工作.

Master 设备的片选操作必须由程序所实现. 例如: 由程序把 SS/CS 管脚的时钟信号拉低电平, 完成 SPI 设备数据通信的前期工作; 当程序想让 SPI 设备结束数据通信时, 再把 SS/CS 管脚上的时钟信号拉高电平.

转自:http://bbs.chinaunix.net/thread-1916003-1-1.html

UART与USART

1.字面区别:

UART:universal asynchronous receiver and transmitter通用异步收/发器

USART:universal synchronous asynchronous receiver and transmitter通用同步/异步收/发器

从名字上可以看出,USART在UART基础上增加了同步功能,即USART是UART的增强型,事实也确实是这样。但是具体增强到了什么地方呢?其实当我们使用USART在异步通信的时候,它与UART没有什么区别,但是用在同步通信的时候,区别就很明显了:大家都知道同步通信需要时钟来触发数据传输,也就是说USART相对UART的区别之一就是能提供主动时钟。如stm32的USART可以提供时钟支持ISO7816的智能卡接口。

2.信号区别

UART 有RX和TX两个信号线;而USART会多一个CLK时钟线。

UART需要固定的波特率,就是说两位数据的间隔要相等。UART总线是异步串口,一般由波特率产生器(产生的波特率等于传输波特率的16倍)、UART接收器、UART发送器组成,硬件上有两根线,一根用于发送,一根用于接收。 显然,如果用通用IO口模拟UART总线,则需一个输入口,一个输出口。

UART是一个并行输入成为串行输出的芯片,通常集成在主板上,多数是16550AFN芯片。因为计算机内部采用并行数据,不能直接把数据发到Modem,必须经过UART整理才能进行异步传输,其过程为:CPU先把准备写入串行设备的数据放到UART的寄存器(临时内存块)中,再通过FIFO(First Input First Output,先入先出队列)传送到串行设备,若是没有FIFO,信息将变得杂乱无章,不可能传送到Modem。

作为接口的一部分,UART还提供以下功能:将由计算机内部传送过来的并行数据转换为输出的串行数据流。将计算机外部来的串行数据转换为字节,供计算机内部使用并行数据的器件使用。在输出的串行数据流中加入奇偶校验位,并对从外部接收的数据流进行奇偶校验。在输出数据流中加入启停标记,并从接收数据流中删除启停标记。处理由键盘或鼠标发出的中断信号(键盘和鼠标也是串行设备)。可以处理计算机与外部串行设备的同步管理问题。

USART收发模块一般分为三大部分:时钟发生器、数据发送器和接收器。控制寄存器为所有的模块共享。时钟发生器由同步逻辑电路(在同步从模式下由外部时钟输入驱动)和波特率发生器组成。发送时钟引脚XCK仅用于同步发送模式下,发送器部分由一个单独的写入缓冲器(发送UDR)、一个串行移位寄存器、校验位发生器和用于处理不同浈结构的控制逻辑电路构成。使用写入缓冲器,实现了连续发送多浈数据无延时的通信。接收器是USART模块最复杂的部分,最主要的是时钟和数据接收单元。数据接收单元用作异步数据的接收。除了接收单元,接收器还包括校验位校验器、控制逻辑、移位寄存器和两级接收缓冲器(接收UDR)。接收器支持与发送器相同的帧结构,同时支持桢错误、数据溢出和校验错误的检测。USART是一个全双工通用同步/异步串行收发模块,该接口是一个高度灵活的串行通信设备。

以上信息整理自网络。

CMSIS标准简介

Cortex微控制器软件标准(Cortex Microcontroller Software Interface Standard)是ARM和一些编译器厂家以及半导体厂家共同遵循的一套标准,是由ARM提出,专门针对CORTEX-M系列的标准。在该标准的约定下,ARM和芯片厂商会提供一些通用的API接口(API:应用程序编程接口,是一些预先定义的函数,目的是提供程序与开发人员基于某软件或硬件的以访问一组例程的能力)来访问CORTEX内核以及一些专用外设,以减少更换芯片以及开发工具等移植工作所带来的金钱以及时间上的消耗。只要是基于M3的芯片,代码均是可以复用的。

该标准完全可扩展,可确保其适合于所有的CORTEX-M处理器系列微控制器。

CMSIS可以分为以下3个基本功能层:

  • 核内外设访问层 Core Peripheral Access Layer(CPAL)

  • 中间件访问层 Middleware Access Layer(MWAL)

  • 设备访问层 Device Peripheral Access Layer(DPAL)

  • 核内外设访问层 Core Peripheral Access Layer(CPAL)

该层用来定义一些CORTEX-M处理器内部的一些寄存器地址以及功能函数。如对内核寄存器,NVIC,调试子系统的访问。一些对特殊用途寄存器的访问被定义成内联函数或是内嵌汇编的形式。该层的实现由ARM提供。

  • 中间件访问层 Middleware Access Layer(MWAL)

该层一定访问中间件的一些通用API,该层也由ARM负责实现,但芯片厂商需要根据自己的设备进行更新,目前该层仍在开发中,还没有更进一步的消息。

  • 设备访问层 Device Peripheral Access Layer(DPAL)

该层和CPAL层类似,用来定义一些硬件寄存器的地址以及对外设的访问函数。另外芯片厂商还需要对异常向量表进行扩展,以实现对自己设备的中断处理。该层可饮用CPAL层定义的地址和函数,该层由具体芯片厂商提供。

ARM官网信息:

https://www.arm.com/zh/products/processors/cortex-m/cortex-microcontroller-software-interface-standard.php

STM32F1和STM32F4 区别

STM32F1和STM32F4 区别   (安富莱整理)
u F1采用Crotex M3内核,F4采用Crotex M4内核。

u  F1最高主频 72MHz, F4最高主频168MHz。

u  F4具有单精度浮点运算单元,F1没有浮点运算单元。

u  F4的具备增强的DSP指令集。F4的执行16位DSP指令的时间只有F1的30%~70%。F4执行32位DSP指令 的时间只有F1的25%~60%。

u  F1内部SRAM最大64K字节, F4内部SRAM有192K字节(112K+64K+16K)。

u  F4有备份域SRAM(通过Vbat供电保持数据),F1没有备份域SRAM。

u  F4从内部SRAM和外部FSMC存储器执行程序的速度比F1快很多。F1的指令总线I-Bus只接到Flash上,从SRAM和FSMC取指令只能通过S-Bus,速度较慢。F4的I-Bus不但连接到Flash上,而且还连接到SRAM和FSMC上,从而加快从SRAM或FSMC取指令的速度。

u  F1最大封装为144脚,可提供112个GPIO;F4最大封装有176脚,可提供140个GPIO。

u  F1的GPIO的内部上下拉电阻配置仅仅针对输入模式有用,输出时无效。而F4的GPIO在设置为输出模式时,上下拉电阻的配置依然有效。即F4可以配置为开漏输出,内部上拉电阻使能,而F1不行。

u  F4的GPIO最高翻转速度为84MHz,F1最大翻转速度只有18MHz。

u  F1最多可提供5个UART串口,F4最多可以提供6个UART串口。

u  F1可提供2个I2C接口,F4可以提供3个I2C接口。

u  F1和F4都具有3个12位的独立ADC,F1可提供21个输入通道,F4可以提供24个输入通道。F1的ADC最大采样频率为1Msps,2路交替采样可到2Msps(F1不支持3路交替采样)。F4的ADC最大采样频率为2.4Msps,3路交替采样可到7.2Msps。

u  F1只有12个DMA通道,F4有16个DMA通道。F4的每个DMA通道有4*32位FIFO,F1没有FIFO。

u  F1的SPI时钟最高速度为 18MHz, F4可以到37.5MHz。

u  F1没有独立的32位定时器(32位需要级联实现),F4的TIM2和TIM5具有32位上下计数功能。

u  F1和F4都有2个I2S接口,但是F1的I2S只支持半双工(同一时刻要么放音,要么录音),而F4的I2S支持全双工,放音和录音可以同时进行。

CPK 组合公钥技术之我见

老刘拿来这篇文章,真的是无意为之。该文是我读工硕的一篇作业,很诡异的是我2015年4月23日发给班主任老师,2015年4月29日就被上传到了百度文库中,且信息中删除了学院、学号、姓名等信息。前几日无意中发现该文档,因为南湘浩老师的CPK8.0暂时还未公开,当老刘发现之后,马上咨询了班主任老师,老师帮忙查询到,她那里应该不是泄露源头,而且那个时间点还未发给任课老师,我之后留言给文档发布者,已经数日未得到任何回应,现在文档发布者获取这篇文章的来源已经一个谜。好在这只是一篇单科作业,只是非常范范的做了一下介绍而已,并不能形成一个完整的知识体系,也不存在什么泄密,也就作罢了!

既然如此,老刘干脆就拿来分享吧!老刘说实话,这篇文章仅仅能作为作业上交

CPK组合公钥技术之我见.doc

关于内核ISO C90 forbids mixed declarations and code的警告

昨天有朋友问,如何去掉linux内核编译时出现的“ISO C90 forbids mixed declarations and code”警告。出现这个警告的原因,主要是因为执行了方法之后又出现了变量的定义:

int a = 0;
printk(" a = %d\n", a);
int b = 0;

在C89(90)标准中,不支持这类写法,将b的定义,写到prink之前即可。

这个问题实际是个仁者见仁智者见智的问题,没有绝对的对与错:有人说C89的定义更好,而有人说C99都推出那么多年,标准做这样的改动一定是要更合理了。呵呵,反正老刘也不好说什么,我建议还是遵从内核中大多数开发者的意见,将变量定义写在一起,当某个变量不需要时,删起来也容易不是。这是一个讨论帖:http://www.gossamer-threads.com/lists/linux/kernel/1132941

如果你就喜欢C99的风格,又不想看到警告,那么干脆,满足自己的欲望,直接将kernel下Makefile中的Wdeclaration-after-statement删掉!(老刘可不提倡!)

# warn about C99 declaration after statement
KBUILD_CFLAGS += $(call cc-option, -Wdeclaration-after-statement,)

    类似的问题其实还有个80线的问题,也是备受争议,现在的显示屏幕分辩率这么高,为什么还要坚持80线呢?老刘现在偶尔写写Java代码,用Idea开发环境,它的默认提示线就是120列。看看国外的驱动代码,基本上仍然遵循着80线,但国人写的驱动代码,就多少列的都有了。不过,老刘还是觉得人家的代码更漂亮,所以,老刘写驱动也一直遵循着传统的标准~。

I2C调试中的错误分析

在调试Linux驱动时,经常会用到I2C总线。通常情况下,都会调用Linux I2C core的数据收发函数,老刘整理了几个常见的错误及应对方式,供大家参考:

1.EOPNOTSUPP错误 — Operation not supported on transport endpoint

  • 由于Linux的错误号一般都是负数,所以在return时,代码返回的是-EOPNOTSUPP,该值的定义为95。

  • 这个错误原因主要是使用了不支持的方法,从软件方面入手即可。

2.EAGAIN错误 — Try again

  • 该值的定义为11,重试错误,这个时候,表明I2C控制器可以操作I2C总线,只不过没有得到器件的响应。

  • 解决这类错误请先从设备连接上入手,看看I2C设备是否连接正常;

  • 确认I2C器件的地址是否正确,需要注意:I2C一般使用的是7bit地址,在有些设备明确写明是7位地址,而有些设备写的不是很清楚,此时可以尝试将设备手册中的地址右移1位,得到7位器件地址;对于10bit的器件,发送时需设置I2C_M_TEN参数,但这类设备似乎很少见。

3.ETIMEDOUT错误 — Connection timed out

  • 该值的定义为110。

  • 这类错误一般是由于I2C总线错误引起的,I2C控制器无法正常操作总线,一般要先检查是否有什么原因导致I2C是否被意外拉低,外设挂的很多时,还需要考虑I2C的驱动能力是否足够,上拉电阻是否到位等因素。

dtb文件的反编译

在调试设备的驱动时,有时写好了dts(i)文件,编译之后发现没有起作用或发生了莫名其妙的问题,原因可能还是dts没有写对。出错的原因往往是由于dts会include很多个dtsi文件,且往往是最后一个定义才会生效。

做如下实验:

&i2c2 {
        status = "okay";
};
&i2c2 {
        status = "lgliu";
};

其中的第二个status老刘是故意写成了okey。这段代码编译之后,生成的i2c2的状态实际被设置为了"lgliu"(假定这个代码是最后一个配置i2c2的代码)。这时候,借助dts的反编译功能,就会发现这个问题了。

编译与反编译代码在kernel/scripts/dtc/目录下:

反编译dtb:

$ kernel/scripts/dtc/dtc -I dtb -O dts  source.dtb  -o dest.dtsi
i2c@2005a000 {
                compatible = "rockchip,rk30-i2c";
                reg = <0x2005a000 0x1000>;
                interrupts = <0x0 0x1a 0x4>;
                #address-cells = <0x1>;
                #size-cells = <0x0>;
                pinctrl-names = "default", "gpio";
                pinctrl-0 = <0x7b 0x7c>;
                pinctrl-1 = <0x7d>;
                gpios = <0x6d 0x14 0x1 0x6d 0x15 0x1>;
                clocks = <0x4d 0x6>;
                rockchip,check-idle = <0x1>;
                status = "lgliu";
                ts@55 {
                        compatible = "goodix,gt8xx";
                        reg = <0x55>;
                        touch-gpio = <0x77 0x8 0x8>;
                        reset-gpio = <0x6d 0x11 0x1>;
                        max-x = <0x500>;
                        max-y = <0x320>;
                };
        };

看这个i2c被合并后的信息,status为最后一次设置i2c2的状态值。

另外,由于dts会将相同的节点进行合并,通过反编译,也可以查看所有挂在某一总线的设备!

ARM Linux 3.x的设备树(Device Tree)

本文转自:http://blog.csdn.net/21cnbao/article/details/8457546, 该文是宋宝华老师的大作,老刘从中受益匪浅,感谢宋老师分享!

1.ARM Device Tree起源

Linus Torvalds在2011年3月17日的ARM Linux邮件列表宣称“this whole ARM thing is a fucking pain in the ass”,引发ARM Linux社区的地震,随后ARM社区进行了一系列的重大修正。在过去的ARM Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。读者有兴趣可以统计下常见的s3c2410、s3c6410等板级目录,代码量在数万行。

社区必须改变这种局面,于是PowerPC等其他体系架构下已经使用的Flattened Device Tree(FDT)进入ARM社区的视野。Device Tree是一种描述硬件的数据结构,它起源于 OpenFirmware (OF)。在Linux 2.6中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx,采用Device Tree后,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。

Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):

  • CPU的数量和类别

  • 内存基地址和大小

  • 总线和桥

  • 外设连接

  • 中断控制器和中断使用情况

  • GPIO控制器和GPIO使用情况

  • Clock控制器和Clock使用情况

它基本上就是画一棵电路板上CPU、总线、设备组成的,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。

2.Device Tree组成和结构

整个Device Tree牵涉面比较广,即增加了新的用于描述设备硬件信息的文本格式,又增加了编译这一文本的工具,同时Bootloader也需要支持将编译后的Device Tree传递给Linux内核

2.1 DTS (device tree source)

.dts文件是一种ASCII 文本格式的Device Tree描述,此文本格式非常人性化,适合人类的阅读习惯。基本上,在ARM Linux在,一个.dts文件对应一个ARM的machine,一般放置在内核的arch/arm/boot/dts/目录。由于一个SoC可能对应多个machine(一个SoC可以对应多个产品和电路板),势必这些.dts文件需包含许多共同的部分,Linux内核为了简化,把SoC公用的部分或者多个machine共同的部分一般提炼为.dtsi,类似于C语言的头文件。其他的machine对应的.dts就include这个.dtsi。譬如,对于VEXPRESS而言,vexpress-v2m.dtsi就被vexpress-v2p-ca9.dts所引用, vexpress-v2p-ca9.dts有如下一行:/include/ "vexpress-v2m.dtsi"

当然,和C语言的头文件类似,.dtsi也可以include其他的.dtsi,譬如几乎所有的ARM SoC的.dtsi都引用了skeleton.dtsi。

.dts(或者其include的.dtsi)基本元素即为前文所述的结点和属性:

/ {  
    node1 {  
        a-string-property = "A string";  
        a-string-list-property = "first string", "second string";  
        a-byte-data-property = [0x01 0x23 0x34 0x56];  
        child-node1 {  
            first-child-property;  
            second-child-property = <1>;  
            a-string-property = "Hello, world";  
        };  
        child-node2 {  
        };  
    };  
    node2 {  
        an-empty-property;  
        a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */  
        child-node1 {  
        };  
    };  
};

上述.dts文件并没有什么真实的用途,但它基本表征了一个Device Tree源文件的结构:

  • 1个root结点"/";

  • root结点下面含一系列子结点,本例中为"node1" 和 "node2";

  • 结点"node1"下又含有一系列子结点,本例中为"child-node1" 和 "child-node2";

  • 各结点都有一系列属性。这些属性可能为空,如" an-empty-property";可能为字符串,如"a-string-property";可能为字符串数组,如"a-string-list-property";可能为Cells(由u32整数组成),如"second-child-property",可能为二进制数,如"a-byte-data-property"。

下面以一个最简单的machine为例来看如何写一个.dts文件。假设此machine的配置如下:

  • 1个双核ARM Cortex-A9 32位处理器;

  • ARM的local bus上的内存映射区域分布了2个串口(分别位于0x101F1000 和 0x101F2000)、GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10170000)、中断控制器(位于0x10140000)和一个external bus桥;

  • External bus桥上又连接了SMC SMC91111 Ethernet(位于0x10100000)、I2C控制器(位于0x10160000)、64MB NOR Flash(位于0x30000000);

  • External bus桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338实时钟(I2C地址为0x58)。

其对应的.dts文件为:

/ {  
    compatible = "acme,coyotes-revenge";  
    #address-cells = <1>;  
    #size-cells = <1>;  
    interrupt-parent = <&intc>;  
  
    cpus {  
        #address-cells = <1>;  
        #size-cells = <0>;  
        cpu@0 {  
            compatible = "arm,cortex-a9";  
            reg = <0>;  
        };  
        cpu@1 {  
            compatible = "arm,cortex-a9";  
            reg = <1>;  
        };  
    };  
  
    serial@101f0000 {  
        compatible = "arm,pl011";  
        reg = <0x101f0000 0x1000 >;  
        interrupts = < 1 0 >;  
    };  
  
    serial@101f2000 {  
        compatible = "arm,pl011";  
        reg = <0x101f2000 0x1000 >;  
        interrupts = < 2 0 >;  
    };  
  
    gpio@101f3000 {  
        compatible = "arm,pl061";  
        reg = <0x101f3000 0x1000  
               0x101f4000 0x0010>;  
        interrupts = < 3 0 >;  
    };  
  
    intc: interrupt-controller@10140000 {  
        compatible = "arm,pl190";  
        reg = <0x10140000 0x1000 >;  
        interrupt-controller;  
        #interrupt-cells = <2>;  
    };  
  
    spi@10115000 {  
        compatible = "arm,pl022";  
        reg = <0x10115000 0x1000 >;  
        interrupts = < 4 0 >;  
    };  
  
    external-bus {  
        #address-cells = <2>  
        #size-cells = <1>;  
        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet  
                  1 0  0x10160000   0x10000     // Chipselect 2, i2c controller  
                  2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash  
  
        ethernet@0,0 {  
            compatible = "smc,smc91c111";  
            reg = <0 0 0x1000>;  
            interrupts = < 5 2 >;  
        };  
  
        i2c@1,0 {  
            compatible = "acme,a1234-i2c-bus";  
            #address-cells = <1>;  
            #size-cells = <0>;  
            reg = <1 0 0x1000>;  
            interrupts = < 6 2 >;  
            rtc@58 {  
                compatible = "maxim,ds1338";  
                reg = <58>;  
                interrupts = < 7 3 >;  
            };  
        };  
  
        flash@2,0 {  
            compatible = "samsung,k8f1315ebm", "cfi-flash";  
            reg = <2 0 0x4000000>;  
        };  
    };  
};

上述.dts文件中,root结点"/"的compatible 属性compatible = "acme,coyotes-revenge";定义了系统的名称,它的组织形式为:,。Linux内核透过root结点"/"的compatible 属性即可判断它启动的是什么machine

在.dts文件的每个设备,都有一个compatible 属性,compatible属性用户驱动和设备的绑定。compatible 属性是一个字符串的列表,列表中的第一个字符串表征了结点代表的确切设备,形式为",",其后的字符串表征可兼容的其他设备。可以说前面的是特指,后面的则涵盖更广的范围。如在arch/arm/boot/dts/vexpress-v2m.dtsi中的Flash结点:

flash@0,00000000 {  
     compatible = "arm,vexpress-flash", "cfi-flash";  
     reg = <0 0x00000000 0x04000000>,  
     <1 0x00000000 0x04000000>;  
     bank-width = <4>;  
 };

compatible属性的第2个字符串"cfi-flash"明显比第1个字符串"arm,vexpress-flash"涵盖的范围更广。

再比如,Freescale MPC8349 SoC含一个串口设备,它实现了国家半导体(National Semiconductor)的ns16550 寄存器接口。则MPC8349串口设备的compatible属性为compatible = "fsl,mpc8349-uart", "ns16550"。其中,fsl,mpc8349-uart指代了确切的设备, ns16550代表该设备与National Semiconductor 的16550 UART保持了寄存器兼容。

接下来root结点"/"的cpus子结点下面又包含2个cpu子结点,描述了此machine上的2个CPU,并且二者的compatible 属性为"arm,cortex-a9"。

注意cpus和cpus的2个cpu子结点的命名,它们遵循的组织形式为:[@],<>中的内容是必选项,[]中的则为可选项。name是一个ASCII字符串,用于描述结点对应的设备类型,如3com Ethernet适配器对应的结点name宜为ethernet,而不是3com509。如果一个结点描述的设备有地址,则应该给出@unit-address。多个相同类型设备结点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名结点。设备的unit-address地址也经常在其对应结点的reg属性中给出。ePAPR标准给出了结点命名的规范。

可寻址的设备使用如下信息来在Device Tree中编码地址信息:

  • reg

  • #address-cells

  • #size-cells

其中reg的组织形式为reg = ,其中的每一组address length表明了设备使用的一个地址范围。address为1个或多个32位的整型(即cell),而length则为cell的列表或者为空(若#size-cells = 0)。address 和 length 字段是可变长的,父结点的#address-cells和#size-cells分别决定了子结点的reg属性的address和length字段的长度。在本例中,root结点的#address-cells = <1>;和#size-cells = <1>;决定了serial、gpio、spi等结点的address和length字段的长度分别为1。cpus 结点的#address-cells = <1>;和#size-cells = <0>;决定了2个cpu子结点的address为1,而length为空,于是形成了2个cpu的reg = <0>;和reg = <1>;。external-bus结点的#address-cells = <2>和#size-cells = <1>;决定了其下的ethernet、i2c、flash的reg字段形如reg = <0 0 0x1000>;、reg = <1 0 0x1000>;和reg = <2 0 0x4000000>;。其中,address字段长度为0,开始的第一个cell(0、1、2)是对应的片选,第2个cell(0,0,0)是相对该片选的基地址,第3个cell(0x1000、0x1000、0x4000000)为length。特别要留意的是i2c结点中定义的 #address-cells = <1>;和#size-cells = <0>;又作用到了I2C总线上连接的RTC,它的address字段为0x58,是设备的I2C地址。

root结点的子结点描述的是CPU的视图,因此root子结点的address区域就直接位于CPU的memory区域。但是,经过总线桥后的address往往需要经过转换才能对应的CPU的memory映射。external-bus的ranges属性定义了经过external-bus桥后的地址范围如何映射到CPU的memory区域。

ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet  
          1 0  0x10160000   0x10000     // Chipselect 2, i2c controller  
          2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash

ranges是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。映射表中的子地址、父地址分别采用子地址空间的#address-cells和父地址空间的#address-cells大小。对于本例而言,子地址空间的#address-cells为2,父地址空间的#address-cells值为1,因此0 0  0x10100000   0x10000的前2个cell为external-bus后片选0上偏移0,第3个cell表示external-bus后片选0上偏移0的地址空间被映射到CPU的0x10100000位置,第4个cell表示映射的大小为0x10000。ranges的后面2个项目的含义可以类推。

Device Tree中还可以有中断连接信息,对于中断控制器而言,它提供如下属性:

  • interrupt-controller – 这个属性为空,中断控制器应该加上此属性表明自己的身份;

  • #interrupt-cells – 与#address-cells 和 #size-cells相似,它表明连接此中断控制器的设备的interrupts属性的cell大小。

在整个Device Tree中,与中断相关的属性还包括:

  • interrupt-parent – 设备结点透过它来指定它所依附的中断控制器的phandle,当结点没有指定interrupt-parent 时,则从父级结点继承。对于本例而言,root结点指定了interrupt-parent = <&intc>;其对应于intc: interrupt-controller@10140000,而root结点的子结点并未指定interrupt-parent,因此它们都继承了intc,即位于0x10140000的中断控制器。

  • interrupts – 用到了中断的设备结点透过它指定中断号、触发方法等,具体这个属性含有多少个cell,由它依附的中断控制器结点的#interrupt-cells属性决定。而具体每个cell又是什么含义,一般由驱动的实现决定,而且也会在Device Tree的binding文档中说明。譬如,对于ARM GIC中断控制器而言,#interrupt-cells为3,它3个cell的具体含义Documentation/devicetree/bindings/arm/gic.txt就有如下文字说明:

01   The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI  
02   interrupts.  
03  
04   The 2nd cell contains the interrupt number for the interrupt type.  
05   SPI interrupts are in the range [0-987].  PPI interrupts are in the  
06   range [0-15].  
07  
08   The 3rd cell is the flags, encoded as follows:  
09         bits[3:0] trigger type and level flags.  
10                 1 = low-to-high edge triggered  
11                 2 = high-to-low edge triggered  
12                 4 = active high level-sensitive  
13                 8 = active low level-sensitive  
14         bits[15:8] PPI interrupt cpu mask.  Each bit corresponds to each of  
15         the 8 possible cpus attached to the GIC.  A bit set to '1' indicated  
16         the interrupt is wired to that CPU.  Only valid for PPI interrupts.

另外,值得注意的是,一个设备还可能用到多个中断号。对于ARM GIC而言,若某设备使用了SPI的168、169号2个中断,而且都是高电平触发,则该设备结点的interrupts属性可定义为:interrupts = <0 168 4>, <0 169 4>;

除了中断以外,在ARM Linux中clock、GPIO、pinmux都可以透过.dts中的结点和属性进行描述。

2.2 DTC (device tree compiler)

将.dts编译为.dtb的工具。DTC的源代码位于内核的scripts/dtc目录,在Linux内核使能了Device Tree的情况下,编译内核的时候主机工具dtc会被编译出来,对应scripts/dtc/Makefile中的“hostprogs-y := dtc”这一hostprogs编译target。

在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SoC被选中后,哪些.dtb文件会被编译出来,如与VEXPRESS对应的.dtb包括:

dtb-$(CONFIG_ARCH_VEXPRESS) += vexpress-v2p-ca5s.dtb \  
        vexpress-v2p-ca9.dtb \  
        vexpress-v2p-ca15-tc1.dtb \  
        vexpress-v2p-ca15_a7.dtb \  
        xenvm-4.2.dtb

在Linux下,我们可以单独编译Device Tree文件。当我们在Linux内核下运行make dtbs时,若我们之前选择了ARCH_VEXPRESS,上述.dtb都会由对应的.dts编译出来。因为arch/arm/Makefile中含有一个dtbs编译target项目。

2.3 Device Tree Blob (.dtb)

.dtb是.dts被DTC编译后的二进制格式的Device Tree描述,可由Linux内核解析。通常在我们为电路板制作NAND、SD启动image时,会为.dtb文件单独留下一个很小的区域以存放之,之后bootloader在引导kernel的过程中,会先读取该.dtb到内存。

2.4 Binding

对于Device Tree中的结点和属性具体是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为.txt。这些文档位于内核的Documentation/devicetree/bindings目录,其下又分为很多子目录。

2.5 Bootloader

Uboot mainline 从 v1.1.3开始支持Device Tree,其对ARM的支持则是和ARM内核支持Device Tree同期完成。为了使能Device Tree,需要编译Uboot的时候在config文件中加入

#define CONFIG_OF_LIBFDT

在Uboot中,可以从NAND、SD或者TFTP等任意介质将.dtb读入内存,假设.dtb放入的内存地址为0x71000000,之后可在Uboot运行命令fdt addr命令设置.dtb的地址,如:

U-Boot> fdt addr 0x71000000

fdt的其他命令就变地可以使用,如fdt resize、fdt print等。 (老刘注–FDT:Flattened Device Tree)

对于ARM来讲,可以透过bootz kernel_addr initrd_address dtb_address的命令来启动内核,即dtb_address作为bootz或者bootm的最后一次参数,第一个参数为内核映像的地址,第二个参数为initrd的地址,若不存在initrd,可以用 -代替。

3. Device Tree引发的BSP和驱动变更

有了Device Tree后,大量的板级信息都不再需要,譬如过去经常在arch/arm/plat-xxx和arch/arm/mach-xxx实施的如下事情:

3.1 注册platform_device,绑定resource,即内存、IRQ等板级信息

透过Device Tree后,形如

static struct resource xxx_resources[] = {  
        [0] = {  
                .start  = …,  
                .end    = …,  
                .flags  = IORESOURCE_MEM,  
        },  
        [1] = {  
                .start  = …,  
                .end    = …,  
                .flags  = IORESOURCE_IRQ,  
         },  
 };  
  
static struct platform_device xxx_device = {  
        .name           = "xxx",  
        .id             = -1,  
        .dev            = {  
                                .platform_data          = &xxx_data,  
        },  
        .resource       = xxx_resources,  
        .num_resources  =  ARRAY_SIZE(xxx_resources),  
};

之类的platform_device代码都不再需要,其中platform_device会由kernel自动展开。而这些resource实际来源于.dts中设备结点的reg、interrupts属性。典型地,大多数总线都与“simple_bus”兼容,而在SoC对应的machine的.init_machine成员函数中,调用of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL);即可自动展开所有的platform_device。譬如,假设我们有个XXX SoC,则可在arch/arm/mach-xxx/的板文件中透过如下方式展开.dts中的设备结点对应的platform_device:

static struct of_device_id xxx_of_bus_ids[] __initdata = {  
        { .compatible = "simple-bus", },  
        {},  
};  
 
void __init xxx_mach_init(void)  
{  
        of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL);  
}  
 
#ifdef CONFIG_ARCH_XXX  
 
DT_MACHINE_START(XXX_DT, "Generic XXX (Flattened Device Tree)")  
        …  
        .init_machine   = xxx_mach_init,  
        …  
MACHINE_END  
#endif

3.2 注册i2c_board_info,指定IRQ等板级信息

形如

static struct i2c_board_info __initdata afeb9260_i2c_devices[] = {  
        {  
                I2C_BOARD_INFO("tlv320aic23", 0x1a),  
        }, {  
                I2C_BOARD_INFO("fm3130", 0x68),  
        }, {  
                I2C_BOARD_INFO("24c64", 0x50),  
        },  
};

之类的i2c_board_info代码,目前不再需要出现,现在只需要把tlv320aic23、fm3130、24c64这些设备结点填充作为相应的I2C controller结点的子结点即可,类似于前面的

i2c@1,0 {  
      compatible = "acme,a1234-i2c-bus";  
      …  
      rtc@58 {  
          compatible = "maxim,ds1338";  
          reg = <58>;  
          interrupts = < 7 3 >;  
      };  
  };

Device Tree中的I2C client会透过I2C host驱动的probe()函数中调用of_i2c_register_devices(&i2c_dev->adapter);被自动展开。

3.3.注册spi_board_info,指定IRQ等板级信息

形如

static struct spi_board_info afeb9260_spi_devices[] = {  
        {       /* DataFlash chip */  
                .modalias       = "mtd_dataflash",  
                .chip_select    = 1,  
                .max_speed_hz   = 15 * 1000 * 1000,  
                .bus_num        = 0,  
        },  
};

之类的spi_board_info代码,目前不再需要出现,与I2C类似,现在只需要把mtd_dataflash之类的结点,作为SPI控制器的子结点即可,SPI host驱动的probe函数透过spi_register_master()注册master的时候,会自动展开依附于它的slave。

3.4 多个针对不同电路板的machine,以及相关的callback

过去,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个machine的一系列callback,譬如:

MACHINE_START(VEXPRESS, "ARM-Versatile Express")  
        .atag_offset    = 0x100,  
        .smp            = smp_ops(vexpress_smp_ops),  
        .map_io         = v2m_map_io,  
        .init_early     = v2m_init_early,  
        .init_irq       = v2m_init_irq,  
        .timer          = &v2m_timer,  
        .handle_irq     = gic_handle_irq,  
        .init_machine   = v2m_init,  
        .restart        = vexpress_restart,  
MACHINE_END

这些不同的machine会有不同的MACHINE ID,Uboot在启动Linux内核时会将MACHINE ID存放在r1寄存器,Linux启动时会匹配Bootloader传递的MACHINE ID和MACHINE_START声明的MACHINE ID,然后执行相应machine的一系列初始化函数。引入Device Tree之后,MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的machine与.dts中root结点的compatible属性兼容关系。如果Bootloader传递给内核的Device Tree中root结点的compatible属性出现在某machine的.dt_compat表中,相关的machine就与对应的Device Tree匹配,从而引发这一machine的一系列初始化函数被执行。

static const char * const v2m_dt_match[] __initconst = {  
        "arm,vexpress",  
        "xen,xenvm",  
        NULL,  
};  
DT_MACHINE_START(VEXPRESS_DT, "ARM-Versatile Express")  
        .dt_compat      = v2m_dt_match,  
        .smp            = smp_ops(vexpress_smp_ops),  
        .map_io         = v2m_dt_map_io,  
        .init_early     = v2m_dt_init_early,  
        .init_irq       = v2m_dt_init_irq,  
        .timer          = &v2m_dt_timer,  
        .init_machine   = v2m_dt_init,  
        .handle_irq     = gic_handle_irq,  
        .restart        = vexpress_restart,  
MACHINE_END

Linux倡导针对多个SoC、多个电路板的通用DT machine,即一个DT machine的.dt_compat表含多个电路板.dts文件的root结点compatible属性字符串。之后,如果的电路板的初始化序列不一样,可以透过int of_machine_is_compatible(const char *compat) API判断具体的电路板是什么。

譬如arch/arm/mach-exynos/mach-exynos5-dt.c的EXYNOS5_DT machine同时兼容"samsung,exynos5250"和"samsung,exynos5440":

static char const *exynos5_dt_compat[] __initdata = {  
        "samsung,exynos5250",  
        "samsung,exynos5440",  
        NULL  
};  
 
DT_MACHINE_START(EXYNOS5_DT, "SAMSUNG EXYNOS5 (Flattened Device Tree)")  
        /* Maintainer: Kukjin Kim  */  
        .init_irq       = exynos5_init_irq,  
        .smp            = smp_ops(exynos_smp_ops),  
        .map_io         = exynos5_dt_map_io,  
        .handle_irq     = gic_handle_irq,  
        .init_machine   = exynos5_dt_machine_init,  
        .init_late      = exynos_init_late,  
        .timer          = &exynos4_timer,  
        .dt_compat      = exynos5_dt_compat,  
        .restart        = exynos5_restart,  
        .reserve        = exynos5_reserve,  
MACHINE_END

它的.init_machine成员函数就针对不同的machine进行了不同的分支处理:

static void __init exynos5_dt_machine_init(void)  
{  
        …  
 
        if (of_machine_is_compatible("samsung,exynos5250"))  
                of_platform_populate(NULL, of_default_bus_match_table,  
                                     exynos5250_auxdata_lookup, NULL);  
        else if (of_machine_is_compatible("samsung,exynos5440"))  
                of_platform_populate(NULL, of_default_bus_match_table,  
                                     exynos5440_auxdata_lookup, NULL);  
}

使用Device Tree后,驱动需要与.dts中描述的设备结点进行匹配,从而引发驱动的probe()函数执行。对于platform_driver而言,需要添加一个OF匹配表,如前文的.dts文件的"acme,a1234-i2c-bus"兼容I2C控制器结点的OF匹配表可以是:

static const struct of_device_id a1234_i2c_of_match[] = {  
        { .compatible = "acme,a1234-i2c-bus ", },  
        {},  
};  
MODULE_DEVICE_TABLE(of, a1234_i2c_of_match);  
 
static struct platform_driver i2c_a1234_driver = {  
        .driver = {  
                .name = "a1234-i2c-bus ",  
                .owner = THIS_MODULE,  
                .of_match_table = a1234_i2c_of_match,  
        },  
        .probe = i2c_a1234_probe,  
        .remove = i2c_a1234_remove,  
};  
module_platform_driver(i2c_a1234_driver);

对于I2C和SPI从设备而言,同样也可以透过of_match_table添加匹配的.dts中的相关结点的compatible属性,如sound/soc/codecs/wm8753.c中的:

static const struct of_device_id wm8753_of_match[] = {  
        { .compatible = "wlf,wm8753", },  
        { }  
};  
MODULE_DEVICE_TABLE(of, wm8753_of_match);  
static struct spi_driver wm8753_spi_driver = {  
        .driver = {  
                .name   = "wm8753",  
                .owner  = THIS_MODULE,  
                .of_match_table = wm8753_of_match,  
        },  
        .probe          = wm8753_spi_probe,  
        .remove         = wm8753_spi_remove,  
};  
static struct i2c_driver wm8753_i2c_driver = {  
        .driver = {  
                .name = "wm8753",  
                .owner = THIS_MODULE,  
                .of_match_table = wm8753_of_match,  
        },  
        .probe =    wm8753_i2c_probe,  
        .remove =   wm8753_i2c_remove,  
        .id_table = wm8753_i2c_id,  
};

不过这边有一点需要提醒的是,I2C和SPI外设驱动和Device Tree中设备结点的compatible 属性还有一种弱式匹配方法,就是别名匹配。compatible 属性的组织形式为,,别名其实就是去掉compatible 属性中逗号前的manufacturer前缀。关于这一点,可查看drivers/spi/spi.c的源代码,函数spi_match_device()暴露了更多的细节,如果别名出现在设备spi_driver的id_table里面,或者别名与spi_driver的name字段相同,SPI设备和驱动都可以匹配上:

static int spi_match_device(struct device *dev, struct device_driver *drv)  
{  
        const struct spi_device *spi = to_spi_device(dev);  
        const struct spi_driver *sdrv = to_spi_driver(drv);  
 
        /* Attempt an OF style match */  
        if (of_driver_match_device(dev, drv))  
                return 1;  
 
        /* Then try ACPI */  
         if (acpi_driver_match_device(dev, drv))  
                 return 1;  
  
         if (sdrv->id_table)  
                 return !!spi_match_id(sdrv->id_table, spi);  
  
         return strcmp(spi->modalias, drv->name) == 0;  
 }  
static const struct spi_device_id *spi_match_id(const struct spi_device_id *id,  
                                                const struct spi_device *sdev)  
{  
        while (id->name[0]) {  
                if (!strcmp(sdev->modalias, id->name))  
                        return id;  
                id++;  
        }  
        return NULL;  
}

4.常用OF API

在Linux的BSP和驱动代码中,还经常会使用到Linux中一组Device Tree的API,这些API通常被冠以of_前缀,它们的实现代码位于内核的drivers/of目录。这些常用的API包括:

int of_device_is_compatible(const struct device_node *device,const char *compat);

判断设备结点的compatible 属性是否包含compat指定的字符串。当一个驱动支持2个或多个设备的时候,这些不同.dts文件中设备的compatible 属性都会进入驱动 OF匹配表。因此驱动可以透过Bootloader传递给内核的Device Tree中的真正结点的compatible 属性以确定究竟是哪一种设备,从而根据不同的设备类型进行不同的处理。如drivers/pinctrl/pinctrl-sirf.c即兼容于"sirf,prima2-pinctrl",又兼容于"sirf,prima2-pinctrl",在驱动中就有相应分支处理:

if (of_device_is_compatible(np, "sirf,marco-pinctrl"))  
     is_marco = 1;  
struct device_node *of_find_compatible_node(struct device_node *from,
         const char *type, const char *compatible);

根据compatible属性,获得设备结点。遍历Device Tree中所有的设备结点,看看哪个结点的类型、compatible属性与本函数的输入参数匹配,大多数情况下,from、type为NULL。

int of_property_read_u8_array(const struct device_node *np,
                     const char *propname, u8 *out_values, size_t sz);
int of_property_read_u16_array(const struct device_node *np,
                      const char *propname, u16 *out_values, size_t sz);
int of_property_read_u32_array(const struct device_node *np,
                      const char *propname, u32 *out_values, size_t sz);
int of_property_read_u64(const struct device_node *np, const char
*propname, u64 *out_value);

读取设备结点np的属性名为propname,类型为8、16、32、64位整型数组的属性。对于32位处理器来讲,最常用的是of_property_read_u32_array()。如在arch/arm/mm/cache-l2x0.c中,透过如下语句读取L2 cache的"arm,data-latency"属性:

of_property_read_u32_array(np, "arm,data-latency",  
                                    data, ARRAY_SIZE(data));

在arch/arm/boot/dts/vexpress-v2p-ca9.dts中,含有"arm,data-latency"属性的L2 cache结点如下:

L2: cache-controller@1e00a000 {  
        compatible = "arm,pl310-cache";  
        reg = <0x1e00a000 0x1000>;  
        interrupts = <0 43 4>;  
        cache-level = <2>;  
        arm,data-latency = <1 1 1>;  
        arm,tag-latency = <1 1 1>;  
}

有些情况下,整形属性的长度可能为1,于是内核为了方便调用者,又在上述API的基础上封装出了更加简单的读单一整形属性的API,它们为int of_property_read_u8()、of_property_read_u16()等,实现于include/linux/of.h:

static inline int of_property_read_u8(const struct device_node *np,  
                                       const char *propname,  
                                       u8 *out_value)  
{  
        return of_property_read_u8_array(np, propname, out_value, 1);  
}  
 
static inline int of_property_read_u16(const struct device_node *np,  
                                       const char *propname,  
                                       u16 *out_value)  
{  
        return of_property_read_u16_array(np, propname, out_value, 1);  
}  
 
static inline int of_property_read_u32(const struct device_node *np,  
                                       const char *propname,  
                                       u32 *out_value)  
{  
        return of_property_read_u32_array(np, propname, out_value, 1);  
}  
int of_property_read_string(struct device_node *np, const char
*propname, const char **out_string);
int of_property_read_string_index(struct device_node *np, const char
    *propname, int index, const char **output);

前者读取字符串属性,后者读取字符串数组属性中的第index个字符串。如drivers/clk/clk.c中的of_clk_get_parent_name()透过of_property_read_string_index()遍历clkspec结点的所有"clock-output-names"字符串数组属性。

const char *of_clk_get_parent_name(struct device_node *np, int index)  
{  
        struct of_phandle_args clkspec;  
        const char *clk_name;  
        int rc;  
 
        if (index < 0)  
                return NULL;  
 
        rc = of_parse_phandle_with_args(np, "clocks", "#clock-cells", index,  
                                        &clkspec);  
        if (rc)  
                return NULL;  
 
        if (of_property_read_string_index(clkspec.np, "clock-output-names",  
                                  clkspec.args_count ? clkspec.args[0] : 0,  
                                          &clk_name) < 0)  
                clk_name = clkspec.np->name;  
 
        of_node_put(clkspec.np);  
        return clk_name;  
}  
EXPORT_SYMBOL_GPL(of_clk_get_parent_name);  
static inline bool of_property_read_bool(const struct device_node *np,
                                         const char *propname);

如果设备结点np含有propname属性,则返回true,否则返回false。一般用于检查空属性是否存在。

void __iomem *of_iomap(struct device_node *node, int index);

通过设备结点直接进行设备内存区间的 ioremap(),index是内存段的索引。若设备结点的reg属性有多段,可通过index标示要ioremap的是哪一段,只有1段的情况,index为0。采用Device Tree后,大量的设备驱动通过of_iomap()进行映射,而不再通过传统的ioremap。

unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

透过Device Tree或者设备的中断号,实际上是从.dts中的interrupts属性解析出中断号。若设备使用了多个中断,index指定中断的索引号。

还有一些OF API,这里不一一列举,具体可参考include/linux/of.h头文件。

5.总结

ARM社区一贯充斥的大量垃圾代码导致Linus盛怒,因此社区在2011年到2012年进行了大量的工作。ARM Linux开始围绕Device Tree展开,Device Tree有自己的独立的语法,它的源文件为.dts,编译后得到.dtb,Bootloader在引导Linux内核的时候会将.dtb地址告知内核。之后内核会展开Device Tree并创建和注册相关的设备,因此arch/arm/mach-xxx和arch/arm/plat-xxx中大量的用于注册platform、I2C、SPI板级信息的代码被删除,而驱动也以新的方式和.dts中定义的设备结点进行匹配。