SPI系统分析
SPI简介
SPI总线是Motorola公司推出的三线同步接口,是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口,是一种高速的,全双工,同步的通信总线,应用也比较广泛,个人碰到的有用于平板上的ISDBT,也就是平板的移动电视。SPI的传输速率还可以,一般会有几M/S。
最简单的SPI总线是三线同步接口,但一般应用中会有四根线:
SCK:时钟线,为SPI数据传输提供同步时钟。
MOSI:数据输入线
MISO:数据输出线
CS:从器件使能信号
SPI的通信是一种主从模式,它有一个主设备,通常是主芯片端,还会有一个或者多个从设备端,也就是外设端。SPI是串行通信协议,数据是一位一位的从MSB或者LSB开始传输的,因此它需要一个时钟脉冲SCK来控制数据输入输出的频率等。有的SPI总线上挂载不止一个设备,而SPI总线是如何决定哪个设备可以进行通信呢,这个就需要CS信号了,当设备的CS信号被使能之后(拉低或者拉高),设备就可以进行通信了。不同设备的CS引脚可以连接在主芯片的不同的GPIO引脚上,这样主设备可以通过对主芯片的GPIO脚的控制来达到使能对应的SPI从设备了。
SPI有四种传输方式,分别由CPOL和CPHA控制:
CPOL是对时钟极性的控制:当CPOL为0时,SCK在空闲状态就为低电平,当CPOL为1时,SCK在空闲状态就为高电平。
CPHA是对时钟相位的控制:如果 CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。
SPI控制器和从设备之间的时钟相位和时钟极性应该一致。
借用一个图片来说明CPOL和CPHA对SPI数据传输时序的影响:
以时钟极性为0时钟相位为0为例,如图CPOL=0时SCK为低,同时在SCK的第一个上升沿中进行数据采样,然后在接下来的下降沿中进行数据输出。
再以时钟极性为1时钟相位也为1的为例,我们可以找到上图中的CPOL=1,在数据传输之前SCK是为高,再看第三个的时钟相位为1的图,在SCK第一个跳变沿也就是由高到低的过程中对应MOSI和MISO也是处于跳变阶段,此时是在进行数据输出,而在SCK的第二个跳变的时候也就是SCK由低到高的时候,MOSI和MISO处于稳定(保持高或者低状态),这时候进行数据采样。
因此我们可以看出,对于SPI的时钟极性相位的设置决定了其数据传输的时序。
SPI软件结构
SPI在内核中主要由三部分构成:
SPI控制器驱动,各个平台都会根据SPI总线在自己平台芯片上的定义来实现自己的SPI控制器驱动。SPI控制器驱动主要是对SPI总线本身的一个控制,如SPI总线的上下电时序,时钟,SPI中断,DMA以及最主要也最重要的SPI通信等资源的控制。SPI控制器在整个SPI系统中作为一个master存在,可以看作是系统中SPI总线的一个管理者,同时,它提供接口出来为各个SPI外设服务。
LINUX SPI核心层,在linux的核心层代码中主要定义了SPI主控制器spi_master,SPI驱动spi_driver,SPI设备spi_device,SPI通信相关的spi_transfer,spi_message等数据结构,这些数据结构是构成整个SPI系统的最基本的元素,同时,在SPI的核心层中又定义了这些基本元素之间的关系。SPI核心层对SPI系统提供了最基础的搭建方法,如向系统注册一个控制器master,向控制器上添加一个新的spi设备,spi设备和控制器的电源管理,spi设备与驱动关联条件等。
SPI设备,spi设备指的是通过spi总线与主芯片进行通信的设备,如数据电视芯片ISDBT,通常是指具有特定功能的电子芯片。spi设备在系统中以一个spi_device的数据结构存在。
下面我们来看看整个SPI系统是如何实现的。
系统开机启动之后会加载初始化配置好的各个模块,其中就会加载spi的核心层模块,在spi.c文件中最后有
postcore_initcall(spi_init);
它的作用是加载已经编译好的spi_init函数:
static int __init spi_init(void)
{
.......
status = bus_register(&spi_bus_type);
if (status < 0)
goto err1;
status = class_register(&spi_master_class);
if (status < 0)
goto err2;
.........
}
在spi_init()当中主要是注册SPI类型的总线以及SPI类型总线相关的class文件。
在spi.c中可以找到对于SPI总线的定义:
struct bus_type spi_bus_type = {
.name = "spi",//总线在系统中的名称spi,
.dev_attrs = spi_dev_attrs,//spi总线作为系统中一个设备的相关属性文件
.match = spi_match_device,//spi总线上设备与设备驱动匹配方式
.uevent = spi_uevent,
.pm = &spi_pm,//SPI总线电源管理接口
};
当spi_init()执行完之后,系统中就会存在一条类型为spi的总线,我们可以在系统sys文件系统中看到它的信息:
当系统定义并添加了spi总线类型之后,在接下来后便会在系统中添加平台的spi总线。
很多平台上都会集成有SPI总线,有些平台上不仅只有一条SPI总线。芯片上集成的SPI总线在linux内核中是作为spi控制器存在的,在spi通信当中,SPI总线是作为一个主设备,而外设对于SPI总线来说是从设备,SPI总线与外设是一种主从关系。在软件架构中这种关系是如何体现出来的呢?首先我们来看看SPI总线是怎么注册到系统当中的。
在marvell的1088平台上,SPI总线作为其SSP (synchronous serial protocol) ports的集成在主芯片上,它可工作在全双工模式下,也就是发送和接收是可以同时进行的,对于SPI总线的时钟极性及相位设置有对应的寄存器,根据datasheet设置对应寄存器值就可以设置SPI总线的时钟极性和相位,同时,主要信号除了时钟SPI_CLK,发送/接收信号SPI_TX/SPI_RX,它还有一个SSPSFRM信号来控制数据传输开始条件,相当于CS信号。
在平台代码上,SPI总线相关硬件资源如DMA,中断,寄存器起始地址等如下定义:
而SPI总线是作为一个platform_device注册到系统当中去的,因此,这些定义的硬件资源如中断和DMA的起始地址都会转换为platform_device的resource中保存起来,然后会向系统注册一个name为pxa988-ssp的platform_device。
既然注册了platform_device,就必须会有其对应的platform_driver:
在pxa_ssp_driver被add到系统当中之后,系统会根据id_table(platform设备与驱动的匹配方式不只比较两者的name是否相同一种哦)中找到的pxa988-ssp将之前注册的platform_device与platform_driver关联起来,同时进入driver的probe:
probe中对ssp设备的一系列的初始化,如获取到SSP的时钟, 初始化SSP的DMA接收和发送地址,将ssp寄存器映射成系统虚拟地址,获取设备中断等工作,最后将初始化好的ssp设备加入到一条链表上:
在marvell 1088平台上,SPI总线只是其支持的的同步串行传输协议(SSP)的其中一种,spi总线的工作实际是对ssp进行一系列用于spi通信的设置后进行的。
对于不同的平台,平台都会现实相对应的SPI总线驱动,首先我们来了解下marvell平台的SPI的数据传输过程。
平台为总线的发送/接收数据线分别提供了一个发送FIFO和一个接收FIFO,这两个FIFO会设置一个阀值,在SPI数据传输过程中,当发送FIFO中数据量小于或等于阀值时,会产一个中断,主芯片通过I/O或者DMA方式将数据填充到发送FIFO当中,当接收FIFO中数据量超过阀值时,同样会产生一个中断,然后主芯片会清空接收FIFO中数据。在DMA模式下,可通过设置SPI DMA控制器的burst值(burst值小于阀值)来控制FIFO中数据量。FIFO作为一个中转站,发送时,数据在某地址空间处通过DMA通信被送往发送FIFO,然后数据被串行化,再通过SPI总线传输到外设当中。而串行数据又经过SPI总线到达接收FIFO,并转化为并行数据保存在接收FIFO当中,然后数据才通过DMA方式搬运到指定地址处。
在空闲状态下,时钟SSPx_CLK及发送/接收SSPx_DXTX线为低,SSPx_FRM为高,系统在进行SPI通信前首先会初始化好SPI总线,然后会将数据打包,设置数据传输过程中的一些软件状态/标志,检测数据包的正确性,清除SPI总线和DMA的一些状态标志位,设置好发送/接收DMA地址,最后会拉低SSPx_FRM,SPI总线开始进行数据传输,在SSPx_CLK 的下降沿发送数据,在SSPx_CLK的上升沿进行数据采样,当一帧数据(8,16,18,32位)传输完成的时候,SSPx_FRM再拉高。
对于marvell平台的SPI总线驱动来说,在开机启动时会根据获取SPI总线设备注册的相关硬件资源如相关控制/状态/数据寄存器地址,DMA地址,中断号等,并根据这些信息初始化一个master,这个master就代表内核中存在的一个SPI总线,在整个SPI系统架构中,它作为一个主设备来与挂载在其线上的从设备进行通信,或者说为它们提供通信服务接口。
各个平台均会实现其对应SPI通信的软件流程,并提供初始化以及数据传输接口给外设。因此,在我们进行SPI外设驱动开发的时候,需要对平台SPI总线驱动有一定的了解,并熟练调用其提供的接口。
下面我们来了解一下SPI外设驱动(以数字电视ISDB-T为例)。
一般我们会在系统中定义一个spi_board_info的结构,用来表示一个spi设备并设置其基本信息:
-
struct spi_board_info {
-
/* the device name and module name are coupled, like platform_bus;
-
* "modalias" is normally the driver name.
-
*
-
* platform_data goes to spi_device.dev.platform_data,
-
* controller_data goes to spi_device.controller_data,
-
* irq is copied too
-
*/
-
char modalias[SPI_NAME_SIZE];//spi设备名称,用于spi设备与驱动的匹配
-
const void *platform_data;//spi设备的私有数据
-
void *controller_data;//spi控制器也就是spi总线的私有数据,用于设置spi fifo阀值等相关控制信息
-
int irq;//spi设备中断号
-
-
/* slower signaling on noisy or low voltage boards */
-
u32 max_speed_hz;//spi传输速率,SPI总线传输速率由平台决定,可覆盖此处设置
-
-
-
/* bus_num is board specific and matches the bus_num of some
-
* spi_master that will probably be registered later.
-
*
-
* chip_select reflects how this chip is wired to that master;
-
* it's less than num_chipselect.
*/
u16 bus_num;//spi设备挂载的SPI总线号,用于区别系统中在于多条SPI总线的情况
u16 chip_select;//片选方式
/* mode becomes spi_device.mode, and is essential for chips
* where the default of SPI_CS_HIGH = 0 is wrong.
*/
u8 mode;//spi通信模式
/* ... may need additional spi_device chip config data here.
* avoid stuff protocol drivers can set; but include stuff
* needed to behave without being bound to a driver:
* - quirks like clock rate mattering when not selected
*/
};
在我的工程中定义了一个如上的SPI设备。然后将设备注册到系统当中:
在将定义好的SPI设备注册到系统当中的时候,系统会遍历所有的SPI总线,并且根据SPI总线号与SPI设备挂载的总线号进行匹配,如果两者相等,则使用定义的spi_board_info信息来创建并初始化一个SPI设备:
-
static void spi_match_master_to_boardinfo(struct spi_master *master,
-
struct spi_board_info *bi)
-
{
-
struct spi_device *dev;
-
-
if (master->bus_num != bi->bus_num)
-
return;
-
-
dev = spi_new_device(master, bi);
-
if (!dev)
-
dev_err(master->dev.parent, "can't create new device for %s\n",
-
bi->modalias);
-
}
-
struct spi_device *spi_new_device(struct spi_master *master,
-
struct spi_board_info *chip)
-
{
-
struct spi_device *proxy;
-
int status;
-
-
/* NOTE: caller did any chip->bus_num checks necessary.
-
*
-
* Also, unless we change the return value convention to use
-
* error-or-pointer (not NULL-or-pointer), troubleshootability
-
* suggests syslogged diagnostics are best here (ugh).
-
*/
-
-
proxy = spi_alloc_device(master);
-
if (!proxy)
-
return NULL;
-
-
WARN_ON(strlen(chip->modalias) >= sizeof(proxy->modalias));
-
-
proxy->chip_select = chip->chip_select;
-
proxy->max_speed_hz = chip->max_speed_hz;
-
proxy->mode = chip->mode;
-
proxy->irq = chip->irq;
-
strlcpy(proxy->modalias, chip->modalias, sizeof(proxy->modalias));
-
proxy->dev.platform_data = (void *) chip->platform_data;
-
proxy->controller_data = chip->controller_data;
-
proxy->controller_state = NULL;
-
-
status = spi_add_device(proxy);
-
if (status < 0) {
-
spi_dev_put(proxy);
-
return NULL;
-
}
-
-
return proxy;
-
}
从以上代码可知,系统创建一个spi_device,并且使用我们前面定义好的spi_board_info结构中的信息来初始化spi_device,最后将初始化好的spi_device添加到系统当中。
这样我们便将一个初始化好的SPI设备注册到系统当中了。
当设备添加到系统当中之后,总线会为设备匹配合适的驱动程序,那么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;
-
-
if (sdrv->id_table)
-
return !!spi_match_id(sdrv->id_table, spi);
-
-
return strcmp(spi->modalias, drv->name) == 0;//比较spi_device的modalias与spi_driver的name是否相同
-
}
由以上代码可知,spi_device匹配驱动的条件是设备的modelias与spi_driver->name是否相等。而根据spi_device初始化的代码可知,spi_device由spi_board_info中的modelias初始化,因此,当我们在定义一个spi_board_info和编写spi设备驱动的时候,注意要将spi_board_info的modalias和spi_driver->name设置相同。
现在我们来看看工程中ISDBT的设备驱动代码:
在这个spi_driver当中,定义了spi_driver的name域,与之前定义的spi_borad_info中的modalias域匹配!probe中主要是进行一些初始化工作如:为该spi设备的spi总线作相应初始化工作,初始化一个spi_device的控制引脚,申请中断,保存设备私有数据等。remove主要是卸载该driver时释放系统中分配的内存资源和中断等。suspend和resume用于设备休眠与唤醒时的一些操作。
设备驱动根据其功能需要来实现不同操作,或者以不同的方式向上层提供控制接口。spi设备驱动最主要的当然是通信功能,spi driver会将数据以spi_message的方式打包然后以链表的方式将数据发送或者接收。
首先我们来了解两个在SPI通信中比较重要的两个数据结构:
-
struct spi_transfer {
-
/* it's ok if tx_buf == rx_buf (right?)
-
* for MicroWire, one buffer must be null
-
* buffers must work with dma_*map_single() calls, unless
-
* spi_message.is_dma_mapped reports a pre-existing mapping
-
*/
-
const void *tx_buf;//指向将要写入到spi_device的数据
-
void *rx_buf;//指向从spi_device中读取的数据
-
unsigned len;//读和写的数据长度(字节)
-
-
dma_addr_t tx_dma;//发送DMA通道的源地址,tx_buf(指向将要发送的数据)的指针被重新经过DMA映射使之成为DMA safe的地址,并作为发送DMA通道的源地址
-
dma_addr_t rx_dma;//接收DMA通道的目标地址,rx_buf(指向从SPI设备中接收到的数据)的指针同样经过DMA映射得到一个DMA safe的地址,并作为接收DMA通道的目标地址
-
-
unsigned cs_change:1;//数据传输完成之后将作用到片选信号上
-
u8 bits_per_word;
-
u16 delay_usecs;//当一个spi_transfer传输完成之后进行的delay时间(微秒级别),然后进行下一个spi_transfer的传输
-
u32 speed_hz;//SPI传输速率
-
-
struct list_head transfer_list;
-
};
我们可以看到,当我们需要向SPI设备发送一段数据的时候,我们会构造一个spi_transfer结构,并将数据保存在其tx_buf指向的内存地址当中,当我们从SPI设备中获取到一段时间的时候,数据被保存在spi_transfer的rx_buf指向的空间中。然后会以DMA的方式进行数据搬运,然而我们知道,平台对于每次DMA传输的数据量是有限制的,在marvell平台上,最大的一次DMA传输数据量为8K,但我们通过SPI传输的数据量在很多时候远不止8K,这时候linux内核又为我们提供了另一个数据:spi_message
-
struct spi_message {
-
struct list_head transfers;//挂载在spi_message上的spi_transfer
-
-
struct spi_device *spi;//指向使用SPI总线通信的SPI外设
-
-
unsigned is_dma_mapped:1;//保存数据的地址是否对于DMA可用
-
-
/* REVISIT: we might want a flag affecting the behavior of the
-
* last transfer ... allowing things like "read 16 bit length L"
-
* immediately followed by "read L bytes". Basically imposing
-
* a specific message scheduling algorithm.
-
*
-
* Some controller drivers (message-at-a-time queue processing)
-
* could provide that as their default scheduling algorithm. But
-
* others (with multi-message pipelines) could need a flag to
-
* tell them about such special cases.
-
*/
-
-
/* completion is reported through a callback */
-
void (*complete)(void *context);//用于通知系统传输完成
-
void *context;//complete的参数
-
unsigned actual_length;
-
int status;//成功传输完成置0,失败置负
-
-
/* for optional use by whatever driver currently owns the
-
* spi_message ... between calls to spi_async and then later
-
* complete(), that's the spi_master controller driver.
-
*/
-
struct list_head queue;
-
void *state;
-
};
spi_message上挂载着一个或者多个spi_transfer,而它是用于执行一次原子的SPI传输过程,也就是说,当spi_message上挂载的spi_transfer开始传输前,它会获取SPI总线的控制权,而在它挂载的spi_transfer传输过程中,它会一直保持着SPI总线的控制权,直到其上所有的spi_transfer传输完成才释放SPI总线。
在我的工程当中,一次SPI的传输如下,我们可以看到,它一次传输中每个spi_message上只挂载了一个spi_transfer:
在我的ISDBT驱动中,在ISDBT正常工作之前系统首先会向芯片发送一系统初始化命令,当一个或者一系统命令作为数据打包好之后,被保存在一个spi_transfer的tx_buf指向的一块内存空间当中(tx_buf是DMA发送通道的源地址,rx_buf是DMA接收通道目标地址,而tx_buf的目标地址和rx_buf的源地址均为FIFO的DATA寄存器(接收和发送FIFO使用相同寄存器地址)),然后会将spi_transfer挂载在spi_message的链表上,当一个spi_message开始传输的时候,它会占用spi总线,直到挂载在它上面的所有的spi_transfer传输完成,它才释放spi总线。
对于SPI来说,首先我们需要了解SPI的工作时序,它的工作模式决定了它CLK和DATA时序的不同,同时我们也要注意它的片选信号,它是作为一个通信开始及结束的标志,或者说某设备拉低它时,某设备就拥有了SPI总线的控制权。同时我们还需要了解平台是如何通过SPI总线传输数据的(数据的串行化和并行化工作是在SPI总线的发送/接收FIFO完成的)。对于SPI设备驱动的来说,我们需要了解如何定义并注册一个SPI设备,并且成功加载一个SPI设备驱动。而对于SPI设备和主芯片端的通信,我们需要了解linux内核为我们提供的SPI相关的数据结构(spi_transfer和spi_message),它们是SPI通信的主要载体。
阅读(1943) | 评论(0) | 转发(0) |