はじめに
前回までの記事では、PIC16F1938を使用してDMX512信号を受信し、
4chのPWMへ出力するところから、
Gamma補正、上限・下限オフセット、EEPROMへの設定保存まで実装してきました。
今回は、このDMX→PWM変換器に『 Free Patch機能』を追加します。
これまでは各PWM出力に対応するDMXアドレスが固定されていましたが、
Free Patch化することで、例えば、
OUT1 → DMX 83
OUT2 → DMX 114
OUT3 → DMX 510
OUT4 → DMX 5
のように、各出力へ任意のDMXアドレスを割り当てられるようにします。
また、今回の実装では大きなDMXアドレスを設定した際に
受信オーバーラン(OERR)が発生する問題にも遭遇しました。
Free Patch機能の実装だけでなく、
割込み処理の実行時間についても考える機会となりました。
今回追加するFree Patchの仕様
これまでは固定パッチだったので、
自由にパッチを当てられる様にするためには、
LCDでのパッチの確認が必要になりました。
また、1~512まで自由にアドレスの当てるために、
ロータリスイッチやDIPスイッチも考えましたが、
今回はロータリエンコーダで設定することにしました。
ロータリエンコーダを回して、
クリックでDMXアドレスの確定をし、
それをEEPROMへ保存することで、
電源がなくなっても、復旧時には記憶させたアドレスでの
再稼働をできるようにしました。
ロータリエンコーダを使ってみる
今回、DMXアドレスを1~512の範囲で変更するために、
ロータリエンコーダを使用することにしました。
ロータリエンコーダは、
見た目はボリュームのような部品ですが、
ボリュームのように回転位置によって
抵抗値が変化するものではありません。
今回使用したロータリエンコーダには
A相とB相の2つの出力があり、
軸を回転させると、
それぞれの端子からON/OFFの信号が出力されます。
そしてA相とB相では、
信号が変化するタイミングが少しずれています。
このA相とB相のどちらが先に変化したかをPICで読み取ることで、
右に回したのか、
左に回したのか、
を判定することができます。
今回使用したのは、Bourns製の「PEC11R-4220F-S0024」です。
ロータリエンコーダの配線
今回のロータリエンコーダは、A相とB相をそれぞれPICへ入力し、Common端子をGNDへ接続しました。
A相とB相には10kΩのプルアップ抵抗を接続しています。
PIC16F1938への接続は次のようにしました。
RB1 : Encoder A
RB2 : Encoder B
RB3 : Encoder SW
また、このロータリエンコーダには押し込みスイッチも付いているため、今回はこのスイッチをDMXアドレスの決定・保存に使用します。
つまり、
回す → DMXアドレスを変更
押す → DMXアドレスを決定・EEPROMへ保存
という操作にしました。
A相とB相の波形を確認してみる
まずはプログラムを書く前に、
ロータリエンコーダを回したときにA相とB相がどのように変化するのか、
オシロスコープで確認してみました。


