Hobby Lab 趣味のモノ作り実験のサイトです。
部品 プロセッサー他 CH32V Variable_BandPass_Filter_MCU
1 MCUバンドパスフィルター紹介
1.1 メイン処理
1.2 割り込み処理
1.3 その他(LCD関係除く)
1.3.1 双二次フィルタ
1.3.2 自動環境検出
1.3.3 デジタル・コンプレッサー
1.3.4 処理高速化 固定小数点演算
1.3.5 全体の処理フローのまとめ
1.4 具体的な動作
1.4.1 パラメータの動作内容
1.4.2 注意点
1.4.3 RBJ Audio EQ Cookbook
1.4.4 ダイレクトフォーム I
1.4.5 ゲーツェルアルゴリズム
1.5 RBJ補足事項
1.5.1 舞台裏にある「双線形変換
1.5.2 各変数の役割と数式の意味
1.5.3 なぜ最後に a0 で割るのか
1.6 プログラムの補足
1.6.1 思い関数は起動時に実行
1.6.2 ローパス・ハイパスへ改造
1.6.3 改造時のポイント
1.6.4 コードの解説
2 プログラム
2.1 system_ch32v20x.c
2.2 io_config.c
2.3 interrupt.c
2.4 bpf_filter.c
2.5 gmt130_lcd.c
2.6 font.c
2.7 main.c
2.8 ch32v20x_it.c
2.9 ch32v20x_conf.h
3 テスト 未完成
3.1 プログラム動作時間測定
3.2 バンドパス特性測定

Pr:Prプロセッサ関係
PrP:プロセッサ
動作比較
 STM32F動作比較
 CH32V203&STM32F 動作比較
 arduino動作比較
raspberrypi関係
 RaspberryPiハード
CH32V関係
 -CH32V開始
 -203K8T6(32Pin)開始
 -203C8T6(48P)開始
 -003J4M6(8Pin)開始
 -003F4P6(20Pin)開始
 -Moun River StudioⅡで
 プログラミング!
  203_GPIO関係
  203_TIME関係
  203_TIME_Encoder
  203_I2C関係
  203_1-Wire関係
  003_DS18B20テスター
  USART(UART)関係
  DS18B20をModBus制御
  可変型フィルター 周辺
  可変型フィルター MCU
 -マニュアル
 203データシート
 203取説
  MBA メモリとバス方式
  PWR 電力制御
  RCC リセット・拡張・クロック
  BKP バックアップレジスタ
  CRC 巡回冗長検査
  RTC リアルタイムクロック
  GPIO GPIOと代替機能
  DMA ダイレクトメモリアクセス制御
  ADTM 高度な制御タイマー
  GPTM 汎用タイマー
  BCTM 基本タイマー
  USART 同期非同期通信
arduino関係
 ESP12関係
 (a)ESP-8266D1mini注意
PrP:Memory  24C128(16kB)
PrP:その他  RS485ドライバー
 CP2102 BRIDGE
 WCH-LinkEエミュレーター
Pr:Wire 電線関係
Pr:Resistance 抵抗
Pr:Capacitor コンデンサ
Pr:Coil コイル
Pr:PassiveElmt 受動素子
Pr:Diode ダイオード関係
Pr:OPAMP オペアンプ関係
PrO:送受信機  Si4735について
 Si4735ラジオを作って見よう1
 Si4735ラジオを作って見よう2
 Si4735テスト中に判ったこと1
PrO:オペアンプ  オペアンプ全般
 バンドパスフィルター
 LM324
 LM358
Pr:Tr トランジスタ関係
2SC1815
 リレードライバー設計
 アンプ設計
 発振器
TLP152
 TLP152テスト
TLP2361
 TLP2361テスト
TLP5754
 TLP5754テスト
Pr:Source 電源関係  ツェナーダイオード
 TL431
 LM317
PrS:Downモジュール
 EGS002_IR2110S
 SKU011012
 ACDC02
 XH_M299
 LM2596
 Mini360_MP23070N
 DROK
 WH140
PrS:UPモジュール
 MT3608
PrS:充電モジュール
 TP4056
Pr:Sensor_AD_時計等
PrS:電圧、電流
ADS1115 16bit4CH I2C A/D
 Hardware
 RaspberryPi_コマンド接続
 RaspberryPi_Python
 Arduino
 CH23V203 MounRiverStudioⅡ
INA226 I2C 直流電圧電流
 Hardware
 Arduino
 RaspberryPi_Python
WCS 電流ホール素子
 Hardware
PrS:温度、気圧、湿度、照度
BNE280 I2C 気圧,湿度,気温
 Hardware
 Arduino
 RaspberryPi_Python
