Chinaunix首页 | 论坛 | 博客
  • 博客访问: 6834184
  • 博文数量: 3857
  • 博客积分: 6409
  • 博客等级: 准将
  • 技术积分: 15948
  • 用 户 组: 普通用户
  • 注册时间: 2008-09-02 16:48
个人简介

迷彩 潜伏 隐蔽 伪装

文章分类

全部博文(3857)

文章存档

2017年(5)

2016年(63)

2015年(927)

2014年(677)

2013年(807)

2012年(1241)

2011年(67)

2010年(7)

2009年(36)

2008年(28)

分类: 嵌入式

2013-10-29 15:32:24

原文地址:SPI系统分析 作者:cainiaopei

    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设备并设置其基本信息:

点击(此处)折叠或打开

  1. struct spi_board_info {
  2.     /* the device name and module name are coupled, like platform_bus;
  3.      * "modalias" is normally the driver name.
  4.      *
  5.      * platform_data goes to spi_device.dev.platform_data,
  6.      * controller_data goes to spi_device.controller_data,
  7.      * irq is copied too
  8.      */
  9.     char        modalias[SPI_NAME_SIZE];//spi设备名称,用于spi设备与驱动的匹配
  10.     const void    *platform_data;//spi设备的私有数据
  11.     void        *controller_data;//spi控制器也就是spi总线的私有数据,用于设置spi fifo阀值等相关控制信息
  12.     int        irq;//spi设备中断号

  13.     /* slower signaling on noisy or low voltage boards */
  14.     u32        max_speed_hz;//spi传输速率,SPI总线传输速率由平台决定,可覆盖此处设置


  15.     /* bus_num is board specific and matches the bus_num of some
  16.      * spi_master that will probably be registered later.
  17.      *
  18.      * chip_select reflects how this chip is wired to that master;
  19.      * 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设备:
    

点击(此处)折叠或打开

  1. static void spi_match_master_to_boardinfo(struct spi_master *master,
  2.                 struct spi_board_info *bi)
  3. {
  4.     struct spi_device *dev;

  5.     if (master->bus_num != bi->bus_num)
  6.         return;

  7.     dev = spi_new_device(master, bi);
  8.     if (!dev)
  9.         dev_err(master->dev.parent, "can't create new device for %s\n",
  10.             bi->modalias);
  11. }
   

点击(此处)折叠或打开

  1. struct spi_device *spi_new_device(struct spi_master *master,
  2.                  struct spi_board_info *chip)
  3. {
  4.     struct spi_device    *proxy;
  5.     int            status;

  6.     /* NOTE: caller did any chip->bus_num checks necessary.
  7.      *
  8.      * Also, unless we change the return value convention to use
  9.      * error-or-pointer (not NULL-or-pointer), troubleshootability
  10.      * suggests syslogged diagnostics are best here (ugh).
  11.      */

  12.     proxy = spi_alloc_device(master);
  13.     if (!proxy)
  14.         return NULL;

  15.     WARN_ON(strlen(chip->modalias) >= sizeof(proxy->modalias));

  16.     proxy->chip_select = chip->chip_select;
  17.     proxy->max_speed_hz = chip->max_speed_hz;
  18.     proxy->mode = chip->mode;
  19.     proxy->irq = chip->irq;
  20.     strlcpy(proxy->modalias, chip->modalias, sizeof(proxy->modalias));
  21.     proxy->dev.platform_data = (void *) chip->platform_data;
  22.     proxy->controller_data = chip->controller_data;
  23.     proxy->controller_state = NULL;

  24.     status = spi_add_device(proxy);
  25.     if (status < 0) {
  26.         spi_dev_put(proxy);
  27.         return NULL;
  28.     }

  29.     return proxy;
  30. }
     从以上代码可知,系统创建一个spi_device,并且使用我们前面定义好的spi_board_info结构中的信息来初始化spi_device,最后将初始化好的spi_device添加到系统当中。
    这样我们便将一个初始化好的SPI设备注册到系统当中了。
    当设备添加到系统当中之后,总线会为设备匹配合适的驱动程序,那么spi设备与驱动匹配的条件是什么呢?
    

