SPI硬件基礎
1、SPI hardware
SPI:Serial Perripheral Interface,串行外圍設備接口,由 Motorola 公司提出,是一種高速、全雙工、同步通信總線。SPI 以主從方式工作,通常是有一個主設備和一個或多個從設備,無應答機制。
本文我們講解標準的 4 線 SPI,四根線如下:
①、CS/SS,Slave Select/Chip Select,片選信號線,用于選擇需要進行通信的從設備。
②、SCK,Serial Clock,串行時鐘,和 I2C 的 SCL 一樣,為 SPI 通信提供時鐘。
③、MOSI/SDO,Master Out Slave In/Serial Data Output,主輸出從輸入。
④、MISO/SDI,Master In Slave Out/Serial Data Input,主輸入從輸出。

2、SPI 四種工作模式
SPI 有四種工作模式,通過時鐘極性(CPOL)和時鐘相位(CPHA)的搭配來得到四種工作模式:
①、CPOL=0,串行時鐘空閑狀態為低電平。
②、CPOL=1,串行時鐘空閑狀態為高電平。
③、CPHA=0,串行時鐘的第一個跳變沿(上升沿或下降沿)采集數據。
④、CPHA=1,串行時鐘的第二個跳變沿(上升沿或下降沿)采集數據。

示例波形圖如下:

SPI 是全雙工的,所以讀寫時序可以一起完成。
3、SPI 傳輸機制

從圖可以看出,主機和從機都有一個串行移位寄存器,主機通過向它的 SPI 串行寄存器寫入一個字節來發起一次傳輸。寄存器通過 MOSI 信號線將字節傳送給從機,從機也將自己的移位寄存器中的內容通過 MISO 信號線返回給主機。這樣,兩個移位寄存器中的內容就被交換。
外設的寫操作和讀操作是同步完成的。如果只進行寫操作,主機只需忽略接收到的字節;反之,若主機要讀取從機的一個字節,就必須發送一個空字節來引發從機的傳輸。



雖然 SPI 四線制支持讀寫同時進行,但實際上我們很多時候并不需要又讀又寫,見以下兩種情況(參考 BMA223 數據手冊):
注意:如下三幅圖示均為 CPOL=1,CPHA=1
1、主機向從機寫數據

主機發送先發送 8 bits,第一個 bit 為 0 代表這次主機是想寫數據到從機,AD6~AD0 表示要寫的寄存器地址。然后,主機就會一直寫下去。在這期間 SDO 一直沒用,一直是高阻態,算是一直讀到1。
2、主機從從機讀數據

這種情況下,主機先發送 8 bits,第一位為 1 代表這次是讀,然后 AD6 ~ AD0 是想要讀的寄存器地址,然后 SDO 開始返回數據。
4、SPI timing diagram

Tcsb_setup:建立時間
Tcsb_hold:保持時間
tsckl:低電平時間
tsckh:高電平時間
SCK period :Tsckl + tsckh
一般情況下Tsckl=tsckh
注意:真實的波形圖如上,高低電平并不是到達最高點才算,0.3Vdd 以下為低電平,0.7Vdd 以上為高電平,計算信號時間長度的時候需要注意這個微小的時間,硬件設計必須注意信號質量風險,軟件開發人員也要會看波形圖。
這里的參數,一般 spi 驅動不需要設置,但是半導體廠商提供的 spi 控制器驅動中,可以修改這些參數。我們寫 SPI 驅動時候,可以根據從設備的要求來修改這些參數。
5、DMA 與 FIFO
不同平臺對于 SPI FIFO 和 DMA 的 buffer size 設置不同:

傳輸 32bytes 以下使用 FIFO,傳輸 32bytes 以上使用 DMA。
DMA 可以自動發起多次傳輸,一次最大 256K 。
6、I2C 與 SPI 對比
功能 | I2C | SPI |
---|---|---|
線數 | 2(SDA,SCL) | 4(MOSI,MISO,SCLK,CS) |
主機數量 | >=1 | ==1 |
類型 | 半雙工 | 全雙工 |
回應機制 | yes | no |
速度 | <=3.4Mbps | high |
應用 | 重要數據 | 大量數據 |
流控 | yes | no |
設備地址 | yes | no |
常規用途 | 命令 | 數據 |
I2C 和 SPI 的速率如下:
I2C模式 | 速度 |
---|---|
標準 | 100KHz |
快速 | 400KHz |
快速+ | 1MHz |
高速 | 3.4MHz |
SPI 速率:幾十 MHz 甚至上百 MHz,速度取決于 CPU 的 SPI 控制器和時鐘 clock
STM32F103 的 SPI 最高支持 18MHz,imx6ull 的 SPI 最高支持 52MHz,其他芯片一般用不到更高的,因為速度越快波形質量越不好,越容易出問題。
具體采用多大速率還和外設有關,比如 EEPROM 的 W25Q128 的 SPI 最高支持 80MHz,ICM20608 傳感器的 SPI 最高支持8MHz。一般用在 flash 上的速度會較快。
7、擴展
SPI 協議其實是包括:Standard SPI、Dual SPI 和 Queued SPI 三種協議接口。
Dual SPI 還是四線制,只是傳輸線可以變為同方向,速度是 Standard SPI 的兩倍。
Queued SPI 是六線制,多了兩根數據線,傳輸速度是 Standard SPI 的四倍。
SPILinux驅動
1、SPI 驅動源文件目錄
Linux common spi driver
kernel-4.14/drivers/spi/spi.cLinux提供的通用接口封裝層驅動
kernel-4.14/drivers/spi/spidev.clinux提供的SPI通用設備驅動程序
kernel-4.14/include/linux/spi/spi.hlinux提供的包含SPI的主要數據結構和函數
spi 控制器驅動,IC 廠商提供,不同廠商命名不同
kernel-4.14/drivers/spi/spi-mt65xx.cMTKSPI控制器驅動
kernel-4.14/drivers/spi/spi-mt65xx-dev.c
kernel-4.14/include/linux/platform_data/spi-mt65xx.h
dts
kernel-4.14/arch/arm/boot/dts/...
kernel-4.14/arch/arm64/boot/dts/...
以上文件對應如下 SPI 驅動軟件架構:

SPI 控制器驅動程序
SPI 控制器不用關心設備的具體功能,它只負責把上層協議驅動準備好的數據按 SPI 總線的時序要求發送給 SPI 設備,同時把從設備收到的數據返回給上層的協議驅動,因此,內核把 SPI 控制器的驅動程序獨立出來。
SPI 控制器驅動負責控制具體的控制器硬件,諸如 DMA 和中斷操作等等,因為多個上層的協議驅動可能會通過控制器請求數據傳輸操作,所以,SPI 控制器驅動同時也要負責對這些請求進行隊列管理,保證先進先出的原則。
SPI 通用接口封裝層
為了簡化 SPI 驅動程序的編程工作,同時也為了降低【協議驅動程序】和【控制器驅動程序】的耦合程度,內核把控制器驅動和協議驅動的一些通用操作封裝成標準的接口,加上一些通用的邏輯處理操作,組成了 SPI 通用接口封裝層。
這樣的好處是,對于控制器驅動程序,只要實現標準的接口回調 API,并把它注冊到通用接口層即可,無需直接和協議層驅動程序進行交互。而對于協議層驅動來說,只需通過通用接口層提供的 API 即可完成設備和驅動的注冊,并通過通用接口層的 API 完成數據的傳輸,無需關注 SPI 控制器驅動的實現細節。
SPI 協議驅動程序
SPI 設備的具體功能是由 SPI 協議驅動程序完成的,SPI 協議驅動程序了解設備的功能和通信數據的協議格式。向下,協議驅動通過通用接口層和控制器交換數據,向上,協議驅動通常會根據設備具體的功能和內核的其它子系統進行交互。
例如,和 MTD 層交互以便把 SPI 接口的存儲設備實現為某個文件系統,和 TTY 子系統交互把 SPI 設備實現為一個 TTY 設備,和網絡子系統交互以便把一個 SPI 設備實現為一個網絡設備。如果是一個專有的 SPI 設備,我們也可以按設備的協議要求,實現自己的專有協議驅動。
SPI 通用設備驅動程序
考慮到連接在 SPI 控制器上的設備的可變性,在內核沒有配備相應的協議驅動程序,對于這種情況,內核為我們準備了通用的 SPI 設備驅動程序,該通用設備驅動程序向用戶空間提供了控制 SPI 控制的控制接口,具體的協議控制和數據傳輸工作交由用戶空間根據具體的設備來完成,在這種方式中,只能采用同步的方式和 SPI 設備進行通信,所以通常用于一些數據量較少的簡單 SPI 設備。
2、SPI 通用接口層
- SPI 通用接口層把具體的 SPI 設備的協議驅動和 SPI 控制器驅動連接在一起。
- 負責 SPI 系統與 Linux 設備模型相關的初始化工作。
- 為協議驅動和控制器驅動提供一系列的標準接口 API 及其數據結構。
- SPI 設備、SPI 協議驅動、SPI 控制器的數據抽象
- 協助數據傳輸而定義的數據結構
kernel-4.14/drivers/spi/spi.c
staticint__initspi_init(void)
{
intstatus;
buf=kmalloc(SPI_BUFSIZ,GFP_KERNEL);
if(!buf){
status=-ENOMEM;
gotoerr0;
}
//創建/sys/bus/spi節點
status=bus_register(&spi_bus_type);
if(status0)
gotoerr1;
//創建/sys/class/spi_master節點
status=class_register(&spi_master_class);
if(status0)
gotoerr2;
if(IS_ENABLED(CONFIG_SPI_SLAVE)){
status=class_register(&spi_slave_class);
if(status0)
gotoerr3;
}
......
}
在這里創建了 SPI 總線,創建 /sys/bus/spi 節點和 /sys/class/spi_master 節點。
重要數據結構:
spi_device
spi_driver
spi_board_info
spi_controller/spi_master
spi_transfer
spi_message
重要 API
spi_message_init
spi_message_add_tail
spi_sync
spi_async
spi_write
spi_read
接下來詳細解析結構體和API,只講解重點部分,完整解析請參考官方文檔
https://www.kernel.org/doc/html/v4.14//driver-api/spi.html
只有熟悉每個結構體存儲的是什么東西,才能真正搞懂 SPI 模塊。
spi_master/spi_controller:描述一個 spi 主機設備
structspi_master{
//Linux驅動模型中的設備
structdevicedev;
//此spi_master設備在全局spi_master鏈表中的節點
structlist_headlist;
//此spi_master編號
s16bus_num;
//此spi_master支持的片選信號數量
u16num_chipselect;
//dma地址對齊
u16dma_alignment;
//此spi_master支持傳輸的mode
u16mode_bits;
u32bits_per_word_mask;
/*limitsontransferspeed*/
u32min_speed_hz;
u32max_speed_hz;
/*otherconstraintsrelevanttothisdriver*/
u16flags;
/*lockandmutexforSPIbuslocking*/
spinlock_tbus_lock_spinlock;//總線自旋鎖
structmutexbus_lock_mutex;//總線互斥鎖
//總線是否處于lock狀態
boolbus_lock_flag;
//準備傳輸,設置傳輸的參數
int(*setup)(structspi_device*spi);
//傳輸數據
int(*transfer)(structspi_device*spi,
structspi_message*mesg);
//設備release時的清除工作
void(*cleanup)(structspi_device*spi);
bool(*can_dma)(structspi_master*master,
structspi_device*spi,
structspi_transfer*xfer);
boolqueued;//是否采用系統的序列化傳輸
structkthread_workerkworker;//序列化傳輸時的線程worker
structtask_struct*kworker_task;//序列化傳輸的線程
structkthread_workpump_messages;//序列化傳輸時的處理函數
spinlock_tqueue_lock;//序列化傳輸時的queue_lock
structlist_headqueue;//序列化傳輸時的msg隊列頭
structspi_message*cur_msg;//序列化傳輸時當前的msg
boolidling;
boolbusy;//序列化傳輸時線程是否處于busy狀態
boolrunning;//序列化傳輸時線程是否在運行
boolrt;//是否實時傳輸
......
int(*prepare_transfer_hardware)(structspi_master*master);
//一個msg的傳輸實現
int(*transfer_one_message)(structspi_master*master,
structspi_message*mesg);
......
/*gpiochipselect*/
int*cs_gpios;
......
};
spi_device:描述一個 spi 從機設備
structspi_device{
//Linux驅動模型中的設備
structdevicedev;
structspi_master*master;//設備所連接的spi主機設備
u32max_speed_hz;//該設備最大傳輸速率
u8chip_select;//CS片選信號編號
u8bits_per_word;//每次傳輸長度
u16mode;//傳輸模式
......
intirq;//軟件中斷號
void*controller_state;//控制器狀態
void*controller_data;//控制參數
charmodalias[SPI_NAME_SIZE];//設備名稱
//CS片選信號對應的GPIOnumber
intcs_gpio;/*chipselectgpio*/
/*thestatistics*/
structspi_statisticsstatistics;
};
spi_driver:描述一個 spi 設備驅動
structspi_driver{
//此driver所支持的spi設備list
conststructspi_device_id*id_table;
int(*probe)(structspi_device*spi);
int(*remove)(structspi_device*spi);
//系統shutdown時的回調函數
void(*shutdown)(structspi_device*spi);
structdevice_driverdriver;
};
spi_board_info:描述一個 spi 從機設備板級信息,無設備樹時使用
structspi_board_info{
//設備名稱
charmodalias[SPI_NAME_SIZE];
constvoid*platform_data;//設備的平臺數據
void*controller_data;//設備的控制器數據
intirq;//設備的中斷號
u32max_speed_hz;//設備支持的最大速率
u16bus_num;//設備連接的spi總線編號
u16chip_select;//設備連接的CS信號編號
u16mode;//設備使用的傳輸mode
};
spi_transfer:描述 spi 傳輸的具體數據
structspi_transfer{
constvoid*tx_buf;//spi_transfer的發送buf
void*rx_buf;//spi_transfer的接收buf
unsignedlen;//spi_transfer發送和接收的長度
dma_addr_ttx_dma;//tx_buf對應的dma地址
dma_addr_trx_dma;//rx_buf對應的dma地址
structsg_tabletx_sg;
structsg_tablerx_sg;
//spi_transfer傳輸完成后是否要改變CS片選信號
unsignedcs_change:1;
unsignedtx_nbits:3;
unsignedrx_nbits:3;
......
u8bits_per_word;//spi_transfer中一個word占的bits
u16delay_usecs;//兩個spi_transfer直接的等待延遲
u32speed_hz;//spi_transfer的傳輸速率
structlist_headtransfer_list;//spi_transfer掛載到的message節點
};
spi_message:描述一次 spi 傳輸的信息
structspi_message{
//掛載在此msg上的transfer鏈表頭
structlist_headtransfers;
//此msg需要通信的spi從機設備
structspi_device*spi;
//所使用的地址是否是dma地址
unsignedis_dma_mapped:1;
//msg發送完成后的處理函數
void(*complete)(void*context);
void*context;//complete函數的參數
unsignedframe_length;
unsignedactual_length;//此msg實際成功發送的字節數
intstatus;//此 msg 的發送狀態,0:成功,負數,失敗
structlist_headqueue;//此msg在所有msg中的鏈表節點
void*state;//此msg的私有數據
};
隊列化
SPI 數據傳輸可以有兩種方式:同步方式和異步方式。
同步方式:數據傳輸的發起者必須等待本次傳輸的結束,期間不能做其它事情,用代碼來解釋就是,調用傳輸的函數后,直到數據傳輸完成,函數才會返回。
異步方式:數據傳輸的發起者無需等待傳輸的結束,數據傳輸期間還可以做其它事情,用代碼來解釋就是,調用傳輸的函數后,函數會立刻返回而不用等待數據傳輸完成,我們只需設置一個回調函數,傳輸完成后,該回調函數會被調用以通知發起者數據傳送已經完成。
同步方式簡單易用,很適合處理那些少量數據的單次傳輸。但是對于數據量大、次數多的傳輸來說,異步方式就顯得更加合適。
對于 SPI 控制器來說,要支持異步方式必須要考慮以下兩種狀況:
- 對于同一個數據傳輸的發起者,既然異步方式無需等待數據傳輸完成即可返回,返回后,該發起者可以立刻又發起一個 message,而這時上一個message還沒有處理完。
- 對于另外一個不同的發起者來說,也有可能同時發起一次message傳輸請求。
隊列化正是為了為了解決以上的問題,所謂隊列化,是指把等待傳輸的 message 放入一個等待隊列中,發起一個傳輸操作,其實就是把對應的 message 按先后順序放入一個等待隊列中,系統會在不斷檢測隊列中是否有等待傳輸的 message,如果有就不停地調度數據傳輸內核線程,逐個取出隊列中的 message 進行處理,直到隊列變空為止。SPI 通用接口層為我們實現了隊列化的基本框架。