BH1750 I2C 照度
 Hardware
 Arduino
 RaspberryPi_Python
DS18B20 1-Wire 温度計
 Hardware
 Arduino
 RaspberryPi_Python
PrS:時間、日時
DS3231 I2C 時計
 Hardware
 Arduino
 RaspberryPi_Python
PrS:表示器
MAR3953 320X480 3.95"
 概要と線や点を描く
 フォントを描く
SSD1306 I2C 0.96"OLED
 Hardware
 Arduino
 RaspberryPi_Python
Pr:Old Processor他

1. MCUバンドパスフィルター紹介

MCU は CH32V203K8T6 を使用して MounRiver Studio Ⅱ で開発します。

MCU では下記の様な処理を行っています。
 なお最後の( )は後で説明する関数等の名前です。

1.1 メイン処理

(1)初期化
 a.時間待ち(Delay_Ms(10)の初期化)の初期化(Delay_Init)
 b.モニター用シリアルの初期化(USART_Printf_Init)
 c.I/Oの初期化(IO_Config)
 d.LCD関係の初期化(LCD_Init)

(2)ループ内処理
 a.設定値確認
ローターリーエンコーダの使用も考えたのですが、安価にするため、手持ちで可変抵抗が大量に有ったので A/D コンバーを使用して以下の数値設定を行いました。
 (a)バンドパスの中心周波数foの設定
 (b)バントパスの通過帯域Btの設定
 (c)バンドパスの処理段数の設定
 (d)バンドパスの出力信号をコンプレサーするゲインの設定
 (e)LCDのバックライトの光度の設定
 b.フィルター設定値のリアルタイム処理(Calculate_BPF_Coeffs)
 c.LCD表示(LCD_Display_Update)

1.2 割り込み処理

a.TIM1(10ms)を使用したLCD表示の間隔フラグ処理
b.TIM4()を使用し
(a)CW等の音声データをデジタル変換処理(Read_ADC2)
(b)フィルター処理(Process_BPF)
(c)PWM(TIM3)出力処理(TIM_SetCompare1)
の処理をしています。

1.3 その他(LCD関係除く)

LCDについては2項で説明します。
大きく分けて4つの主要なアルゴリズムを見つけることができます。
このコードは、マイコンなどの処理能力が限られた環境で、音声信号から特定の周波数成分だけを抽出し、 リアルタイムで音量を整えて出力するための「高速デジタル信号処理(DSP)アルゴリズム」の集合体です。
CH32V203はFPUをもっていないため、1秒間に8,000回(8kHz)の割り込みの中でいかにコンパクトに処理するかなど、 それぞれのアルゴリズムについて、分かりやすくかみ砕いて説明します。

1.3.1 双二次フィルタ

IIRバイカッド・バンドパスフィルタと言います。 このアルゴリズムは RBJ フィルターアルゴリズムで概略を説明しています。
•実施している場所:
Calculate_BPF_Coeffs および Process_BPF 内の for (int i = 0; i < stages; i++) ループ

• 仕組みと説明:
特定の周波数(f0:中心周波数)とその周辺の音(bw:帯域幅)だけを通過させ、それ以外のノイズをカットするアルゴリズムです。
Calculate_BPF_Coeffs では、一般的な Audio EQ Cookbook という数式をベースにフィルタの係数(b0〜b2, a1〜a2)を計算しています。
Process_BPF では、過去の入力データ(x1, x2)と過去の出力データ(y1, y2)を掛け合わせることで、少ない計算量で急峻なフィルタ特性(多段接続:stages)を実現しています。

1.3.2 自動環境検出

IIR型フィルタの直流分を除去(センター電圧設定の自動化)
• 実施している場所:
 auto_center(センター電圧検出)と noise_energy(ノイズ電圧検出)の計算部分

• 仕組みと説明:  直近のデータの平均値を滑らかに追従していくアルゴリズムです。
• センター電圧(直流成分)の追従: 音声の基準となる真ん中の電圧(無音時の電圧)がずれても、 ((auto_center * 1023) >> 10) + input_raw という式で、過去99.9%のデータと新しい0.1%のデータを混ぜ合わせ、自動的に基準線(センター)を割り出しています。
• 環境ノイズの追従: 音が小さい時(50mV以下)の平均的な音量を同じ仕組み(過去99%と新しい1%のブレンド)で追従し、周囲のホワイトノイズの大きさを自動検出しています。

1.3.3 デジタル・コンプレッサー

デジタル・コンプレッサー・リミッター(信号拡大・制限)
• 実施している場所:
信号拡大機能 の if (mv_val > limit_mv) 分岐部分

