ないものは作る!

電気回路、マイコンなどの工作を中心に書いていきます

MIDIキーボードのソフトウェア

ソフトウェアで行うのは、レジスタに入ってきたキーのON/OFF状態が変わったら、状態が変わったキーに応じたMIDIメッセージをTxから出力することです。

まずは、キーボードスキャンのために、前回示したタイミングチャートの通りに信号を出力させます。この処理にかかわるコードを抜粋して示します。

const byte ScanLineNumber = 6;    // スキャンライン本数
const byte ScanStartPin = 2;      // スキャンライン先頭Pin
const byte SenseLineNumber = 11;  // センスライン本数
const byte SenseStartPin = 8;     // センスライン先頭Pin
const byte CurrentScanClrPin = B00000011; // スキャンPinを一旦全部0にするためのマスク
byte CurrentScanSetPin = B00000100;       // スキャンPinでHighレベルのPinを示す

void setup() {
  Serial.begin(31250);
  // Pin入出力設定
  for(int i=0; i<ScanLineNumber; ++i){
    pinMode(i + ScanStartPin, OUTPUT);
  }
  for(int i=0; i<SenseLineNumber; ++i){
    pinMode(i + SenseStartPin, INPUT);
  }
  // 最初のピンの値を立てる
  PORTD &= CurrentScanClrPin; // 全スキャンPinを0にしてから
  PORTD |= CurrentScanSetPin; // 1bit立てる
  // Timer2をCTCモードで
  TCCR2A = 0;
  TCCR2B |= (1 << WGM21); // CTC Mode
  TCCR2B |= ((0 << CS20)|(1 << CS21)|(0 << CS22));  // 8分周
  OCR2A = 17;   // 1回目の割り込みカウント:データを取るポイント:スキャンラインLevelがLow->Highに遷移後、Levelが落ち着くまで待つ時間
  OCR2B = 20;   // 2回目の割り込みカウント:今のスキャンラインをHigh->Lowに、次をLow->Highに切り替えて、カウンタをリセットする
  TIMSK2 |= ((1 << OCIE2A)|(1 << OCIE2B));  // 割り込み許可
}

// TIMER2 COMPB Interrupt : Scan pin level change
ISR (TIMER2_COMPB_vect) {
  // スキャン用のPin番号を一つ進めて
  if(CurrentScanSetPin == B10000000){ // スキャンPinが一番上のビットのとき
    CurrentScanSetPin = B00000100;    // 1番目のスキャンPinにセットする
  }else{                              // それ以外は
    CurrentScanSetPin <<= 1;          // スキャンPin番号を増やす
  }
  // 値を立てるPinを変える
  PORTD &= CurrentScanClrPin; // スキャン用の全Pinを0にしてから
  PORTD |= CurrentScanSetPin; // 1bit立てる
}

setup()関数では、スキャン出力に使用するピンを出力に設定し、タイマの設定を行います。タイマはCTC動作で動かし、1つ目の割り込みでスキャンした結果の取り込み、2つ目の割り込みでタイマをリセットします。2回目の割り込みで呼ばれる関数、「ISR (TIMER2_COMPB_vect)」でスキャン用のピンのHigh/Lowを切り替えます。

 

スキャンしたデータの読み込み以降の処理は、下図の様になっています。

f:id:toysound:20200920143850p:plain



キーボードスキャンの結果を確認する割り込み処理は可能な限り短時間で済ませたいので、前回の値と比較し異なるbitがあったらレジスタ値を一旦リングバッファに格納して、すぐに割り込み処理を終了させます。割り込み処理でレジスタの変化が検出される毎にリングバッファにレジスタ値を積んでいく処理を繰り返します。 この割り込み処理は、下記の様になります。

// キーの状態変化検出用
word SenseBuffIndex = 0;
byte CurrentDataPB[6] = {0, 0, 0, 0, 0, 0};
byte PreviusDataPB[6] = {0, 0, 0, 0, 0, 0};
byte CurrentDataPC[6] = {0, 0, 0, 0, 0, 0};
byte PreviusDataPC[6] = {0, 0, 0, 0, 0, 0};

RingBuff RcvDatas;