黄色:A相、 水色:B相
上図:右回し 下図:左回し
ロータリエンコーダをゆっくり回してみると、
A相とB相が同時に変化するのではなく、
少しずれて変化していることが確認できました。
この2つの信号を、
enc_new = (ENC_A << 1) | ENC_B;
として1つの値にまとめます。
例えばA相、B相がともにHighの場合は、
A = 1
B = 1
→ 0b11
→ 3
となります。
同じようにすると、
A B 値
0 0 → 0
0 1 → 1
1 0 → 2
1 1 → 3
の4つの状態として扱うことができます。
実際にロータリエンコーダを回して確認すると、
回転方向によって状態の変化する順番が逆になりました。
今回の配線では、
3 → 2 → 0 → 1 → 3
と、
3 → 1 → 0 → 2 → 3
という2種類の変化になります。
この違いを利用すれば、
ロータリエンコーダをどちらの方向へ回したのか判定できます。
状態の変化から回転方向を判定する
現在の状態だけでは回転方向を判断できないため、
1つ前の状態と現在の状態を組み合わせて判定します。
そこで、
transition = (enc_old << 2) | enc_new;
としました。
enc_oldを2bit左へずらし、
そこへenc_newを組み合わせることで、
前回の状態 + 今回の状態
を1つの値として扱えるようになります。
そして、その値をswitch文で判定します。
switch (transition) {
case 0b1110:
case 0b1000:
case 0b0001:
case 0b0111:
encoder_step--;
break;
case 0b1101:
case 0b0100:
case 0b0010:
case 0b1011:
encoder_step++;
break;
}
ロータリエンコーダを1クリック回すと、
A相・B相の状態は4回変化します。
そのため、1回信号が変化しただけではDMXアドレスを変更せず、
if (encoder_step >= 4) {
edit_address++;
encoder_step = 0;
}
のように、4回分の状態変化を確認してからアドレスを1つ変更するようにしました。
これで、ロータリエンコーダを1クリック回すごとに、
001
002
003
004
・・・
とDMXアドレスを変更できるようになりました。
ロータリエンコーダの回転速度で変化量を変える
ここまでで、ロータリエンコーダを1クリック回すごとに、
DMXアドレスを1ずつ変更できるようになりました。
しかし、実際に操作してみると問題がありました。
DMXアドレスは1~512まであるため、
1クリックで1ずつしか変化しない場合、
001
002
003
004
・・・
510
511
512
と、大きくアドレスを変更するためには、
ロータリエンコーダを何度も回さなければなりません。
例えば、
DMXアドレスを1から200まで変更するだけでも、
かなりの操作量になります。
そこで今回は、
ロータリエンコーダを回す速度によって、アドレスの変化量を変える
ことにしました。
ゆっくり回したときは細かく設定できるように「1」ずつ、
少し速く回したときは「5」ずつ、
さらに速く回したときは「20」ずつ変化させます。
ゆっくり回す → 1ずつ変更
少し速く回す → 5ずつ変更
速く回す → 20ずつ変更
回転する間隔を測定する
ロータリエンコーダの回転速度を判定するために、
Timer0を使用しました。
Timer0で1msごとに割込みを発生させ、
volatile unsigned long system_ms = 0;
という変数を1ずつ増加させます。
if (INTCONbits.T0IF) {
TMR0 = 6;
system_ms++;
INTCONbits.T0IF = 0;
}
PIC16F1938を16MHzで動作させているため、
Fosc = 16MHz
Fosc / 4 = 4MHz
1命令周期 = 0.25μs
となります。
Timer0のプリスケーラを1:16に設定すると、
0.25μs × 16 = 4μs
となり、Timer0は4μsごとに1カウントします。
1msにするためには、
1000μs ÷ 4μs = 250カウント
必要です。
Timer0は8bitなので0~255の256カウントです。
そこで、
256 - 250 = 6
より、
TMR0 = 6;
として、約1msごとにsystem_msを増加させています。
前回回した時刻との差を求める
ロータリエンコーダが1クリック分回転したところで、
現在のsystem_msと前回の時刻との差を求めます。
encoder_interval = system_ms - last_encoder_time;
last_encoder_time = system_ms;
例えば、
前回:system_ms = 1000
今回:system_ms = 1200
であれば、
1200 - 1000 = 200ms
なので、比較的ゆっくり回したことが分かります。
反対に、
前回:system_ms = 1000
今回:system_ms = 1030
であれば、
1030 - 1000 = 30ms
なので、速く回したと判断できます。
この時間差を使って、アドレスの変化量を次のように変更しました。
if (encoder_interval < 40) {
encoder_increment = 20;
}
else if (encoder_interval < 100) {
encoder_increment = 5;
}
else {
encoder_increment = 1;
}
これによって、ゆっくり回したときには、
083 → 084 → 085 → 086
と細かく調整でき、
速く回したときには、
083 → 103 → 123 → 143
のように、大きくアドレスを移動できるようになりました。
実際に操作してみても、1ずつしか変更できなかったときに比べて、かなり使いやすくなりました。
DMXアドレスの1と512をつなげる
さらに操作しやすくするために、
DMXアドレスの上限と下限をつなげることにしました。
例えば、512付近から1付近へ移動したい場合、
そのまま逆方向へ何度も回すのではなく、
510
511
512
1
2
3
と移動できるようにします。
アドレスを増加させる場合は、
if (edit_address + encoder_increment > 512) {
edit_address = 1;
}
else {
edit_address += encoder_increment;
}
としました。
反対方向についても、
if (edit_address <= encoder_increment) {
edit_address = 512;
}
else {
edit_address -= encoder_increment;
}
とすることで、1より小さくなる場合は512へ移動します。
これで、DMXアドレス1~512の範囲をロータリエンコーダだけで素早く移動できるようになりました。
LCDにDMXアドレスを表示する
ロータリエンコーダでDMXアドレスを変更できるようになったので、
次は現在設定しているアドレスをLCDで確認できるようにします。
今回は20文字×4行のLCDを使用し、
4つのPWM出力に設定されているDMXアドレスを
次のように表示することにしました。
1ch ADDR : 083
2ch ADDR : 114
3ch ADDR : 510
4ch ADDR : 005
これなら、4つのPWM出力にどのDMXアドレスが割り当てられているのかを、
一画面ですべて確認できます。

