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を切り替えます。
スキャンしたデータの読み込み以降の処理は、下図の様になっています。
キーボードスキャンの結果を確認する割り込み処理は可能な限り短時間で済ませたいので、前回の値と比較し異なる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つだけです。
回路全体のブロック図は下図のようになります。
マイコンからキーボードスキャン用に6本出力し、センスの入力として11本マイコンに入力します。高速化のためにレジスタを直接操作できるよう、入出力はレジスタ単位で分割しました。Arduino NANOで使用可能なデジタルI/Oピンは18本あるので、1本余る計算です。この余りのピンは、後にデバッグ情報の出力に使用しました。たった1pinでも役に立つんですね。
キーボードの状態を検出するには、PD2~PD7の各線を順次Highにして、PB0~5、PC0~4の線を読み取ってキーのON/OFF状態を調べます。
前回スキャンしたときと比較して、どれかのbitの状態が変わっていれば、キーのON/OFF状態が変わったことになります。タイミングチャートのINT AがPD2~7のLOW/Highの切り替えタイミングで、INT BがPB0~5、PC0~4の読み取りタイミングです。
キースキャン回路の解析結果から、スイッチを5個追加できることがわかっているので、基板上にタクトスイッチを4個、フットペダル用の口を1個設けることにしました。回路図(というより実体配線図?)を示します。
MIDI OUTとなるTXにはバッファとして74HCT125を使いました。MIDIコネクタへの配線が上書きされているのは、ご愛嬌。ググって出てくる回路図は、コネクタのどちら側から見た絵なのか書いてないものが多いんですよね。
完成品の表は、こんな感じ。
裏は、こんなです。
MIDI送るだけなら、こんなシンプルな回路で済んでしまうんですね。
雷で宅内のネットワーク機器が被害を受けた
雷が激しい日、我が家のインターネット環境が被害を受けました。
接続は、こんな感じ(あくまで、ざっくりとした絵です)。
10m以上の長いケーブルで接続していたポート(上図のバツ印のポート)だけがダメになりました。長い線でつながったポートのうち、片方だけが死んでます。光モデムのLANポートが一つ未使用だったので、インターネット接続は難を逃れ、現在はこのポートを分岐して使用しています。やはり長い配線は、雷の影響を受けやすいんですかねー。
キーボードのキースキャン回路を解析する
前回紹介したMIDIキーボードの製作過程を紹介していきます。
MIDIキーボードで既製品の鍵盤を使うにあたって、ケーブルの信号の仕様を知る必要があります。そこで、鍵盤の下にある基板の回路図を起こすことにしました。以下は、鍵盤を一部ばらしたところです。
メンブレン式のPCキーボードと同じつくりになっていて、押した鍵盤はシリコンのドームの力で元に戻るようになっています。各接点には基板の裏側にあるダイオードとつながっていて、一般的なキーボードスキャンの回路構成のようです。これなら使うのも簡単そうです。基板は2枚構成になっていて、基板同士はリボンケーブル(上の写真の右側ケーブル)で接続されています。
基板を取り出して、イメージスキャナにかけます。そして、回路パターンをはっきりさせるために、ペイントソフトで加工します。
ところどころ見えないところは、テスターで導通を確認しながら仕上げました。ここまでわかれば、回路図を作るのも簡単です。
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等との接続が簡単になります。
回路図でA16ピンを見ると、まだ5つスイッチを付ける余裕があることから、フットペダルとか、別のスイッチを追加してやろうと思います。
MIDIキーボードを作りました
元々、標準サイズの鍵盤で奥行きの小さいMIDIキーボードを探していました。5オクターブは欲しかったので、これに目をつけました。
・・・が、どこも品切れだったんです。いつ入荷するか悶々と待ち続けている間、むかし子供に買ったキーボードが使われていないことを思い出しました。こんなやつです。
ベロシティ(鍵盤を叩く強弱のことね)もつけられない安物ですが、だからこそ惜しくない。改造してMIDIキーボードに仕立てることにしました。
ばらしてみると、鍵盤部分だけ分離できるため、結構コンパクトになることがわかりました。まずは、完成品の全景をお披露目します。
見栄えは、まぁアレですが、比較的苦労せず作ることができました。自作部分はこんな感じ。
手持ちのユニバーサル基板を使用したのですが、基板同士の接合部やスイッチの幅がぴったりで、気持ちイイです。基板同士の接合はピンヘッダとピンソケット。
以下、スペックです。
- マイコン基板は格安Arduino NANO互換品(ATmega328 16MHz)
- キーボードマトリックスのスキャンはソフトウェア
- ペダル用ジャックx1、押しボタンスイッチx4
- MIDI OUTx1
- 電源はUSBケーブルで供給
- MIDI OUTのバッファに74HCT125を使用
今日はここまで。
カメラブロアーみたいなハンダ吸い取り器
ずうっと気になっていたんです。
カメラブロアーみたいなハンダ吸い取り器!
こんなやつ↓
しかし、お高い。試しに買うには高すぎる!
百均のカメラブロアーを改造した記事を発見したので、自分でも作ってみました。近所の百均ではカメラブロアーを見つけられなかったので、アマゾンで購入。こんなやつ↓
ブロアーなので、吐くばっかりで吸ってくれません。まず、お尻の穴をふさぎます。
これで、吸うこともできるようになりました。
吸口はシリコンチューブを装着します。
この先っぽにどうやって?
しばらく悩んだ挙げ句、付属のブラシに目をつけました。毛をすべて剥ぎ取ります。
これなら、シリコンチューブを挿せそうです。
シリコンチューブは内径3mm、外径4mmです。ちょうどぴったり❤
使ってみました・・・全然ハンダを吸いません(~_~;)
シリコンチューブが太いのかな・・・