// TIMER2 COMPA Interrupt : Sense pin level capture
ISR (TIMER2_COMPA_vect) {
  CurrentDataPB[SenseBuffIndex] = PINB & B00111111;  // センスに使うPinだけ取り出す
  CurrentDataPC[SenseBuffIndex] = PINC & B00011111;  // センスに使うPinだけ取り出す
  if(PreviusDataPB[SenseBuffIndex] != CurrentDataPB[SenseBuffIndex]){
    RcvDatas.push(portB, SenseBuffIndex, PreviusDataPB[SenseBuffIndex], CurrentDataPB[SenseBuffIndex]);
    PreviusDataPB[SenseBuffIndex] = CurrentDataPB[SenseBuffIndex];
  }
  if(PreviusDataPC[SenseBuffIndex] != CurrentDataPC[SenseBuffIndex]){
    RcvDatas.push(portC, SenseBuffIndex, PreviusDataPC[SenseBuffIndex], CurrentDataPC[SenseBuffIndex]);
    PreviusDataPC[SenseBuffIndex] = CurrentDataPC[SenseBuffIndex];
  }
  // スキャン毎のセンスデータのバッファのIndexを更新
  SenseBuffIndex++;
  if(SenseBuffIndex == ScanLineNumber){
    SenseBuffIndex = 0;
  }
}

 この処理では、自作のRingBuffクラスを利用します。レジスタの変化を検出すると、今のレジスタの値、以前のレジスタの値、どのレジスタか(PBかPCか)をリングバッファに格納します。

 

残りの処理は、定期的に呼ばれるloop()関数で処理します。RingBuffクラスのpop()関数で変化したbitを一つずつ取り出し、得られたMIDIノートナンバーに応じてSerial.write()関数でMIDIメッセージを出力します。

byte NoteOn[]        = {0x90, 0x00, 0x7f};  // NOTE ON  0x90 0xNN(0-127) 0xNN(0-127)
byte NoteOff[]       = {0x90, 0x00, 0x00};  // NOTE OFF 0x90 0xNN(0-127) 0xNN(0-127)
byte SustainOn[]     = {0xB0, 0x40, 0x7f};  // Sustain ON  0xB0 0x40 0xNN(0-127)
byte SustainOff[]    = {0xB0, 0x40, 0x00};  // Sustain OFF 0xB0 0x40 0xNN(0-127)
byte PrgChg[]        = {0xC0, 0x00};        // Program Change 0xC0, 0xNN(0-127)
byte ModulationOn[]  = {0xB0, 0x01, 0x7f};  // Modulation ON  0xB0 0x01 0xNN(0-127)
byte ModulationOff[] = {0xB0, 0x01, 0x00};  // Modulation OFF 0xB0 0x01 0xNN(0-127)
byte Allnoteoff[]    = {0xB0, 0x7B, 0x00};  // ALL NOTE OFF   0xB0 0x7B 0x00
byte CurrentProgram = 0;                    // 現在のプログラムナンバー

void loop() {
  if(RcvDatas.existData()){
    byte KeyNumber;
    bool isOn;
    RcvDatas.pop(KeyNumber, isOn);
    if(isOn){
      switch(KeyNumber){
        case 35:  // PEDAL
          Serial.write(SustainOn, 3);
          break;
        case 34:  // Program Change INC
          if(CurrentProgram == 127){
            CurrentProgram = 0;
          }else{
            CurrentProgram++;
          }
          PrgChg[1] = CurrentProgram;
          Serial.write(PrgChg, 2);
          break;
        case 33:  // Program Change DEC
          if(CurrentProgram == 0){
            CurrentProgram = 127;
          }else{
            CurrentProgram--;
          }
          PrgChg[1] = CurrentProgram;
          Serial.write(PrgChg, 2);
          break;
        case 32:  // Modulation
          Serial.write(ModulationOn, 3);
          break;
        case 31:  // All Note Off
          Serial.write(Allnoteoff, 3);
          break;
        default:  // key on
          NoteOn[1] = KeyNumber;
          Serial.write(NoteOn, 3);
          break;
      }
    }else{
      switch(KeyNumber){
        case 35:  // PEDAL
          Serial.write(SustainOff, 3);
          break;
        case 34:  // Program Change INC
          break;
        case 33:  // Program Change DEC
          break;
        case 32:  // Modulation
          Serial.write(ModulationOff, 3);
          break;
        case 31:  // All Note Off
          break;
        default:  // key off
          NoteOff[1] = KeyNumber;
          Serial.write(NoteOff, 3);
          break;
      }
    }
  }
}

 RingBuffクラスの実装は、以下のソースを参照してください。リングバッファのライブラリなどは探せばありそうなのですが、性能を出したかったのとメモリを節約したかったので、自前で作成しました。

