1. 程式人生 > >linux基礎之spi驅動程式理解

linux基礎之spi驅動程式理解

1.1     重要的資料結構
1.  spi_device
雖然使用者空間不需要直接用到spi_device結構體,但是這個結構體和使用者空間的程式有密切的關係,理解它的成員有助於理解SPI裝置節點的IOCTL命令,所以首先來介紹它。
在核心中,每個spi_device代表一個物理的SPI裝置。它的成員如程式清單 1.1所示。
程式清單 1.1 spi_device

  1. struct spi_device {  
  2.          structdevice        dev;  
  3.          structspi_master *master;  
  4.          u32                      max_speed_hz;    /* 通訊時鐘最大頻率 */
  5.          u8                        chip_select;    /* 片選號 */
  6.          u8                        mode;           /*SPI裝置的模式,下面的巨集是它各bit的含義  */
  7. #define       SPI_CPHA         0x01                /* 取樣的時鐘相位                            */
  8. #define       SPI_CPOL          0x02               /* 時鐘訊號起始相位:高或者是低電平*/
  9. #define       SPI_MODE_0    (0|0)                    
  10. #define       SPI_MODE_1    (0|SPI_CPHA)
  11. #define       SPI_MODE_2    (SPI_CPOL|0)
  12. #define       SPI_MODE_3    (SPI_CPOL|SPI_CPHA)
  13. #define       SPI_CS_HIGH   0x04                    /* 為1時片選的有效訊號是高電平*/
  14. #define       SPI_LSB_FIRST         0x08            /* 傳送時低位元在前  */
  15. #define       SPI_3WIRE        0x10                 /* 輸入輸出訊號使用同一根訊號線 */
  16. #define       SPI_LOOP         0x20                 /* 迴環模式 */
  17.          u8                        bits_per_word;    /* 每個通訊字的字長(位元數) */
  18.          int                        irq;             /*使用到的中斷 */
  19.          void                     *controller_state;  
  20.          void                     *controller_data;  
  21.          char                     modalias[32];      /* 裝置驅動的名字*/
  22. };  




       由於一個SPI總線上可以有多個SPI裝置,因此需要片選號來區分它們,SPI控制器根據片選號來選擇不同的片選線,從而實現每次只同一個裝置通訊。
       spi_device的mode成員有兩個位元位含義很重要。SPI_CPHA選擇對資料線取樣的時機,0選擇每個時鐘週期的第一個沿跳變時取樣資料,1選擇第二個時鐘沿取樣資料;SPI_CPOL選擇每個時鐘週期開始的極性,0表示時鐘以低電平開始,1選擇高電平開始。這兩個位元有四種組合,對應SPI_MODE_0~SPI_MODE_3。
       另一個比較重要的成員是bits_per_word。這個成員指定每次讀寫的字長,單位是位元。雖然大部分SPI介面的字長是8或者16,仍然會有一些特殊的例子。需要說明的是,如果這個成員為零的話,預設使用8作為字長。
       最後一個成員並不是裝置的名字,而是需要繫結的驅動的名字。


2.   spi_ioc_transfer
       在使用者使用裝置節點的IOCTL命令傳輸資料的時候,需要用到 spi_ioc_transfer結構體,它的成員如程式清單 1.2所示。
程式清單 1.2  spi_ioc_transfer

  1. struct spi_ioc_transfer {  
  2.             __u64               tx_buf;                   /* 寫資料緩衝  */
  3.             __u64               rx_buf;                   /* 讀資料緩衝  */
  4.             __u32               len;                      /* 緩衝的長度 */
  5.             __u32               speed_hz;                 /* 通訊的時鐘頻率 */
  6.             __u16               delay_usecs;    /* 兩個spi_ioc_transfer之間的延時 */
  7.             __u8                 bits_per_word;           /* 字長(位元數)  */
  8.             __u8                 cs_change;               /* 是否改變片選 */
  9.             __u32               pad;                                
  10. };  




       每個 spi_ioc_transfer都可以包含讀和寫的請求,其中讀和寫的長度必須相等。所以成員len不是tx_buf和rx_buf緩衝的長度之和,而是它們各自的長度。SPI控制器驅動會先將tx_buf寫到SPI總線上,然後再讀取len長度的內容到rx_buf。如果只想進行一個方向的傳輸,把另一個方向的緩衝置為0就可以了。
