``摘要:针对“ 影音娱乐”的挑战主题,设计了一款基于Arty的WiFi电子钢琴。利用普通电脑键盘的104个按键代替钢琴的88个音符键,不同的按键对应不同的音符。FPGA驱动键盘与WiFi模块,Qt建立服务器并控制音响播放音符文件。FPGA通过WiFi模块与远端服务器和音响建立连接,用户可以感受“无线钢琴”的体验。
关键词:Arty;WiFi;电子钢琴;服务器;
首先看一下演示视频:
另外,本设计报告的技术要点包括:1 )Basys3驱动键盘获取键值并发送;2)Arty-Board接收键值并控制WiFi模块;3)QT编写服务器并根据键值播放音符。由于本挑战赛主要针对Arty开发板,所以详细阐述“2)Arty-Board接收键值并控制WiFi模块”,其余两个技术要点不做重点,只作介绍并共享源码。
0 引言你是否爱音乐,是否有个钢琴梦,但又因动辄上万元的价格止住思绪。没关系,让我们来DIY一个电子钢琴,还是无线的哦!本设计通过电脑键盘,代替钢琴键盘,实现88个音符的无线演奏。 在进行本设计之前,我们必须有一些Arty和Basys3的开发功底。好在,Digilent官网已经为我们提供了基础开发例程。所以,如果你是一个新手,那么一定要先看下面的例程。 Arty-Board例程(教程链接):
Basys3例程(教程链接): 学会善用Digilent和Xilinx的开放资源,可以加快我们的开发速度。
1 硬件设计1.1 系统框图
图1 系统框图 如图1 所示,首先将键盘通过USB接口连接Basys3(因为Arty没有USB Host的接口啊,所以借助了Basys3),Basys3驱动键盘、读取键值,并利用串口通信将键值传递给Arty。Arty拿到键值之后,再通过WiFi模块传输到远端服务器。服务器使用的是家用计算机,利用Qt编写了TCP通信的程序,使之与Arty端的WiFi模块建立连接。上位机方面,制作了88个音符wav文件,利用QSound类根据键值播放对应的音符WAV文件。用户端敲击键盘,计算机端播放音符,从而实现了WiFi电子钢琴的功能。 1.2 硬件原料如图2所示,从左至右依次为:音响、Arty、WiFi模块,Basys3、键盘。
图2 原料 1.3 WiFi模块原理图及PCB(附件)WiFi模块采用的是ESP8266,一直蛮受欢迎的一款WiFi芯片。芯片原厂是上海乐鑫,经深圳安信可封装成模块后二次出售。可以通过AT命令或SDK开发。蛮简单的一个板子,原理图和PCB如图3和图4,大家见笑了。
图3 WiFi模块原理图
图4 WiFi模块PCB 2 软件设计2.1 Basys3驱动Key-Board将键盘连接Basys3的USB HID,在USB HID的下方有个微控制器PIC24FJ128GB106 ,在为PIC24FJ128GB106 的内部固化了USB转PS2信号的固件,所以连接FPGA芯片的并不是USB信号,而是PS2信号。 程序代码以共享,见附件。
图5 代码结构图
图6 顶层文件 图5为代码的结构图,主要分为两个部分:1)PS2信号的解析;2)串口发送。我们打开顶层文件top.v,如图6,可以看到共有3个输入信号,1个输出信号。输入信号分别为clk(控制时钟),PS2Data和PS2Clk,输出信号为tx。FPGA在程序中,通过PS2Data和PS2Clk两个信号解析出键值,和“按下”“松开”的状态。将键值通过tx信号发送出去。所以Basys3与Arty之间就是通过tx信号线相连。 具体代码无法仔细分析,大家看一下我共享出来的代码吧。 2.2 Arty通过MicroBlaze软核接收键值、控制WiFi按步骤依次来吧: 1)打开Vivado,创建新工程
2)键入工程名和路径。注意不能有中文和空格!!然后,点击Next.
3 )选择RTL Project,勾选Do not specify sources at this time
4)选择Boards,板卡列表中选择Arty,再点击Next。注意:如果你没有发现Arty这个选项,说明你没有安装Digilent板卡文件。因为,Vivado下载安装后,Boards文件只包含Xilinx的板卡。Arty属于第三方Digilent公司生产的开发板,所以不包含在Vivado中。 那么怎么办呢? 很简单,请仔细阅读本文的引言部分:2)如何安装Digilent板卡文件到Vivado安装路径。
5)然后就可以看到项目预配置的信息,点击Finish即可。 6)创建新的Block Design,在左侧导航栏点击Create Block Design,在弹出的菜单中输出设计名称为system(可随意).
创建完成。
7)在Block Design右下角点击Board,然后就可以看到很多为Arty定制的IP核,这些IP核囊括了Arty板子上所有的外设资源。
8)首先拖拽System Clock到空白区域,Vivado会自动连接
9)双击时钟IP,配置输出clk_out1为166.667 MHz,clk_out12为200 MHz。选择Reset Type为Active Low,即低电平有效。
10)拖拽出DDR3 SDRAM。
11)删除clk_ref_i和sys_clk_i。
12)做下图所示的连接
13)点击上方的Run Connection Automation。注意:自动连接之后会自动产生resetn的输入引脚,要将其连接Clocking Wizard核的resetn输入脚。务必连接,否则系统不工作。
14)点击Add IP添加MicroBlaze核
15)点击Run Block Automation,打开MicroBlaze控制菜单。Local Memory设为32KB,Cache Configuration设为16KB。还有务必选择Clock Connection为/mig_7series_0/ui_clk,点击OK。
16)在Board中选择USB UART,添加到Block Design。
17)点击Run Connection Automation,勾选All Automation复选框,点击OK。点击Regenerate Layout可以调整视图,或者在空白处点击向左拖拽。
18)点击Validate Design来检查是否有连接错误 19)确认无误后,回到Source界面。
20)右击Block Design的文件名system,在快捷菜单中点击Create HDL Wrapper,切记勾选Let Vivado manage wrapper and auto-update,并点击OK。
21)由于默认的USB UART连接Arty Board的USB,但我们希望用这个UART连接WiFi,所以就需要重新建立一个XDC的管脚约束文件。 快捷键ALT+A,弹出菜单后选择Add or create constraints,点击next。
点击Create File,然后在弹出的菜单中输入文件名,点击OK即可。
编辑新建的XDC管脚约束文件,添加以下内容即可。 set_property PACKAGE_PIN F3 [get_ports u***_uart_rxd] set_property PACKAGE_PIN D13 [get_ports u***_uart_txd] set_property IOSTANDARD LVCMOS33 [get_ports u***_uart_txd] set_property IOSTANDARD LVCMOS33 [get_ports u***_uart_rxd] 22)点击Generate Bitstream,这会耗费一定的时间。 23)在主工具栏的File菜单下选择 Export→Export Hardware。勾选Include Bitstream,并点击OK。这会将我们的Block Design作为BSP导出到Xilinx SDK中。
23)在File菜单下点击Launch SDK。
24)在新打开的Xilinx SDK中可以看到这样的界面
25)创建一个新的 Xilinx→Application Project,输入项目名称,选择硬件平台,点击Next。在下一级菜单中选择Empty Application,点击Finish即可。
26)接下来就是C程序编程了,东西比较多,无法细讲,比如下面三个经常用到的头文件。因为我用的是串口WIFI模块,所以SDK中C程序主要就是串口的接收、发送和中断。我把源码分享出,大家感兴趣的可以下载下来看看。 #include "xparameters.h" #include "xuartlite.h" #include "microblaze_sleep.h"
2.3 服务器端Qt编程上位机端的程序主要包括两点: 1)服务器的搭建: 由于本设计,即电子钢琴是通过WIFI来进行键值无线传输的。为保证减值传输的可靠性,这里采用TCP/IP通信的手段。Arty板卡连接的WIFI模块作为客户端,计算机的网卡作为服务器端。 2)音符的播放: 网上可以下载到钢琴88键的WAV音频文件,这里通过WIFI传输的减值不同,播放不同的音频文件。 因为这次是FPGA的挑战赛,所以我也不好意思在这里臭显摆QT的编程知识。懂的人都知道并不难,使用QT的QTcpserver,QTcpclient,Qsound,QDir类便可以实现上述的功能。源码同样共享,感兴趣的人可以下载下来看看。
开发环境:Qtcreator5.7.01. 打开Qtcreator,选择Application -> Qt Widgets Application,点击选择。
2. 设置项目名称为MyTcpServer,设置系项目路径,点击下一步
3. 然后选择编译工具软件预装了Qt5.7.0 MSVC2013-64bit版本的qt kit。值得一提的是,我这里添加了一个Qt5.7-Static,这是我自己静态编译的一个qt kit。那么二者有什么不同呢?是这样的,用前者编译链接生成的exe可执行文件,只能在本机运行(或者说只能在具备Qt5.7.0的依赖库的计算机上运行);而通过后者编译链接生成的exe可执行文件,可以在任何Windows操作系统中运行,无需依赖库。这是因为后者将项目所需的依赖库编译到了exe本体中。 Qt的强大还不止于此,它生成的exe可执行文件可以运行在windows,linux,android等系统的硬件平台上,只要你设置了对应版本的编译器。
4. 程序源码已经在共享在本文末尾的附件中,我们来看一下程序中有哪些文件吧。如下图,需要用到的文件其实并不多,因为Qt已经帮助开发者“造好了很多轮子”,用户只需要负责“驾驶”就行了。不多说,直接看代码,开车了。
---MyTcpSercer.pro 该文件是项目的配置文件,里面包含了系统的很多make信息,如下图。Core gui是qt最基本的界面组件。Network提供了网络支持,因为我们用到了QTcpServer和QTcpSocket类来进行WiFi无线通信,所以必须添加Qt += network。Multimedia提供了多媒体影音的支持,因为我们用到了QSound类来播放音符文件,所以必须添加Qt += multimedia。
- #-------------------------------------------------
- #
- # Project created by QtCreator 2017-03-20T18:54:40
- #
- #-------------------------------------------------
- QT += core gui
- QT += network
- QT += multimedia
- greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
- TARGET = MyTcpServer
- TEMPLATE = app
- SOURCES += main.cpp
- mytcpserver.cpp
- HEADERS += mytcpserver.h
- FORMS += mytcpserver.ui
复制代码
---mytcpserver.ui 该文件是项目的界面文件,所以用ui后缀来命名。如下图,界面包括3部分,首先是接收窗口:这里将Arty传输过来的“键值-音符”信息显示在空白处。其次是发送窗口,在TextEditor中输入需要发送的数值,点击“发送”按钮便可以将消息发送给Arty。最后是网络设置串口,也是最重要的一个环节。这里的本机IP地址可以在程序运行时自动获取,本地端口号可以随意设置,点击监听之后,便可以接收Arty的连接请求。
---mytcpserver.h 该文件是Qt主类的头文件,里面包含了Tcp/Ip的用户自定义函数原型声明。以及界面操作的“信号-槽函数”。多说一句,“信号与槽”的机制真的是很方便,优雅而实用。
- #ifndef MYTCPSERVER_H
- #define MYTCPSERVER_H
- #include <QMainWindow>
- #include <QTcpServer>
- #include <QTcpSocket>
- #include <QNetworkInterface>
- #include <QMessageBox>
- #include <QDir>
- #include <QSound>
- #include <QDebug>
- namespace Ui {
- class MyTcpServer;
- }
- class MyTcpServer : public QMainWindow
- {
- Q_OBJECT
- public:
- explicit MyTcpServer(QWidget *parent = 0);
- ~MyTcpServer();
- private:
- Ui::MyTcpServer *ui;
- QTcpServer *tcpServer;
- QList<QTcpSocket*> tcpClient;
- QTcpSocket *currentClient;
- QDir *dir;
- private slots:
- void NewConnectionSlot();
- void disconnectedSlot();
- void ReadData();
- void on_btnConnect_clicked();
- void on_btnSend_clicked();
- void on_btnClear_clicked();
- };
- #endif // MYTCPSERVER_H
复制代码
---mytcpserver.cpp 该源文件中包含了mytcpserver.h中所有函数的具体实现。
- #include "mytcpserver.h"
- #include "ui_mytcpserver.h"
- MyTcpServer::MyTcpServer(QWidget *parent) :
- QMainWindow(parent),
- ui(new Ui::MyTcpServer)
- {
- ui->setupUi(this);
- tcpServer = new QTcpServer(this);
- ui->edtIP->setText(QNetworkInterface().allAddresses().at(1).toString()); //获取本地IP
- ui->btnConnect->setEnabled(true);
- ui->btnSend->setEnabled(false);
- connect(tcpServer, SIGNAL(newConnection()), this, SLOT(NewConnectionSlot()));
- }
- MyTcpServer::~MyTcpServer()
- {
- delete ui;
- }
- // newConnection -> newConnectionSlot 新连接建立的槽函数
- void MyTcpServer::NewConnectionSlot()
- {
- currentClient = tcpServer->nextPendingConnection();
- tcpClient.append(currentClient);
- ui->cbxConnection->addItem(tr("%1:%2").arg(currentClient->peerAddress().toString().split("::ffff:")[1])
- .arg(currentClient->peerPort()));
- connect(currentClient, SIGNAL(readyRead()), this, SLOT(ReadData()));
- connect(currentClient, SIGNAL(disconnected()), this, SLOT(disconnectedSlot()));
- }
- // 客户端数据可读信号,对应的读数据槽函数
- void MyTcpServer::ReadData()
- {
- // 由于readyRead信号并未提供SocketDecriptor,所以需要遍历所有客户端
- for(int i=0; i<tcpClient.length(); i++)
- {
- QByteArray buffer = tcpClient[i]->readAll();
- if(buffer.isEmpty()) continue;
- static QString IP_Port, IP_Port_Pre;
- IP_Port = tr("[%1:%2]:").arg(tcpClient[i]->peerAddress().toString().split("::ffff:")[1])
- .arg(tcpClient[i]->peerPort());
- // 若此次消息的地址与上次不同,则需显示此次消息的客户端地址
- if(IP_Port != IP_Port_Pre)
- ui->edtRecv->append(IP_Port);
- ui->edtRecv->append(buffer);
- //更新ip_port
- IP_Port_Pre = IP_Port;
- //播放音符
- for(int i=0; i<buffer.length(); i++)
- {
- qDebug() << buffer.at(i);
- switch(buffer.at(i))
- {
- //主键盘区 第一行
- case 0x16:QSound::play(dir->currentPath().append("WavPiano52-C.wav"));break;
- case 0x1E:QSound::play(dir->currentPath().append("WavPiano54-D.wav"));break;
- case 0x26:QSound::play(dir->currentPath().append("WavPiano56-E.wav"));break;
- case 0x25:QSound::play(dir->currentPath().append("WavPiano57-F.wav"));break;
- case 0x2E:QSound::play(dir->currentPath().append("WavPiano59-G.wav"));break;
- case 0x36:QSound::play(dir->currentPath().append("WavPiano61-A.wav"));break;
- case 0x3D:QSound::play(dir->currentPath().append("WavPiano63-B.wav"));break;
- case 0x3E:QSound::play(dir->currentPath().append("WavPiano64-C.wav"));break;
- case 0x46:QSound::play(dir->currentPath().append("WavPiano66-D.wav"));break;
- case 0x45:QSound::play(dir->currentPath().append("WavPiano68-E.wav"));break;
- case 0x4E:QSound::play(dir->currentPath().append("WavPiano69-F.wav"));break;
- case 0x55:QSound::play(dir->currentPath().append("WavPiano71-G.wav"));break;
- //主键盘区 第二行
- case 0x15:QSound::play(dir->currentPath().append("WavPiano40-C.wav"));break;
- case 0x1D:QSound::play(dir->currentPath().append("WavPiano42-D.wav"));break;
- case 0x24:QSound::play(dir->currentPath().append("WavPiano44-E.wav"));break;
- case 0x2D:QSound::play(dir->currentPath().append("WavPiano45-F.wav"));break;
- case 0x2C:QSound::play(dir->currentPath().append("WavPiano47-G.wav"));break;
- case 0x35:QSound::play(dir->currentPath().append("WavPiano49-A.wav"));break;
- case 0x3C:QSound::play(dir->currentPath().append("WavPiano51-B.wav"));break;
- case 0x43:QSound::play(dir->currentPath().append("WavPiano52-C.wav"));break;
- case 0x44:QSound::play(dir->currentPath().append("WavPiano54-D.wav"));break;
- case 0x4D:QSound::play(dir->currentPath().append("WavPiano56-E.wav"));break;
- case 0x54:QSound::play(dir->currentPath().append("WavPiano57-F.wav"));break;
- case 0x5B:QSound::play(dir->currentPath().append("WavPiano59-G.wav"));break;
- case 0x5D:QSound::play(dir->currentPath().append("WavPiano61-A.wav"));break;
- //主键盘区 第三行
- case 0x1C:QSound::play(dir->currentPath().append("WavPiano28-C.wav"));break;
- case 0x1B:QSound::play(dir->currentPath().append("WavPiano30-D.wav"));break;
- case 0x23:QSound::play(dir->currentPath().append("WavPiano32-E.wav"));break;
- case 0x2B:QSound::play(dir->currentPath().append("WavPiano33-F.wav"));break;
- case 0x34:QSound::play(dir->currentPath().append("WavPiano35-G.wav"));break;
- case 0x33:QSound::play(dir->currentPath().append("WavPiano37-A.wav"));break;
- case 0x3B:QSound::play(dir->currentPath().append("WavPiano39-B.wav"));break;
- case 0x42:QSound::play(dir->currentPath().append("WavPiano40-C.wav"));break;
- case 0x4B:QSound::play(dir->currentPath().append("WavPiano42-D.wav"));break;
- case 0x4C:QSound::play(dir->currentPath().append("WavPiano44-E.wav"));break;
- case 0x52:QSound::play(dir->currentPath().append("WavPiano45-F.wav"));break;
- //主键盘区 第四行
- case 0x1A:QSound::play(dir->currentPath().append("WavPiano16-C.wav"));break;
- case 0x22:QSound::play(dir->currentPath().append("WavPiano18-D.wav"));break;
- case 0x21:QSound::play(dir->currentPath().append("WavPiano20-E.wav"));break;
- case 0x2A:QSound::play(dir->currentPath().append("WavPiano21-F.wav"));break;
- case 0x32:QSound::play(dir->currentPath().append("WavPiano23-G.wav"));break;
- case 0x31:QSound::play(dir->currentPath().append("WavPiano25-A.wav"));break;
- case 0x3A:QSound::play(dir->currentPath().append("WavPiano27-B.wav"));break;
- case 0x41:QSound::play(dir->currentPath().append("WavPiano28-C.wav"));break;
- case 0x49:QSound::play(dir->currentPath().append("WavPiano30-D.wav"));break;
- case 0x4A:QSound::play(dir->currentPath().append("WavPiano32-E.wav"));break;
- //主键盘区 小键盘
- case 0x69:QSound::play(dir->currentPath().append("WavPiano40-C.wav"));break;
- case 0x72:QSound::play(dir->currentPath().append("WavPiano42-D.wav"));break;
- case 0x7A:QSound::play(dir->currentPath().append("WavPiano44-E.wav"));break;
- case 0x6B:QSound::play(dir->currentPath().append("WavPiano45-F.wav"));break;
- case 0x73:QSound::play(dir->currentPath().append("WavPiano47-G.wav"));break;
- case 0x74:QSound::play(dir->currentPath().append("WavPiano49-A.wav"));break;
- case 0x6C:QSound::play(dir->currentPath().append("WavPiano51-B.wav"));break;
- case 0x75:QSound::play(dir->currentPath().append("WavPiano52-C.wav"));break;
- case 0x7D:QSound::play(dir->currentPath().append("WavPiano54-D.wav"));break;
- case 0x79:QSound::play(dir->currentPath().append("WavPiano56-E.wav"));break;
- case 0x77:QSound::play(dir->currentPath().append("WavPiano57-F.wav"));break;
- //case 0x4A:QSound::play(dir->currentPath().append("WavPiano59-G.wav"));break;
- case 0x7C:QSound::play(dir->currentPath().append("WavPiano61-A.wav"));break;
- case 0x7B:QSound::play(dir->currentPath().append("WavPiano63-B.wav"));break;
- case 0x70:QSound::play(dir->currentPath().append("WavPiano35-G.wav"));break;
- case 0x71:QSound::play(dir->currentPath().append("WavPiano37-A.wav"));break;
- case 0x5A:QSound::play(dir->currentPath().append("WavPiano39-B.wav"));break;
- default:break;
- }
- }
- }
- }
- // disconnected -> disconnectedSlot 客户端断开连接的槽函数
- void MyTcpServer::disconnectedSlot()
- {
- //由于disconnected信号并未提供SocketDescriptor,所以需要遍历寻找
- for(int i=0; i<tcpClient.length(); i++)
- {
- if(tcpClient[i]->state() == QAbstractSocket::UnconnectedState)
- {
- // 删除存储在combox中的客户端信息
- ui->cbxConnection->removeItem(ui->cbxConnection->findText(tr("%1:%2")
- .arg(tcpClient[i]->peerAddress().toString().split("::ffff:")[1])
- .arg(tcpClient[i]->peerPort())));
- // 删除存储在tcpClient列表中的客户端信息
- tcpClient[i]->destroyed();
- tcpClient.removeAt(i);
- }
- }
- }
- // 监听--断开
- void MyTcpServer::on_btnConnect_clicked()
- {
- if(ui->btnConnect->text()=="监听")
- {
- bool ok = tcpServer->listen(QHostAddress::Any, ui->edtPort->text().toInt());
- if(ok)
- {
- ui->btnConnect->setText("断开");
- ui->btnSend->setEnabled(true);
- }
- }
- else
- {
- for(int i=0; i<tcpClient.length(); i++)//断开所有连接
- {
- tcpClient[i]->disconnectFromHost();
- bool ok = tcpClient[i]->waitForDisconnected(1000);
- if(!ok)
- {
- // 处理异常
- }
- tcpClient.removeAt(i); //从保存的客户端列表中取去除
- }
- tcpServer->close(); //不再监听端口
- ui->btnConnect->setText("监听");
- ui->btnSend->setEnabled(false);
- }
- }
- // 发送数据
- void MyTcpServer::on_btnSend_clicked()
- {
- QString data = ui->edtSend->toPlainText();
- if(data == "") return; // 文本输入框为空时
- //全部连接
- if(ui->cbxConnection->currentIndex() == 0)
- {
- for(int i=0; i<tcpClient.length(); i++)
- tcpClient[i]->write(data.toLatin1()); //qt5除去了.toAscii()
- }
- //指定连接
- else
- {
- QString clientIP = ui->cbxConnection->currentText().split(":")[0];
- int clientPort = ui->cbxConnection->currentText().split(":")[1].toInt();
- // qDebug() << clientIP;
- // qDebug() << clientPort;
- for(int i=0; i<tcpClient.length(); i++)
- {
- if(tcpClient[i]->peerAddress().toString().split("::ffff:")[1]==clientIP
- && tcpClient[i]->peerPort()==clientPort)
- {
- tcpClient[i]->write(data.toLatin1());
- return; //ip:port唯一,无需继续检索
- }
- }
- }
- }
- void MyTcpServer::on_btnClear_clicked()
- {
- ui->edtRecv->clear();
- }
复制代码
---main.cpp 最后是main函数文件,main函数是整个项目程序的入口函数,其作用就是实例化一个MyTcpServer类,显示界面。
- #include "mytcpserver.h"
- #include <QApplication>
- int main(int argc, char *argv[])
- {
- QApplication a(argc, argv);
- MyTcpServer w;
- w.show();
- return a.exec();
- }
复制代码
至此,挑战赛完成!
本文相关附件如下:
``
|