全ソース
// Copyright 2020, toshiyuki ohshima
// BSD License
const word RinBuffMaxEntry = 128; // リングバッファのエントリ数
const byte ScanLineNumber = 6;    // スキャンライン本数
const byte ScanStartPin = 2;      // スキャンライン先頭Pin
const byte SenseLineNumber = 11;  // センスライン本数
const byte SenseStartPin = 8;     // センスライン先頭Pin
const byte CurrentScanClrPin = B00000011; // スキャンPinを一旦全部0にするためのマスク
byte CurrentScanSetPin = B00000100;       // スキャンPinでHighレベルのPinを示す
// キーの状態変化検出用
word SenseBuffIndex = 0;
byte CurrentDataPB[6] = {0, 0, 0, 0, 0, 0};
byte PreviusDataPB[6] = {0, 0, 0, 0, 0, 0};
byte CurrentDataPC[6] = {0, 0, 0, 0, 0, 0};
byte PreviusDataPC[6] = {0, 0, 0, 0, 0, 0};
// センスデータ・キー番号対応表
byte SenseToKey[6][11] = {                      // SenseToKey[][0-5]:PB, SenseToKey[][6-10]:PC
//PB0,PB1,PB2,PB3,PB4,PB5,PC0,PC1,PC2,PC3,PC4 
  {96, 78, 90, 72, 84, 66, 36, 42, 48, 54, 60}, // スキャンラインがPD2(A07)のとき
  {95, 77, 89, 71, 83, 65, 35, 41, 47, 53, 59}, // スキャンラインがPD3(A09)のとき
  {94, 76, 88, 70, 82, 64, 34, 40, 46, 52, 58}, // スキャンラインがPD4(A11)のとき
  {93, 75, 87, 69, 81, 63, 33, 39, 45, 51, 57}, // スキャンラインがPD5(A13)のとき
  {92, 74, 86, 68, 80, 62, 32, 38, 44, 50, 56}, // スキャンラインがPD6(A15)のとき
  {91, 73, 85, 67, 79, 61, 31, 37, 43, 49, 55}  // スキャンラインがPD7(A17)のとき
};
// MIDI Data (ALL MIDI CH=1)
//   Note#35:PEDAL : Sustain
//   Note#34:SW1   :Program Change INC
//   Note#33:SW2   :Program Change DEC
//   Note#32:SW3   :Modulation(固定値)
//   Note#31:SW4   :All Note Off
byte NoteOn[]        = {0x90, 0x00, 0x7f};  // NOTE ON  0x90 0xNN(0-127) 0xNN(0-127)
byte NoteOff[]       = {0x90, 0x00, 0x00};  // NOTE OFF 0x90 0xNN(0-127) 0xNN(0-127)
byte SustainOn[]     = {0xB0, 0x40, 0x7f};  // Sustain ON  0xB0 0x40 0xNN(0-127)
byte SustainOff[]    = {0xB0, 0x40, 0x00};  // Sustain OFF 0xB0 0x40 0xNN(0-127)
byte PrgChg[]        = {0xC0, 0x00};        // Program Change 0xC0, 0xNN(0-127)
byte ModulationOn[]  = {0xB0, 0x01, 0x7f};  // Modulation ON  0xB0 0x01 0xNN(0-127)
byte ModulationOff[] = {0xB0, 0x01, 0x00};  // Modulation OFF 0xB0 0x01 0xNN(0-127)
byte Allnoteoff[]    = {0xB0, 0x7B, 0x00};  // ALL NOTE OFF   0xB0 0x7B 0x00
byte CurrentProgram = 0;                    // 現在のプログラムナンバー
// ------------------------------------------------------------------
// リングバッファ・クラス群
enum sReg {
  portB = 0,
  portC = 6
};

class SenseData_{
  public:
    sReg mSenseKeyOffset;   // SenseToKey[][]の何個目のデータからターゲットのレジスタのデータが始まるのか(PBレジスタのデータか、PCレジスタのデータかの区別)
    word mScanLine;         // 何番目のラインをスキャンしたか (0-5)
    byte mChangedData;      // レジスタのどのbitが変化したか示す
    byte mNewData;          // レジスタ変化後の生のレジスタ値
    byte mCheckedBitMask;   // mChangedDataの何bit目からチェックを始めるか:ビットマスク
    word mCheckedBitIndex;  // mChangedDataの何bit目からチェックを始めるか:Index値
};