speed_hz和bits_per_word這兩個成員可以為每次通訊配置不同的通訊速率(必須小於spi_device的max_speed_hz)和字長,如果它們為0的話就會使用spi_device中的配置。
delay_usecs可以指定兩個spi_ioc_transfer之間的延時,單位是微妙。一般不用定義。
cs_change指定這個cs_change結束之後是否需要改變片選線。一般針對同一裝置的連續的幾個spi_ioc_transfer,只有最後一個需要將這個成員置位。這樣省去了來回改變片選線的時間,有助於提高通訊速率。


1.2     獲得同SPI裝置通訊的裝置節點
為了在使用者空間獲得和SPI裝置直接通訊的裝置節點,必須有兩個條件要滿足:首先要有SPI控制器驅動,其次是要在核心初始化的時候註冊一個spi_board_info,它的modalias成員必須為“spidev”。有了這兩個條件,就可以和SPI裝置進行通訊了。控制器的驅動一般由晶片廠家提供,開發者只需提供第二個條件。
spi_board_info的定義如程式清單 1.3所示。
程式清單 1.3  struct spi_board_info

  1. struct spi_board_info {  
  2.          char            modalias[32];          /* 要繫結的驅動的名字 */
  3.          constvoid  *platform_data;                                          
  4.          void            *controller_data;  
  5.          int               irq;              
  6.          u32             max_speed_hz;          /* 通訊時鐘最大速率 */
  7.          u16             bus_num;              /* 匯流排編號  */
  8.          u16             chip_select;           /* 片選號 */
  9.          u8               mode;                 /* 和spi_device中的mode成員類似   */
  10. };  


       要了解這個結構體各個成員的意義請參考程式清單 1.1。
       定義並註冊structspi_board_info的位置一般是核心的arch/xxx/mach-xxxx/board-xxxx.c,比如3250的核心,這個檔案是arch/arm/mach-lpc32xx/board-smartarm3250.c。定義並註冊struct spi_board_info的程式碼如程式清單 1.4所示。
程式清單 1.4  定義並註冊spi_board_info
  1. staticint __init smartarm3250_spi_usp_register(void)  
  2. {  
  3.          structspi_board_info info =  
  4.          {  
  5.                    .modalias= "spidev",  
  6.                    .max_speed_hz= 5000000,  
  7.                    .bus_num= 0,  
  8.                    .chip_select= 0,  
  9.          };  
  10.          returnspi_register_board_info(&info, 1);  
  11. }  
  12. arch_initcall(smartarm3250_spi_usp_register);  



       由於3250核心程式碼在arch/arm/mach-lpc32xx/board-smartarm3250.c已經定義了一個smartarm3250_spi_eeprom_register函式,因此在增加程式清單 1.4程式碼前先將這個函式註釋掉。
程式清單 1.4註冊了一個掛在0號SPI總線上的裝置資訊,它的片選號為0。增加完這段程式碼後將核心重新編譯。在核心啟動的時候,會為這個裝置建立一個spi_device並和0號SPI匯流排的驅動進行繫結。同時核心會為這個裝置申請一個主裝置號為153的的裝置號,次裝置號和註冊的順序有關,最多支援32個同類裝置。
核心重新編譯並重啟之後,如果系統中運行了udev,/dev下就會生成一個spidevX.D裝置節點,其中X是匯流排編號,D是片選號。對於程式清單 1.4的程式碼應該自動生成的裝置節點是spidev0.0。
       一般SPI控制器驅動由晶片廠商提供,開發者所要在核心做的工作就是新增類似程式清單 1.4的內容。這樣核心空間的工作減少了,使用者空間的工作量加大了,因為使用者空間的開發者需要全面瞭解SPI裝置的工作方式和介面協議。