spi_message 就是一次 SPI 數據交換的原子操作,不可打斷。
3、SPI 控制器驅動層
SPI 控制器驅動層負責最底層的數據收發,主要有以下功能:
- 申請必要的硬件資源,比如中斷、DMA 通道、DMA 內存緩沖區等等
- 配置 SPI 控制器的工作模式和參數,使之可以和相應的設備進行正確的數據交換
- 向通用接口層提供接口,使得上層的協議驅動可以通過通用接口層訪問控制器驅動
- 配合通用接口層,完成數據消息隊列的排隊和處理,直到消息隊列變空為止
SPI 主機驅動就是 SOC 的 SPI 控制器驅動。Linux 內核使用 spi_master/spi_controller 表示 SPI 主機驅動,spi_master 是個結構體,定義在 include/linux/spi/spi.h 文件中。
SPI 主機驅動的核心就是申請 spi_master,然后初始化 spi_master,最后向 Linux 內核注冊 spi_master。
API 如下:
spi_alloc_master 函數:申請 spi_master。
spi_master_put 函數:釋放 spi_master。
spi_register_master函數:注冊 spi_master。
spi_unregister_master 函數:注銷 spi_master。
spi_bitbang_start函數:注冊 spi_master。
spi_bitbang_stop 函數:注銷 spi_master。
SPI 主機驅動的加載
以 MTK 為例,源碼來自于小米開源項目
https://github.com/MiCode/Xiaomi_Kernel_OpenSource
小米每做一個項目,都會把 kernel 部分開源,因為需要遵循 Linux GPL 開源協議。
【設備】聲明在設備樹中
kernel-4.14/arch/arm64/boot/dts/mediatek/mt6885.dts