class RingBuff {
public:
  RingBuff()
    : mTopIndex(0)
    , mTailIndex(0)
  {
    for(int i=0; i<RinBuffMaxEntry; ++i){
      volatile SenseData_ *target = &mSenseData[i];
      target->mSenseKeyOffset = 0;
      target->mScanLine = 0;
      target->mChangedData = 0;
      target->mNewData = 0;
      target->mCheckedBitMask = B00000000;
      target->mCheckedBitIndex = 0;
    }
  };
  void push(sReg senseReg, word scanLine, byte oldData, byte newData){
    volatile SenseData_ *target = &mSenseData[mTailIndex];
    target->mSenseKeyOffset = senseReg;
    target->mScanLine = scanLine;
    target->mChangedData = oldData ^ newData;
    target->mNewData = newData;
    target->mCheckedBitMask = B00000001;
    target->mCheckedBitIndex = 0;
    pushIndex();
  };
  void pop(byte& KeyNumber, bool& isOn){
    // 右から左にmChangedDataから取り出すbitをずらしていく。取り出したら、mChangedData上の取り出したbitをクリアする。
    volatile SenseData_ *target = &mSenseData[mTopIndex];
    while(target->mCheckedBitMask != B01000000){                            // データの存在が考えられる一番上のbitまで調べる
      if((target->mChangedData & target->mCheckedBitMask) != 0){            // ターゲットのbitが立っていたとき
        KeyNumber = SenseToKey[target->mScanLine][target->mCheckedBitIndex + target->mSenseKeyOffset]; // キー番号に変換する
        isOn = (target->mNewData & target->mCheckedBitMask) ? true : false; // ONになったか、OFFになったか、判定
        target->mChangedData &= ~target->mCheckedBitMask;                   // リングバッファ上の今回値を返すbitをクリアする:残りのbitはmTopIndexを更新しないことで再度処理する
        if((target->mChangedData & ~target->mCheckedBitMask) != 0){         // まだ立っているbitがあるとき
          incCheckBit();                                                    // 次にpop()が呼ばれたときのために、調べるbitの場所を更新する
        }else{                                                              // もう立っているbitがないとき
          popIndex();                                                       // 取り出したSenseDataをリングバッファから削除する
        }
        return;                                                             // 1bit処理したらpop()関数を抜ける
      }
      incCheckBit();                                                        // 次のbitを調べるために、調べるbitの場所を更新する
    }
  };
  bool existData(){ // リングバッファに処理するべきデータが残っているか調べる
    return (mTopIndex != mTailIndex) ? true : false;
  };
private:
  void pushIndex(){ // mTailIndexをひとつ進める:リングバッファにデータを入れる毎に呼ぶ
    word candidateIndex = mTailIndex;
    candidateIndex++;
    if(candidateIndex == RinBuffMaxEntry){
      candidateIndex = 0;
    }
    mTailIndex = candidateIndex;
  };
  void popIndex(){  // mTopIndexをひとつ進める:リングバッファからデータを取り出す毎に呼ぶ
    word candidateIndex = mTopIndex;
    candidateIndex++;
    if(candidateIndex == RinBuffMaxEntry){
      candidateIndex = 0;
    }
    mTopIndex = candidateIndex;
  };
  void incCheckBit(){ // mTopIndexのデータの立っているbitを調べるIndex値をインクリメントする
    mSenseData[mTopIndex].mCheckedBitMask <<= 1;
    mSenseData[mTopIndex].mCheckedBitIndex++;
  };
  volatile SenseData_ mSenseData[RinBuffMaxEntry];
  volatile word mTopIndex;
  volatile word mTailIndex;
};

RingBuff RcvDatas;
// ------------------------------------------------------------------

