CurieNano 直接播放SPI 中的声音-Arduino中文社区 - Powered by Discuz! Archiver

Zoologist 发表于 2017-9-20 21:19

CurieNano 直接播放SPI 中的声音

大多数 Arduino 都不支持 DAC,但是都会支持 PWM。理论上有足够大存储空间的开发板都可以用来直接播放声音,从原理上来说就是将PWM 输出当作是可变的模拟信号,通过PWM 间接实现了 DAC 的功能。之前介绍过用Curie Nano 直接播放声音【参考1】,这次将音频信息存储在板子上的SPINOR中 。Curie Nano板载SPI 芯片的大小是2MB 和主控的存储空间相比大了很多。根据估计,最多可以存储 2*1024K/8K=256S的音频信息。基本上可以存储一首歌曲。

播放音频的这个大目标可以分解为两个个小目标:一.      将歌曲信息存储到 SPI NOR 中;二.      从SPINOR读取歌曲信息,按照原始的采样率播放出来下面就是每一步的做法。将歌曲存储到 SPI NOR 中首先,需要将音频的编码转化为8000Hz,8Bits。通常来说8000Hz 是人类感知音频旋律的下限,低于这个采样率,人们能够明显感知到与原始旋律存在差别。笔者使用的是免费的“Switch Sound File Converter”这款软件。设置输出为 8000Hz,8 Bits 单声道的 PCM 未压缩格式。
接下来是要提取转化后的音频信息。对于单声道的WAV 文件来说,文件头是非常小的,完全可以和数据混合在一起而不需要特别处理。当然,在这步还需要计算一个大概的时间从而得到是否需要裁减文件大小。这步结束后,得到的是一个二进制文件。下面的目标是将这个二进制存入SPI NOR中。这个步骤也是整个代码中最复杂和繁琐的部分。在 Arduino 101 中,自带了CurieSerialFlash 库,可以完成 SPI 的很多操作。在执行代码前,需要运行示例文件中的 EraseEveryThing 将SPINOR 内容清除干净。SPINOR 的特性要求只有擦除之后才能写入。后面的代码中,如果遇到创建程序错误,不妨先考虑一下是不是忘记擦除了。在这里,PC 上需要运行一个上位机程序,通过串口将数据发送给Arduino 101。101 上运行的代码负责接收和写入SPINOR 中。为了让上下位机配合工作起来,这里还设计了一个简单的传输协议(理论上用 ZMODEM 之类的协议是最好的,但是会导致整体复杂度升高很多)。协议很简单,传输分为两个阶段。第一个阶段是下位机等待上位机发送4字节长的文件大小;收到之后,下位机发送字符“N”通知上位机继续发送;然后就开始了第二阶段的传输,上位机将数据“打包”成一个带有顺序号码以及校验和的包。下位机收到之后进行检查,如果不正确,下位机发送“R”通知上位机重新发送;如果正确,下位机发送“N”通知上位机继续发送。重复着一个过程直到数据传输完毕。下位机代码:#include <SerialFlash.h>
#include <SPI.h>

//文件名
const char *filename = "1990.bin";

//缓冲大小,可以根据实际情况调整越大效率越高
#define BUFFERSIZE 16
//发送操作的超时设定
#define TIMEOUT 3000UL

//Arduino 101 上 SPI CS 是 21Pin
const int FlashChipSelect = 21;

SerialFlashFile file;
char buffer;

//文件大小
unsigned long filesize=0;
byte *p=(byte *)&filesize;
unsigned long total=0;

void setup() {
//收到的文件大小字节数,一共4位
byte c=0;
//每次收到的数值
byte r;

Serial.begin(115200);

//使用 Arduino 硬串口输出 Debug 信息
Serial1.begin(115200);
Serial1.println("Hello, world?");
   
//初始化 SPI 芯片
if (!SerialFlash.begin(ONBOARD_FLASH_SPI_PORT, ONBOARD_FLASH_CS_PIN)) {
      Serial1.println("SPI init fail");
      while (1); }   

//如果没有收够4字节,则一直等待接收
while (c<4) {
    while (Serial.available() > 0)
      {
      //比如文件大小为 0x10000, 那么上位机发出来的顺序是
      // 00 00 01 00, 因此这里需要一个顺序的调换
      r=Serial.read();
      *(p+c)=r;
      c++;
      } //while (Serial.available() > 0)
} //while (c<4)

Serial1.print("filesize=");Serial1.println(filesize);
if(SerialFlash.create(filename, filesize)!= true)
{
         Serial1.println("Can't create file!");
         while (1) {};
    }
Serial1.println("Create file success!!");
// Open the file
file = SerialFlash.open(filename);
   
//通知上位机继续发送
Serial.print('N');
}

