ないものは作る!

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

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立てる
}

MIDIキーボードのマイコン基板

前々回説明したキーボードのスキャン回路を駆動するマイコン基板を作成します。今回のマイコンのお仕事は2つだけです。

  1. キーマトリクスをスキャンして鍵盤のON/OFFの状態変化を検出
  2. この変化内容に応じてMIDI信号を出力する

回路全体のブロック図は下図のようになります。

 f:id:toysound:20200920141852p:plain

マイコンからキーボードスキャン用に6本出力し、センスの入力として11本マイコンに入力します。高速化のためにレジスタを直接操作できるよう、入出力はレジスタ単位で分割しました。Arduino NANOで使用可能なデジタルI/Oピンは18本あるので、1本余る計算です。この余りのピンは、後にデバッグ情報の出力に使用しました。たった1pinでも役に立つんですね。

キーボードの状態を検出するには、PD2~PD7の各線を順次Highにして、PB0~5、PC0~4の線を読み取ってキーのON/OFF状態を調べます。

 f:id:toysound:20200920142614p:plain

 前回スキャンしたときと比較して、どれかのbitの状態が変わっていれば、キーのON/OFF状態が変わったことになります。タイミングチャートのINT AがPD2~7のLOW/Highの切り替えタイミングで、INT BがPB0~5、PC0~4の読み取りタイミングです。

 

キースキャン回路の解析結果から、スイッチを5個追加できることがわかっているので、基板上にタクトスイッチを4個、フットペダル用の口を1個設けることにしました。回路図(というより実体配線図?)を示します。

f:id:toysound:20200914230840p:plain

MIDI OUTとなるTXにはバッファとして74HCT125を使いました。MIDIコネクタへの配線が上書きされているのは、ご愛嬌。ググって出てくる回路図は、コネクタのどちら側から見た絵なのか書いてないものが多いんですよね。

完成品の表は、こんな感じ。

f:id:toysound:20200914231647j:plain

裏は、こんなです。

f:id:toysound:20200914231913j:plain

 

MIDI送るだけなら、こんなシンプルな回路で済んでしまうんですね。

雷で宅内のネットワーク機器が被害を受けた

雷が激しい日、我が家のインターネット環境が被害を受けました。

接続は、こんな感じ(あくまで、ざっくりとした絵です)。

f:id:toysound:20200906173922p:plain



10m以上の長いケーブルで接続していたポート(上図のバツ印のポート)だけがダメになりました。長い線でつながったポートのうち、片方だけが死んでます。光モデムのLANポートが一つ未使用だったので、インターネット接続は難を逃れ、現在はこのポートを分岐して使用しています。やはり長い配線は、雷の影響を受けやすいんですかねー。

キーボードのキースキャン回路を解析する

前回紹介したMIDIキーボードの製作過程を紹介していきます。

MIDIキーボードで既製品の鍵盤を使うにあたって、ケーブルの信号の仕様を知る必要があります。そこで、鍵盤の下にある基板の回路図を起こすことにしました。以下は、鍵盤を一部ばらしたところです。

f:id:toysound:20200830190915j:plain

メンブレン式のPCキーボードと同じつくりになっていて、押した鍵盤はシリコンのドームの力で元に戻るようになっています。各接点には基板の裏側にあるダイオードとつながっていて、一般的なキーボードスキャンの回路構成のようです。これなら使うのも簡単そうです。基板は2枚構成になっていて、基板同士はリボンケーブル(上の写真の右側ケーブル)で接続されています。

 

基板を取り出して、イメージスキャナにかけます。そして、回路パターンをはっきりさせるために、ペイントソフトで加工します。

f:id:toysound:20200830191713j:plain

f:id:toysound:20200830191727j:plain

ところどころ見えないところは、テスターで導通を確認しながら仕上げました。ここまでわかれば、回路図を作るのも簡単です。

image/svg+xml 96 95 94 93 92 91 90 89 88 87 86 85 84 83 82 81 80 79 78 77 76 75 74 73 72 71 70 69 68 67 66 65 64 63 61 60 59 58 57 56 55 54 53 52 51 50 49 48 A4 A6 A8 A10 47 46 45 44 43 42 41 40 39 37 36 A12 A14 62 38 A17 A7 A9 A16 A11 A13 A15 A2 A5 A3 A1