点击(此处)折叠或打开

  1. static int spi_match_device(struct device *dev, struct device_driver *drv)
  2. {
  3.     const struct spi_device    *spi = to_spi_device(dev);
  4.     const struct spi_driver    *sdrv = to_spi_driver(drv);

  5.     /* Attempt an OF style match */
  6.     if (of_driver_match_device(dev, drv))
  7.         return 1;

  8.     if (sdrv->id_table)
  9.         return !!spi_match_id(sdrv->id_table, spi);

  10.     return strcmp(spi->modalias, drv->name) == 0;//比较spi_device的modalias与spi_driver的name是否相同
  11. }
    由以上代码可知,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通信中比较重要的两个数据结构:
    

点击(此处)折叠或打开

  1. struct spi_transfer {
  2.     /* it's ok if tx_buf == rx_buf (right?)
  3.      * for MicroWire, one buffer must be null
  4.      * buffers must work with dma_*map_single() calls, unless
  5.      * spi_message.is_dma_mapped reports a pre-existing mapping
  6.      */
  7.     const void    *tx_buf;//指向将要写入到spi_device的数据
  8.     void        *rx_buf;//指向从spi_device中读取的数据
  9.     unsigned    len;//读和写的数据长度(字节)

  10.     dma_addr_t    tx_dma;//发送DMA通道的源地址,tx_buf(指向将要发送的数据)的指针被重新经过DMA映射使之成为DMA safe的地址,并作为发送DMA通道的源地址
  11.     dma_addr_t    rx_dma;//接收DMA通道的目标地址,rx_buf(指向从SPI设备中接收到的数据)的指针同样经过DMA映射得到一个DMA safe的地址,并作为接收DMA通道的目标地址

  12.     unsigned    cs_change:1;//数据传输完成之后将作用到片选信号上
  13.     u8        bits_per_word;
  14.     u16        delay_usecs;//当一个spi_transfer传输完成之后进行的delay时间(微秒级别),然后进行下一个spi_transfer的传输
  15.     u32        speed_hz;//SPI传输速率

  16.     struct list_head transfer_list;
  17. };
    我们可以看到,当我们需要向SPI设备发送一段数据的时候,我们会构造一个spi_transfer结构,并将数据保存在其tx_buf指向的内存地址当中,当我们从SPI设备中获取到一段时间的时候,数据被保存在spi_transfer的rx_buf指向的空间中。然后会以DMA的方式进行数据搬运,然而我们知道,平台对于每次DMA传输的数据量是有限制的,在marvell平台上,最大的一次DMA传输数据量为8K,但我们通过SPI传输的数据量在很多时候远不止8K,这时候linux内核又为我们提供了另一个数据:spi_message
    

点击(此处)折叠或打开

  1. struct spi_message {
  2.     struct list_head    transfers;//挂载在spi_message上的spi_transfer

  3.     struct spi_device    *spi;//指向使用SPI总线通信的SPI外设

  4.     unsigned        is_dma_mapped:1;//保存数据的地址是否对于DMA可用

  5.     /* REVISIT: we might want a flag affecting the behavior of the
  6.      * last transfer ... allowing things like "read 16 bit length L"
  7.      * immediately followed by "read L bytes". Basically imposing
  8.      * a specific message scheduling algorithm.
  9.      *
  10.      * Some controller drivers (message-at-a-time queue processing)
  11.      * could provide that as their default scheduling algorithm. But
  12.      * others (with multi-message pipelines) could need a flag to
  13.      * tell them about such special cases.
  14.      */

  15.     /* completion is reported through a callback */
  16.     void            (*complete)(void *context);//用于通知系统传输完成 
  17.     void            *context;//complete的参数
  18.     unsigned        actual_length;
  19.     int            status;//成功传输完成置0,失败置负

  20.     /* for optional use by whatever driver currently owns the
  21.      * spi_message ... between calls to spi_async and then later
  22.      * complete(), that's the spi_master controller driver.
  23.      */
  24.     struct list_head    queue;
  25.     void            *state;
  26. };
    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通信的主要载体。



    


    
 

阅读(644) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~