• 仕組みと説明:
音量を一定の範囲に収めつつ、小さな音を聴き取りやすくするアルゴリズムです。
現在の音量(mv_val)が目標値(limit_mv)を超えている場合は、音が割れないように最大値(±1500)へ抑え込み(リミッター)、 目標値より小さい場合は、一律で音量を引き上げて拡大(コンプレッサー)しています。ここでも割り算の回数を最小限に抑える工夫がされています。

1.3.4 処理高速化 固定小数点演算

浮動小数点の計算をビットシフトによる高速化
マイコンにとって負担の大きい「実数(float)の割り算」を一切排除し、「整数の掛け算とビットシフト」に置き換えるアルゴリズム(手法)です。
例えば、係数をあらかじめ16384倍(\(2^{14}\))しておき、掛け算した後に >> 14(16384で割る処理)を行うことで、
整数だけで小数点以下の計算を高速にシミュレートしています(Q14フォーマット)。
ただし一部高速化により 1024/1000 = 1.024 で2.4%程度の誤差を生じているところもあります。(誤差をあまり考えなくても良いとは思いますが)
実施している場所を4例あげます
(1)双二次IIRフィルタ(バンドパス)
• 元々の数式:
  \(\begin{aligned}y_{n}=\frac{b_{0}x_{n}+b_{1}x_{n-1}+b_{2}x_{n-2}-a_{1}y_{n-1}-a_{2}y_{n-2}}{a_{0}}\end{aligned}\)
  (特定の周波数成分のみを抽出する)
 標準的な差分方程式です。通常は b0 や a1 などの係数が 0.1234 のような少数になります。

• 整数化での変更内容:
Calculate_BPF_Coeffs 内で、あらかじめ係数をすべて a0 で割り算し、さらに 16384倍(2^14) して整数(int32_t)として保存しています。
これにより、音声処理本体(Process_BPF)の中では、
int32_t y = (b0*x + ... ) >> 14;
という、重い float 演算と割り算を一切使わない「整数の掛け算と右ビットシフト(16384で割る)」だけの爆速な式に変更されています。

・あらかじめ係数を a0 で割って正規化。
・係数を16384倍(2^14)して整数化。
・計算後に >> 14 で桁を戻す(Q14形式)。

(2)1次IIRローパスフィルタ(環境検出)
• 元々の数式:
  \(Y_{n}=\alpha \cdot Y_{n-1}+(1-\alpha )\cdot X_{n}\)
  (過去のデータから滑らかな平均値を出す)
 直流成分(センター電圧)を検出する式は、通常
  auto_center = auto_center * 0.999f + input_raw * 0.001f; のように float で記述します。

• 整数化での変更内容:
0.999≒1023/1024 、0.001≒1/1024 であることを利用しています。
プログラム内の ((auto_center * 1023) >> 10) + input_raw という式は、全体を1024倍(Q10フォーマット)のスケールで管理し、 >> 10(1024で割る)によって、float を一切使わずに「直近約1000サンプルの移動平均」を追従する処理へ変更されています
(ノイズ検出の * 1014 と * 10 も同様に α=0.99, 1-α=0.01 の整数化です)。

・小数の係数 α=0.999\ や 0.99 を排除。
・1024倍(2^10)して整数化(Q10形式)。
・* 1023 や * 1014 の掛け算と >> 10 に置換。

(3)電圧・mV変換
• 元々の数式:
  \(\begin{aligned}\text{mV}=\text{DigitalValue}\times \frac{3300\text{mV}}{4096\text{階調}}\end{aligned}\)
  (AD変換値を実際の電圧[mV]に換算する)

• 整数化での変更内容:
  ・3300/4096 の割り算を排除。
  ・4096 = 2^12 であることを利用。
  ・* 3300 をした後に >> 12 を行う処理に変更。

(4)動的ゲイン制御
(コンプレッサー)
• 元々の数式:
  \(\begin{aligned}x_{\text{out}}=x\times \frac{1500}{\text{mv_val}}\text{ または }\frac{1500}{\text{limit_mv}}\end{aligned}\)
  (音量を一定に保ち、小音量を拡大する)

• 整数化での変更内容:
  ・通常 float で行う「ゲイン(倍率)」の算出をスキップ。
  ・x * 1500 を先に計算し、最後に現在の電圧等で割る整数処理に変更。


1.3.5 全体の処理フローのまとめ

このプログラムは、1秒間に何万回も送られてくる音声の1サンプルごとに、以下のステップをすべて整数演算だけで超高速に実行しています。

[音声入力]
  ↓
