Make a midi keyboard(StyloCard)

Author Avatar
ZLXT 4月 06, 2019

仿制StyloCard midi键盘

  最近在油管上看到这个效果惊艳的StyloCard键盘,简洁但不简单,极少的元件实现超棒的效果(其实是因为没钱买电子元件),于是决定仿制一个玩玩。

  硬件方面十分简单,通过电阻的串联使每个按键都带有不同的电位,再用带有ADC的引脚读取即可识别按键下的按键。

PCB板

成品(右边笔的焊盘因为用的次数多了,导致表面铜脱落了)

  难点在于用软件使Attiny85模拟成为USB设备(基于V-USB),以及实现midi设备的通讯协议。因为Attiny85可以使用Arduino来遍程,因此有极其丰富的库来进行调用。


画知识点:V-USB,MIDI协议



V-USB

  我在Github找到了两个针对Attiny85的,可用的库分别是USBMIDIDigisparkMIDI。两个库都是基于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)