DMXアドレス1~512をEEPROMへ保存
LCDにDMXアドレスを表示できるようになったので、
次は設定したアドレスをEEPROMへ保存します。
EEPROMへ保存しておくことで、
電源を切った後でも設定したDMXアドレスを保持し、
次回起動時に同じ設定から使用できるようにします。
DMXアドレスは1byteでは保存できない
ここで、これまでのEEPROM保存とは少し違う点がありました。
PIC16F1938のEEPROMは、
1つのアドレスに8bitのデータを保存します。
8bitで表現できる範囲は、
0 ~ 255
です。
しかし、DMX512で使用するアドレスは、
1 ~ 512
となります。
そのため、例えばDMXアドレス510を保存しようとしても、
1byteだけでは保存することができません。
そこで今回は、1つのDMXアドレスを2byteに分けてEEPROMへ保存することにしました。
上位8bitと下位8bitに分ける
DMXアドレスを保存する際は、
16bitの値を上位8bitと下位8bitに分割します。
例えば、設定したアドレスをstored_addressへ格納した後、
addr_high = stored_address[output_index] >> 8;
addr_low = stored_address[output_index] & 0xFF;
としました。
>> 8では、値を8bit右へシフトすることで上位8bitを取り出します。
一方、
& 0xFF
では、下位8bitだけを取り出しています。
例えばDMXアドレス510は16進数では、
510 = 0x01FE
です。
これを2つに分けると、
上位8bit : 0x01
下位8bit : 0xFE
となります。
この2つをEEPROMへ別々に保存します。
EEPROMの保存場所
これまでの記事で、
Upper/Lower Offsetの保存にEEPROMの0~7番地を使用しています。
そこでFree Patch用のDMXアドレスは、
8番地以降を使用することにしました。
#define EEPROM_MIN_OFFSET_ADDR 0
#define EEPROM_MAX_OFFSET_ADDR 4
#define EEPROM_DMX_ADDR_BASE 8
DMXアドレスは1chにつき2byte必要になるため、
次のように割り当てます。
EEPROM 保存内容
0 ~ 3 Lower Offset
4 ~ 7 Upper Offset
8 1ch DMXアドレス 上位8bit
9 1ch DMXアドレス 下位8bit
10 2ch DMXアドレス 上位8bit
11 2ch DMXアドレス 下位8bit
12 3ch DMXアドレス 上位8bit
13 3ch DMXアドレス 下位8bit
14 4ch DMXアドレス 上位8bit
15 4ch DMXアドレス 下位8bit
これで、既存のOffset設定を残したまま、
4ch分のDMXアドレスを保存できます。
エンコーダを押してEEPROMへ保存する
ロータリエンコーダの押し込みスイッチが押されたら、
address_save_request = 1;
として、main関数内でEEPROMへの保存処理を行います。
if (address_save_request) {
stored_address[output_index] = edit_address;
addr_high = stored_address[output_index] >> 8;
addr_low = stored_address[output_index] & 0xFF;
EEPROM_Write(
EEPROM_DMX_ADDR_BASE + (output_index * 2),
addr_high
);
EEPROM_Write(
EEPROM_DMX_ADDR_BASE + (output_index * 2) + 1,
addr_low
);
address_save_request = 0;
}
output_indexは0~3なので、
output_index = 0 → EEPROM 8、9
output_index = 1 → EEPROM 10、11
output_index = 2 → EEPROM 12、13
output_index = 3 → EEPROM 14、15
となります。
これで、ロータリエンコーダを回してアドレスを選択し、
押し込むことで、
そのアドレスをEEPROMへ保存できるようになりました。
保存後は次のチャンネルへ移動する
今回は4つの出力を順番に設定できるようにしました。
1chの設定を保存したら2ch、2chを保存したら3chへ進みます。
output_index++;
if (output_index >= 4) {
output_index = 0;
}
edit_address = stored_address[output_index];
4chまで設定すると再び1chへ戻るため、
1ch
↓
2ch
↓
3ch
↓
4ch
↓
1ch
と、ロータリエンコーダ1つだけで4chすべてのアドレスを設定できます。
電源投入時にEEPROMから読み出す
最後に、電源投入時にはEEPROMへ保存しておいた上位8bitと下位8bitを読み出します。
for (unsigned char i = 0; i < 4; i++) {
addr_high = EEPROM_Read(
EEPROM_DMX_ADDR_BASE + (i * 2)
);
addr_low = EEPROM_Read(
EEPROM_DMX_ADDR_BASE + (i * 2) + 1
);
stored_address[i]
= ((unsigned int)addr_high << 8) | addr_low;
}
保存時とは反対に、
((unsigned int)addr_high << 8)
として上位8bitを元の位置へ戻し、下位8bitのaddr_lowとOR演算して16bitの値へ戻します。
例えばEEPROMから、
addr_high = 0x01
addr_low = 0xFE
と読み出した場合、
0x01 << 8
↓
0x0100
となり、
0x0100
| 0x00FE
---------
0x01FE
となります。
0x01FEは10進数で510なので、保存前のDMXアドレス510を復元できます。
EEPROMの値が正しいか確認する
EEPROMがまだ一度も書き込まれていない場合や、何らかの理由で1~512以外の値が読み出された場合も考慮しました。
if (stored_address[i] < 1 || stored_address[i] > 512) {
stored_address[i] = i + 1;
}
これによって、保存されている値がDMXアドレスとして使用できない場合は、
1ch → 1
2ch → 2
3ch → 3
4ch → 4
を初期値として使用します。
これで、ロータリエンコーダで設定したDMXアドレスをEEPROMへ保存し、電源を切った後でもFree Patchの設定を保持できるようになりました。
保存したアドレスでDMXを受信する
ここまでで、4つのPWM出力それぞれにDMXアドレスを設定し、
その値をEEPROMへ保存できるようになりました。
次は、保存したDMXアドレスを使って、
実際にDMXデータを受信します。
これまでのプログラムでは、
PWM1~4が受信するDMXアドレスは固定されていました。
今回は、
1ch ADDR : 083
2ch ADDR : 114
3ch ADDR : 510
4ch ADDR : 005
のように、設定したアドレスがバラバラでも、
それぞれのPWM出力へ正しいDMX値を渡す必要があります。
受信中のDMXアドレスと設定値を比較する
DMX受信では、Start Codeを受信した後、
dmx_count = 1;
として、現在何ch目のデータを受信しているのかを数えています。
そこで、
現在受信しているアドレス
↓
dmx_count
設定したアドレス
↓
stored_address[]
の2つを比較します。
最初は次のように、for文を使って4ch分を確認することにしました。
for (unsigned char i = 0; i < 4; i++) {
if (dmx_count == stored_address[i]) {
dimmer[i] = data;
}
}
例えば、
stored_address[0] = 83
stored_address[1] = 114
stored_address[2] = 510
stored_address[3] = 5
の場合、DMXの5ch目を受信すると、
dmx_count == stored_address[3]
が成立するため、
dimmer[3] = data;
となります。
同じように83ch目ならdimmer[0]、
114ch目ならdimmer[1]へ受信した値が入ります。
これによって、DMXアドレスを固定することなく、
EEPROMに保存したアドレスを使って
各PWM出力へデータを割り当てられるようになりました。
一番大きなアドレスまで受信する
Free Patch化する前は、
受信するDMXアドレスの範囲も固定されていました。
しかし今回は、
5
83
114
510
のように、設定されるアドレスの順番も大きさも決まっていません。
そのため、4つの設定値の中から最も大きなアドレスを探し、
highest_address = stored_address[0];
for (unsigned char i = 1; i < 4; i++) {
if (stored_address[i] > highest_address) {
highest_address = stored_address[i];
}
}
としてhighest_addressへ保存しました。
先ほどの例なら、
highest_address = 510
となります。
DMX受信は、
このhighest_addressまで到達したところで、
今回必要なデータをすべて受信したと判断します。
if (dmx_count >= highest_address) {
if (valid_frame_count < 3) {
valid_frame_count++;
}
if (valid_frame_count >= 3) {
dmx_ready = 1;
}
dmx_state = DMX_WAIT_BREAK;
}
これで、どのような順番でDMXアドレスを設定しても、
必要な最大アドレスまで受信できるようになりました。
大きなDMXアドレスを設定するとOERRが発生
ここまでのプログラムで実際に動作確認をしてみました。
小さいDMXアドレスを設定している間は問題なく動作し、
ロータリエンコーダからアドレスを変更することもできました。
ところが、510chなどの大きなDMXアドレスを設定して動作させると、
OERR(Overrun Error)が発生し、調光しなくなる問題が発生しました。
以前、DMX受信を作成したときにもOERRには悩まされ、
PIC16F1938の動作クロックを8MHzから16MHzへ変更しています。
今回は16MHzで動作しているにもかかわらず、
Free Patch機能を追加したことで再びOERRが発生しました。
そこで、今回新しく追加したDMX受信部分を見直しました。
for文を使わず直接比較する
特に気になったのが、
DMXデータを受信するたびに実行していたこの処理です。
for (unsigned char i = 0; i < 4; i++) {
if (dmx_count == stored_address[i]) {
dimmer[i] = data;
}
}
DMX512は250kbpsで通信しており、
1スロットは約44μsです。
つまり、この処理は1回だけ実行されるのではなく、
DMXデータを受信するたびに繰り返し実行されます。
そこで今回は4chしかないこともあり、for文を使わず直接比較する形へ変更しました。
if (dmx_count == stored_address[0]) {
dimmer[0] = data;
} else if (dmx_count == stored_address[1]) {
dimmer[1] = data;
} else if (dmx_count == stored_address[2]) {
dimmer[2] = data;
} else if (dmx_count == stored_address[3]) {
dimmer[3] = data;
}
この変更後、
再び大きなDMXアドレスを設定して確認したところ、
OERRは発生しなくなり、
510chを含む設定でも正常に調光できるようになりました。
今回のプログラムでは、for文を使った場合でも処理内容そのものは間違っていません。
しかし、割込み処理の中では、
「正しい結果になるか」
だけではなく、
「次のデータが来るまでに処理を終えられるか」
も重要だということを、
実際のOERRを通して確認することができました。
特に今回のようなDMX受信では、
短い間隔で次々とデータが到着するため、
割込み処理の中はできるだけ短くする必要がある
ことを改めて実感しました。
実際の動き
ここまでで、
・ロータリエンコーダによるDMXアドレス変更
・LCDへのアドレス表示
・EEPROMへの保存
・電源再投入後の設定復元
・Free PatchによるDMX受信
まで実装できました。
実際にDMXアドレスを、
1ch ADDR : 083
2ch ADDR : 114
3ch ADDR : 510
4ch ADDR : 005
のように設定し、
それぞれのアドレスへDMX値を送信して確認しました。
設定したアドレスに応じて、
それぞれのPWM出力が独立して調光できることを確認できました。
また、ロータリエンコーダで変更したアドレスはEEPROMへ保存されるため、
一度電源を切って再投入しても、
設定したアドレスがそのまま復元されます。
Free Patch機能を追加したことで、
これまで固定だったDMXアドレスを、
実際の現場や用途に応じて自由に変更できるようになりました。
Upper / Lower Offset、Gamma補正との共存
今回のFree Patchは、
これまで実装してきた機能へ追加する形で作成しています。
そのため、
DMX受信
↓
Free Patchで各PWM出力へ割り当て
↓
Upper / Lower Offset
↓
Gamma補正
↓
PWM出力
という流れになります。
Free Patchを追加した後も、
これまで実装してきたUpper / Lower OffsetやGamma補正が
正常に動作することを確認しました。
設定機能が増えてきましたが、
それぞれの処理を分けて作ってきたことで、
既存機能を残したまま新しい機能を追加することができました。
動作確認中に発生したトラブル
今回の動作確認中、
約9秒に1度、
4chすべてが一瞬フリッカーする現象が発生しました。
最初は、
・DMX受信処理の負荷
・OERR
・dmx_readyの状態
など、プログラム側の問題を疑いました。
しかし確認を進めていくと、
原因はDMXのData−線の接触不良でした。
Data−が完全に外れていたわけではなく、
接触が悪い状態でもDMX信号をある程度受信できていたため、
最初は配線不良とは気付きませんでした。
Data−を正常に接続し直すと、
フリッカーは発生しなくなりました。
RS-485は差動通信のため、
片側の信号線が不安定な状態でも、
一見すると通信できているように
見える場合があることを実際に確認できました。
今回のように、一定周期で発生する不具合でも、
必ずしもプログラム側に原因があるとは限らないということも勉強になりました。
まとめ
今回は、これまで作成してきたDMX→4ch PWM変換プログラムへ、
Free Patch機能を追加しました。
今回実装した内容は、
・ロータリエンコーダによるDMXアドレス変更
・回転速度による変更量の切り替え
・DMXアドレス1~512の折り返し
・LCDへの4chアドレス表示
・EEPROMへのDMXアドレス保存
・電源再投入後の設定復元
・各PWM出力への任意DMXアドレス割り当て
となります。
特に今回は、DMXアドレスが1~512まであるため、
EEPROMへ保存する際に1byteでは足りず、
上位8bitと下位8bitの2byteに分けて保存する方法を使用しました。
また、Free Patch化したDMX受信処理では、
最初にfor文を使用して4つのアドレスを比較していましたが、
大きなDMXアドレスを設定した際にOERRが発生しました。
4ch分を直接比較するように変更することで改善できたことから、
割込み処理ではプログラムが正しく動くだけでなく、
決められた時間内に処理を終えることも重要だと学ぶことができました。
今回で、
DMX受信
↓
Free Patch
↓
Upper / Lower Offset
↓
Gamma補正
↓
4ch PWM出力
まで、一通りの機能を組み合わせることができました。
次回
今回で、PIC16F1938を使用したDMX→4ch PWM変換の学習については、一旦一区切りとします。
PWM、UART、RS-485、DMX、LCD、EEPROM、ロータリエンコーダなど、
これまで個別に学習してきた機能を、
1つのプログラムとして組み合わせるところまで
進むことができました。
次回からは、これまで学習してきた内容を活かしながら、
もう少し実践的な組み込み制御の製作にも挑戦していきたいと思います。
今回のソースコード
今回作成したDMX Free Patchのソースコードは、GitHubで公開しています。
PIC16F1938を使用したこれまでの学習用プログラムについても、同じリポジトリにまとめています。
https://github.com/nichebebe/embedded-learning/blob/main/DMX_Free_Patch.X/dmx_free_patch.c

コメント