Make a midi keyboard(StyloCard)
仿制StyloCard midi键盘
最近在油管上看到这个效果惊艳的StyloCard键盘,简洁但不简单,极少的元件实现超棒的效果(其实是因为没钱买电子元件),于是决定仿制一个玩玩。
硬件方面十分简单,通过电阻的串联使每个按键都带有不同的电位,再用带有ADC的引脚读取即可识别按键下的按键。
PCB板
成品(右边笔的焊盘因为用的次数多了,导致表面铜脱落了)
难点在于用软件使Attiny85模拟成为USB设备(基于V-USB),以及实现midi设备的通讯协议。因为Attiny85可以使用Arduino来遍程,因此有极其丰富的库来进行调用。
画知识点:V-USB,MIDI协议
V-USB
我在Github找到了两个针对Attiny85的,可用的库分别是USBMIDI和DigisparkMIDI。两个库都是基于V-USB来实现的。为了偷懒,我选择使用DigisparkMIDI(我买的小板子是国产的Digispark)。
#include <DigiMIDI.h>//去掉Debug后的示例
DigiMIDIDevice midi;
void setup() {
}
void loop() {
midi.update(); //进行更新
midi.delay(500);//延时
midi.sendNoteOn(62,32);//发送按键按下的信息
}
示例里大部分都是Debug用的测试代码,有控制效果的只有几行。
void sendNoteOn(int key, int velocity=0, uchar channel=1) {
send(NoteOn,key,velocity,channel);
}
void sendNoteOff(int key, int velocity=0, uchar channel=1) {
send(NoteOff,key,velocity,channel);
}
void sendProgramChange(int inProgramNumber, uchar channel=1) {
send(ProgramChange,inProgramNumber,0,channel);
}
void sendControlChange(int inControlNumber, int inControlValue=0, uchar channel=1) {
send(ControlChange,inControlNumber,inControlValue,channel);
}
void sendPolyPressure(int inNoteNumber, int inPressure, uchar channel=1) {
send(AfterTouchPoly,inNoteNumber,inPressure,channel);
}
void sendAfterTouch(int inPressure, uchar channel=1) {
send(AfterTouchChannel, inPressure, 0, channel);
}
void sendPitchBend(int inPitchValue, uchar channel=1) {
const unsigned bend = inPitchValue - 0;/*MIDI_PITCHBEND_MIN;*/
send(PitchBend, (bend & 0x7f), (bend >> 7) & 0x7f, channel);
}
void sendPitchBend(double inPitchValue, uchar channel=1) {
const int value = inPitchValue;// * MIDI_PITCHBEND_MAX;
sendPitchBend(value, channel);
}
以上是封装好的可以直接使用的函数,具体作用可以看下边MIDI协议的讲解。
#include <DigiMIDI.h>
DigiMIDIDevice midi;
void setup() {
pinMode(2, INPUT);//设置带有ADC的引脚为输入
}
void loop() {
midi.update();
midi.delay(100);//延时1ms
int n = analogRead(1);//读取引脚的电压值
if (n > 400 && n <= 550 )//判断是否按下
{
midi.sendNoteOn(69, 255); //按下按键
midi.delay(100);
midi.sendNoteOff(69, 255);//松开按键
}
这是我代码中的一部分,其余部分原理一样,就不再进行赘述。
**重点是对V-USB的理解。**这里做简单介绍,详细可以去官网或者下文的引用博客中学习。
首先要对USB协议有一定的了解,可以看下《圈圈教你玩USB》(书中的实例是基于51加一款USB芯片完成的)中关于USB协议的部分。
AVRUSB针对AVR单片机,实现使用单片机的IO口来模拟USB的通信端口,由软件来实现USB通信协议,将普通的AVR单片机模拟成一个USB低速设备,从而实现AVR单片机与计算机之间的通信和控制。缺点是:只能模拟USB1.1协议,且模拟占用了十分多的CPU资源,过于复杂的项目不建议使用。
硬件上需要单片机具有至少2 kB闪存,128字节RAM和至少12 MHz时钟速率(可以使用12 MHz,15 MHz,16 MHz 18 MHz或20 MHz晶振或12.8 MHz或16.5 MHz内部RC振荡器)。
V-USB代码文件有:
- usbconfig.h 用户配置文件
- iarcompat.h 为兼容IAR编译器而定义的宏
- usbdrv.h usb驱动接口文件的头文件
- usbdrv.c usb驱动接口文件
- usbdrvasm.asm 为兼容IAR编译器而使用的底层接口函数文件的别名
- usbdrvasm.S 汇编语言编写的底层接口函数
- oddebug.h 调试用函数的头文件(不使用调试功能时可以不添加)
- oddebug.c 包含调试用的函数(不使用调试功能时可以不添加)
其中用户需要注意的有文件是usbconfig.h和usbdrv.c。
通过对usbconfig.h可以改变的设置有:
- USB_CFG_IOPORTNAME
- 定义USB数据线使用的端口。只要是通用的IO都可以,没有特殊的要求。
- USB_CFG_DMINUS_BIT
- USB数据线D-使用的引脚。
- USB_CFG_DPLUS_BIT
- USB数据线D+使用的引脚。因为D+要求同时连接到INT0上,所以一般情况下需要使用3个IO口。如果D+使用的引脚就是INT0,那么可以少使用一 个IO端口。 - USB_CFG_VENDOR_ID
- 设备生产商的ID号
- USB_CFG_DEVICE_ID
- 设备的产品ID号。这两个参数就是Windows识别USB设备的主要参数。需要注意的是,这两个参数都是低字节在前,高字节在后。
- USB_CFG_DEVICE_VERSION
- 设备的版本号次版本号在前,主版本号在后。在Windows的设备管理中可以看到这个版本号
- USB_CFG_VENDOR_NAME
- 设备生产商的名称,它在Windows的设备管理中可以看到。这里一般写入的是网址。
- USB_CFG_VENDOR_NAME_LEN
- 设备生产商名称的长度。
- USB_CFG_DEVICE_NAME
- 设备的名称,它在Windows的设备管理中可以看到。设备名称和生产商的名称都是以字符的方式定义的,它们目前不支持中文。
- USB_CFG_DEVICE_NAME_LEN
- 设备名称的长度。
usbdrv.c中是封装好的功能函数有:
- usbInit(void);
- 初始化函数
- usbPoll(void);
- USB事件处理函数,需要在主循环中进行调用
- usbFunctionSetup(uchar data[8]);
- 一般功能设置
- usbSetInterrupt(uchar *data, uchar len);
- 此函数设置将在下一次中断IN传输期间发送的消息。
- usbFunctionRead(uchar *data, uchar len);
- 主机从单片机中读取数据
- usbFunctionWrite(uchar *data, uchar len);
- 主机向单片机写入数据
以上函数是常用功能,还有其它函数在文件中也有较多注释(没用过,也不太懂,就不乱讲了)。
MIDI协议
由于MIDI协议中涉及较多与项目无关的控制,就不再依次进行介绍,捡重点部分进行说明。
我们对键盘的控制,就是通过USB向电脑上的软件发送MIDI音轨的信息,如同直接操作软件上的音轨。换句话来讲就是当你在MIDI键盘上按下一个琴键,你不是在制造一个声音而是发出一条MIDI指令。发送信息的种类有:时间差(delta-time),MIDI事件( MIDI events),非MIDI事件( Non-MIDI events)和系统码事件(sysex event)。
时间差
时间差是可变常量(variable length quantity),表示的是将要发生的事件与前一事件之间的时间差值。两个事件同时发生,时间差设为零。
非MIDI事件和系统码事件
这两种事件中包含了MIDI文件中的非MIDI信息。有版权信息,乐器名称,设备名称,音源设备编号等等。与实现键盘演奏关系不大,就不再进行赘述。
MIDI事件
MIDI事件有音符事件、控制器事件和系统信息事件等。命令的格式一般为:时间差+指令种类+参数(包含音调和力度)组成。
命令列表(Markdown的表格无法渲染,只能用图片了)
MIDI音符的有效范围是0-127,16进制是00-7FH,对映软件上的127个按键,由小到大依次对应音符。
音符的力度,也称为按键的速度,范围是1-127,也即01-FF,当按下或松开音符的力度为0时,表示松开音符。
按下音符(noteon)
例如命令:按下中音A 为00 96 45 70。其中00为时间差,96为命令类型和通道选择(这里是第7个通道),45为音符,70为力度(上面的命令为16进制)。
松开音符(noteoff)
命令:松开中音A 为00 86 45 70。同上,只有命令类型改变了。
滑音(Pitch Wheel)
用不同的滑音参数调整MIDI器件来改变音符的值,同时对于不同的MIDI器件的通道,滑音的信息是不同的。00 E6 00 40。其中 00时间差,E6表示滑音,在第7通道使用滑音,设置滑音值为0,代入公式:参数是0-(-8192)=8192,8192的7位双字节表示成8192 mod 128=00H(字节的最高位设置为0),8192 div 128=128*64=40H(字节的最高位设置为0)。
其它的都没用过就不进行讲解了。
这是在MIDI库的源码中,对于命令的实现,其中的命令类型已经事先定义好了。
其中send函数的实现
因为库的作者也是修改别人的库,所以其中标What is this?的参数实际上是时间差。send函数使用的是usbSetInterrupt(uchar *data, uchar len);函数进行的数据发送,正好可以一次性发送完整个命令,不用分成多次进行传输。
后记
因为对音乐,乐理什么的一窍不通,所以一些高级的应用完全没有头绪,滑音也没能实现。虽然项目实现的不太好,但是找到了V-USB这个方便的库,对于一些便宜的芯片来说十分好用。而且Attiny还可以很方便得实现微型游戏机,等闲了可以搞一个出来。关于MIDI协议和V-USB更详细得内容可以查看参考文献和官方示例。
更新
趁着PCB制作厂商推出价格优惠制板了,成品看起来还是不错的。
参考:
1.AVR-USB(http://lionwq.spaces.eepw.com.cn/articles/categorys/category/1824)
2.MIDI协议(https://blog.csdn.net/shao941122/article/details/46124865)