LPC810メモ:疑似正弦波を鳴らす

自由研究とかでありそうな「電子オルゴール」を作ってみたい。スイッチを押すと適当に音楽が鳴るおもちゃ。
その第一段階として、SCTによるPWMを用いた「擬似正弦波」の出力を試してみる。

前提とする環境はこちら→LPC810メモ 共通の準備
SCTなど基本的なサンプルについてはこちら→LPC810みっかぼ自作サンプル集

PWMで擬似的に正弦波を出力する理屈はいろんなサイトで解説されているが、自分はこのmbedのサンプル、「とりあえず使ってみる(音を出してみる)」を見て理解した。

理解したところをまとめると、以下のとおり。

  • PWMにより高速にHIGH/LOWを切り替えると、擬似的にHIGH(1)とLOW(0)の中間の電圧を出力していると見なせる。
  • → duty比、すなわちHIGHになる時間の割合を0%〜100%まで変化させることで、擬似的に0〜1の任意の電圧を出力できる。
  • → 一定時間毎に、周波数に応じた角速度で角度を更新しつつ、sin関数の値に応じてdutyを設定すれば、ほぼ正弦波のような出力が可能。

ここまでが一般論として、実際にLPC810で実装する時に決めた点が以下。

  • 角度表現は、浮動小数点を使ったらROM容量的に苦しいのは明らかなので、整数で処理する。
    • 具体的には、円一周分を「65536単位角度」とする。通常言うところの「360度」や「2piラジアン」が、本サンプルでは「65536単位角度」に相当する。 uint16_tで管理すれば1周するとoverflowして0に戻るから便利。
  • 同じくや標準sin関数が使えるとは思わないので、以下仕様の簡易的なものを独自で用意
    • uint16_tで上記の「角度単位」で入力
    • 出力は-1 .. 1ではなくて、-20..20の整数(int8_t)
    • 実装は以下。1k単位角度(=1024単位角度)毎にハードコーディングされた値を返すだけ。
int8_t SctSoundUtil::sin(const uint16_t degree) {
    const uint16_t kiloDegree {static_cast<uint16_t>(degree / 1024)};
    constexpr static uint8_t sinTable[]
      {0, 2, 4, 6, 8, 9, 11, 13, 14, 15, 17, 18, 18, 19, 20, 20,
       20, 20, 20, 19, 18, 18, 17, 15, 14, 13, 11, 9, 8, 6, 4, 2};
    return kiloDegree < 32 ? sinTable[kiloDegree] : -sinTable[kiloDegree - 32];
}
    • この程度のいい加減なsin関数でも、グラフにすると十分にsin関数っぽい。自分程度の聴力ならこれ以上の精度は必要ないはず、と視覚的に納得しておく。


  • SCT-PWMの設定について
    • H/Lの2本で使う。
    • HがPWM用。120カウントでリセットするので、12MHz / 120 = 100KHz。つまり、dutyを0..120の121段階で設定できる。
    • Lは10KHzで割り込みを発生するタイマーとして使う。割り込み発生の度に、プログラムでdutyを設定する。
  • 音階と角速度について
    • 10KHzで割り込み発生するその一回分、すなわち1/10000秒を便宜上「単位時間」と呼ぶ。
    • 本サンプルにおける角速度は、単位角度/単位時間。例えば、440Hzの音を出したければ、440Hz x 65536単位角度 / 10000単位時間 ≒ 2884、したがって、割り込み1回につき2884単位角度だけ加算していけば良い。
    • 音階は半音毎に0..47の48段階で定義、4オクターブ分。ピアノの真ん中のドの1オクターブ下のド、を0とする。
    • 音階毎の角速度も、以下のような感じであらかじめハードコーディングしておく。
uint16_t SctSoundUtil::toneAngularVelocity(const uint8_t tone) {
    constexpr static uint16_t toneTable[]
        {1618,1714,1816,1925,2039,2160,2289,2425,2569,2722,2884,3055,
	 3237,3429,3633,3850,4078,4321,4578,4850,5139,5444,5768,6111,
	 6474,6859,7267,7700,8157,8643,9156,9701,10278,10889,11537,12223,
	 12949,13719,14535,15400,16315,17286,18313,19402,20556,21779,23074,24446};
    return toneTable[tone];
}

前置き終了、以下サンプル。

サンプルコード

main関数を含むsample.cppは最後。
SctSoundUtil.hとSctSoundUtil.cppは、今後使い回す都合上別ファイル。

SctSoundUtil.h
#ifndef SCTSOUNDUTIL_H_
#define SCTSOUNDUTIL_H_