void loop() {
byte buffer;
int i;
byte checksum;
Next:
//接收数据的字节数
int counter=0;
//接收超时的变量
unsigned long elsp=millis();
//如果接收数量小于缓冲区大小并且未超时,那么继续接收
while ((counter<BUFFERSIZE)&&(millis()-elsp<TIMEOUT))
    {
      while (Serial.available()>0)
      {
          buffer=Serial.read();
          counter++;
      } //while   
    }   
//如果接收数量不足退出上面的循环
if (counter!=BUFFERSIZE) {
    //通知上位机重新发送
    Serial.print("R");
    Serial1.print("R");
    goto Next;      
}

//检查接收到的数据校验和
checksum=0;
for (i=0;i<BUFFERSIZE-1;i++)
    {
      checksum=checksum+buffer;
      //Serial.print(buffer);
      //Serial.print("   ");
       //Serial1.print(buffer);
       //Serial1.print("   ");
    }
//校验失败通知上位机重新发送
if (checksum!=0){
       Serial.print("R");
       Serial1.print("R");
       goto Next;
    }
//写入文件
file.write(buffer, BUFFERSIZE-2);
//如果当前收到的总数据小于文件大小,那么要求上位机继续发送
if (total<filesize)
    {
      Serial.print('N');
      //有效值只有 BUFFERSIZE-2
      total=total+BUFFERSIZE-2;
      Serial1.print("N");
    }
    //否则停止
    else {
         Serial1.print("Total received");
         Serial1.print(total);
      while (1==1) {}
    } //else

}上面的代码中,使用的是 16 Bytes 作为串口缓冲,一次传输的有效数据只有16-2=14个,效率为(16-2)/16=87.5%,如果觉得慢,可以尝试加大缓冲,这样能够明显加快速度。对应发送数据的上位机代码如下:
unit Unit2;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, CPortCtl, CPort;
//每次发送的数据大小为 BUFFERSIZE - 2
//其余2个分别是 Checksum 和 order
Const
BUFFERSIZE=16;

type
TForm2 = class(TForm)
    ComPort1: TComPort;
    Button1: TButton;
    Button2: TButton;
    OpenDialog1: TOpenDialog;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure ComPort1RxChar(Sender: TObject; Count: Integer);
private
    { Private declarations }
public
    { Public declarations }
end;

var
Form2: TForm2;
stream: TFileStream;
BytesSend:integer;
Index:BYTE;
Buffer:array of byte;   //发送文件的缓存
implementation

{$R *.dfm}

procedure TForm2.Button1Click(Sender: TObject);
begin
    ComPort1.ShowSetupDialog;
end;

procedure TForm2.Button2Click(Sender: TObject);
begin
if OpenDialog1.Execute then
    begin
      Memo1.Lines.Add(OpenDialog1.FileName);
      stream := TFileStream.Create(OpenDialog1.FileName,fmOpenRead);
      Memo1.Lines.Add(IntToStr(stream.size)+' Bytes');
      ComPort1.Open;
      Memo1.Lines.Add('Connected!');
      //发送文件大小
      Buffer:=stream.size and $FF;
      Buffer:=(stream.size shr 8) and $FF;
      Buffer:=(stream.size shr 16) and $FF;
      Buffer:=(stream.size shr 24) and $FF;
      ComPort1.Write(Buffer,4);
      Memo1.Lines.Add(IntToStr(Buffer)+''+IntToStr(Buffer)+''+IntToStr(Buffer)+''+IntToStr(Buffer));
      BytesSend:=0;
      Form2.Refresh;
   end;
end;

