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

Arduino中文社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 4590|回复: 2

CurieNano 直接播放SPI 中的声音

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


播放音频的这个大目标可以分解为两个个小目标:
一.      将歌曲信息存储到 SPI NOR 中;
二.      从SPINOR读取歌曲信息,按照原始的采样率播放出来
下面就是每一步的做法。
将歌曲存储到 SPI NOR
首先,需要将音频的编码转化为8000Hz,8Bits。通常来说8000Hz 是人类感知音频旋律的下限,低于这个采样率,人们能够明显感知到与原始旋律存在差别。笔者使用的是免费
的“Switch Sound File Converter”这款软件。设置输出为 8000Hz,8 Bits 单声道的 PCM 未压缩格式。
image002.png

接下来是要提取转化后的音频信息。对于单声道的WAV 文件来说,文件头是非常小的,完全可以和数据混合在一起而不需要特别处理。当然,在这步还需要计算一个大概的时间从而得到是否需要裁减文件大小。这步结束后,得到的是一个二进制文件。
下面的目标是将这个二进制存入SPI NOR中。这个步骤也是整个代码中最复杂和繁琐的部分。在 Arduino 101 中,自带了CurieSerialFlash 库,可以完成 SPI 的很多操作。
在执行代码前,需要运行示例文件中的 EraseEveryThing 将SPINOR 内容清除干净。SPINOR 的特性要求只有擦除之后才能写入。后面的代码中,如果遇到创建程序错误,不妨先考虑一下是不是忘记擦除了。
在这里,PC 上需要运行一个上位机程序,通过串口将数据发送给  Arduino 101。101 上运行的代码负责接收和写入SPINOR 中。为了让上下位机配合工作起来,这里还设计了一个简单的传输协议(理论上用 ZMODEM 之类的协议是最好的,但是会导致整体复杂度升高很多)。
协议很简单,传输分为两个阶段。第一个阶段是下位机等待上位机发送4字节长的文件大小;收到之后,下位机发送字符“N”通知上位机继续发送;然后就开始了第二阶段的传输,上位机将数据“打包”成一个带有顺序号码以及校验和的包。下位机收到之后进行检查,如果不正确,下位机发送“R”通知上位机重新发送;如果正确,下位机发送“N”通知上位机继续发送。重复着一个过程直到数据传输完毕。
下位机代码:
[kenrobot_code]#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[BUFFERSIZE];

//文件大小
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[BUFFERSIZE];
  int i;
  byte checksum;
Next:
  //接收数据的字节数
  int counter=0;
  //接收超时的变量
  unsigned long elsp=millis();
  //如果接收数量小于缓冲区大小并且未超时,那么继续接收
  while ((counter<BUFFERSIZE)&&(millis()-elsp<TIMEOUT))
    {
      while (Serial.available()>0)
        {
          buffer[counter]=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

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

[mw_shl_code=pascal,true]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[0..BUFFERSIZE-1] 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[0]:=stream.size and $FF;
      Buffer[1]:=(stream.size shr 8) and $FF;
      Buffer[2]:=(stream.size shr 16) and $FF;
      Buffer[3]:=(stream.size shr 24) and $FF;
      ComPort1.Write(Buffer,4);
      Memo1.Lines.Add(IntToStr(Buffer[0])+'  '+IntToStr(Buffer[1])+'  '+IntToStr(Buffer[2])+'  '+IntToStr(Buffer[3]));
      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[Count]='N') or ((s[Count]='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[BUFFERSIZE-2]:=checksum;
           //放置顺序号
           Buffer[BUFFERSIZE-1]:=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[Count]='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.[/mw_shl_code]
配合上位机程序,把 1990.bin 存储到 CuireNano的SPI NOR中。
image003.png
SPINOR读取歌曲信息,按照原始的采样率播放出来
同样使用CurieSerialFlash库,使用 readfile读取音频信息,再用 PWM Pin进行输出:
[mw_shl_code=bash,true]#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[BUFFERLEN];

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[j]);
                        delayMicroseconds(125);
                }
      analogWrite(OUTPIN, buffer[BUFFERLEN-1]);
   }  
  file.seek(0);
}[/mw_shl_code]

从前面的音频格式转换过程可以得知,应该用 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

 楼主| 发表于 2017-9-20 21:21 | 显示全部楼层
发表于 2017-9-21 08:46 | 显示全部楼层
虽然看不懂  还是给楼主点赞哈哈哈
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|Archiver|手机版|Arduino中文社区

GMT+8, 2024-11-28 07:46 , Processed in 0.159761 second(s), 18 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表