#include "LPC8xx.h"

class SctSoundUtil {
public:
	static void init();
	static void setCallback(void (*c)()) { callBack = c; };
	static void setDuty(const uint8_t); // dutyを0..120の間で設定
	static void sctEvent();
	static void start();
	static void stop();

	// SCTと直接関係ないユーティリティ
	static int8_t sin(const uint16_t degree);
	static uint16_t toneAngularVelocity(const uint8_t tone);

private:
	static void (*callBack)(); // 10KHzに一回呼び出されるコールバック
};

#endif
SctSoundUtil.cpp
#include "SctSoundUtil.h"

// SCTの割り込みハンドラ
extern "C" { void SCT_IRQHandler(void) { SctSoundUtil::sctEvent(); } }
void (*SctSoundUtil::callBack)();
void SctSoundUtil::sctEvent() {
	if(LPC_SCT->EVFLAG & (1 << 4)){
		callBack();
		LPC_SCT->EVFLAG = 1 << 4;
	}
}

void SctSoundUtil::init() {
    // SCT、8番pinに出力0番設定、RESET以外無効化、割り込み利用。詳細略。
    LPC_SYSCON->SYSAHBCLKCTRL |= (1<<7);
    LPC_SWM->PINASSIGN6 = 0x00ffffffUL;
    LPC_SWM->PINENABLE0 = 0xffffffbfUL;
    LPC_SYSCON->SYSAHBCLKCTRL |= (1<<8);
    LPC_SYSCON->PRESETCTRL &= ~(0x1<<8);
    LPC_SYSCON->PRESETCTRL |= (0x1<<8);
    NVIC->ISER[0] = (0x1 << 9);

    // UM10601 10.6.1 SCT configuration register
    LPC_SCT->CONFIG = (0 << 0 )  // UNIFY = 0 -> 16bitのタイマ2本(L,H)で利用する
        | (1 << 17) | (1 << 18); // AUTOLIMIT_L = 1、AUTOLIMIT_H = 1 -> matchの0番(後で定義)を、カウントアップの上限とする

    // 以下、タイマL関連の設定
    LPC_SCT->CTRL_L |= (1 << 4) | ((1 - 1) << 5);
    	// BIDIR_L = 1 ->bidirectional mode、PRE_L = 1 -> 1クロックで1カウント。

    // カウンタにmatchする値を定義
    // UM10601 10.6.20 SCT match reload registers 0 to 4 (REGMODEn bit = 0)
    LPC_SCT->MATCHREL[0].L = 120; // clock 12MHzなので、100KHz相当
    LPC_SCT->MATCHREL[1].L = 60;  // 仮の値、duty設定時に更新される

    // event0番(match 0、LIMITに達した時に発生)
    // 10.6.22 SCT event state mask registers 0 to 5
    // 10.6.23 SCT event control registers 0 to 5
    LPC_SCT->EVENT[0].STATE = 0x01; // state0で発生
    LPC_SCT->EVENT[0].CTRL = (0 << 0) | (0 << 4) | (1 << 12);
    	// MATCHSEL = 0 -> match0に対応、HEVENT = 0 -> タイマLに対応、COMBMODE = 1 -> match だけに対応

    // event1番(match 1、カウンタが0にもどって発生)
    LPC_SCT->EVENT[1].STATE = 0x01; // state0で発生
    LPC_SCT->EVENT[1].CTRL = (1 << 0) | (0 << 4) | (1 << 12);
    	// MATCHSEL = 1 -> match1に対応、HEVENT = 0 -> タイマLに対応、COMBMODE = 1 -> match だけに対応

    // 出力0番
    LPC_SCT->OUT[0].SET = (1 << 0); // event 0 でset
    LPC_SCT->OUT[0].CLR = (1 << 1); // event 1 でclear

    // 以下、タイマH関連の設定
    LPC_SCT->CTRL_H |= (0 << 4) | (1 - 1 ) << 5;
    	// BIDIR_L = 0 -> こちらはbidirectional modeにしない、PRE_L = 1クロックで1カウント

    // match0の設定
    LPC_SCT->MATCHREL[0].H = 1200; // 10000Hz

    // event4(state0でmatch 0に達した時に発生、割り込みを起こす)
    LPC_SCT->EVENT[4].STATE = 0x01; // state0で発生
    LPC_SCT->EVENT[4].CTRL = (0 << 0) | (1 << 4) | (1 << 12);
        // MATCHSEL = 0 -> match0に対応、HEVENT = 1 -> タイマHに対応、COMBMODE = 1 -> match だけに対応

    // event4発生時には割り込みを発生させる。
    // 10.6.14 SCT flag enable register
    LPC_SCT->EVEN = 1 << 4;
}