【1.3.2】入力のズレ(直流成分)を自動で取り除く
  ↓
【1.3.1】特定の周波数(欲しい音)だけをフィルタで抜き出す
  ↓
【1.3.2】周囲の雑音レベルを裏でこっそり計測・更新する
  ↓
【1.3.3】小さな音を大きく、大きすぎる音を小さく整える
  ↓
[スピーカー(DAC)へ出力]

【1.3.4】(全体を通して)floatを使わずビットシフトで爆速処理

1.4 具体的な動作

1.4.1 パラメータの動作内容

初期設定(パラメータ)での具体的な動作
設定された値をもとに、このプログラムがどのように動くかを解説します。
(1) Fo/BW
• f0 = 1500(中心周波数) / bw = 1500(通過帯域幅)
• 意味: 1500Hz の音を中心に、そこから上下に広がる 1500Hz幅(750Hz 〜 2250Hz) の音声成分だけを綺麗に通過させ、 それ以外の高音・低音(ブーンという電源ノイズなど)をバッサリとカットします。

(2) ATT
• att_order = 2(処理段数)
• 意味: フィルタの「次数」が 2次 であることを示します。
• プログラム内での動き: stages = att_order / 2 で計算されるため、stages = 1(1段階)になります。
Process_BPF 内の for ループは 1回だけ まわり、最低限の計算量で効率よく1500Hzの音を抽出します。

(3) AGC/SGC
• sgc = 1500(コンプレッサー・リミット値)
• 意味: 信号を拡大・制限する基準電圧を 1500mV に設定しています。
• プログラム内での動き:
• フィルタを通過した音の強さ(mv_val)が 1500mV を超えた大音量のとき:音が割れないように自動的にボリュームを絞り、 最大でも ±1500mV の幅(DAC出力でいうと約548〜3548)にカチッと制限(リミット)します。
• 音の強さが 1500mV 以下の小音量のとき:x = (x * 1500) / limit_mv の式において limit_mv が 1500 なので、(x * 1500) / 1500 = x となり、 音量はそのまま(拡大も縮小もせず)維持されます。

小さな音を「2倍」や「4倍」に拡大するためには、sgc(limit_mv)の値を小さく設定変更する必要があります。
結論からお伝えすると、設定すべき値は以下の通りです。
  • 2倍に拡大したい場合: global_params.sgc = 750;
  • 4倍に拡大したい場合: global_params.sgc = 375;

なぜ sgc を小さくすると音が大きくなるのか?(仕組みの解説)

プログラム内の (4) 信号拡大機能 にある以下の数式がポイントです。
途中の波(limit_mv以下の小さな音)も比率を上げて拡大
x = (x * 1500) / limit_mv;

この式は、「どんなに小さな音(limit_mv 以下の音)であっても、一律で最大ボリューム(1500)まで引き上げる(=拡大する)」 という計算を行っています。

① sgc = 750(2倍拡大)のとき
小さな音(波の振幅 x)に対して、プログラムは次のように計算します。
新しい音 = x × 1500 / 750 = x × 2

元の音 x が綺麗に2倍に拡大されます。

② sgc = 375(4倍拡大)のとき
同様に、375に設定した場合は以下のようになります。

新しい音 = x × 1500 / 375 = x × 4

元の音 x が4倍に拡大されます。


1.4.2 注意点

設定を変更する際の注意点(音割れとノイズ)
sgc を小さくする(倍率を上げる)ほど、かすかな音でもハッキリ聴こえるようになりますが、以下のデメリットも発生します。
(1) 環境ノイズも一緒に拡大される
• 元の音が4倍になれば、背景の「サー」というノイズも4倍に膨らみます。
• ただし、このプログラムには (2) ノイズ電圧の自動検出 が備わっており、50mV以下の無音状態のときは自動的にノイズレベル(nzv)を裏で計測しています。
 この nzv を使って出力をカットする処理(ノイズゲートなど)が別途必要になる場合があります。

(2)大音量時の歪み(音割れ)
• sgc = 375 の場合、元の音が 375mV を超えるとすべて 1500mV に強制固定(リミッター作動)されます。
• 倍率を上げすぎると、少し大きな声や音が入っただけで波形が天井に張り付き、音がビリビリと歪む(クリッピングする)原因になります。
まずは sgc = 750(2倍) あたりから試してみて、音の聴こえ方やノイズのバランスを確認するのがおすすめです。


1.4.3 RBJ (Robert Bristow-Johnson) Audio EQ Cookbook

