Arduino中国 | Flamingo EDA

CAT | 电子积木

在Arduino上进行声音的播放可以有几种方案,一种是利用语音芯片对事先存储好的声音数据进行播放,这种方案一般只能进行语音数据的播放,音质比较差,面且播放的时长比较受限;第二种办法是播放MP3文件,但这种方式需要专门的MP3解压芯片;第三种可以借助的办法就是下面要介绍的WAV声音播放模块 ,其做法是将事先录制好的WAV文件存储到SD卡中,Arduino负责从SD卡中读出未经压缩的声音文件,再送到WAV模块中进行播放:

目前这一方案受限于所采用的DAC芯片精度的影响,最高只能做到16bit,22KHz,Mono格式的WAV文件,不过这对于大部分应用场合来讲已经足够了。

使用该模块时需要事先准备好符合我们要求WAV文件,有不少音频转换软件可以完成该任务。下面我们以Audacity(http://audacity.sourceforge.net/)为例,讲述如何讲MP3文件转换成我们所需要的格式。

运行Audacity之后,选择“File”菜单下的“Open”命令,打开需要进行转换的MP3文件:

通常MP3文件都是立体声(Stereo)格式的,我们需要将其分离成两个单独的音轨,这可以通过选择左侧声音文件标题下拉框中的“Split Stereo Track”命令来完成:

为了避免这两个音轨在混音之后音量过大,我们可以分在每个音轨上将增益(Gain)设置成-6dB,修改的方法是拖动相应音轨左侧标有正负号的滑动条:

将下来我们需要从每条音频的标题下拉菜单下选择“Mono”命令,将其转换成单声道格式,注意每一路都需要做这个转换:

现在我们可以选择“Project”菜单下的“Quick Mix”命令来完成单声道文件的混音:

通过上述的步骤,我们已经将立体声(Stereo)格式的声音数据转化成单声道(Mono)格式的声音数据,此外我们还需要设置相应的采样位数和采样频率。对于采样位数的设置,我们可以从每条音频的标题下拉菜单中选择“Set Sample Format”下的“16-bit”命令,将采样位数设置为16位:

而采样频率的设置则可以通过Auda对话框底部的“Project rate”进行设置,我们的WAV播放模块最高只支持22Khz:

所有参数都设置好之后,我们就可以导出所需要的声音文件了。选择“Edit”菜单下的“Preferences…”命令,在打开的对话框中从“Uncompressed Export”下拉框中选择“WAV (Microsoft 16 bit PCM)”后单击“Ok”按钮:

最后选择“File”菜单下的“Export As WAV…”命令,导出我们所需要的WAV文件就可以了,为了确保之后Arduino能够正常处理,该WAV文件需要用字母命名,并且文件名称不能超过8个字节。为了确保产生的文件是正确的,我们还可以在Windows中右键单击该文件后,从弹出的菜单中选择“属性”命令打开属性对话框,然后从“摘要”中检查生产的文件是否符合我们的要求:

生成的文件我们可以通过读卡器将其复制到SD卡中,为了保证读取的正确性,可以考虑先格式化后再将WAV文件复制到SD卡中。注意SD卡应该格式化成FAT格式的,而不要格式化成FAT32格式的。

带有WAV文件的SD卡准备好之后,我们可以用Arduino IDC扩展板将SD卡模块和WAV声音播放模块都连接到Arduino上:

通过Arduino皤放SD卡中存储的WAV文件我们需要用到wavehc库(http://code.google.com/p/wavehc/)。我们测试时使用的是wavehc20101009.zip这个文件,你可以通过wavehc的官方网站下载该文件,也可以通过这个地址直接下载我们在测试时使用的库文件包。

在将下载后的wavhc库解压缩之后,将压缩包中的WaveHC上当复制到Arduino安装目录中的libraries目录下。wavhc库默认使用了Arduino上的2、3、4、5这4个引脚,但 我们的Arduino IDC扩展板上的IDC-6座则是与Arduino的6、7、8、9这4个引脚相连接的,因此我们需要对WaveHC目录下的WavePinDefs.h这个文件进行修改, 相应的对应关系为2->9、3->7、4->6、5->8。你可以手工修改这个文件中以MCP_DAC_开头的那些宏定义,也可以直接下载我们已经修改好的WavePinDefs.h文件

测试时我们使用的是wavhc库压缩包examples目录下的daphc,其实现的功能是不断从SD卡中查找以.WAV为扩展名的WAV文件进行播放,相应的代码为:

/*
 * This example plays every .WAV file it finds on the SD card in a loop
 */
#include "WaveHC.h"
#include "WaveUtil.h"

SdReader card;    // This object holds the information for the card
FatVolume vol;    // This holds the information for the partition on the card
FatReader root;   // This holds the information for the volumes root directory
WaveHC wave;      // This is the only wave (audio) object, since we will only play one at a time

uint8_t dirLevel; // indent level for file/dir names    (for prettyprinting)
dir_t dirBuf;     // buffer for directory reads

/*
 * Define macro to put error messages in flash memory
 */
#define error(msg) error_P(PSTR(msg))

// Function definitions (we define them here, but the code is below)
void play(FatReader &dir);

//////////////////////////////////// SETUP
void setup()
{
  Serial.begin(9600);           // set up Serial library at 9600 bps for debugging

  putstring_nl("\nWave test!");  // say we woke up!

  putstring("Free RAM: ");       // This can help with debugging, running out of RAM is bad
  Serial.println(FreeRam());

  //  if (!card.init(true)) { //play with 4 MHz spi if 8MHz isn't working for you
  if (!card.init()) {         //play with 8 MHz spi (default faster!)  
    error("Card init. failed!");  // Something went wrong, lets print out why
  }

  // enable optimize read - some cards may timeout. Disable if you're having problems
  card.partialBlockRead(true);

  // Now we will look for a FAT partition!
  uint8_t part;
  for (part = 0; part < 5; part++) {   // we have up to 5 slots to look in
    if (vol.init(card, part))
      break;                           // we found one, lets bail
  }
  if (part == 5) {                     // if we ended up not finding one  :( 
    error("No valid FAT partition!");  // Something went wrong, lets print out why
  }

  // Lets tell the user about what we found
  putstring("Using partition ");
  Serial.print(part, DEC);
  putstring(", type is FAT");
  Serial.println(vol.fatType(),DEC);     // FAT16 or FAT32?

  // Try to open the root directory
  if (!root.openRoot(vol)) {
    error("Can't open root dir!");      // Something went wrong,
  }

  // Whew! We got past the tough parts.
  putstring_nl("Files found (* = fragmented):");

  // Print out all of the files in all the directories.
  root.ls(LS_R | LS_FLAG_FRAGMENTED);
}

//////////////////////////////////// LOOP
void loop()
{
  root.rewind();
  play(root);
}

/////////////////////////////////// HELPERS
/*
 * print error message and halt
 */
void error_P(const char *str)
{
  PgmPrint("Error: ");
  SerialPrint_P(str);
  sdErrorCheck();
  while(1);
}
/*
 * print error message and halt if SD I/O error, great for debugging!
 */
void sdErrorCheck(void)
{
  if (!card.errorCode()) return;
  PgmPrint("\r\nSD I/O error: ");
  Serial.print(card.errorCode(), HEX);
  PgmPrint(", ");
  Serial.println(card.errorData(), HEX);
  while(1);
}
/*
 * play recursively - possible stack overflow if subdirectories too nested
 */
void play(FatReader &dir)
{
  FatReader file;
  while (dir.readDir(dirBuf) > 0) {    // Read every file in the directory one at a time

    // Skip it if not a subdirectory and not a .WAV file
    if (!DIR_IS_SUBDIR(dirBuf)
         && strncmp_P((char *)&dirBuf.name[8], PSTR("WAV"), 3)) {
      continue;
    }

    Serial.println();            // clear out a new line

    for (uint8_t i = 0; i < dirLevel; i++) {
       Serial.print(' ');       // this is for prettyprinting, put spaces in front
    }
    if (!file.open(vol, dirBuf)) {        // open the file in the directory
      error("file.open failed");          // something went wrong
    }

    if (file.isDir()) {                   // check if we opened a new directory
      putstring("Subdir: ");
      printEntryName(dirBuf);
      dirLevel += 2;                      // add more spaces
      // play files in subdirectory
      play(file);                         // recursive!
      dirLevel -= 2;
    }
    else {
      // Aha! we found a file that isnt a directory
      putstring("Playing ");
      printEntryName(dirBuf);              // print it out
      if (!wave.create(file)) {            // Figure out, is it a WAV proper?
        putstring(" Not a valid WAV");     // ok skip it
      } else {
        Serial.println();                  // Hooray it IS a WAV proper!
        wave.play();                       // make some noise!

        uint8_t n = 0;
        while (wave.isplaying) {// playing occurs in interrupts, so we print dots in realtime
          putstring(".");
          if (!(++n % 32))Serial.println();
          delay(100);
        }
        sdErrorCheck();                    // everything OK?
        // if (wave.errors)Serial.println(wave.errors);     // wave decoding errors
      }
    }
  }
}

如果一切正常,此时你应该就能够在WAV声音播放模块上接的耳机或者音箱中听到你存储在SD卡中的WAV文件在播放时的效果了。上述Arduino代码在运行的时候会试图通过串口传输一些调试信息,必要的时候可以打开Arduino的串口来查看相应的原因(比如SD卡中没有正常找到)。

· · ·

通过WiFi接入局域网已经是一种越来越常见的方式了,之前我们已经介绍过如何通过有线的方式接入局域,那Arduino是不是也可能通过WiFi以无线的方式接入局域网呢?答案当然是肯定的,就是利用我们这里要介绍的基于SPI接口的WiFi无线通信模块。

要在Arduino中使用该模块,首先我们需要先从https://github.com/asynclabs/WiShield下载相应的库,或者可以直接从这里下载我们测试时所使用的版本。将下载后的库文件解压缩到Arduino(我们测试时使用的是Arduino-0021)安装目录的libraries目录下,然后重命名为WiFi并重新启动Arduino IDE。

为了实现同无线接入点(AP)或者无线路由器(Router)的通信,WiFi模块需要进行相应的配置,包括无线网络ID(SSID)和加密方式等。目前该WiFi模块能够支持WEP、WPA/TKIP-PSK和WPA2/AES-PSK三种常用的加密方式。在开始实验该WiFi模块之前,你需要先了解清楚自己的无线接入点或者无线路由器的配置,我们使用的是一款NETGEAR的无线路由器,采用WPA加密方式,其相应的配置如下图所示:

在下载的WiFi软件包中的examples目录下有一些已经写好的例子,这里我们使用的是WebServer这个程序。WebServer这个例子由两部分组成:webserver.c和WebServer.pde,其中WebServer.pde是我们需要修改的文件,主要是根据无线路由器的设置进行相应的参数调整。实验中我们修改后的代码如下所示:

#include <WiShield.h> 

#define WIRELESS_MODE_INFRA	1
#define WIRELESS_MODE_ADHOC	2

// Wireless configuration parameters ----------------------------------------
unsigned char local_ip[] = {192,168,0,14};	// IP地址
unsigned char gateway_ip[] = {192,168,0,1};	// 网关地址
unsigned char subnet_mask[] = {255,255,255,0};	// 子网掩码
const prog_char ssid[] PROGMEM = {"LOTUS"};	// SSID

unsigned char security_type = 2;	// 0 - open; 1 - WEP; 2 - WPA; 3 - WPA2

// WPA/WPA2 passphrase
const prog_char security_passphrase[] PROGMEM = {"12345678"};	// max 64 characters

// WEP 128-bit keys
// sample HEX keys
prog_uchar wep_keys[] PROGMEM = {	0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,	// Key 0
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Key 1
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Key 2
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // Key 3
				};

// setup the wireless mode
// infrastructure - connect to AP
// adhoc - connect to another WiFi device
unsigned char wireless_mode = WIRELESS_MODE_INFRA;

unsigned char ssid_len;
unsigned char security_passphrase_len;
//---------------------------------------------------------------------------

void setup()
{
	WiFi.init();
}

// This is the webpage that is served up by the webserver
const prog_char webpage[] PROGMEM = {"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<center><h1>Hello World!! WiFi</h1></center>"};

void loop()
{
	WiFi.run();
}

对代码的修改主要集在中如下几个部分:

  • 相关地址和ID,包括IP地址、网关地址、子网掩码和无线网络ID(SSID)
unsigned char local_ip[] = {192,168,0,14};	// IP地址
unsigned char gateway_ip[] = {192,168,0,1};	// 网关地址
unsigned char subnet_mask[] = {255,255,255,0};	// 子网掩码
const prog_char ssid[] PROGMEM = {"LOTUS"};	// SSID
  • 无线网络加密类型
unsigned char security_type = 2;

该参数取值为0时表示不加密,取值为1时表示采用WEP加密方式,取值为2时表示采用WPA/TKIP-PSK加密方式,取值为3时表示采用WPA2/AES-PSK加密方式。

  • 无线网络密码
// WPA/WPA2 passphrase
const prog_char security_passphrase[] PROGMEM = {"12345678"};	// max 64 characters

// WEP 128-bit keys
// sample HEX keys
prog_uchar wep_keys[] PROGMEM = {	0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,	// Key 0
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Key 1
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Key 2
					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // Key 3
				};

其中security_passphrase提供给WPA/WPA2加密方式使用的,而wep_keys则是提供给WEP(128位密钥)加密方式使用的,你需要根据自己WiFi网络的设置,填入相应的参数。我们实验中采用的是WPA加密方式,所以我们只需要对security_passphrase这一参数进行设置。

在将程序下载到Arduino中以后,硬件上的连接相对比较简单,该模块采用的是SPI接口,所以我们可以使用IDC扩展板,使用6芯IDC连接线将模块连接到扩展板上就可以了:

根据所采用的加密方式不同,WiFi模块上电后需要一段初始化的时间,我们可以使用ping命令来检查模块是否已经正常到无线AP或者路由器上:

ping -t 192.168.0.14

一但连接正常,你就可以通过游览器来测试这一WiFi WebServer了:

除了这个WebServer的例子以外,在我们下载的库文件中还有其他一些例子, 有兴趣的话可以逐个运行一下。在实际使用的时候,你可以以这些例子为基础,开发自已所需要的功能。

由于WiFi模块需要对无线AP或者路由器有一定的要求,目前我们所使用的NETGEAR WGR614 v5无线路由器在测试中通过正常通过不加密或者是WPA方式加密的时候正常工作,目前WEP方式还没有通过测试,你在其它无线AP或者路由器上测试时可能会有不同的现象出现;-)

·

Arduino在同Flash和或者Processing这样的软件在完成交互的时候,串口可能是最常用到的方式,但要让这些软件能操作串口有时也不是一件容易的事情。就拿Flash来说吧,出于对安全性的考虑,根本就不允许直接的硬件操作,于是才有人想出了先做一个串口代理,然后再通过XMLSocket将串口收到的数据转发给Flash这样一招。由于中间环节变多,调试和使用起来自然就麻烦一些了。

键盘改造(Keyboard Hack)大概也是互动设计中大家经常使用的一种方式,先将自己的作品实现成能够用键盘进行控制的方式,然后通过对键盘的改造,加入其它的传感器和配套的电路,将传感器的动作转化为键盘的按键动作。这种方案实施的时候可以用Arduino读出传感器的数值,然后控制继电器的开合,模拟键盘上的按键动作,此时对键盘的改造其实就是如何在键盘的按键上加接继电器。

上述键盘改造的方案其实可以借助Arduino的功能进行改进,具体的做法是将Arduino虚拟成一个USB键盘,这样可以在需要的时候让Arduino发出相应的一个或者多个按钮动作给电脑,这样其实可以做到:

  • 在需要的时候(比如定时)给电脑输入一串字母,效果和人真正在键盘上输入是一样的。
  • 将Arduino检测到的传感器动作转换成一个或者一系列按键动作,然后在Flash中处理这些按键动作。

按照上面的思路, 我们用Arduino开发了一个USB虚拟键盘的原型。该原型的主体是一块Arduino,用来完成对USB协议的模拟,以便电脑能够将其实别成一个普通的USB键盘,此外还因为偷懒用到了USBtinyISP上的USB接口,这样可以少焊一些东西;-)