Axxと表記したところがリボンケーブルの各線と対応しています。スイッチに付けた数字はMIDIのキーナンバーです。

 

キーボードスキャンの回路構成としては、出力側のA1,A2,A3,A4,A5,A6,A8,A10,A12,A14,A16の各ピンをGNDに1KΩ程度の抵抗器でプルダウンします。スイッチの状態を検出するには、入力側のA7,A9,A11,A13,A15,A17の各ピンを順次Highレベルにしてスキャンし、出力のLow,Highを見てやります。鍵盤からのケーブルはむき出しでは使いにくいので、先っぽに基板を付けてピンソケットで信号を取り出せるようにします。この基板にプルダウン抵抗も一緒に付けてやります。こうしておけば、Arduino等との接続が簡単になります。

f:id:toysound:20200830193653p:plain

回路図でA16ピンを見ると、まだ5つスイッチを付ける余裕があることから、フットペダルとか、別のスイッチを追加してやろうと思います。

MIDIキーボードを作りました

元々、標準サイズの鍵盤で奥行きの小さいMIDIキーボードを探していました。5オクターブは欲しかったので、これに目をつけました。

https://www.soundhouse.co.jp/images/shop/prod_img/m/maudio_keystation61mkiii.jpg

・・・が、どこも品切れだったんです。いつ入荷するか悶々と待ち続けている間、むかし子供に買ったキーボードが使われていないことを思い出しました。こんなやつです。

https://jp.yamaha.com/files/PSR-E203-0001_900x348_9f68c4bfaef5571d9a786d2309b91e4c.jpg

ベロシティ(鍵盤を叩く強弱のことね)もつけられない安物ですが、だからこそ惜しくない。改造してMIDIキーボードに仕立てることにしました。

 

ばらしてみると、鍵盤部分だけ分離できるため、結構コンパクトになることがわかりました。まずは、完成品の全景をお披露目します。

f:id:toysound:20200829175321j:plain

見栄えは、まぁアレですが、比較的苦労せず作ることができました。自作部分はこんな感じ。

f:id:toysound:20200829181638j:plain

 手持ちのユニバーサル基板を使用したのですが、基板同士の接合部やスイッチの幅がぴったりで、気持ちイイです。基板同士の接合はピンヘッダとピンソケット。

f:id:toysound:20200829195227j:plain

以下、スペックです。

  • マイコン基板は格安Arduino NANO互換品(ATmega328 16MHz)
  • キーボードマトリックスのスキャンはソフトウェア
  • ペダル用ジャックx1、押しボタンスイッチx4
  • MIDI OUTx1
  • 電源はUSBケーブルで供給
  • MIDI OUTのバッファに74HCT125を使用

今日はここまで。

 

カメラブロアーみたいなハンダ吸い取り器

ずうっと気になっていたんです。

カメラブロアーみたいなハンダ吸い取り器!

こんなやつ↓

https://m.media-amazon.com/images/I/31-KyY8r42L._AC_SS350_.jpg

しかし、お高い。試しに買うには高すぎる!

百均のカメラブロアーを改造した記事を発見したので、自分でも作ってみました。近所の百均ではカメラブロアーを見つけられなかったので、アマゾンで購入。こんなやつ↓

https://images-na.ssl-images-amazon.com/images/I/71QmVhZHv6L._AC_SX425_.jpg
ブロアーなので、吐くばっかりで吸ってくれません。まず、お尻の穴をふさぎます。

f:id:toysound:20200829123941j:plain

これで、吸うこともできるようになりました。

吸口はシリコンチューブを装着します。

f:id:toysound:20200829124524j:plain

この先っぽにどうやって?

しばらく悩んだ挙げ句、付属のブラシに目をつけました。毛をすべて剥ぎ取ります。

f:id:toysound:20200829125147j:plain

これなら、シリコンチューブを挿せそうです。

f:id:toysound:20200829125602j:plain

シリコンチューブは内径3mm、外径4mmです。ちょうどぴったり❤

 

使ってみました・・・全然ハンダを吸いません(~_~;)

シリコンチューブが太いのかな・・・