前几天终于收到了在amazone上订购的电烙铁和焊锡,于是当天晚上就开工制做了Slot 1扩展卡。随后两天晚上抽空编写并测试了NDS和Arduino两部分SPI通信的代码。顺便打个广告:有兴趣的朋友可以加入QQ群:362445156 (Arduino极客群)。
本篇博客将介绍三部分内容:
一、Slot 1扩展卡的自制过程
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;
get_token();
var = toupper(*token)-'A';
variables[var] = data;
get_token();
var = toupper(*token)-'A';
variables[var] = data;
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();
// send and receive a character, blocking
SPDR = out;
while (!(SPSR & (1<<7)));
return SPDR;
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;
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;
//spi_transfer(0xee);
do_spi(0xee);
//Serial.println("A");
//pinMode(7,INPUT);
//Serial.println(digitalRead(7));
AWRITE 6, I
DREAD 7, J
PRINT "digit pin 7:", J
AREAD 5, J
PRINT "analog pin 5:", J
DELAY 1000
- Slot 1扩展卡的自制过程;
- Arduino和NDS两部分SPI通信代码实现;
- 通过简单Demo演示应用效果。
最后完成后的效果见图0:
Slot 1扩展卡的自制,我用了两种方案:(1)用开源DS brut项目的PCB来打样扩展卡;(2)用正版NDS Slot 1游戏卡手工制做扩展卡。这两种方案我都用了,但由于人在国外,PCB打样在淘宝上找的商家,不方便寄到国外。这里主要讲第二种方案,即用已有的正版游戏卡制做Slot 1扩展卡的过程。
首先去Walmart买了一张最廉价的NDS游戏,才$8,如图1,图2。
然后将卡带取出,并将插入NDS主机卡槽的金手指部分用美工刀切下,并焊接好引出到外部的针脚(一共只需焊接7个引脚,金手指和外部引脚的焊接顺序参见第一篇博文:扩展NDS掌机连接Arduino方案设计一文中的图1)。引线长度要合适,刚好能将引出的针脚露出到卡带外部,引出部分不能太短,不然不容易接线。另外,需要将卡带底壳的上部切出7个小口,方便7个引脚的固定。这些工序完成后如图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
{
}
注意:由于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);
data = do_dread(pin);
}
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);
data = do_aread(pin);
}
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_DWRITE,SPI_COMMAND_AWRITE,SPI_COMMAND_DREAD,SPI_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 spi_trans(volatile byte out)
{
}
#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)
{
}
byte spi_transfer(volatile byte out)
//ISR (SPI_STC_vect)
{
}
void loop (void)
{
} // 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所示。
然后,在NDS端输入以下一段BASIC程序:
FOR I=0 TO 255
NEXT
然后输入"!"或"RUN",回车后这段BASIC程序便开始执行,运行结果会输出在下屏,并按每秒一次进行更新显示,结果如图6,图7所示:
至此,该项目核心的软件和硬件都基本完成了。现在只需要发挥人的想象力便可用它制做出各种有趣的作品,比如:
- 寻迹小车。将本项目的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