1. 什么是SPI?
Serial Peripheral Interface是一種同步4線串口鏈路,用於連接傳感器、內存和外設到微控制器.他是一種簡單的事實標准,還不足以復雜到需要一份正式的規范.SPI使用主/從配置模式.
有3根控制數據傳輸,其中包含並行數據線:MOSI(Masterout Slave in)和MISO(Masterin Slave out). 有四種時鍾模式用於數據交換:mode-0和mode-3是經常使用的模式.當沒有數據要傳輸時,SCK線會處於空閑狀態(低或高).
大多時候系統中一根SPI總線上捆着很多的從設備,SPI控制器靠片選信號線來激活某個從設備.所有的SPI從設備都必須支持片選信號,有的還會有額外的中斷信號線連接至主控制器.
不像USB、SMBus這類串行總線,對於SPI(從)設備功能來說,即使是低級別的協議,各個廠家間的SPI設備可能並不能協同工作.
- SPI可能被用在請求/相應式設備協議,比如觸摸屏還有內存芯片;
- 可半雙工/全雙工傳輸數據;
- 不同設備可能使用的字長不一樣,有些是8bit字長,像數字采樣設備就可能是12或者20-bit字長流;
- 大多時候總是先發送字的MSB位,也可能是LSB位;
- 有時候SPI可用在鏈式設備中:移位寄存器
類似的,SPI也極少支持自舉的協議類型.SPI從設備樹只能靠配置表來手動配置.
有些個SPI設備只用3根線:SCK,data,nCSx,其中data也叫做MOMI或SISO,就是MOSI和MISO合並以實現半雙工,每次要么讀要么寫.
微控制器大部分支持主控制器和從設備的.這里內核只支持主控制器部分了.
2. 誰會用到它?又是在哪些系統中呢?
linux開發者使用SPI來為嵌入式系統編寫設備驅動.SPI可以用來控制外部芯片,而且眾多的MMC、SD卡都支持SPI協議.一些老式的PC機還使用SPIflash或者BIOS代碼.
SPI從芯片包含很廣的范圍像數模轉換,內存還有並行的USB控制器,甚至以太網適配器,數不勝數.
大多數系統使用的SPI都是一集成了多個設備的主板.一些提供像擴展接頭的SPI鏈路,可以使用GPIO來創建低速的"bitbanging"適配器.很少會熱插撥一個SPI控制器的,使用他就是處於低成本和簡潔操作,如果側重動態重新配置,那么USB會是個更好的選擇.
許多可以運行linux的微控制器都已經集成了一個或多個SPI模式的I/O接口.打開SPI支持功能就無需特別的MMC/SD/SDIO控制器就可以使用MMC和SD卡了.
3. SPI的幾個clockmode到底是什么了?CPOL和CPHA又是什么了?他們如何區分呢?
這很容易混淆,而且在供應商的文檔里你會發現找不到有幫助的東西,這四種模式結合了兩個模式位來標志.
- CPOL表示初始時鍾極性,CPOL=0表示時鍾開始值是低電平,所以第一階段(前沿)的時候會處在上升沿,第二階段(后沿)是下降沿.CPOL=1表示時鍾開始是高電平,所以第一階段(前沿)就是下降沿;
- CPHA表示時鍾相位用於采樣數據.CPHA=0說明在(前沿)期間進行采樣.CPHA=1說明在后沿進行采樣;
由於在進行采樣之前,信號必須是處於穩定狀態,CPHA=0意味着它的數據會在第一個時鍾周期的邊沿(前沿)之前的半個時鍾被寫入.片選使之成為了可能.
芯片手冊並不總是會說"uses SPI mode X"在盡可能多的單詞中.但是她們的時序圖將使CPOL和CPHA模式更加清晰.
在SPI模式編碼中,CPOL是在高位而CPHA在地位.所以當一個時序圖現實時鍾起點是低電平(CPOL=0),並且數據穩定在采樣期間為時鍾后沿,則就是SPI模式1.
請注意時鍾模式是和片選相關的,它剛一設置好片選就被激活了.所以主機master在選擇一個從設備之前必須將時鍾設置為無效,然后從設備在它的片選線被激活的時候通過采樣時鍾水平就會告訴時鍾極性選擇.這就是為什么許多模式支持例如模式0和模式3:他們不在乎時鍾極性,也不在乎在時鍾上升沿的時鍾數據的輸入/輸出.
關於時鍾極性和相位,可以參看這篇博文:http://www.cnblogs.com/jason-lu/articles/3713319.html
4. 這些驅動程序接口是如何工作的?
在<linux/spi/spi.h>頭文件中包含有內核文檔,做為主要的源碼,你應該詳讀內核API文檔的相關章節.本文只是概覽,在了解細節前有個大致的圖景是好的.
SPI請求會進入到I/O隊列中.請求給定的SPI設備也是按照FIFO順序進行的,通過完成機制異步通知.也同簡單的同步措施:先寫在讀出來.
有倆類SPI驅動:
- 控制器驅動(Controller drivers)...集成在SOC中的控制器,經常扮演Master和Slave雙角色.這類驅動直接接觸到硬件層的寄存器甚至使用DMA.亦或者扮演bitbanger,僅需要GPIO腳;
- 協議驅動(Protocoldrivers)...在控制器和slave或者控制器和另外一條SPI鏈路上的Master傳遞消息.協議驅動是將控制器讀到的數據,比如是一堆0,1代碼,解析成有意義的協議數據;
對於協議驅動應該是我們要寫的,spi在linux內核中有spi子系統分為spi核心層,就類似USBcore一樣是主控制器部分,另一個就是spi設備層了.前者內核幫咱寫好了,為了讓你的spi設備能工作,就得借助spicontroller driver導出的一些設施來編寫protocoldrivers了.
struct spi_device結構封裝了倆類驅動間的master-side接口.
有一個最小化SPI編程接口的core,專注於使用板級初始化代碼提供的設備表並借助於驅動模型來連接controller和protocol驅動.在sysfs文件系統中,SPI視圖:
1 /sys/devices/.../CTLR ... physical node for a given SPI controller 2 3 /sys/devices/.../CTLR/spiB.C ... spi_device on bus "B", 4 chipselect C, accessed through CTLR. 5 6 /sys/bus/spi/devices/spiB.C ... symlink to that physical 7 .../CTLR/spiB.C device 8 9 /sys/devices/.../CTLR/spiB.C/modalias ... identifies the driver 10 that should be used with this device (for hotplug/coldplug) 11 12 /sys/bus/spi/drivers/D ... driver for one or more spi*.* devices 13 14 /sys/class/spi_master/spiB ... symlink (or actual device node) to 15 a logical node which could hold class related state for the 16 controller managing bus "B". All spiB.* devices share one 17 physical SPI bus segment, with SCLK, MOSI, and MISO.
需要注意的是控制器類狀態的實際位置取決於您是否開啟CONFIG_SYSFS_DEPRECATED標志.此時,唯一的特定類狀態是總線編號("B" in "spiB"),所以/sys/class下的那些入口項是唯一的識別總線的標志.
5. 板級初始化代碼中是如何聲明SPI設備的?
linux需要多種信息來配置SPI設備.這些信息就是由板級初始代碼提供的.
5.1 聲明controllers
第一類信息:是一個類表,表明存在那種的SPI控制器.對於SOC來說,就是平台設備platformdevices,這就需要一些platform_data來進行正確的操作.structplatform_device就包含設備的第一個寄存器的起始物理地址和IRQ號.
platform也將抽象注冊spi控制器這一操作,也可以將這段通用代碼共享到使用同一種控制器的多個板子中去.
舉個例子在arch/../mach-*/board-*.c中,可能會看到如下的代碼:
1 #include <mach/spi.h> /* for mysoc_spi_data */ 2 3 /* if your mach-* infrastructure doesn't support kernels that can 4 * run on multiple boards, pdata wouldn't benefit from "__init". 5 */ 6 static struct mysoc_spi_data __initdata pdata = { ... }; 7 8 static __init board_init(void) 9 { 10 ... 11 /* this board only uses SPI controller #2 */ 12 mysoc_register_spi(2, &pdata); 13 ... 14 }
而特定的SOC平台的實用代碼可能會像如下的樣子:
1 #include <mach/spi.h>
2 static struct platform_device spi2 = { ... }; 3 void mysoc_register_spi(unsigned n, struct mysoc_spi_data *pdata) 4 { 5 struct mysoc_spi_data *pdata2; 6 7 pdata2 = kmalloc(sizeof *pdata2, GFP_KERNEL); 8 *pdata2 = pdata; 9 ... 10 if (n == 2) { 11 spi2->dev.platform_data = pdata2; 12 register_platform_device(&spi2); 13 14 /* also: set up pin modes so the spi2 signals are 15 * visible on the relevant pins ... bootloaders on 16 * production boards may already have done this, but 17 * developer boards will often need Linux to do it. 18 */ 19 } 20 ... 21 }
platform_data包含哪些內容具體看如何使用他們了,內置時鍾還是外部時鍾都有所區別.
5.2 聲明Slave設備
第二類信息也是個列表:目標板上有哪種SPIslave設備,提供板級數據使設備能夠正常工作.
arch/.../mach-*/board-*.c文件中有個小列表:列出了板載的SPI設備有哪些.這通常只有極少數.這個代碼可能像如下這樣:
1 static struct ads7846_platform_data ads_info = { 2 .vref_delay_usecs = 100, 3 .x_plate_ohms = 580, 4 .y_plate_ohms = 410, 5 }; 6 7 static struct spi_board_info spi_board_info[] __initdata = { 8 { 9 .modalias = "ads7846", 10 .platform_data = &ads_info, 11 .mode = SPI_MODE_0, 12 .irq = GPIO_IRQ(31), 13 .max_speed_hz = 120000 /* max sample rate at 3V */ * 16, 14 .bus_num = 1, 15 .chip_select = 0, 16 },
同樣的,不論板載信息是什么,每個芯片可能需要多種類型.上面告訴我們這個設備的最大SPI時鍾頻率是120K,irq線是怎么連接的等等.
board_info中應該提供足夠的信息使芯片驅動未加載前系統可以正常工作.最為麻煩的方面就是spi_device.mode中SPI_CS_HIGH位域了,在知道如何取消選擇之前使和一個設備共享總線是不可能的.
你的板級初始代碼會使用SPI設施來注冊哪個table,當SPImastercontroller驅動注冊完成后就可以使用了:
1 spi_register_board_info(spi_board_info,ARRAY_SIZE(spi_board_info));
正如其他的板級設置,你無需注銷他們.
這被廣發應用於卡式計算機中,如內存條,CPU,還有一些其他的板卡上小的東西也許僅僅只有30平方厘米.在這樣的系統中,你的arch/.../mach-.../board-*.c文件將主要提供一些信息,關於插在這個主板上的設備的信息.這些信息當然包括能夠通過卡槽連接的SPI設備了.
5.3 非靜態設備
開發板相較於最終的產品扮演者不同的角色,比如潛在的熱插撥SPI設備或控制器的需求.
在這種情形下你得使用spi_busnum_to_master()來查看busmaster,也可能使用spi_new_device()為熱插撥板卡提供board_info信息.最后,你得手動的注銷你注冊過得資源:spi_unregister_device().
當linux通過SPI提供對MMC/SD/SDIO/DataFlash的支持時,所需的配置就是動態的了.幸運的是,這類設備都支持基礎的設備標識符探測,因而他們是可以熱插撥的.
6. 如何編寫一個SPI Protocol Driver?
現在的大部分的SPI驅動都是內核空間的,也有支持用戶空間的驅動.這里只涉及內核空間的驅動.
SPI Protocol 驅動有點像整合的platform驅動:
1 static struct spi_driver CHIP_driver = { 2 .driver = { 3 .name = "CHIP", 4 .owner = THIS_MODULE, 5 }, 6 7 .probe = CHIP_probe, 8 .remove = __devexit_p(CHIP_remove), 9 .suspend = CHIP_suspend, 10 .resume = CHIP_resume, 11 }; 12 13 };
驅動核心會自動嘗試將這個驅動綁定到board_info中modalias域為CHIP的設備上.你的probe應該是這個樣子的,除非你打算編寫管理總線的代碼:
1 static int __devinit CHIP_probe(struct spi_device *spi) 2 { 3 struct CHIP *chip; 4 struct CHIP_platform_data *pdata; 5 6 /* assuming the driver requires board-specific data: */ 7 pdata = &spi->dev.platform_data; 8 if (!pdata) 9 return -ENODEV; 10 11 /* get memory for driver's per-chip state */ 12 chip = kzalloc(sizeof *chip, GFP_KERNEL); 13 if (!chip) 14 return -ENOMEM; 15 spi_set_drvdata(spi, chip); 16 17 ... etc 18 return 0; 19 }
當進入probe時,驅動就可能激發I/O請求並使用spi_message發送到SPI設備.
spi_message是一系列的協議操作,並以原子方式進行:
- 什么時候開始讀寫...由spi_transfer的請求次序來決定;
- 使用那個I/O緩沖...每個spi_transfer在每個傳輸方向上都依附一個buffer,支持雙工(有倆個pointer)和半雙工;
- 每次傳輸后的可選定義延遲...spi_transfer.delay_usecs設置;
- 傳輸后片選信號是高是低...spi_transfer.cs_change標志決定;
- 下一個message是否傳輸到同一個設備...看spi_transfer.cs_change在最后一次transfer中的狀態,可以減少片選信號變更操作時的開銷.
遵循標准內核規則,在你的message中提供安全的DMA緩沖.除非硬件請求,controller驅動並不強制使用DMA機制來減少額外的復制.
如果使用標准的dma_map_single()(系統提供的)來處理那些緩沖不合適,可以使用spi_message.is_dma_mapped來通知contrller驅動,你已經提供了相應的DMA地址(驅動自身提供的?).
- 基本的I/O原語是spi_async().異步請求可能在上下文(中斷處理,task,etc)中提交,通過消息回調來報告完成(completion).任何錯誤會取消片選,一切spi_message會被中止.
- 同樣也有spi_sync(),spi_read(),spi_write and spi_write_then_read().這些只會在可能引起睡眠的上下文中提交,他們比spi_async()要安全可靠.
- spi_write_then_read()應該只包含短小的片段用於數據傳輸,在開銷可以忽略的地方調用他.這被設計用來支持RPC風格的請求:寫一個8bit的命令,接着讀一個16bit的響應--spi_w8r16()正是這樣做的.
有些驅動需要修改spi_device屬性,比如transfer模式,字大小或者是時鍾頻率.可以在第一次I/O完成前在probe()中調用spi_setup()來修改.當然,在設備沒有messages時,可以在任何時候進行調用.
spi_device也許是驅動的最低層了,其上層可能包括sysfs(尤其是傳感器數據讀取),輸入層,ALSA,網絡,MTD,字符設備驅動框架,或者是其他linux子系統.
在和SPI設備交互時,有兩類內存驅動是需要管理的.
- I/O緩沖使用通常的linux規則,且必須是DMA安全的.可以從堆中或者空閑內存頁池中申請.絕不可以使用棧和加注任何static聲明.
- spi_message和spi_transfer元數據是用來將I/O緩沖粘合成一個protocol事務.
如果你喜歡,spi_message_alloc()和spi_message_free()可以方便的申請spi_message並初始化他們.
7. 我該如何編寫SPI Masster Controller驅動呢?
一個SPI Controller可能會被注冊在一個平台總線上.無論是哪條總線,都需要編寫一個驅動去綁定這個設備(SPI Controller).要知道,一個SPI Controller對應一條總線,對應一個spi_master實例.
這類驅動最主要的任務就是提供一個spi_master.
使用spi_alloc_master()函數來為master分配內存,然后用spi_master_get_devdata()函數來為其私有數據分配內存.框架如下:
1 struct spi_master *master; 2 struct CONTROLLER *c; 3 4 master = spi_alloc_master(dev, sizeof *c); 5 if (!master) 6 return -ENODEV; 7 8 c = spi_master_get_devdata(master);
這個驅動當然要去初始化spi_master的各個字段,包括總線號(可能會和平台設備的ID相同)和三個方法(用來和SPI核心以及SPI protocol層交互),當然也會初始化其自身的內部狀態.
當初始化完spi_master之后,就需要使用spi_register_master()函數來將其注冊近系統.這時,SPI控制器的設備節點和任何之前預聲明的SPI(從)設備就可以正常使用了,驅動模型核心層會將她們和其對應的驅動進行綁定.
如果你需要卸載SPI controller驅動,則調用spi_unregister_master()函數,這是和spi_register_master()函數對應的.
7.1 總線編號
總線編號是很重要的,因為Linux就是通過它來確定一個SPI總線的(再說一次,一個SPI總線對應一個SPI主控制器,就是spi_master,也對應一個總線編號).有效的總線編號是從零開始的.在SOC上,總線編號應該和芯片制造廠家定義的編號相同(這就是說產品ID).例如,硬件控制器SPI2的總線編號就是2.
SPI(從)設備就是通過總線編號來確定它是掛接在哪條總線上的(spi_board_info中定義).
如果你沒有類似這樣硬件廠商分配的總線編號,並且由於某種原因你不能分配到總線編號,那么就設定為負的總線編號.此時系統將會動態分配一個總線編號.不過,這樣你就需要面對的是一個非靜態的配置了.
設置SPI Masster的方法:
master->setup(struct spi_device *spi)
此方法將會設置SPI設備(SPI Masster當然也是設備)的時鍾頻率,SPI模式和字的大小.驅動程序可以更改由board_info結構確定的默認的值,然后調用spi_setup(spi)函數來使用新的配置.這個函數有可能會睡眠.除非每一個SPI(從)設備都有自己的配置寄存器,否則不要用上面的方法去改變SPI的配置.不然的話該驅動可能會損壞系統中正在其他SPI(從)設備上處理的IO操作.
** BUG ALERT: for some reason the first version of
** many spi_master drivers seems to get this wrong.
** When you code setup(), ASSUME that the controller
** is actively processing transfers for another device
(BUG警告:由於某種原因第一個版本的許多spi_master的驅動都會出現這種錯誤.當你運行setup()函數的時候,假定這個控制器正在處理另一個(從)設備的傳輸)
master->transfer(struct spi_device *spi, struct spi_message *message)
此函數不能睡眠,其功能是安排傳輸的發生並且調用complete()回調函數.這兩個過程通常發生在其他的傳輸完成之后,當控制器處於空閑狀態的時候.
master->cleanup(struct spi_device *spi)
您的控制器驅動可能會使用spi_device.controller_state來持有一個狀態標志(這個狀態是與設備的動態相關聯的,也就是說這個字段標識了設備的當前狀態).如果你這樣做了,那么你必須提供一個cleanup()方法來釋放這個狀態標志.
7.2 SPI消息隊列
大部分的驅動程序將要管理I/O隊列給進transfer().
該隊列可能僅僅是概念性的,例如,一個僅僅為禮品傳感器訪問的驅動可能會更好的工作於PIO.
但是隊列可能會更真實一點,使用message->queue,PIO,DMA(特別的如果根文件系統是在SPI flash上的話).和IRQ的處理程序的上下文, tasklets,或者工作隊列.你的驅動可以很花哨,或者很簡單,根據你的需要了.這樣的transfer()函數通常只是簡單的將一個消息添加到一個隊列上,然后開始一些異步傳輸引擎(除非它已經在運行了).
本文轉自:https://www.kernel.org/doc/Documentation/spi/spi-summary