1.3     使用者空間同裝置節點的介面
       對於/dev/spidevX.D裝置節點,可以進行各種操作,這一小節介紹它支援的函式介面。
1.  open/close
       開啟和關閉裝置節點沒有特別之處,直接使用open/write就可以了。
2.  read/write
       讀寫SPI裝置可以直接使用read/write函式,但是每次讀或者寫的大小不能大於4096Byte。
3.  IOCTL命令
       使用者空間對spidev裝置節點使用IOCTL命令失敗會返回-1。
l        SPI_IOC_RD_MODE
讀取SPI裝置對應的spi_device.mode,mode的含義請參考程式清單 1.1。使用的方法如下:
                   ioctl(fd,SPI_IOC_RD_MODE, &mode);
              其中第三個引數是一個uint8_t型別的變數。
l        SPI_IOC_WR_MODE
設定SPI裝置對應的spi_device.mode。使用的方式如下:
                   ioctl(fd,SPI_IOC_WR_MODE, &mode);
l SPI_IOC_RD_LSB_FIRST
檢視裝置傳輸的時候是否先傳輸低位元位。如果是的話,返回1。使用的方式如下:
                   ioctl(fd,SPI_IOC_RD_LSB_FIRST, &lsb);
              其中lsb是一個uint8_t型別的變數。返回的結果存在lsb中。
l SPI_IOC_WR_LSB_FIRST
設定裝置傳輸的時候是否先傳輸低位元位。當傳入非零的時候,低位元在前,當傳入0的時候高位元在前(預設)。使用的方式如下:
                   ioctl(fd,SPI_IOC_WR_LSB_FIRST, &lsb);
l SPI_IOC_RD_BITS_PER_WORD
讀取SPI裝置的字長。使用的方式如下:
                   ioctl(fd,SPI_IOC_RD_BITS_PER_WORD, &bits);
              其中bits是一個uibt8_t型別的變數。返回的結果儲存在bits中。
l SPI_IOC_WR_BITS_PER_WORD
設定SPI通訊的字長。使用的方式如下:
                   ioctl(fd,SPI_IOC_WR_BITS_PER_WORD, &bits);
l SPI_IOC_RD_MAX_SPEED_HZ
讀取SPI裝置的通訊的最大時鐘頻率。使用的方式如下:
                   ioctl(fd,SPI_IOC_RD_MAX_SPEED_HZ, &speed);
              其中speed是一個uint32_t型別的變數。返回的結果儲存在speed中。
l SPI_IOC_WR_MAX_SPEED_HZ
設定SPI裝置的通訊的最大時鐘頻率。使用的方式如下:
                   ioctl(fd,SPI_IOC_WR_MAX_SPEED_HZ, &speed);
l SPI_IOC_MESSAGE(N)
一次進行雙向/多次讀寫操作。使用的方式如下:
                   structspi_ioc_transfer  xfer[2];
                   ......
                   status= ioctl(fd, SPI_IOC_MESSAGE(2), xfer);
其中N是本次通訊中xfer的陣列長度。spi_ioc_transfer的資訊請參考程式清單 1.2。


/************************************************************************************/
如果想要在使用者空間編寫spi驅動,這就要在核心的arch/.../mach-*/board-*.c 中宣告一個spi_board_info,
它的名字一定要是“spidev”,比如:


  1. struct spi_board_info info =  
  2. {  
  3.  .modalias = "spidev",  
  4.  .max_speed_hz = 5000000,  
  5.  .bus_num = 0,  
  6.  .chip_select = 0,  
  7. };  


 return spi_register_board_info(&info, 1);
這樣只要控制器驅動載入了,spidev模組就會和這個裝置繫結,併為裝置申請一個裝置號,主裝置號為153,次裝置號和裝置載入的次序有關。
目前spidev支援最多32個裝置。裝置的名字是spidevX.D,其中X是匯流排編號,D是裝置的片選號。如果正確安裝並配置了udev,/dev目錄下便會生成spidevX.D
裝置節點。直接對這些裝置節點操作就行了。