void setup() {
  Serial.begin(31250);
  // Pin入出力設定
  for(int i=0; i<ScanLineNumber; ++i){
    pinMode(i + ScanStartPin, OUTPUT);
  }
  for(int i=0; i<SenseLineNumber; ++i){
    pinMode(i + SenseStartPin, INPUT);
  }
  // 最初のピンの値を立てる
  PORTD &= CurrentScanClrPin; // 全スキャンPinを0にしてから
  PORTD |= CurrentScanSetPin; // 1bit立てる
  // Timer2をCTCモードで
  TCCR2A = 0;
  TCCR2B |= (1 << WGM21); // CTC Mode
  TCCR2B |= ((0 << CS20)|(1 << CS21)|(0 << CS22));  // 8分周
  OCR2A = 17;   // 1回目の割り込みカウント:データを取るポイント:スキャンラインLevelがLow->Highに遷移後、Levelが落ち着くまで待つ時間
  OCR2B = 20;   // 2回目の割り込みカウント:今のスキャンラインをHigh->Lowに、次をLow->Highに切り替えて、カウンタをリセットする
  TIMSK2 |= ((1 << OCIE2A)|(1 << OCIE2B));  // 割り込み許可
}

void loop() {
  if(RcvDatas.existData()){
    byte KeyNumber;
    bool isOn;
    RcvDatas.pop(KeyNumber, isOn);
    if(isOn){
      switch(KeyNumber){
        case 35:  // PEDAL
          Serial.write(SustainOn, 3);
          break;
        case 34:  // Program Change INC
          if(CurrentProgram == 127){
            CurrentProgram = 0;
          }else{
            CurrentProgram++;
          }
          PrgChg[1] = CurrentProgram;
          Serial.write(PrgChg, 2);
          break;
        case 33:  // Program Change DEC
          if(CurrentProgram == 0){
            CurrentProgram = 127;
          }else{
            CurrentProgram--;
          }
          PrgChg[1] = CurrentProgram;
          Serial.write(PrgChg, 2);
          break;
        case 32:  // Modulation
          Serial.write(ModulationOn, 3);
          break;
        case 31:  // All Note Off
          Serial.write(Allnoteoff, 3);
          break;
        default:  // key on
          NoteOn[1] = KeyNumber;
          Serial.write(NoteOn, 3);
          break;
      }
    }else{
      switch(KeyNumber){
        case 35:  // PEDAL
          Serial.write(SustainOff, 3);
          break;
        case 34:  // Program Change INC
          break;
        case 33:  // Program Change DEC
          break;
        case 32:  // Modulation
          Serial.write(ModulationOff, 3);
          break;
        case 31:  // All Note Off
          break;
        default:  // key off
          NoteOff[1] = KeyNumber;
          Serial.write(NoteOff, 3);
          break;
      }
    }
  }
}

// TIMER2 COMPA Interrupt : Sense pin level capture
ISR (TIMER2_COMPA_vect) {
  CurrentDataPB[SenseBuffIndex] = PINB & B00111111;  // センスに使うPinだけ取り出す
  CurrentDataPC[SenseBuffIndex] = PINC & B00011111;  // センスに使うPinだけ取り出す
  if(PreviusDataPB[SenseBuffIndex] != CurrentDataPB[SenseBuffIndex]){
    RcvDatas.push(portB, SenseBuffIndex, PreviusDataPB[SenseBuffIndex], CurrentDataPB[SenseBuffIndex]);
    PreviusDataPB[SenseBuffIndex] = CurrentDataPB[SenseBuffIndex];
  }
  if(PreviusDataPC[SenseBuffIndex] != CurrentDataPC[SenseBuffIndex]){
    RcvDatas.push(portC, SenseBuffIndex, PreviusDataPC[SenseBuffIndex], CurrentDataPC[SenseBuffIndex]);
    PreviusDataPC[SenseBuffIndex] = CurrentDataPC[SenseBuffIndex];
  }
  // スキャン毎のセンスデータのバッファのIndexを更新
  SenseBuffIndex++;
  if(SenseBuffIndex == ScanLineNumber){
    SenseBuffIndex = 0;
  }
}

// TIMER2 COMPB Interrupt : Scan pin level change
ISR (TIMER2_COMPB_vect) {
  // スキャン用のPin番号を一つ進めて
  if(CurrentScanSetPin == B10000000){ // スキャンPinが一番上のビットのとき
    CurrentScanSetPin = B00000100;    // 1番目のスキャンPinにセットする
  }else{                              // それ以外は
    CurrentScanSetPin <<= 1;          // スキャンPin番号を増やす
  }
  // 値を立てるPinを変える
  PORTD &= CurrentScanClrPin; // スキャン用の全Pinを0にしてから
  PORTD |= CurrentScanSetPin; // 1bit立てる
}