はじめに
前回の記事でDMX受信自体には成功しました。
今回は、実際に受信したDMXの値をPWMにして
アウトプットすることにチャレンジしました。
なお4ch分のDMXを読み取り、
それをPWMに変換して出力させることにチャレンジしてみました。
今回の回路

DMX512信号をUSARTで受信し、
CCP1~CCP4を使いPWMを出力させる回路となります。
実際のコード
PINの初期設定は、
過去のPWM出力の記事(PIC16F1938 3ch ADC → 3ch PWMの作り方~AN8でハマった話とPWM100%にならない話~)などを参考にしています。
なお、今回の周波数は1kHzとしています。
またDMX受信に関しての初期設定などは、
前回の記事(PIC16F1938でDMX512を受信してLEDを制御してみる【DMX受信編】)
を参考に仕上げました。
①DMX値をそのままCCPR1Lに代入してみる
/*********DMXデータをPWMに反映させる関数********/
void pwm_apply(unsigned char i, unsigned char data) {
switch (i) {
case 0:
CCPR1L = data;
break;
case 1:
CCPR2L = data;
break;
case 2:
CCPR3L = data;
break;
case 3:
CCPR4L = data;
break;
}
}
/********DMXを受信しデータを保存する関数*******/
void dmx_receive(void) {
if (!PIR1bits.RCIF) {
return;
}
if (RCSTAbits.FERR) {
dummy = RCREG;
dmx_count = 0;
return;
}
unsigned char data = RCREG;
if (dmx_count == 0) {
start_code = data;
}
else if ((dmx_count <= 512) && (start_code == 0x00)) {
for (unsigned char i = 0; i < 4; i++){
if (dmx_count == my_address[i]){
dimmer[i] = data;
}
}
}
dmx_count++;
}
/*********main関数**********/
void main(void) {
Pin_Init();
USART_Init();
PWM_Init();
while (1) {
if (RCSTAbits.OERR) {
RCSTAbits.CREN = 0;
RCSTAbits.CREN = 1;
}
dmx_receive();
for (unsigned char i = 0; i < 4; i++){
pwm_apply(i, dimmer[i]);
}
}
}
このコードを書き込み動かしてみました。
フェーダを徐々に上げて行き、
それに沿ってLEDもきれいに調光しました。
しかし、フェーダが50%ぐらいでLEDはMAXになってしまいました。
CCPRxLは、8bitであり
DMXデータも8bitであるので、
そのまま代入で問題ないと思っていました。
しかし実際に試してみると、
フェーダが50%程度の位置でLEDがMAXになってしまいました。
データシートを確認すると、PWMのデューティ比は
(CCPRxL : DCxB) ÷ {4(PR2+1)}
で決まります。
今回の設定では、
PR2 = 124
であるため、PWMの最大デューティ値は
4 × (124 + 1) = 500
となります。
一方、CCPRxLに127を書き込むと内部的には
CCPRxL = 127, DCxB = 0の時、
10bit duty値は 127 << 2 = 508
となり、PWMの最大値500を超えてしまいます。
そのためDMX値が約50%の時点でPWM出力が飽和し、
LEDがMAXになっていたのでした。
②DMX値をスケーリングしてみる
①方法では、フェーダが半分でMAXだったので、
次は、計算してスケーリングさせてみました。
void pwm_apply(unsigned char i, unsigned char data) {
unsigned int duty;
duty = ((unsigned int) data * 500) / 255;
switch (i) {
case 0:
CCPR1L = duty >> 2;
CCP1CONbits.DC1B = duty & 0x03;
break;
//
//後略
}
上の “duty=” の式で、0~255を0~500にスケーリングできます!
これを書き込み動かしてみました。

波形はこんな感じです。
かなり広範囲のエリアを撮影しています。
図は high になっているエリアだけが、
ちゃんとしたDutyを出しています。
つまり、LEDはチラチラして見ていられませんでした。
なぜ、これがうまく動かないのか、
全く理解できません。
いろいろと数値を変えてみたり、試してみましたが、
全く症状は治りません。
③高周波数で試す
ここでChatGPTに相談すると、
”PIC16で32bit計算と割り算が重いからだ”
と言われ、言われるがまま、
T2CONbits.T2CKPS = 0b01; // prescaler 1:4
PR2 = 255;
とし、
void pwm_apply(unsigned char i, unsigned char data) {
switch (i) {
case 0:
CCPR1L = data;
CCP1CONbits.DC1B = 0;
break;
//省略
}
}
①と同様にdataをそのまま入れました。
今度はとてもきれいな調光
PRx = 255であるので、
調光はとてもスムーズでした。
PWMのデューティ値は、
CCPRxL の 8bit と DCxB の 2bit を合わせた
10bit値として扱われます。
つまり、
PWM Duty値 = (CCPRxL << 2) + DCxB
となります。
そのため、
フェーダをフルに使っている!
余計な計算がない!
となります。
やはり、計算処理やループ処理に時間がかかり、
DMX受信を取りこぼしている可能性が高いことが分かりました。
でも、この場合、
PWM = 8000000 / (4 × 4 × (255 + 1)) = 約1950Hz
となってしまいます。
照明用のPWM制御では、
1kHz前後のPWM信号が使われることも多いので、
今回もまずは1kHzで動かす方針にしました。
そのため、
T2CONbits.T2CKPS = 0b10; // prescaler 1:16
PR2 = 124;
に戻し、1kHzで勝負がしたいと思いました。
④計算を軽くする
上の反省を生かして次は、
ビットシフト指示で演算をさせてみます。
void pwm_apply(unsigned char i, unsigned char data) {
unsigned int duty;
duty = ((unsigned int) data << 1) - (data >> 5);
if (duty > 500) {
duty = 500;
}
switch (i) {
case 0:
CCPR1L = duty >> 2;
CCP1CONbits.DC1B = duty & 0x03;
break;
//省略
}
}
これでばっちり動きました。
周波数も1kHzです!
この計算の意味は、
DMX : 0 ~ 255
PWM: 0 ~ 500 (PR2=124 ∵4 × (PR2 +1) = 4 × 125 = 500)
この最大値を合わせに行きたい。
もし、DMX値を 2倍すると
0 ~ 510
少しだけど、大き過ぎる。
500 ÷ 255 = 1.961
であるので、
2倍して0.04マイナスしたい
2倍にするのは、
×2 : 左に 1bit シフト
これは簡単。問題は-0.04。
ここで、右のbitシフトを見てみます
1bit >> : 1/2 = 0.5
2bit >> : 1/4 = 0.25
3bit >> : 1/8 = 0.125
4bit >> : 1/16 = 0.0625
5bit >> : 1/32 = 0.03125
0.04に最も近いのは5bit 右シフトの1/32であることが分かりました。
理想値 : 500 / 255 = 1.96078…
近似値 : 2 – 0.03125 = 1.96875
めっちゃ近くなります!
フェーダMAX時は、
255 × 1.96875 = 502.03…
PWMは500までなので、
式の下で500以上は500になるようにしています。
今回使った近似式は以下です。
duty = data × 2 – data ÷ 32
C言語では、これをビットシフトで次のように書きました。
duty = ((unsigned int)data << 1) – (data >> 5);
これで動かした様子は次の動画です。
1ch目のフェーダを上げたときはスムーズな調光をしています。
しかし2ch目のフェーダはちらつきが酷く、
3ch, 4ch目のフェーダに関しては、
2番目のLEDが反応してしまっていて、
完全におかしな挙動をしてしまっています。
DMXの1ch目はしっかりと拾えているが、
2ch目は怪しく、
3ch、4ch目は完全にアウトといった感じだと思います。
PICの仕事をもう少し減らしてあげる
少しPICの仕事を減らすために、
for文を消してみました。
/********DMXを受信しデータを保存する関数*******/
void dmx_receive(void) {
if (!PIR1bits.RCIF) {
return;
}
if (RCSTAbits.FERR) {
dummy = RCREG;
dmx_count = 0;
return;
}
unsigned char data = RCREG;
if (dmx_count == 0) {
start_code = data;
}
else if ((dmx_count <= 512) && (start_code == 0x00)) {
if (dmx_count == 1) dimmer[0] = data;
if (dmx_count == 2) dimmer[1] = data;
if (dmx_count == 3) dimmer[2] = data;
if (dmx_count == 4) dimmer[3] = data;
}
dmx_count++;
}
/*********main関数**********/
void main(void) {
Pin_Init();
USART_Init();
PWM_Init();
while (1) {
if (RCSTAbits.OERR) {
RCSTAbits.CREN = 0;
RCSTAbits.CREN = 1;
}
dmx_receive();
pwm_apply(0, dimmer[0]);
pwm_apply(1, dimmer[1]);
pwm_apply(2, dimmer[2]);
pwm_apply(3, dimmer[3]);
}
}
}
これで動かしてみましたが、
結果は上の動画とほぼ変わらず。。。
次に1chずつ受信させてみることにしました。
1ch目は動いていることは確認済みなので、
2ch目、3ch目、4ch目をそれぞれ順に試してみました。
結果は、2ch目、3ch目はうまく調光しましたが、
4ch目だけは無反応。。。
そこで、CCP4が死んでいる可能性も考えられるので、
pwm_apply(3, dimmer[0])
このコードを書き込んでみると、
ちゃんとLEDは反応しました。
つまりPWMをつくるCCP4はちゃんと働いているが、
4ch目のデータを取りこぼしていることが確認できました。
考察と次回予告
今回は動作確認を優先し、
ポーリングでDMX受信してみました。
32bit計算や割り算をさせることが
PICにとってかなり重い動作であることが確認できました。
DMXは250kbpsで連続してデータが届くため、
ポーリング受信ではメインループ内の処理時間によって
受信を取りこぼす可能性があります。
そのため、次回はUSART受信割り込みを使い、
より安定したDMX受信を目指します。
シリーズ一覧
STEP1 UART
STEP2 RS485
STEP3 DMX送信
STEP4 DMX理解
STEP5 DMX受信
▶ STEP6 DMX→PWM(今回)
STEP7 現場仕様化


コメント