STM32でフェーダーのバタつきを完全攻略!ADCノイズ対策の決定版
はじめに
照明制御やオーディオミキサーなど、アナログフェーダー(スライドボリューム)を使う組み込みシステムで必ず直面する問題があります。
「フェーダーを触っていないのに出力値がチラチラ変わる」
いわゆる「バタつき」問題です。
本記事では、STM32マイコンで実際に運用しているDMX照明コントローラーで採用している3段階のノイズ対策を詳しく解説します。
アナログフェーダー読み取りの難しさ
アナログフェーダーを正確に読み取るのは、なかなか難しい点があります。
昔のハードウェア対策
大昔(1980〜90年代)は、ダイオード+コンデンサによる積分回路でノイズを抑えていました。
ダイオード コンデンサ
フェーダー ──┤>├──┬──┤├── GND
│
└──→ ADC入力
問題点: 積分回路を入れると、その分立ち上がりが遅れます。
フェーダーを素早く動かしても、出力がゆっくり追従する「もっさり感」が出てしまいます。
現代のアプローチ
現在はプログラム(ソフトウェア)で対処します。
- ハードウェア積分なし → 高速応答を維持
- ソフトウェアフィルタ → 柔軟なパラメータ調整
- 上昇/下降で異なる処理 → ハードウェアでは困難な処理が可能
ハードウェア側の最低限の対策
ソフトウェアで対処するとはいえ、ハードウェア側でも最低限の対策は必須です。
1. パスコン(バイパスコンデンサ)
フェーダー基板にはパスコン(0.1μF程度) を入れます。
VCC ──┬── フェーダー ──┬── GND
│ │
─┴─ 0.1μF ─┴─ 0.1μF
│ │
GND GND
⚠️ 注意: パスコンの配置や値を間違えると発振してNGになります。
- 容量が大きすぎる → 発振の原因
- 配線が長い → インダクタンスで発振
- 電源ラインに近すぎる → ノイズを拾う
2. 電源の多段安定化
本システムでは以下の電源構成を採用しています:
┌─────────────────────────────────────────────────────────┐
│ 12V給電 → 5V安定化 → 3.3V安定化 → A/Dサンプリング │
│ ↓ ↓ ↓ ↓ │
│ 入力電源 ロジック用 ADC基準電圧 高精度読み取り │
└─────────────────────────────────────────────────────────┘
| 段階 | 電圧 | 用途 | 備考 |
|---|---|---|---|
| 入力 | 12V | 外部給電 | AC/DCアダプタ等 |
| 1段目 | 5V | デジタル回路 | 7805等のリニアレギュレータ |
| 2段目 | 3.3V | ADC・マイコン | LDO推奨(低ノイズ) |
ポイント:
- スイッチング電源のリプルは12V→5Vで大幅に減衰
- 5V→3.3VはLDO(Low Dropout Regulator)で低ノイズ化
- ADCの基準電圧は3.3Vラインから取得
ハード+ソフトの組み合わせ
結論として、ハードウェアだけでもソフトウェアだけでも不十分です。
┌─────────────────────────────────────────────────────────────┐
│ ハードウェア対策 ソフトウェア対策 │
│ ├─ パスコン ├─ ヒステリシス │
│ ├─ 多段電源安定化 ├─ デッドバンド │
│ └─ 適切な配線 └─ スムージング │
│ ↓ ↓ │
│ 高周波ノイズ除去 低周波バタつき防止 │
│ └────────────┬────────────┘ │
│ ↓ │
│ 安定したフェーダー動作 │
└─────────────────────────────────────────────────────────────┘
目次
なぜバタつくのか?原因の理解
ADCノイズの発生源
┌─────────────────────────────────────────────────────────┐
│ フェーダー → ADC → マイコン → DMX出力 → 照明器具 │
└─────────────────────────────────────────────────────────┘
↑ ↑
│ └── 量子化ノイズ、電源ノイズ
└── 接触抵抗変動、温度ドリフト
| ノイズ源 | 原因 | 影響範囲 |
|---|---|---|
| 量子化ノイズ | 12bit ADCの最下位ビットの揺れ | ±1〜2 LSB |
| 電源ノイズ | スイッチング電源、モーター | ±3〜5 LSB |
| 接触抵抗 | ボリュームの摺動子劣化 | ±2〜4 LSB |
| 温度ドリフト | 周囲温度変化 | 長期的な変動 |
典型的な症状
フェーダーを50%位置に固定しても:
時刻 ADC生値 そのまま出力すると...
0.00s 127 照明127
0.01s 128 照明128 ← チカッ
0.02s 127 照明127 ← チカッ
0.03s 129 照明129 ← チカッ
0.04s 127 照明127
人間の目は1〜2%の明るさ変化も敏感に察知します。
対策1: ADCヒステリシス
概念
ヒステリシス(履歴効果)とは、変化量が閾値を超えない限り前回値を維持する手法です。
AD_HYST=3 の場合
入力値: 127 → 128 → 129 → 127 → 130 → 131 → 128
↓ ↓ ↓ ↓ ↓ ↓ ↓
出力値: 127 → 127 → 127 → 127 → 130 → 131 → 128
無視 無視 無視 更新! 更新 更新
実装コード
// dmx_config.h
#define AD_HYST 3 // ヒステリシス幅(±3で変化を無視)
// main.c
uint16_t ReadADC1_Hyst(uint8_t channel, uint16_t prev) {
uint16_t dd = ReadADC1(channel);
// 重要: 端(0と255)は即座に更新
// → フェーダー最小・最大で確実に0%/100%を出すため
if (dd == 0 || dd == 255) {
return dd;
}
// 変化量の絶対値を計算
int16_t diff = (int16_t)dd - (int16_t)prev;
if (diff < 0) diff = -diff;
// 閾値以下なら前回値を維持
if (diff <= AD_HYST) {
return prev;
}
return dd;
}
ポイント解説
| 処理 | 理由 |
|---|---|
dd == 0 || dd == 255 |
端の例外処理が重要! ヒステリシスで端に到達しない問題を回避 |
int16_tでキャスト |
uint16_t同士の引き算でアンダーフローを防ぐ |
AD_HYST = 3 |
経験的に3〜5が最適。大きすぎると応答が鈍い |
効果
Before: 127 → 128 → 129 → 127 → 130 (4回変化)
After: 127 → 127 → 127 → 127 → 130 (1回変化)
ノイズ起因の変化を約75%削減!
対策2: 出力デッドバンド
問題: LED照明の低輝度応答
多くのLED照明器具は、DMX値1〜5程度ではほとんど発光しないか、チラつく問題があります。
DMX値: 0 1 2 3 4 5 6 7 ...
実際の光: × × △ △ △ ○ ○ ○ ...
消灯 不安定 安定発光
解決策: デッドバンド
0以外の出力には一定値を加算して、LED照明の安定動作領域へジャンプさせます。
計算値: 0 1 2 3 4 5 6 ...
出力値: 0 4 5 6 7 8 9 ...
└──────────────────────┘
OUT_DEADBAND=3 を加算
重要: 上昇時のみ加算
単純に「0以外なら加算」とすると、下降時に0まで到達しないバグが発生します。
❌ 間違った実装:
計算値: 10 → 5 → 3 → 1 → 0
出力値: 13 → 8 → 6 → 4 → 0 ← 急に4→0でジャンプ!
✅ 正しい実装(上昇時のみ加算):
計算値: 0 → 1 → 3 → 5 → 3 → 1 → 0
出力値: 0 → 4 → 6 → 8 → 3 → 1 → 0
上昇時加算 下降時はそのまま
実装コード
// dmx_config.h
#define OUT_DEADBAND 3 // 上昇時に加算する値
// main.c - id()関数
void id(void) {
uint16_t i, d1, d2, d3, d4, d5;
d1 = Vd[9]; // マスターフェーダー値
Od[9] = Vd[9]; // マスターはそのまま出力
for (i = 1; i <= 8; i++) {
d2 = Vd[i]; // チャンネル入力値
d3 = d2 * d1; // マスターを掛ける
d4 = d3 / 255; // 0-255にスケーリング
// ★ 核心部分: 上昇/下降で処理を分ける
if (Od[i] > d4 + OUT_DEADBAND) {
// 下降中: デッドバンドなしで滑らかに減少
d5 = d4;
} else if (Od[i] >= d4 && d4 > 0) {
// 下降途中: 引き続き下降
d5 = d4;
} else if (d4 == 0) {
// 完全消灯を目指す
d5 = 0;
} else {
// 上昇中: デッドバンド加算
d5 = d4 + OUT_DEADBAND;
}
// スムージング(後述)
if (Od[i] > d5) {
Od[i]--;
} else if (Od[i] < d5) {
Od[i]++;
}
}
}
状態遷移図
┌──────────────┐
d4増加 │ │ d4減少
┌────────────────│ 通常状態 │────────────────┐
│ │ Od = d4+DB │ │
│ └──────────────┘ │
│ ↑ │
│ │ d4増加 ↓
│ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │
└───────────────→│ 上昇中 │ │ 下降中 │
│ Od = d4+DB │ │ Od = d4 │
└──────────────┘ └──────────────┘
│
│ d4=0
↓
┌──────────────┐
│ 消灯 │
│ Od = 0 │
└──────────────┘
対策3: スムージング処理
概念
急激な値変化を防ぐため、1回の処理で1ステップずつしか変化させない手法です。
目標値: 100 → 200(急変)
出力値: 100 → 101 → 102 → ... → 199 → 200(滑らかに変化)
実装
// 1ステップずつ変化
if (Od[i] > d5) {
Od[i]--; // 目標より大きければ1減らす
} else if (Od[i] < d5) {
Od[i]++; // 目標より小さければ1増やす
}
DMXリフレッシュレートとの関係
DMX512は通常25〜44Hzでリフレッシュします。
30Hz × 255ステップ = 約8.5秒で0→255
↓
十分滑らかなフェード
実装コード全体
dmx_config.h
#ifndef __DMX_CONFIG_H
#define __DMX_CONFIG_H
// ADCヒステリシス値(バタつき防止)
// 大きいほど安定、小さいほど応答速い
#define AD_HYST 3
// DMX出力オフセット(照明器具のLED応答向上用)
// 上昇時のみ加算、下降時は加算しない
#define OUT_DEADBAND 3
// リフレッシュレート選択
#define DMX_30HZ // または DMX_25HZ
// チャンネル数
#define DMX_CHANNELS 512
#endif
main.c(抜粋)
//==============================================================================
// ヒステリシス付きADC読み取り
//==============================================================================
uint16_t ReadADC1_Hyst(uint8_t channel, uint16_t prev) {
uint16_t dd = ReadADC1(channel);
// 端(0と255)は即座に更新
if (dd == 0 || dd == 255) {
return dd;
}
int16_t diff = (int16_t)dd - (int16_t)prev;
if (diff < 0) diff = -diff;
if (diff <= AD_HYST) {
return prev;
}
return dd;
}
//==============================================================================
// 出力値計算(スムージング + デッドバンド)
//==============================================================================
void id(void) {
uint16_t i, d1, d2, d3, d4, d5;
d1 = Vd[9];
Od[9] = Vd[9];
for (i = 1; i <= 8; i++) {
d2 = Vd[i];
d3 = d2 * d1;
d4 = d3 / 255;
// 上昇/下降で処理を分ける
if (Od[i] > d4 + OUT_DEADBAND) {
d5 = d4; // 下降中
} else if (Od[i] >= d4 && d4 > 0) {
d5 = d4; // 下降途中
} else if (d4 == 0) {
d5 = 0; // 消灯
} else {
d5 = d4 + OUT_DEADBAND; // 上昇中
}
// 1ステップずつ変化
if (Od[i] > d5) {
Od[i]--;
} else if (Od[i] < d5) {
Od[i]++;
}
}
}
パラメータ調整ガイド
AD_HYST(ヒステリシス幅)
| 値 | 効果 | 用途 |
|---|---|---|
| 1-2 | 高応答・やや不安定 | 高精度要求 |
| 3-4 | バランス良好 | 一般用途(推奨) |
| 5-8 | 超安定・応答鈍い | ノイズ環境劣悪 |
OUT_DEADBAND(出力オフセット)
| 値 | 効果 | 用途 |
|---|---|---|
| 0 | オフセットなし | 高品質LED |
| 3-5 | チラつき防止 | 一般LED(推奨) |
| 8-10 | 安全マージン大 | 安価なLED |
調整フロー
1. まずAD_HYST=3, OUT_DEADBAND=3で試す
↓
2. バタつきが残る → AD_HYSTを+1
↓
3. 低輝度でチラつく → OUT_DEADBANDを+1
↓
4. 応答が遅い → AD_HYSTを-1
評価と改善案
現在の実装の評価
| 項目 | 評価 | コメント |
|---|---|---|
| バタつき抑制 | ⭐⭐⭐⭐⭐ | 実運用で問題なし |
| 応答速度 | ⭐⭐⭐⭐ | スムージングで若干遅延 |
| コード量 | ⭐⭐⭐⭐⭐ | シンプルで保守性良好 |
| CPU負荷 | ⭐⭐⭐⭐⭐ | 整数演算のみ、軽量 |
| 0到達性 | ⭐⭐⭐⭐⭐ | 修正後は確実に0到達 |
改善案
1. 移動平均フィルタ追加
#define FILTER_SIZE 4
uint16_t adc_buffer[FILTER_SIZE];
uint8_t adc_index = 0;
uint16_t ReadADC1_Filtered(uint8_t channel) {
adc_buffer[adc_index] = ReadADC1(channel);
adc_index = (adc_index + 1) % FILTER_SIZE;
uint32_t sum = 0;
for (int i = 0; i < FILTER_SIZE; i++) {
sum += adc_buffer[i];
}
return sum / FILTER_SIZE;
}
2. 適応型ヒステリシス
// 中間値では大きく、端では小さく
uint16_t adaptive_hyst(uint16_t value) {
if (value < 10 || value > 245) {
return 1; // 端では高精度
}
return AD_HYST; // 中間では安定重視
}
3. 加速度対応スムージング
// 変化量が大きいほど速く追従
int16_t diff = d5 - Od[i];
if (diff > 10) {
Od[i] += 3; // 大きな変化は速く
} else if (diff > 0) {
Od[i]++; // 小さな変化はゆっくり
} else if (diff < -10) {
Od[i] -= 3;
} else if (diff < 0) {
Od[i]--;
}
まとめ
フェーダーのバタつき対策は3段階のアプローチが効果的です:
┌─────────────────────────────────────────────────────────────┐
│ ADC読み取り → 値計算 → 出力 │
│ ↓ ↓ ↓ │
│ ヒステリシス デッドバンド スムージング │
│ (入力安定化) (低輝度対策) (急変防止) │
└─────────────────────────────────────────────────────────────┘
| 対策 | 効果 | 注意点 |
|---|---|---|
| ヒステリシス | ノイズ無視 | 端の例外処理必須 |
| デッドバンド | LED安定動作 | 上昇時のみ加算 |
| スムージング | 滑らかな変化 | リフレッシュレート考慮 |
これらを組み合わせることで、安定かつ応答性の良いフェーダー制御が実現できます。
参考
- STM32F10x Standard Peripheral Library
- DMX512 Protocol Specification (ANSI E1.11)
タグ: STM32 ADC ノイズ対策 DMX512 照明制御 組み込み C言語