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

扩展NDS掌机连接Arduino (6)-自制NDS Slot 1扩展卡、Arduino端代码实现+简单Demo (附源码)

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

    前几天终于收到了在amazone上订购的电烙铁和焊锡,于是当天晚上就开工制做了Slot 1扩展卡。随后两天晚上抽空编写并测试了NDS和Arduino两部分SPI通信的代码。顺便打个广告:有兴趣的朋友可以加入QQ群:362445156 (Arduino极客群)。

 
本篇博客将介绍三部分内容:
  • Slot 1扩展卡的自制过程;
  • Arduino和NDS两部分SPI通信代码实现;
  • 通过简单Demo演示应用效果。
 
最后完成后的效果见图0:

图0. 最后制做完成并连接后的效果。
 
一、Slot 1扩展卡的自制过程
Slot 1扩展卡的自制,我用了两种方案:(1)用开源DS brut项目的PCB来打样扩展卡;(2)用正版NDS Slot 1游戏卡手工制做扩展卡。这两种方案我都用了,但由于人在国外,PCB打样在淘宝上找的商家,不方便寄到国外。这里主要讲第二种方案,即用已有的正版游戏卡制做Slot 1扩展卡的过程。
 
首先去Walmart买了一张最廉价的NDS游戏,才$8,如图1,图2。

图1. 全新的NDS游戏等待拆解。
 

图2. 打开盒子,游戏卡在右边。
 
然后将卡带取出,并将插入NDS主机卡槽的金手指部分用美工刀切下,并焊接好引出到外部的针脚(一共只需焊接7个引脚,金手指和外部引脚的焊接顺序参见第一篇博文:扩展NDS掌机连接Arduino方案设计一文中的图1)。引线长度要合适,刚好能将引出的针脚露出到卡带外部,引出部分不能太短,不然不容易接线。另外,需要将卡带底壳的上部切出7个小口,方便7个引脚的固定。这些工序完成后如图3,图4所示。

图3. 焊接好金手指部分和外部引脚效果。
 

图4. 焊接好金手指部分和外部引脚效果。
 
最后将正反面壳盖盖上,就完成了自制Slot 1扩展卡带的制做。如果卡带盖子盖得不严实,可以用胶带在外面围着粘一圈。
 
注意:本部分焊接要十分小心,由于金手指部分布线较密,不要将相邻的两个布线焊到一起。
 
二、Arduino和NDS两部分SPI通信代码的实现
在确保第一部分工作准确完成之后,接下来就需要编写代码进行SPI通信测试。
 
2.1 NDS部分SPI通信代码的实现
在本系列博文的第5篇:扩展NDS掌机连接Arduino (5)--NDS端BASIC语言解释器的移植与扩展 中,已经提供了NDS端的SPI发送和接收代码,但没有进行过测试。接下来就是需要去测试它是否能正常工作。NDS端是SPI Master,测试过程需要Arduino端 (SPI Slave)配合,这里我只贴出完成后的NDS端Send和Recv两个函数的代码。
 