在将相应的程序下载到Arduino之后,接上USBtinyISP上的那个USB接口(注意不是Arduino板上的),电脑就会识别出一个通用的USB键盘出来。接下来的事情就是如何在Arduino中运行的程序里进行相应的USB数据包的发送,也就是对按键动作的模拟。这里我们实现的一个功能是在该原型上接了7个红外开关,分别用来模拟A、B、C、D、E、F、G这7个键被按下的动作。或者换句话说,当第一个红外开关被挡住的时候,将等价于键盘上的A键被按下的动作,而当第二个红外开关被挡住的时候,则等价于键盘上的B键被按下的动作,依次类推。

测试时实际运行时的效果还算理想,虽然目前还不能实现完全的定制,但最核心的键盘功能已经可以被模拟出来了。

测试时发现的一个问题是有关于USB设备初始化的,如果红外开关在上电的时候就被挡住的话,有可能会导致虚拟出来的USB键盘无法被Windows所识别。最初以为是电源的问题,但是将传感器分别供电后依然没有改观,目前怀疑是在初始化的过程中,接在这些红外开关上的单片机引脚上实际是有相应的电流的,这有可能会导致该问题的产生。解决办法有两个:一是先不要给红外传感器供电,当键盘设备被电脑识别出来后再给传感器供电;二是在上电过程中不要阻挡红外传感器。目前我只测试了红外开关这样一种传感器,可能并不是在所有的情况下都会出现这样的问题。