spidev的裝置節點的介面包括open/close/read/write/ioctl。
~~~~~~~~~~~~~~~~~~~~~~~~~
其中open/close沒有什麼特別之處。
read/write的話有大小的限制,讀寫的大小預設不能超過4096位元組。這個大小是一個模組載入引數,可以修改。
允許多個使用者同時開啟裝置節點,spidev使用mutext進行互斥,多個使用者同時讀寫時只有一個活動的使用者,其他使用者睡眠。


spidev的ioctl命令。
~~~~~~~~
SPI_IOC_RD_MODE:讀取spi_device的mode。
SPI_IOC_RD_LSB_FIRST:如果是SPI_LSB_FIRST的方式則返回1。
SPI_IOC_RD_BITS_PER_WORD:讀取spi_device的bits_per_word.
SPI_IOC_RD_MAX_SPEED_HZ:讀取spi_device的max_speed_hz.
SPI_IOC_WR_MODE:設定spi_device的mode,並呼叫spi_setup立即使設定生效。
SPI_IOC_WR_LSB_FIRST:設定spi使用SPI_LSB_FIRST的傳輸模式。立即生效。
SPI_IOC_WR_BITS_PER_WORD:讀取字長。
SPI_IOC_WR_MAX_SPEED_HZ:設定時鐘速率。
無論讀取,使用者傳輸的第三個引數都被當作緩衝地址指標。讀取時存放結果,寫入時存放要寫的內容。


SPI_IOC_MESSAGE:這個命令用來進行復雜的通訊。引數涉及到一個結構體。各個成員的意義與spi_transfer一致。

  1. struct spi_ioc_transfer {  
  2.  __u64  tx_buf;  
  3.  __u64  rx_buf;  
  4.  __u32  len;  
  5.  __u32  speed_hz;  
  6.  __u16  delay_usecs;  
  7.  __u8  bits_per_word;  
  8.  __u8  cs_change;  
  9.  __u32  pad;  
  10.  /* If the contents of 'struct spi_ioc_transfer' ever change 
  11.   * incompatibly, then the ioctl number (currently 0) must change; 
  12.   * ioctls with constant size fields get a bit more in the way of 
  13.   * error checking than ones (like this) where that field varies. 
  14.   * 
  15.   * NOTE: struct layout is the same in 64bit and 32bit userspace. 
  16.   */
  17. };  


核心文件中一個例子:

  1. staticvoid do_msg(int fd, int len)    
  2. {    
  3.  struct spi_ioc_transfer xfer[2];    
  4.  unsigned char  buf[32], *bp;    
  5.  int   status;    
  6.  memset(xfer, 0, sizeof xfer);    
  7.  memset(buf, 0, sizeof buf);    
  8.  if (len > sizeof buf)    
  9.   len = sizeof buf;    
  10.  buf[0] = 0xaa;    
  11.  xfer[0].tx_buf = (__u64) buf;    
  12.  xfer[0].len = 1;    
  13.  xfer[1].rx_buf = (__u64) buf;    
  14.  xfer[1].len = len;    
  15.  status = ioctl(fd, SPI_IOC_MESSAGE(2), xfer);    
  16.  if (status < 0) {    
  17.   perror("SPI_IOC_MESSAGE");    
  18.   return;    
  19.  }    
  20.  printf("response(%2d, %2d): ", len, status);    
  21.  for (bp = buf; len; len--)    
  22.   printf(" %02x", *bp++);    
  23.  printf("/n");    
  24. }    



核心在documentation/spi目錄下有spidev的例子。



注意
~~~~
雖然多個使用者不能同一時刻對spi進行設定或讀寫,但是同一使用者卻無法組織其他使用者修改同一裝置的設定。
舉例來說,usr1開啟裝置節點,然後使用ioctl設定了時鐘速率,此時usr1執行緒被排程出去,然後usr2操作同一個裝置,將它的時鐘設為另一個值。
此時usr1重新排程去使用read函式,則達不到預期的效果。
建議不要有兩個程式操作spidevX.D裝置節點。