• プログラム内の場所: Calculate_BPF_Coeffs(フィルタ係数を計算している部分)
• 世間での知名度: デジタル信号処理(DSP)やオーディオプログラミングの世界では、ゲーツェルアルゴリズムと同じくらい超有名な業界標準アルゴリズムです。
• 解説:
プログラム中の omega = 2.0f * PI * f0 / SAMPLING_FREQ; や、それに続く alpha, b0, a1 などの数式は、
エンジニアの Robert Bristow-Johnson 氏が公開した「Audio EQ Cookbook(オーディオEQクックブック)」という有名な設計レシピの数式を寸分違わずそのまま使用しています。
2次IIRフィルタ(バイカッドフィルタ)で正確なバンドパス特性を作るための最もポピュラーなアルゴリズム名です。

1.4.4 ダイレクトフォーム I (Direct Form I)

• プログラム内の場所: Process_BPF 内の for (int i = 0; i < stages; i++) ループ(フィルタ演算部分)
• 世間での知名度: デジタルフィルタを実装する際の基本構造(トポロジー)の名前です。
• 解説:
以下の数式とデータ保存の処理は、まさに「ダイレクトフォーム I(直接型I)」と呼ばれるアルゴリズムの形をしています。
int32_t y = (bq[i].b0 * x + bq[i].b1 * bq[i].x1 ... ) >> 14;
bq[i].x2 = bq[i].x1; bq[i].x1 = x; // 過去の入力を保存
bq[i].y2 = bq[i].y1; bq[i].y1 = y; // 過去の出力を保存

現在の入力(x)、1つ前の入力(x1)、2つ前の入力(x2)、そして過去の出力(y1, y2)をそのまま掛け算するこの構造は、 固定小数点(整数演算)で計算したときに「数値のオーバーフロー(桁あふれ)が起きにくい」という強いメリットがあるため、 組み込みシステムで非常によく選ばれるアルゴリズムです。

1.4.5 ゲーツェルアルゴリズムとの違いと関係性

もしこのシステムの目的が「特定の周波数が鳴っているかどうかを検知したい(例:DTMF信号や特定のブザー音の検出)」のであれば、まさにゲーツェルアルゴリズムに置き換えることができます。
• ゲーツェルアルゴリズム:
「1500Hzの音の強さ(エネルギー)が今どれくらいあるか」を数値化して検出する(出力は音ではなく、ただの数値)。
• 今回のRBJ / ダイレクトフォームI:
「1500Hzの音だけを残した音声波形」を作り出して、そのままスピーカー等から出力する(出力は加工された音声信号)。
目的が「音を加工して綺麗に聴かせたい」のであれば今のアルゴリズムが最適ですし、「特定の音が鳴った瞬間をトリガーにしたい」のであればゲーツェルアルゴリズムが最適、という使い分けになります。

1.5 RBJ補足事項

RBJ Audio EQ Cookbookの数式がなぜあの形になっているのか、その理由は「アナログ回路の超定番フィルターを、デジタルで寸分違わず再現するための数学的な翻訳」を行っているからです。
なぜあの複雑な omega や alpha の計算から、綺麗に1500Hzだけを抜き出す係数が生まれるのか、その舞台裏を3つのステップで紐解きます。

1.5.1 舞台裏にある「双線形変換(Bilinear Transform)」

アナログ回路で1500Hzのバンドパスフィルターを作る場合、抵抗(R)やコンデンサ(C)、コイル(L)を組み合わせた回路の特性は「S平面」という連続的な世界の数式で表されます。
しかし、マイコン(デジタル)は「1サンプルごと」の飛び飛びの世界(Z平面)で動いています。
このアナログの数式をデジタルに翻訳するアルゴリズムを双線形変換(Bilinear Transform)と呼びます。
RBJ Cookbookは、この「アナログからデジタルへの超複雑な数式変換」を、「誰でも値を入れるだけで使えるように事前にエンジニアのRobert氏がノートに解いて整理してくれたレシピ」なのです。

1.5.2 各変数の役割と数式の意味

プログラムに出てくる各行が、具体的に何を計算しているのかを解説します。

① omega(オメガ) の計算:周波数のデジタル化
float omega = 2.0f * PI * f0 / SAMPLING_FREQ;

なぜこの形?: デジタルの世界では、周波数をそのまま(1500Hzのように)扱えません。
「サンプリング周波数に対して、1サンプルあたり何ラジアン回転するか」というデジタル角周波数に変換する必要があります。
これが omega です。

② alpha(アルファ) の計算:フィルターの「鋭さ(Q値)」の決定
float alpha = sinf(omega) * sinhf(logf(2.0f) / 2.0f * bw / f0 * omega / sinf(omega));