procedure TForm2.ComPort1RxChar(Sender: TObject; Count: Integer);
var
s,t: String;
aSize,i:integer;
checksum:byte;
begin
ComPort1.ReadStr(s, Count);
if (s='N') or ((s='R')and(BytesSend=0)) then
    begin
      Memo1.Lines.Add('Received N');
      if BytesSend<stream.size then
         begin
         fillchar(Buffer,BUFFERSIZE,0);
         // 读取文件并取得实际读取出来的长度
         aSize:=stream.Read(Buffer,BUFFERSIZE-2);
         //计算Checksum, 就是 Buffer 第一个到倒数第二个加起来要求为0
         checksum:=0;
         for i := 0 to BUFFERSIZE-3 do
             begin
               checksum:=checksum-Buffer;
             end;
         //放置Checksum
         Buffer:=checksum;
         //放置顺序号
         Buffer:=Index;
         inc(Index);
         ComPort1.Write(Buffer,BUFFERSIZE);
         {t:='';
         for i := 0 to BUFFERSIZE-1 do
             begin
               t:=t+ IntToHex(Buffer,2)+' ';
             end;
            Memo1.Lines.Add(t);
         }
         BytesSend:=BytesSend+aSize;
         Form2.Caption:=IntToStr(BytesSend)+'/'+IntToStr(stream.size);
         Form2.Refresh;
         end
      else
         Memo1.Lines.Add('Completed!');
    end;
//如果收到 R 就再次发送
if s='R' then
    begin
      ComPort1.Write(Buffer,BUFFERSIZE);
      {   t:='';
         for i := 0 to BUFFERSIZE-1 do
             begin
               t:=t+ IntToHex(Buffer,2)+' ';
             end;
            Memo1.Lines.Add(t);
      }
      Memo1.Lines.Add('R');
    end;

end;

procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if Stream<>NIL then stream.Destroy;
ComPort1.Close;
end;

end.配合上位机程序,把 1990.bin 存储到 CuireNano的SPI NOR中。从SPINOR读取歌曲信息,按照原始的采样率播放出来同样使用CurieSerialFlash库,使用 readfile读取音频信息,再用 PWM 的Pin进行输出:#include <CurieSerialFlash.h>
#include <SPI.h>

#define OUTPIN 3
#define BUFFERLEN 1024
const int FlashChipSelect = 21; // digital pin for flash chip CS pin
SerialFlashFile file;
byte buffer;

void setup() {

Serial.begin(115200);

if (!SerialFlash.begin(ONBOARD_FLASH_SPI_PORT, ONBOARD_FLASH_CS_PIN)) {
    Serial.println("Unable to access SPI Flash chip");
}
file = SerialFlash.open("1990.bin");
analogWriteFrequency(OUTPIN, 64000);

}
void loop() {

for (uint32_t i=0;i<file.size() / sizeof(buffer);i++)
   {
      file.read(buffer,BUFFERLEN);
      for (unsigned int j = 0; j < BUFFERLEN-1; j++)
                {
                        analogWrite(OUTPIN, buffer);
                        delayMicroseconds(125);
                }
      analogWrite(OUTPIN, buffer);
   }
file.seek(0);
}
从前面的音频格式转换过程可以得知,应该用 8000Hz 的速度进行输出,读取文件速度如果无法跟上那么就会出现中断卡顿的问题。为了解决研究这个问题,使用示波器进行简单的实验。实验的方法是:读取之前设置GPIO 为高,读取完成之后设置 GPIO 为低。测试结果如下:1.   file.read(buffer,16);2.   file.read(buffer,64);3.   file.read(buffer,128);最终,选择每次从SPINOR中取出 1024 Bytes,这个操作会花费将近100us,相应的,代码在读取之后省去一个125us的Delay。
在播放的时候,直接用D3播放声音比较小,还可以在喇叭前面添加诸如 LM358 用来放大声音。
工作视频:
https://v.qq.com/x/page/r0549d1loji.html

Zoologist 发表于 2017-9-20 21:21

<embed src="https://imgcache.qq.com/tencentvideo_v1/playerv3/TPout.swf?max_age=86400&v=20161117&vid=r0549d1loji&auto=0" allowFullScreen="true" quality="high" width="480" height="400" align="middle" allowScriptAccess="always" type="application/x-shockwave-flash"></embed>

单片机菜鸟 发表于 2017-9-21 08:46

虽然看不懂还是给楼主点赞哈哈哈
页: [1]
查看完整版本: CurieNano 直接播放SPI 中的声音