SPI发送部分代码如下:
void _send(unsigned char* send_str, int len)
{
int i=0, max_size = 1024;
unsigned char* p = send_str;
while(i
{
writeBlocking_cardSPI(*p);
do_delay(1);
p++;
i++;
}
}

void do_send(unsigned char* send_str, unsigned char* recv_buff, int max_len)
{
int i=0;
while(send_str[i] && i< max_len)
{
recv_buff[i] = send_str[i];
i++;
}
 
recv_buff[i]='\0';
 
////////
//int len=strlen(send_str);
//setupConsecutive_cardSPI(len);
_send((unsigned char*)send_str, max_len);
 
}
 
以上代码中调用了do_delay(1)函数,意即NDS每发送1个字节的数据就等待1ms。NDS需要等待因为NDS的执行速度远比Arduino快,如果NDS只发送不等待,会使得Arduino无法处理接收数据。而通过我不断测试,NDS等待1ms的时间比较保险,在大量数据传送过程中不致于Arduino处理不过来。实际上,我也测试过NDS等待0.1ms, 0.5ms待不同的值,在数据量较小的情况下(10字节以内),Arduino也能正常处理而不会丢失数据。本段文字描述的延时都是在Arduino通过串口传回结果的前提下进行测试,而Arduino操作串口通信会消耗大量处理的时间,因此如果不使用串口显示结果,延时可以做到非常小。比如和一个网友交流过这个问题,他使用Slot 2接口可以做到512kbs的SPI通信速度,而Slot 1可以提供更高的速度,至少也可以做到这个速度。
上面代码中的do_delay()函数的实现采用NDS的Timer 0硬件计时器完成,每次函数调用时才占用该计时器,函数执行完毕便立即释放Timer 0计时器硬件资源。其代码如下:
 
void do_delay(int millisecond)
{
uint ticks = 0;
uint oldtick;
double ms=millisecond;
if(millisecond==-99)
ms=0.5;
 
timerStart(0, ClockDivider_1024, 0, NULL);
 
ticks += timerElapsed(0);
oldtick = ticks;
 
double fesp=ms/1000*TIMER_SPEED+oldtick; //esp = (ticks-oldtick)/TIMER_SPEED*1000;
uint esp=(uint)fesp;
while(ticks
ticks += timerElapsed(0);
 
timerStop(0);
}
 
SPI接收部分代码如下:
int do_recv(unsigned char* buff, int num_byte, unsigned char* stop_byte)
{
u8 read_byte=0;
int i=0;
for(i=0; i
{
  writeBlocking_cardSPI(0x00);
  while(readBlocking_cardSPI(&read_byte) != CARD_SPI_STATUS_OK);
 
  if( (NULL != stop_byte) && ((char)read_byte == *stop_byte) )
  return i;
 
  buff[i] = (char)read_byte;
  }
  
  return i;
}
 
注意:由于NDS端是SPI Master,根据SPI通信原理,Slave不能主动和Master进行通信。因此,当Master需要接收数据时,需要主动发起通信请求,然后Slave接收到该请求后,就可以将相应的数据传给Master。
 
2.2 为NDS部分BASIC语言解释器添加4条Arduino命令
在本系列博文的第5篇:扩展NDS掌机连接Arduino (5)--NDS端BASIC语言解释器的移植与扩展 最后,已经提到需要添加的四条命令,即DWRITE, AWRITE, DREAD, AREAD。分别为写数字引脚(类似Arduino的 digitalWrite() ),写PWM引脚(analogWrite()),读数字引脚(digitalRead()),读模拟引脚(analogRead())。添加命令过程参见本系列第5篇博文,具体实现代码如下。

(1)DWRITE 命令
格式:DWRITE pin, val。  pin为Arduino数字引脚编号;val为待写入的值,值1对应Arduino的HIGH,0对应LOW。
例如:DWRITE 6, 1
代码
void exec_dwrite()
{
int pin, value;
get_exp(&pin);
 
get_token();
if(*token != ',') 
serror(19);
 
get_exp(&value);
 
do_dwrite(pin, value);
}
 
void do_dwrite(int pin, int value)
{
unsigned char send_str[5];
 
send_str[0] = '\\'; //command sign
send_str[1] = SPI_COMMAND_DWRITE; //command type
send_str[2] = (unsigned char)pin; //pin
send_str[3] = ((unsigned char)(value!=0?1:0)); //value
send_str[4] = '\0';
_send(send_str, 4);
}
 
(2)AWRITE 命令
格式AWRITE pin, val。  pin为Arduino带PWM功能的数字引脚编号;val为待写入的值,值范围为0~255。
例如AWRITE 6, 110
代码
void exec_awrite()
{
int pin, value;
get_exp(&pin);
 
get_token();
if(*token != ',') 
serror(20);
 
get_exp(&value);
 
do_awrite(pin, value);
}

void do_awrite(int pin, int value)
{
unsigned char send_str[5];
 
send_str[0] = '\\'; //command sign
send_str[1] = SPI_COMMAND_AWRITE; //command type
send_str[2] = (unsigned char)pin; //pin
send_str[3] = (unsigned char)(value); //value
send_str[4] = '\0';
_send(send_str, 4); 
}
 
(3)DREAD 命令
格式DREAD pin, var。  pin为Arduino数字引脚编号;var为BASIC解释器内置变量,即变量字母A~Z。命令成功执行后,Arduino的pin引脚的结果将写入由var指定的BASIC解释器内置变量中。
例如DREAD 7, J
代码
void exec_dread()
{
int pin, var, data;
 
get_exp(&pin);
 
get_token();
if(*token != ',') 
serror(21);

  get_token(); 
  var = toupper(*token)-'A';

data = do_dread(pin);

  variables[var] = data;
}

int do_dread(int pin)
{
unsigned char value=0x0;
unsigned char send_str[4];
 
send_str[0] = '\\'; //command sign
send_str[1] = SPI_COMMAND_DREAD; //command type
send_str[2] = (unsigned char)pin; //pin
send_str[3] = '\0';
 
_send(send_str, 3);
 
do_recv(&value,1,NULL);
//printf("recv:%d\n",value);
return value;
}
 
(4)AREAD 命令
格式AREAD pin, var。  pin为Arduino模拟引脚编号;var为BASIC解释器内置变量,即变量字母A~Z。命令成功执行后,Arduino的pin模拟引脚的结果将写入由var指定的BASIC解释器内置变量中。
例如AREAD 5, H
代码
void exec_aread()
{
int pin, var, data;
 
get_exp(&pin);
 
get_token();
if(*token != ',') 
serror(22);

  get_token(); 
  var = toupper(*token)-'A';

data = do_aread(pin);

  variables[var] = data;
}

int do_aread(int pin)
{
int value;
unsigned char recv_str[2];
unsigned char send_str[4];
 
send_str[0] = '\\'; //command sign
send_str[1] = SPI_COMMAND_AREAD; //command type
send_str[2] = (unsigned char)pin; //pin
send_str[3] = '\0';
 
_send(send_str, 3);
 
recv_str[0]=recv_str[1]=0;
do_recv(recv_str,2,NULL);
 
//the first byte send from arduino is the high byte of the result of analog read.
value = recv_str[0]; 
value <<= 8;
value |= recv_str[1];
 
return value;
}
 
说明:由于在SPI通信中,数据交换以8位,即1个字节为单位。而Arduino的ADC,即模拟引脚数据值为0~1023,即10位数据。因此读取一次Arduino的模拟引脚的数据需要2次SPI数据发送才能完成。我在Arduino端的代码实现中,将模拟引脚的数据按2次发送,先发送高字节,再发送低字节,具体参考第2.3部分内容。而上述NDS接收代码中,则做对应处理,即先接收的字节为高字节,后接收的为低字节,然后合并两个字节内容:
 
//the first byte send from arduino is the high byte of the result of analog read.
value = recv_str[0]; 
value <<= 8;
value |= recv_str[1];
 
2.3 Arduino部分SPI通信代码的实现
Arduino部分代码主要完成两部分功能:
(1)配置Arduino为SPI Slave端;
(2)接收NDS端发送过来的SPI命令,并解析执行命令。
第(1)部分功能的详细分析过程详见本系列博文2:扩展NDS掌机连接Arduino (2)--NDS端SPI通信协议解析。而第(2)部分中,我将NDS发送的命令进行了简单的封装(类似SD卡读写命令的原理一样)。命令的格式采用如下形式:
第1字节:'\\',为命令起始标志字节。
第2字节:为表示具体命令的字节。例如本篇上述2.2小节内容中NDS封装了4条操作Arduino的命令,分别使用SPI_COMMAND_DWRITESPI_COMMAND_AWRITESPI_COMMAND_DREADSPI_COMMAND_AREAD。其定义如下:
 
#define SPI_COMMAND_DWRITE   '~'
#define SPI_COMMAND_AWRITE   '!'
#define SPI_COMMAND_DREAD   '@'
#define SPI_COMMAND_AREAD   '#'
 
第3字节:为表示pin引脚号的字节。
第4字节:为表示需要写入pin引脚的值(只适用于DWRITE,AWRITE两条命令)。

这里我使用了4个特殊字符,实际上随便使用什么字符都可以,只要保持NDS端和Arduino端定义的一致就可以。完整的Arduino封装代码如下:
 
// Written by Vincent Gao (c_gao)
//BLOG: http://blog.congao.net
//EMAIL: dr.c.gao@gmail.com
// Sep. 2014

#include "pins_arduino.h"
//#include "SPI.h"
#define SS 10                 // PB2
#define MOSI 11               // PB3
#define MISO 12               // PB4
#define SCK 13                // PB5

// what to do with incoming data
byte command = 0;
byte led_pin = 6;
byte led_status = 1;

void setup()
{
  byte clr;
  
  Serial.begin(9600);
  
  // setup SPI interface
  pinMode(SS, INPUT);
  pinMode(MOSI, INPUT);
  pinMode(MISO, OUTPUT);
  pinMode(SCK, INPUT);
  
  // enable SPI interface, CPOL=1, CHPA=1
  SPCR = (1<<6)|(1<<3)|(1<<2);
  // dummy read
  clr = SPSR;
  clr = SPDR;
  
  //attachInterrupt (0, ss_falling, FALLING);
  //SPI.attachInterrupt();
}

byte spi_trans(volatile byte out)
{
  // send and receive a character, blocking
  SPDR = out;
  while (!(SPSR & (1<<7)));
  return SPDR;
}

#define SPI_COMMAND_DWRITE '~'
#define SPI_COMMAND_AWRITE '!'
#define SPI_COMMAND_DREAD '@'
#define SPI_COMMAND_AREAD '#'
boolean is_recvdata = false;
boolean is_command = false;
boolean wait_command_info = false;
int wait_num_byte = 0;
char buf[4];
int index = 0;
int pin;
int value;

byte do_spi(volatile byte out)
{
  SPDR = out;
  while (!(SPSR & (1<<7)));
  byte d = SPDR;
  //Serial.write(d);

  if(d == '\\') // it is a command
  {
    is_command = true;
    index = 0;
    wait_command_info = false;
    return d;
  }
  
  if(is_command)
  {
    is_command = false;
    buf[index++] = d;
    wait_command_info = true;
    switch(d)
    {
      case SPI_COMMAND_DWRITE:
      case SPI_COMMAND_AWRITE:
        wait_num_byte = 2;
        break;
      case SPI_COMMAND_DREAD:
      case SPI_COMMAND_AREAD:
        wait_num_byte = 1;
        break;
      default:
        wait_num_byte = 0;
        break;
    }
    return d;
  }
  
  if(wait_command_info && (wait_num_byte > 0))
  {
    buf[index++]=d;
    wait_num_byte--;
    //Serial.print(wait_num_byte);
  }
  
  if(wait_command_info && (wait_num_byte == 0))
  {
      //deal with command
      index = 0;
      wait_command_info = false;
      //Serial.println("AAA");
      switch(buf[0])
      {
        case SPI_COMMAND_DWRITE:
          pin = buf[1];
          value = buf[2];
          //buf[3]=0;
          //Serial.println(buf);
          pinMode(pin, OUTPUT);
          digitalWrite(pin, value);
          break;
        case SPI_COMMAND_AWRITE:
          pin = buf[1];
          value = buf[2];
          pinMode(pin, OUTPUT);
          analogWrite(pin, value);
          break;
        case SPI_COMMAND_DREAD:
          pin = buf[1];
          pinMode(pin, INPUT);
          value = digitalRead(pin);
          //SPDR = 0xff & value;
          spi_trans(0xff & value);
          //Serial.println(value);
          break;
        case SPI_COMMAND_AREAD:
          pin = buf[1];
          pinMode(pin, INPUT);
          value = analogRead(pin);
          spi_trans(value>>8);
          spi_trans(0xff & value);
          break;
        default:
          break;
      }
    return d;
  }
    
  return d;
}

byte spi_transfer(volatile byte out)
//ISR (SPI_STC_vect)
{
  static byte sss=LOW;
  
  SPDR = out;
  while (!(SPSR & (1<<7)));
  byte d = SPDR;
  
  Serial.write(d);

  if(d == 'A')
  {
    if(sss==LOW)
    {
      digitalWrite(led_pin, HIGH);
      sss=HIGH;
    }
    else
    {
      digitalWrite(led_pin, LOW);
      sss=LOW;
    }
  }    
  return 1;
}
 

void loop (void)
{
  //spi_transfer(0xee);
  do_spi(0xee);
  //Serial.println("A");
  //pinMode(7,INPUT);
  //Serial.println(digitalRead(7));
}  // end of loop
 
说明:
  • setup()函数完成对Arduino端SPI Slave模式的配置,并配置SPI通信模式为Mode 3(CPOL=1, CHPA=1)。
  • byte do_spi(volatile byte out)函数为主体功能函数,该函数实现对发送过来的4条SPI命令进行解析和执行。
  • byte spi_trans(volatile byte out)函数实现向NDS发送数据。
  • byte spi_transfer(volatile byte out)函数是之前用于测试SPI时使用,最后没有使用,也没有删除。
三、Demo演示应用效果
这部分内容,我使用一个简单的Demo来演示本方案实际运行的效果。
 
3.1 硬件配置
NDS端
(1)我使用初版NDS;
(2)SuperCard Mini SD烧录卡+1GB mini SD卡+读卡器;
(3)上文自制好的扩展卡,并插入NDS主机的Slot 1卡槽。
 
Arduino端
(1)面包板上的最小Arduino系统(详见本系列第1篇博文:扩展NDS掌机连接Arduino (1)--Arduino端最小系统实现),并用杜绑线和自制的NDS扩展卡并连接好;
(2)LED灯,连接至Arduino的数字引脚 6,该引脚同时也是PWM引脚;
(3)寻迹传感器(数字传感器),连接至Arduino的数字引脚 7;
(4)土壤湿度传感器(模拟传感器),连接至Arduino的模拟引脚 5;
(5)无CPU 的Arduino UNO板,用于上传Arduino程序。
 
其它
(1)用水打湿的餐巾纸,用于裹在土壤湿度传感器的感应片上,以获得不同的湿度结果;
(2)一小张白色的纸片,可移开或盖在寻迹传感器上,以获得不同的结果。
 
3.2 软件配置
NDS端:将我移植扩展的最新版(下文附完整工程代码下载)BASIC解释器编译好后,复制到Mini SD卡上,并将Mini SD卡插入SuperCard Mini SD烧录卡。
 
Arduino端:将上文的完整Arduino代码(下文附完整工程代码下载)编译并上传至最小Arduino系统。
 
3.3 Demo
启动NDS,并从SuperCard Mini SD卡运行BASIC解析器。首先输入下行命令并回车:
DWRITE 6, 1
然后输入"!"或"RUN",回车后如果LED灯被点亮,说明硬件连接正确无异常,如图5所示。

图5. NDS通过BASIC解释器发送DWRITE命令点亮Arduino的LED。
 
然后,在NDS端输入以下一段BASIC程序:
 
FOR I=0 TO 255
  AWRITE 6, I
  DREAD 7, J
  PRINT "digit pin 7:", J
  AREAD 5, J
  PRINT "analog pin 5:", J
  DELAY 1000
NEXT
 
然后输入"!"或"RUN",回车后这段BASIC程序便开始执行,运行结果会输出在下屏,并按每秒一次进行更新显示,结果如图6,图7所示:

图6. 用纸片盖住寻迹传感器(传感器灯变红),而湿度传感器则不接触湿餐巾纸,则相应的NDS屏上输出:digit pin 7: 1      analog pin 5: 1023。结果正确。

图7. 将纸片从寻迹传感器移开(传感器灯变绿),而湿度传感器用湿餐巾纸包裹住,则相应的NDS屏上输出:digit pin 7: 0      analog pin 5: 728。并且值在不停刷新,结果正确。
至此,该项目核心的软件和硬件都基本完成了。现在只需要发挥人的想象力便可用它制做出各种有趣的作品,比如:
  • 寻迹小车。将本项目的NDS和Arduino固定在小车底座上,用3个寻迹传感器通过DREAD命令返回数据给NDS,并决定左右转弯或直走的决策,然后用AWRITE命令驱动连接在PWM引脚上的电机驱动板即可实现。同时配合由我添加的BASIC画图命令可以实现在NDS上屏画出上车的运动轨迹。或将小车运动时的速度,位置等信息记录下来存到NDS烧录卡上的SD卡内,以做后续析和处理。
  • 万用表。万用表功能非常实用,利用本方案实现上却并不复杂。Arduino端外接两条杜邦线作为测笔,通过一小段万用表代码即可实现。
  • GPS记录仪。将本项目外接一个GPS模块,将GPS的经纬度信息回传到NDS,可实现将你的运动行踪记录到SD卡内,或画到屏幕上。如果你编程不错,还可以直接在NDS上写个小程序将离线地图显示在NDS上,并把经纬度坐标显示在该地图上,然后你可以带上这个NDS出门跑步,开车,骑车,回来后就可以看到你的行踪轨迹了。
  • N路逻辑分析仪。Arduino端如果使用atmega328,它提供了13个数字引脚,理论上最多可以作13路逻辑分析仪,不过按上文对SPI通信速率的分析和实验,预计频率只能做到1MHz左右。
  • N路示波器。原理同N路逻辑分析仪,可使用NDS上屏显示波型。
  • 家庭环境监视机。归功于NDS自带的WiFi功能,使用libwifi库,可将Arduino端连接的各种传感器数据,如温度,湿度,气压,PM2.5值,一氧化碳指数等信息传到远程服务器。例如国内免费的Yeelink云平台,这样你就可以随时随地查看家里的情况了。话说,3DS homebrew channel马上就要开放了,因此还可以用3DS上的摄像头拍的照片也上传上去,远程监视家里的情况。
  • 还有很多...
 
后记:
我也测试了超声波传感器,但由于NDS的Slot 1只提供3.3V电压,而我手上的超声波传感器在该电压下工作不正常,能返回数据,但不正确,到我外接5V电压时则就变得正确。
 
全部工程代码(含NDS端BASIC解释器代码,以及Arduino代码):http://www.51hei.com/f/NDS扩展Arduino源码.zip
关闭窗口

相关文章