• なぜこの形?: ここが一番複雑に見える部分ですが、これはバンドパスの「急峻さ(どれだけ急激に周波数をカットするか)」を計算しています。
• 実は、RBJ Cookbookの標準仕様では、帯域幅(bw)を「オクターブ単位」で指定する場合、この sinhf や logf(2.0f) を使った長大な式になります。
この計算によって、指定した bw(1500Hz幅)の境界で、音のエネルギーがちょうど半分(-3dB)になるようなマジックナンバー(alpha)が導き出されます。

③ b0 ~ b2, a0 ~ a2 の計算:アナログ特性の配置
float b0 = alpha; float b1 = 0.0f; float b2 = -alpha;
float a0 = 1.0f + alpha; float a1 = -2.0f * cosf(omega); float a2 = 1.0f - alpha;

• なぜこの形?: 双線形変換の計算を最後まで解き進めると、バンドパスフィルターの場合は「必ずこの引き算と足し算の組み合わせ(パターン)になる」ことが数学的に証明されています。
• 例えば、b1 = 0.0f になっているのは、バンドパスフィルター(中心で左右対称な特性)をデジタル化すると、過去1サンプル前の入力(x1)の影響度がちょうどプラスマイナスゼロで相殺される特性を持っているからです。
• a1 = -2.0f * cosf(omega) は、狙った中心周波数(omega)の位置にフィルターの「極(共振点)」を配置するための計算です。

1.5.3 なぜ最後に a0 で割るのか?(正規化)

プログラムの後半で、すべての係数を a0 で割っています。
bq[i].b0 = (int32_t)((b0 / a0) * 16384.0f);

デジタルフィルター(ダイレクトフォームI)の本来の基本式は、以下のようになっています。
siki5.png
\(a_{0}y_{n}+a_{1}y_{n-1}+a_{2}y_{n-2}=b_{0}x_{n}+b_{1}x_{n-1}+b_{2}x_{n-2}\)

しかし、これだと毎サンプルの計算(Process_BPF)の中で、毎回 a0 で割り算をしなければならず、マイコンの処理が非常に重くなってしまいます。
そこで、「起動時の初期設定の中で、あらかじめ全ての係数を a0 で割っておけば、本番のループ内では a0 = 1 とみなせて割り算をサボれる!」 というテクニックです。
これが、Cookbook(レシピ)通りに計算した後に、わざわざ / a0 をしている理由です。
アナログ回路の数式を、マイコンが最も得意とする「掛け算と足し算」だけで動くように極限まで整理した結果、この美しくも不思議な数式の形になっているのですね。

1.6 プログラムの補足

この数式の中にある sinf や logf、cosf などの重い浮動小数点関数が「起動時(Calculate_BPF_Coeffs)にしか呼ばれていない」理由について簡単に説明します。
またこのRBJの式をベースにして、別のフィルター(高音を削るローパスや、低音を削るハイパス)に改造するための係数の書き換えルールを説明します。

1.6.1 重い関数が「起動時」にしか呼ばれない理由

sinf や cosf、logf などの浮動小数点関数が起動時にしか呼ばれないのは、「フィルターの特性(中心周波数や帯域幅)が変わらない限り、係数の値は常に一定だから」です。
音声処理(Process_BPF)は、例えばサンプリング周波数が 44.1kHz であれば、1秒間に44,100回という猛烈なスピードで実行されます。
もしこの1回ずつの処理(1サンプルごと)の中で sinf などを計算させてしまうと、マイコンの処理能力は一瞬で限界を超え、音が途切れてしまいます。
しかし、フィルターのターゲットが「1500Hz」と決まっているなら、それらを計算した結果(b0 や a1 などの答え)も毎回同じ数値になります。
そのため、「時間のかかる複雑な計算は、起動時に1回だけ済ませて変数(bq[i])に保存しておき、本番の音声処理ではその保存した数値を使い回す」という、 組み込みシステムにおける王道の高速化アルゴリズム(手法)をとっています。

1.6.2 ローパス・ハイパスへ改造するための係数書き換えルール

RBJ Cookbookの素晴らしい点は、omega と alpha の計算式はそのまま全く変えずに、後半の b0〜b2 と a0〜a2 の組み合わせ(足し算・引き算)を変えるだけで、別のフィルターに一瞬で改造できる点です。
プログラム内の以下の部分を書き換えるルールをまとめました。
// ★ここをフィルターの種類に応じて書き換えます
float b0 = ...;
float b1 = ...;
float b2 = ...;
float a0 = ...;
float a1 = ...;
float a2 = ...;