另外一个还没有弄明白的问题是需要在初始化的时候,将输入引脚设置成高,估计可能是对AVR引脚上的内部上拉电阻进行设置,有空了去查一下AVR的相应资料看看;-)

No tags

在近距离无线通信中,蓝牙是一种我们非常熟悉的方式,现今很多智能电子设备都配置了蓝牙的功能,以方便同其它设备(特别是电脑)之间的通信。由于蓝牙协议相对比较复杂,要想在Arduino上直接支持蓝牙的全部功能目前可能还比较麻烦,但如果只是要实现同其它设备之间的数据通信,那可以考虑一下这里介绍的蓝牙串口模块。

所谓蓝牙串口模块,可能简单地理解为传统串口连接线的一种无线替代方案。举个例子来说,如果我们要在Arduino和计臬机间实现基于蓝牙的无线通信,就可以先在Arduino上连接一个蓝牙串口模块,然后再在计算机上接一个蓝牙适配器,最后通过Arduino提供的串口操作函数,就可以很方便地向计算机发送数据,或者接收从计算机那端发送过来的数据。

这里介绍的蓝牙串口模块可以配合Arduino专用传感器扩展板和相应的连接线使用,硬件连接上只需要用COM连接线将蓝牙串口模块上的接口,与Arduino传感器扩展板上的COM口连接起来就可以了,非常简单:

注意使用的时候,蓝牙串口模块上的模式选择开关(MS)要设置到正常模式(NM)位置处:

这里我们将实验的是如何在计算机和Arduino实现蓝牙通信,这种情况下蓝牙串口模块处于从模式工作状态下,而机算机则处于主模式工作状态下。首先我们要在计算机上安装相应的软件,来对周围所有的蓝牙进行管理,经过试用后我们发现,IVT BlueSoleil无疑可能是最佳的选择,大家可以通过BlueSoleil网站下载相应的适用版本。

在安装好BlueSoleil软件后,在计算机的USB口上插入蓝牙适配器,蓝牙适配器的作用主要是完成同蓝牙串口模块的通信,其稳定性对蓝牙通信的质量影响非常大,所以可能的话尽量找配套或者经过测试的蓝牙适配器:

接上蓝牙适配器之后,如果硬件被正确识别,我们会发现任务栏右下角的蓝牙图标会变成蓝色,单击该图标后从弹出的菜单中选择“浏览蓝牙位置”菜单项:

在打开的“蓝牙位置”窗口中双击“搜索设备”图标,查找附近的蓝牙设备:

如果一切正常,我们就可以看到查找到的蓝牙串口模块:

双击查找到的蓝牙设备后,系统会自动录找该设备所对应的服务,你也可以直接双击“搜索服务”来强制系统查找该设备所提供的服务。对于蓝牙串口模块这一电子积木来讲,只提供了“蓝牙串口”这一种服务:

双击“蓝牙串口” 图标,系统会试图同蓝牙串口模块建立起连接,此时会弹出一个对应框要求输入相应的匹配密码,该密码出厂时默认为1234:

连接成功之后,“蓝牙串口”图标会变成绿色的,同时会显示出相应的串口编号(如COM45):

此时蓝牙串口模块上标为L的灯会长亮,表示连接已经正常建立,可以进行数据通信了。余下的工作其实相对已经非常容易了,按照前面的介绍,其实我们就是只需要在Arduino上执行相应的串口操作命令就可以了。下面是我们测试时所用到的代码:

int val = 0;
int ledPin = 13;
void setup()
{
  Serial.begin(9600);
   Serial.println("Started");
}

void loop()
{
  val = Serial.read();
  if (-1 != val) {
    if ('A' == val || 'a' == val) {
      Serial.println("Hello from Arduino!");
    }else if ('C' == val || 'c' == val) {
      digitalWrite(ledPin, HIGH);
      delay(500);
      digitalWrite(ledPin, LOW);
    }
  }
}

接下来就是将相应的代码下载到Arduino里并运行了。在使用标准版本的Arduino时请注意,由于Arduino只有一个串口,因此在下载时请先断开蓝牙串口模块,等程序下载进去之后再接上蓝牙串口模块(需要同时用到多个串口的话可以考虑使用Arduino MEGA)。测试时尽量使用SSCOM这样的串口调试工具来进行测试,注意连接时使用的波特率应该设置为9600,8N1:

实验中发现有的软件无法正常打开蓝牙串口进行读取(比如Arduino IDE 0018),此时可以将蓝牙串口的自动连接属性去掉,办法是在找到的蓝牙串口上右键单击,然后从弹出的菜单中选择“属性”菜单项:

接着从弹出的属性对话框中选择“选项”页,去掉“自动选择”选项框:

最后说明一点,该蓝牙串口模块的一些参数(如配对密码)是可能通过AT命令进行设置的,因此设计了一个模式选择(MS)开关,使用它可以让柳模块在上电的时候进入命令模式(AT)或者正常模式(NM)。

· ·

对于大部分简单的传感器来讲,通常有三根连接线就足够了:地GND、电源和信号,为此我们设计了这一通用传感器模块:

该模块左侧的黑色座可以通过传感器连接线连接到传感器扩展板,右侧的绿色座则可以用来连接传感器,比如下面的红外反射开关:

这样我们就可以将这些传感器连接到传感器扩展板上了,从而进一步做到同Arduino的连接:

No tags

<< Latest posts

Older posts >>

Theme Design by devolux.nh2.me