本系列上一篇文章介紹了如何構建一個最小Arduino系統,這為擴展NDS做好了Arduino端的硬件準備。接下來的工作:
(1)硬件部分:主要為NDS端Slot 1卡的改造以及引線連接這個最小Arduino系統;
(2)軟件部分:主要為兩端SPI通信的實現,以及NDS端通信部分軟件架構和API封裝;
(3)技術演示:一個Demo。
由于上述第(1)部分需要的部分工具和材量目前還沒到位,因此本篇先介紹第二部分的SPI通信協議解析。以下內容首先基本介紹SPI通信協議,然后針對NDS硬件詳細介紹如何開發使用NDS的Slot 1硬件接口部分的SPI協議。最后簡單介紹如何實現Arduino作為Slave端的SPI如何實現。
一、什么是SPI ?
SPI是Serial Peripheral Interface的縮寫,意即串口外圍接口,它與UART,IIC一樣是單片機和嵌入式通信的重要協議。SPI最早由Motorola開發,它是一種異步,串行,主從模式,全雙工的通信協議。有許多外圍設備使用SPI協議工作,比如我在STM32F4 Discovery + FreeRTOS + 中文字庫 + 12864LCD一文中使用的12864 OLED顯示屏就使用SPI總線。
SPI可以一主(Master)一從(Slave)模式工作,也可以一主多從模式工作,但后者同一時刻只有一個從機與主機通信。一主一從模式工作如圖1所示:
其中:
(1) SCLK (Serial Clock) 為時鐘,由Master提供,
(2) MOSI (Master Output, Slave Input) 為Master向Slave端單向傳輸數據的通道,
(3) MISO (Master Input, Slave Output) 為Slave向Master端單向傳輸數據的通道,
(4) SS (Slave Select) 為Slave選中信號,低電平時表示選中,高電平時表示不選中。
特別要注意的一點是連線方式和UART串口不同,在SPI連線中,MOSI始終連MOSI,MISO始終連MISO,無需交叉連接。
SPI協議規范比較松散,因此有很多變種。比如有三線模式SPI,它將MOSI與MISO合并為一線,提供半雙工工作模式,即同時只能有一個方向的數據傳輸,優點是線少(只需3根),缺點是半雙工,數據不能雙向同時傳輸。其它還有多數據線模式SPI等,在此不作詳述。
二、SPI如何工作?
關于SPI的工作原理,這里有一篇文章圖文結合,講得非常詳細:SPI - Serial Peripheral Interface - for Arduino。這里我摘主要內容來講一下。
圖2用邏輯分析儀截取了一段SPI通信的時序圖。結合該圖分析SPI原理會非常清楚。
首先,當SS為A區域(高電平)時,不進行通信。當SS進入B區域時通信開始。通信開始后,SCK打出通信時鐘信號,在C時間段,MOSI打出1字節的高低電平信號,為'0b01000110',查看ASCII碼表得知為字符'F',完成該字節的傳輸花了2us時間。然后停頓1us后,再在D時間段打出‘0b01100000‘,對應字符'a'。同理,在E時間段發送'b'字符。SS信號到G時間段開始轉為高電平,通信中止,完成了'Fab'三個字符從Master到Slave的數據傳輸。
上述‘F‘字符的二進制可以從圖3清楚分析得到。由于這里2us完成了一個字節的傳輸,因此此例中,理論傳輸速度可以容易計算得到:8b*1s/2us=4,000,000bps,即4Mbps。
上圖中,SCLK信號線由低電平轉高電平(高電平采樣,上升沿有效)時進行信號1個bit的采樣,對應MOSI線為低電平時為0,高電平時為1。這里SCLK在上升沿時對MOSI或MISO數據線進行采樣,這是SPI通信中的Mode3。在SPI通信中,一共有4種模式,可以通過設置CPOL和CPHA這兩個寄存器位來實現這4種通信模式。CPOL是Clock POLarity,CPHA是Clock PHAse。這兩個設置位最早由Freescale公司設定,后被廣泛使用。4種模式的設置如下:
(1)Mode 0:時鐘正常為低電平(CPOL = 0),數據在低電平轉為高電平時采樣,即上升沿采樣(CPHA = 0)。
(2)Mode 1:時鐘正常為低電平(CPOL = 0),數據在高電平轉為低電平時采樣,即下降沿采樣(CPHA = 1)。
(3)Mode 2:時鐘正常為高電平(CPOL = 1),數據在高電平轉為低電平時采樣,即下降沿采樣(CPHA = 0)。
(4)Mode 3:時鐘正常為高電平(CPOL = 1),數據在低電平轉為高電平時采樣,即上升沿采樣(CPHA = 1)。
Arduino的默認模式為Mode 0。這個Mode x中x的值就是CPOL和CPHA二進制組合的值,所以很好記。
這4種模式的圖示見圖4至圖7。
三、NDS的SPI分析
可以這么說,NDS掌機與所有外設的通信連接都采用SPI接口。例如電源管理模塊、觸摸屏、麥克風、以及Slot1卡帶都使用SPI通信。其中,前三者通過SPI控制寄存器REG_SPICNT和SPI數據寄存器REG_SPIDATA管理,REG_SPICNT和REG_SPIDATA分別映射于內存地址:0x040001C0和0x040001C2。由于本篇主要目的是為了通過slot 1接口擴展,因此對這三者的控制不做介紹。
Slot 1卡帶的SPI通信由AUXSPICNT寄存器和AUXSPIDATA寄存器控制,這兩者分為控制寄存器和數據寄存器,分別映射在0x040001A0和0x040001A2的內存地址。均為16位寄存器。
3.1 Slot 1卡帶方問權歸屬設置
由于這兩個寄存器NDS的ARM7和ARM9兩個CPU均可訪問,因此在使用前需要先設置訪問權歸屬,這可以通過設置位于0x04000204的REG_EXMEMCNT寄存器第11位實現,置0為ARM9訪問,置1為ARM7訪問。詳見圖8。
一般我會讓ARM9主CPU來控制SPI的通信,因此可以這么設置:
REG_EXMEMCNT &= (~(1<<11));
3.2 SPI總線初始化
設置完Slot 1的訪問權歸屬后,接下來需要對SPI進行初始化。這里的初始化很簡單,因為在真正使用SPI通信前,我們將關閉SPI通信,這通過使AUXSPICNT寄存器最高位(第15位)置0實現:
接下來,是配置SPI各個參數。從圖9可知,如果需要1MHz的通信速率,可以置AUXSPICNT第0和1位為0b01實現。然后再設置SPI通信的IRQ使能,即置第14位為1。另外因為Slot 1接口有兩種工作模式:ROM模式和SPI通信模式,我們要使用的當然是SPI通信模式,因此還需要置第13位為1。我打算使用SPI通信的方式為使用時打開,不使用時就關閉,默認為關閉狀態,因此我將不直接設置各參數到AUXSPICNT寄存器上,而是先賦給一個16位變量u16 config,在需要通信時再將這個config值直接賦給AUXSPICNT寄存器,使之立即生效可用。于是有:
config = 1 | (1<<14) | (1<<13);
3.3 SPI通信
當該NDS通過該SPI與外界進行實際通信時,操作如下:
(1)發送數據:檢查AUXSPICNT寄存器第7位,查看是否SPI總線忙,如果忙,則等待直至不忙(第7位為0)。然后將config值賦給AUXSPICNT寄存器并同時置該寄存器第6位為1,意即SS信號打低電平準備SPI通信。實現如下:
AUXSPICNT = config | (1<<6);
注意一點:非官方文檔GBA/NDS Technical Info中有一行說明:
The "Hold" flag should be cleared BEFORE transferring the LAST data unit, the chipselect will be then automatically cleared after the transfer, the program should issue a WaitByLoop(12) (on NDS7, or longer on NDS9) manually AFTER the LAST transfer.
這說明傳完數據后,SS線(第6位)會自動清0,即關閉通信。另外通信結束后需要等待一段時間,可能是因為數據在物理信道上傳輸和存儲到接收方寄存器的過程需要時間。
然后便可以將8位單字節數據賦給AUXSPIDATA寄存器,硬件會自動將數據發送出去。注意SPI通信一般單次傳輸8 bit,AUXSPIDATA寄存器也是如此,見圖10。
注意:上述文獻中也有一行說明:
During transfer, the Busy flag in AUXSPICNT is set, and the written DATA value is transferred to the device (via output line), simultaneously data is received (via input line). Upon transfer completion, the Busy flag goes off, and the received value can be then read from AUXSPIDATA, if desired.
這行英文說明數據一傳出去,如果外面傳有過來的數據,則同時也就通過MISO接收到了。而且數據傳輸一結束,busy位(即AUXSPICNT寄存器第7位)將自動清0,于是接收的數據就能從AUXSPIDATA寄存器取到了。
(2)接收數據:上面的那段英文和我的注釋已經說得很清楚了:只要busy位為0,即可從AUXSPIDATA寄存器獲取接收數據。這是因為標準SPI通信是全雙工的,一方在一個時鐘節拍內發出1 bit的同時,也接收另一方發過來的1 bit。這在維基百科詞條:Serial Peripheral Interface Bus里有段原文說明:
三、Arduino的SPI分析
由于Arduino的開發包的高度封裝,因此Arduino端的SPI通信編程相對來講容易的多。這可以采用兩種方式實現:
(1)完全通過底層操作ATmega CPU的寄存器實現。
(2)通過SPI庫來實現主要功能,適當添加部分寄存器操作。
DS brut采用第(1)種方式,代碼不便閱讀和理解,編程調試也較難。我將采用第二種方式實現。
由于Arduino的SPI庫只支持將Arduino作為Master設備去連接外部的Slave設備,因此需要稍做寄器存器設置:
// turn on SPI in slave mode
SPCR |= _BV(SPE);
另外,為提高執行效率,可采用中斷方式處理SPI通信:
// turn on interrupts
SPCR |= _BV(SPIE);
以上兩部分代碼放入setup()函數中便可。
如果Arduino UNO端采用10號數字引腳作為SS線,則可以將10號線連接到2號線,這樣當SS線電平被拉低或拉高時便會觸發2號線的中斷。由于2號線的中斷號為0,因此我們可在setup()函數最后加入以下代碼來捕獲0號中斷:
// interrupt for SS falling edge
attachInterrupt (0, ss_falling, FALLING);
以下是完整的Arduino 做為SPI Slave的代碼的Demo,來源:SPI - Serial Peripheral Interface - for Arduino。
// Written by Nick Gammon
// April 2011
#include "pins_arduino.h"
// what to do with incoming data
byte command = 0;
// start of transaction, no command yet
void ss_falling ()
{
} // end of interrupt service routine (ISR) ss_falling
void setup (void)
{
} // end of setup
// SPI interrupt routine
ISR (SPI_STC_vect)
{
} // end of interrupt service routine (ISR) SPI_STC_vect
void loop (void)
{
// all done with interrupts
} // end of loop
圖12是上述代碼中斷執行的時序圖。
此篇結束,敬請期待下篇。