① ローパスフィルター(LPF:高音を削り、低音を残す)
モコモコした音にしたい場合や、高域のノイズをカットしたい場合に使用します。
• b0 = (1.0f - cosf(omega)) / 2.0f
• b1 = 1.0f - cosf(omega)
• b2 = (1.0f - cosf(omega)) / 2.0f
• a0 = 1.0f + alpha
• a1 = -2.0f * cosf(omega)
• a2 = 1.0f - alpha

② ハイパスフィルター(HPF:低音を削り、高音を残す)
シャカシャカした音にしたい場合や、低域の「ブーン」という電源雑音(ハムノイズ)をカットしたい場合に使用します。
• b0 = (1.0f + cosf(omega)) / 2.0f
• b1 = -(1.0f + cosf(omega))
• b2 = (1.0f + cosf(omega)) / 2.0f
• a0 = 1.0f + alpha
• a1 = -2.0f * cosf(omega)
• a2 = 1.0f - alpha

1.6.3 改造時のポイント

分母の係数(a0, a1, a2)は、ローパス・ハイパス・バンドパスのすべてで完全に共通です。分子の係数(b0, b1, b2)だけを上記の通りに書き換えるだけで、 同じ関数構造のまま別のフィルターとして動作させることができます。
数式の一部を入れ替えるだけで、全く性質の違うフィルターに変身させられるのは、RBJ Cookbookの最大の強みですね。

「ローパスフィルター用」に書き換えた Calculate_LPF_Coeffs の具体的なソースコードを提示します。
RBJ Cookbookのルールに従い、分母(a0〜a2)の計算や、後半のQ14固定小数点(16384倍の整数)への変換処理はそのまま完全に維持し、分子(b0〜b2)の計算式だけをローパスフィルター用に書き換えています。

ローパスフィルター用 係数計算ソースコード

1.6.4 コードの解説と動作のポイント

• 引数 f0 の意味が「遮断周波数(カットオフ周波数)」になります:
バンドパスのときは f0 は「中心周波数」でしたが、ローパスのときは「これ以上の高音をカットする境界の周波数」という意味に変わります。
• 初期設定(パラメータ)の組み合わせ:
先ほどの初期設定のままで実行した場合:
• f0 = 1500; (1500Hz以上の高音をバッサリ削り、低音だけを通す)
• bw = 1500; (カットオフ境界周辺の滑らかさ・急峻さを決定)

これによって、人の話し声の「サ行」のようなシャカシャカした高域ノイズや、電子回路のキーンという高周波ノイズを綺麗にシャットアウトし、 マイルドでこもった低音重視の音声波形を作ることができます。音声本体の処理関数(Process_BPF)は、中身を1行も書き換えることなく、そのままこのローパスの係数を読み込んで爆速動作します。




2. プログラム

関係するファイルは
1: system_ch32v20x.c(system_ch32v20x.h)
  system_ch32v20x.c ファイルをダウンロード
  system_ch32v20x.h ファイルをダウンロード
2: io_config.c
  io_config.c ファイルをダウンロード
3: interrupt.c
  interrupt.c ファイルをダウンロード
4: bpf_filter.c
  bpf_filter.c ファイルをダウンロード
5: gmt130_lcd.c(gmt130_lcd.h)
  gmt130_lcd.c ファイルをダウンロード
  gmt130_lcd.h ファイルをダウンロード
6: font.c(font.h)
  font.c ファイルをダウンロード
  font.h ファイルをダウンロード
7: main.c(main.h)
  main.c ファイルをダウンロード
  main.h ファイルをダウンロード
8: ch32v20x_it.c(ch32v20x_it.h)
  ch32v20x_it.c ファイルをダウンロード
  ch32v20x_it.h ファイルをダウンロード
9: ch32v20x_conf.h
  ch32v20x_conf.h ファイルをダウンロード
で全てです。

2.1 system_ch32v20x.c

内部クロックを使用して、周波数は144MHzとしています。

2.2 io_config.c