// dutyはL側のタイマーのmatch1として表現する。カウンタの都合上、0..120の範囲。
void SctSoundUtil::setDuty(const uint8_t duty) { LPC_SCT->MATCHREL[1].L = duty; }

void SctSoundUtil::start() { LPC_SCT->CTRL_L &= ~(1 << 2); LPC_SCT->CTRL_H &= ~(1 << 2); }
void SctSoundUtil::stop() { LPC_SCT->CTRL_L |= 1 << 2; LPC_SCT->CTRL_H |= 1 << 2; }

// 荒いsin関数。定義域は0..65535だがこれが1周分(0..2π)に相当、値域は0..40で、-1..1に相当。
int8_t SctSoundUtil::sin(const uint16_t degree) {	const uint16_t kiloDegree {static_cast<uint16_t>(degree / 1024)};
	constexpr static uint8_t sinTable[]
	  {0, 2, 4, 6, 8, 9, 11, 13, 14, 15, 17, 18, 18, 19, 20, 20,
	   20, 20, 20, 19, 18, 18, 17, 15, 14, 13, 11, 9, 8, 6, 4, 2};
	return kiloDegree < 32 ? sinTable[kiloDegree] : -sinTable[kiloDegree - 32];
}

// 247Hz(ピアノ中央の一個下のド)を0として、1増える度に半音上がる音の角速度を返す
// 定義域は0..47、4オクターブ分。それ以上は問答無用で領域外アクセス。
// なお、sinと同様の角度表現(0..65535が0..2πに相当)で、1/10000秒あたりの角度を返す。
uint16_t SctSoundUtil::toneAngularVelocity(const uint8_t tone) {
	constexpr static uint16_t toneTable[]
		{1618,1714,1816,1925,2039,2160,2289,2425,2569,2722,2884,3055,
		 3237,3429,3633,3850,4078,4321,4578,4850,5139,5444,5768,6111,
		 6474,6859,7267,7700,8157,8643,9156,9701,10278,10889,11537,12223,
		 12949,13719,14535,15400,16315,17286,18313,19402,20556,21779,23074,24446};
	return toneTable[tone];
}
sample.cpp

(2016/5/26 修正)

#include "LPC8xx.h"
#include "SctSoundUtil.h"

struct {
	uint8_t scale; // 音階(0 .. 47)
	uint16_t verocity; // 音階に対応する角速度
	uint16_t angle; // 現在の角度
} tone{0,0,0}; // 初期値、全部0で初期化

static int count{0}; // 1/10000秒毎にカウントアップする
static void handler() { // 1/10000秒毎に呼ばれるハンドラ
	if(count % 2500 == 0) { // 250ms毎に音階を更新
		tone.verocity = SctSoundUtil::toneAngularVelocity(tone.scale);
		tone.scale++;
		if(tone.scale > 47) { tone.scale = 0; } // 一周したら戻す
	}
	count++;
	tone.angle += tone.verocity; // 角速度分進める
	SctSoundUtil::setDuty(SctSoundUtil::sin(tone.angle) * 3 + 20 * 3); // sin関数に従いduty決定
}

int main(void) {
	SctSoundUtil::setCallback(handler);
	SctSoundUtil::init();
	SctSoundUtil::start();
	return 0;
}

接続


  • 電源を入れると、ド、ド#、レ、レ#、....と半音づつ4オクターブ分の音階が鳴る。
  • 下の丸いのはCR2032の電池ケース。写真では電池は抜いてある。
  • 上の丸いのは8Ωスピーカ、秋月で80円。
  • LPC810の上の四角いパーツは、PAM8012というアンプモジュール。秋月で300円。
    • このパーツだけ結構高い。詳しい人はトランジスタとかでなんとかするのだろうか。
  • 縦に伸びてる抵抗(1KΩ)と右上のコンデンサ(0.47μF)は、ローパスフィルタのつもり。
    • 抵抗値とコンデンサの容量に根拠なく、手持ちのパーツから適当に差しただけだが、効果てきめんだった。

補足/雑感

  • 聞いてみても「おお、正弦波だ!!」とはならなかった。「正弦波」がどんな音なのか知らないから。
  • それでも、この方式だと音を2つ加算して和音にしたり、振れ幅を小さくして音量を変更したりできるのが利点のはず。

みっかぼの無料Androidアプリはこちら。