【驅動】
kernel-4.14/drivers/spi/spi-mt65xx.c


匹配以后,probe 函數執行,申請 spi_master,初始化 spi_master,最后向 Linux 內核注冊 spi_master。


4、軟件流程

看懂該圖,對 SPI 驅動框架就有完整的了解了。
1、2、3 按順執行,首先有 spi 總線的注冊,然后是 spi 控制器驅動加載,然后是設備驅動加載。
區別在于,spi 控制器驅動加載時,是靠 platform 總線匹配設備(控制器)與驅動。spi 設備驅動加載時,是靠 spi 總線匹配設備(外設IC)與驅動。
init flow

spi_register_master 的調用序列圖

隊列化的工作機制及過程


當協議驅動程序通過 spi_async 發起一個 message 請求時,隊列化和工作線程被激活,觸發一些列的操作,最終完成 message 的傳輸操作。
spi_sync 與 spi_async 類似,只是有一個等待過程。
5、SPI 設備驅動
【設備】聲明在設備樹中


注意:設備的聲明,slave device node 應該包含在你所要掛載的 &spi node 下,將 device 綁定在 master 上。然后通過 pinctrl 方式指定 GPIO,并在驅動中操作 pinctrl 句柄。
【驅動】demo
Linux 內核使用 spi_driver 結構體來表示 spi 設備驅動,我們在編寫 SPI 設備驅動的時候需要實現 spi_driver。spi_driver 結構體定義在 include/linux/spi/spi.h 文件中。
spi_register_driver:注冊 spi_driver
spi_unregister_driver:銷掉 spi_driver
/*probe函數*/
staticintxxx_probe(structspi_device*spi)
{
/*具體函數內容*/
return0;
}
/*remove函數*/
staticintxxx_remove(structspi_device*spi)
{
/*具體函數內容*/
return0;
}
/*傳統匹配方式ID列表*/
staticconststructspi_device_idxxx_id[]={
{"xxx",0},
{}
};
/*設備樹匹配列表*/
staticconststructof_device_idxxx_of_match[]={
{.compatible="xxx"},
{/*Sentinel*/}
};
/*SPI驅動結構體*/
staticstructspi_driverxxx_driver={
.probe=xxx_probe,
.remove=xxx_remove,
.driver={
.owner=THIS_MODULE,
.name="xxx",
.of_match_table=xxx_of_match,
},
.id_table=xxx_id,
};
/*驅動入口函數*/
staticint__initxxx_init(void)
{
returnspi_register_driver(&xxx_driver);
}
/*驅動出口函數*/
staticvoid__exitxxx_exit(void)
{
spi_unregister_driver(&xxx_driver);
}
module_init(xxx_init);
module_exit(xxx_exit);
在驅動入口函數中調用 spi_register_driver 來注冊 spi_driver。
在驅動出口函數中調用 spi_unregister_driver 來注銷 spi_driver。
spi 讀寫數據demo
/*SPI多字節發送*/
staticintspi_send(structspi_device*spi,u8*buf,intlen)
{
intret;
structspi_messagem;
structspi_transfert={
.tx_buf=buf,
.len=len,
};
spi_message_init(&m);/*初始化spi_message*/
spi_message_add_tail(t,&m);/*將spi_transfer添加到spi_message隊列*/
ret=spi_sync(spi,&m);/*同步傳輸*/
returnret;
}
/*SPI多字節接收*/
staticintspi_receive(structspi_device*spi,u8*buf,intlen)
{
intret;
structspi_messagem;
structspi_transfert={
.rx_buf=buf,
.len=len,
};
spi_message_init(&m);/*初始化spi_message*/
spi_message_add_tail(t,&m);/*將spi_transfer添加到spi_message隊列*/
ret=spi_sync(spi,&m);/*同步傳輸*/
returnret;
}
除了 init、exit、probe、remove、read、write 函數外,其他的函數看需求實現,這幾個是最基本的。
6、總結
Linux 是 總線、設備、驅動 的框架,理解了這個框架,就能理解所有的模塊驅動框架。
SPI 驅動比 I2C 驅動還是簡單很多的。
end
原文標題:SPI 硬件+Linux驅動詳解
文章出處:【微信公眾號:一口Linux】歡迎添加關注!文章轉載請注明出處。
-
Linux
+關注
關注
87文章
11457瀏覽量
212763 -
SPI硬件
+關注
關注
0文章
2瀏覽量
873 -
傳輸機制
+關注
關注
0文章
3瀏覽量
1213
原文標題:SPI 硬件+Linux驅動詳解
文章出處:【微信號:yikoulinux,微信公眾號:一口Linux】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
科普一下CAN總線的基礎知識

示波器基礎知識
五分鐘讀懂WiFi基礎知識
SPI通信協議的基礎知識解析
記錄一下SPI基礎知識與軟件開發環境
【科普】卷積神經網絡基礎知識

評論