関数名と概要:
void IO_Config(void) {
 // クロック有効化
 // 1-1. GPIOクロックおよびSPI1クロック有効化 PB7 OSC
 // PA5(SCK), PA7(MOSI) -> SPIオルタネート出力GMT130 (SCK)(SDA)
 // PB3(DC),PB4(RES)-> 一般出力
 // PB7(AGC),PB6(BF),PB5(Fo)設定ロック入力(SWをGNDで設定値固定)
 // 1-2. SPI1 設定
 // 2-1. アナログ入力ピン設定 (PA0〜PA4: 可変抵抗, PB0: 音声入力)
 // 2-2. ADC1 & ADC2設定
 // ADC1初期化 (メインループの可変抵抗用)
 // ADC2初期化 (割り込みの音声用に追加)
 // 2-3.校正
 // 3.TIM共通
 // TIM1_1 GMT130用 PWM スクリーン輝度制御
 // TIM1_2 LCDの表示タイミング 200ms周期
 // 3-1. バックライトPWMピン設定 (PA8 - TIM1_CH1)
 // 3-2. タイマー基本設定 (144MHzベースで100Hzを作る)
 // 3-3. TIM1_CH1設定 (バックライトPWM用)
 // 3-4. TIM1_CH2設定 (200ms割り込み生成用) *使用していない
 // 3-5. タイマーの「更新(Update)」割り込みを有効化
 // 3-6. NVIC(割り込みコントローラ)の設定
 // 3-7. 高級タイマー出力許可 & タイマースタート
 // 4.TIM3 音声出力 PWM
 // 4-1. 音声出力PWM設定 (PA6 - TIM3_CH1)
 // 4-2. タイマー基本設定 (144MHzベースで約35.16 kHzを作る)
 // 4-3. TIM3_CH1設定 PWM
 // TIM4 AD BPF処理割り込み
 // 144MHz(SYSCLK)ベースで8kHzを作る設定例
}

uint16_t Read_ADC(uint8_t ch) {}
// チャンネル切り替え用ADC読込関数

uint16_t Read_ADC2(uint8_t ch) {}
// 割り込み専用のADC2読み込み関数

2.3 interrupt.c

LCD表示とバンドパス処理用の割り込みを設定
関数名と概要:
void TIM1_UP_IRQHandler(void){}
// TIM1 LCD表示タイミング用 200ms

void TIM4_IRQHandler(void) {}
// 定期サンプリング用タイマー割り込み (TIM4などを用いて8kHzを生成)

2.4 bpf_filter.c

今回最も重要な関数

2.5 gmt130_lcd.c

LCDの制御(ドライバー)関数
関数名と概要:
static void SPI1_WriteByte(uint8_t data) {}
// SPI1経由での1バイト送信

static void LCD_WriteCommand(uint8_t cmd) {}
// コマンド送信

static void LCD_WriteData8(uint8_t data) {}
// 1バイトデータ送信

static void LCD_WriteData16(uint16_t data) {}
/* // 2バイトデータ(カラー等)送信 不使用のためコメントアウト

void LCD_Init(void) {}
// 初期化関数

static void LCD_SetWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) {}
// 描画エリアの設定

void LCD_SetRotation(uint8_t mode) {}
// 画面の向き(ローテーション)を設定する関数

void LCD_SetBrightness(uint8_t percent) {}
// 輝度(バックライトPWM)制御

void LCD_Clear(uint16_t color) {}
// 画面全体のクリア

void LCD_DrawFillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t color) {}
// 指定エリアの塗りつぶし

void LCD_DrawChar(uint8_t x, uint8_t y, char c, uint16_t color, uint16_t bg) {}
// 1文字描画 (ascii_8x16フォント用)

void LCD_DrawString(uint8_t x, uint8_t y, const char* str, uint16_t color, uint16_t bg) {}
// 文字列描画

void LCD_DrawBar(uint8_t x, uint8_t y, uint8_t val, uint8_t max_val) {}
// バーグラフ描画 [■■■□]

void LCD_Display_Update(SystemParams* params) {}
//LCD表示内容更新

2.6 font.c

8×16ドットのフォントデータ
変数名:
const unsigned char ascii_8x16[] = {}

2.7 main.c

各関数の呼び出し

2.8 ch32v20x_it.c

今回はこのファイルを編集していません。

2.9 ch32v20x_conf.h

今回はこのファイルを編集していません。





3. テスト

3.1 プログラム動作時間測定

フィルターの段数(stages)が 2次、4次、6次、8次(stages = 1〜4)
「2次(1ステージ)のときは処理時間 〇〇 μs だったが、8次(4ステージ)まで増やすと 〇〇 μs まで増加した。しかし、8kHzのサンプリング周期(125 μs)に対して十分な余裕(マージン)があることを確認できた」

固定小数点化(整数演算)の絶大な効果
浮動小数点版の関数(Process_BPF_Float)と、高速整数演算版(Process_BPF)の処理時間をそれぞれ測定して比較
「浮動小数点(float)でそのまま計算させると処理時間に 〇〇 μs かかり周期を圧迫したが、整数&ビットシフト版にすることで処理時間が 〇分の1 に短縮された」

3.1 バンドパス特性測定





































更新日 2026/10/24 14:47  管理者 平林 剛Hirabayashi Takeshi