专注电子技术学习与研究
当前位置:单片机教程网 >> Arduino >> 浏览文章

扩展NDS掌机连接Arduino (2)-NDS端SPI通信协议解析

作者:c_gao   来源:转自c_gao   点击数:  更新时间:2014年07月05日   【字体:

本系列上一篇文章介绍了如何构建一个最小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. SPI总线以一主一从模式工作时的连线。
其中:
(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通信时序图。
 
图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。

图3. 'F'字符的传输时序。
 
上图中,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。


图4. Mode 0 (CPOL = 0CPHA = 0)
 

图5. Mode 1 (CPOL = 0CPHA = 1)
 

图6. Mode 2 (CPOL = 1CPHA = 0)
 

图7. Mode 2 (CPOL = 1CPHA = 1)
 

三、NDS的SPI分析

可以这么说,NDS掌机与所有外设的通信连接都采用SPI接口。例如电源管理模块、触摸屏、麦克风、以及Slot1卡带都使用SPI通信。其中,前三者通过SPI控制寄存器REG_SPICNT和SPI数据寄存器REG_SPIDATA管理,REG_SPICNTREG_SPIDATA分别映射于内存地址:0x040001C00x040001C2。由于本篇主要目的是为了通过slot 1接口扩展,因此对这三者的控制不做介绍。
 
Slot 1卡带的SPI通信由AUXSPICNT寄存器和AUXSPIDATA寄存器控制,这两者分为控制寄存器和数据寄存器,分别映射在0x040001A00x040001A2的内存地址。均为16位寄存器。

3.1 Slot 1卡带方问权归属设置

由于这两个寄存器NDS的ARM7和ARM9两个CPU均可访问,因此在使用前需要先设置访问权归属,这可以通过设置位于0x04000204REG_EXMEMCNT寄存器第11位实现,置0为ARM9访问,置1为ARM7访问。详见图8。


图8. REG_EXMEMCNT寄存器。

一般我会让ARM9主CPU来控制SPI的通信,因此可以这么设置:

REG_EXMEMCNT &= (~(1<<11));
 
3.2 SPI总线初始化

设置完Slot 1的访问权归属后,接下来需要对SPI进行初始化。这里的初始化很简单,因为在真正使用SPI通信前,我们将关闭SPI通信,这通过使AUXSPICNT寄存器最高位(第15位)置0实现:


图9. AUXSPICNT寄存器各位说明。

接下来,是配置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。


图10. AUXSPIDATA寄存器说明。
注意:上述文献中也有一行说明:

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里有段原文说明:
 

图11. Wikipedia中关于SPI通信的描述。

三、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 ()
{
  command = 0;
}  // end of interrupt service routine (ISR) ss_falling

void setup (void)
{

  // have to send on master in, *slave out*
  pinMode(MISO, OUTPUT);

  // turn on SPI in slave mode
  SPCR |= _BV(SPE);

  // turn on interrupts
  SPCR |= _BV(SPIE);

  // interrupt for SS falling edge
  attachInterrupt (0, ss_falling, FALLING);
  
}  // end of setup


// SPI interrupt routine
ISR (SPI_STC_vect)
{
  byte c = SPDR;
 
  switch (command)
  {
  // no command? then this is the command
  case 0:
    command = c;
    SPDR = 0;
    break;
    
  // add to incoming byte, return result
  case 'a':
    SPDR = c + 15;  // add 15
    break;
    
  // subtract from incoming byte, return result
  case 's':
    SPDR = c - 8;  // subtract 8
    break;

  } // end of switch

}  // end of interrupt service routine (ISR) SPI_STC_vect


void loop (void)
{
// all done with interrupts
}  // end of loop
 
图12是上述代码中断执行的时序图。


图12. 中断服务过程调用时序图。
 
此篇结束,敬请期待下篇。
关闭窗口

相关文章