概要
マイコンで音を鳴らす、というとタイマーとPWMを使って矩形波を作って
音階を鳴らすというのがよくありますが、CH32V203のスペックであれば
16bitDACをタイマー制御しても余裕だなと気付いたので
今回は「WAV音源を鳴らす」というのを試してみたいと思います。
前回に引き続き、開発環境はMounRiver Studioで。
DAC制御
まずはざっくりとDAC制御部分を作ってみます。
DACは秋月で購入できるPT8211というのを使ってみました。
https://akizukidenshi.com/catalog/g/g117061/
データシートにも情報が少ないのですがなんとかなるでしょう。
このスペックで100円というのは気軽に使えて嬉しいです。
データシートにこのタイミング図があるのでこれを作っていきます。
図ではBCKが入り続けていますが、おそらくBCKの立ち上がりのタイミングで
内部の16bitシフトレジスタをシフトしていって、WSの立ち上がり/立ち下がりのタイミングで
各チャンネルの16bitデータが確定されるという構造なのだと思われます。
(こういうデータシートに慣れてくるとタイミング図からそう考えることができますが、慣れていないとBCKの意味が分かりにくいですね。)
さてこれをプログラムで実装していきます。
この関数の構造は、前回のWS2812Bの制御関数と似た作りにしてみました。
RチャンネルLチャンネルを合わせたデータを32bitで受け取って、
RチャンネルのMSBから順番にDINに入れて送り出していきます。
#define PT8211_OUTPUT_PORT (GPIOB)
#define PT8211_OUTPUT_BCK GPIO_Pin_3
#define PT8211_OUTPUT_WS GPIO_Pin_4
#define PT8211_OUTPUT_DIN GPIO_Pin_5
#define PT8211_PERIPH_PORT RCC_APB2Periph_GPIOB
void GPIO_PT8211_INIT(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
RCC_APB2PeriphClockCmd(PT8211_PERIPH_PORT, ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = PT8211_OUTPUT_WS;
GPIO_Init(PT8211_OUTPUT_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = PT8211_OUTPUT_BCK;
GPIO_Init(PT8211_OUTPUT_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = PT8211_OUTPUT_DIN;
GPIO_Init(PT8211_OUTPUT_PORT, &GPIO_InitStructure);
}
void PT8211_Out(uint32_t data)
{
uint32_t bitMask = 0x80000000;
uint32_t i;
//Right
PT8211_OUTPUT_PORT->BCR = (PT8211_OUTPUT_WS); //WS=0
for(i=0; i<16; i++){
PT8211_OUTPUT_PORT->BCR = (PT8211_OUTPUT_BCK); //BCK=0
if (data & bitMask) {
PT8211_OUTPUT_PORT->BSHR = PT8211_OUTPUT_DIN; //DIN=1
} else {
PT8211_OUTPUT_PORT->BCR = PT8211_OUTPUT_DIN; //DIN=0
}
asm("nop; nop; nop; nop;");
PT8211_OUTPUT_PORT->BSHR = PT8211_OUTPUT_BCK; //BCK=1
asm("nop; nop; nop; nop;");
bitMask >>= 1;
}
PT8211_OUTPUT_PORT->BSHR = (PT8211_OUTPUT_WS); //WS=1
for(i=0; i<16; i++){
PT8211_OUTPUT_PORT->BCR = (PT8211_OUTPUT_BCK); //BCK=0
if (data & bitMask) {
PT8211_OUTPUT_PORT->BSHR = PT8211_OUTPUT_DIN; //DIN=1
} else {
PT8211_OUTPUT_PORT->BCR = PT8211_OUTPUT_DIN; //DIN=0
}
asm("nop; nop; nop; nop;");
PT8211_OUTPUT_PORT->BSHR = PT8211_OUTPUT_BCK; //BCK=1
asm("nop; nop; nop; nop;");
bitMask >>= 1;
}
}
GPIO_PT8211_INITは使用するピンを決めて、各ピンを出力用に設定しています。
メインのタイミング制御となるのはPT8211_Out関数の方です。
asm("nop; nop; nop; nop;");
の部分は、例によって「何もしない時間」を入れているわけですが、
DINを変化させてからBCKを立ち上げるまで少し時間を空けたほうが、
より確実にデータが安定するだろうという考えからです。
(本当は両方のデータシートを読んだり実測したりしてタイミングを正確に取ったほうが良いですが、nop4回にしたのは適当です。)
このPT8211_Out関数1回にかかる時間が約4.2マイクロ秒でした。(HSIクロック144MHzにて)
これを連続で呼ぶと1秒間に238095回呼べるわけなので、238kHzのビットレートで再生できることになります。
(データシート上も、最大384kHzまで対応と書いてあります)
ただまあ、実用上は48kHzもあれば十分「高音質」の部類ですからそんなにはいらないわけです。
48kHzの音源を再生するなら、20.83マイクロ秒周期のタイマーでこの関数が呼び出せれば良いということになりますので、
16マイクロ秒ほどの「空き時間」があります。
次の段階では、この「空き時間」を使って不揮発メモリから音源データを読み出すというところを
作っていきたいと思いますが、今回の実験では音源データは直接マイコンに書いちゃうことにします。
タイマー制御
//TIM2_Init set//
void TIM2_Init( uint16_t preiod, uint16_t prescall)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
RCC_APB1PeriphClockCmd( RCC_APB1Periph_TIM2, ENABLE ); //TIM2 clk enable
TIM_TimeBaseInitStructure.TIM_Period = preiod; //upper limit set
TIM_TimeBaseInitStructure.TIM_Prescaler = prescall; //prescaller set
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //clock divede=1
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Down; //count down mode
TIM_TimeBaseInit( TIM2, &TIM_TimeBaseInitStructure); //TIM2 init
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //TIM2 int at update
TIM_ARRPreloadConfig( TIM2, ENABLE ); //TIM2 preload enable
TIM_Cmd( TIM2, ENABLE ); //TIM2 enable
}
// Interrupt_Init //
void Interrupt_Init(void)
{
NVIC_InitTypeDef NVIC_InitStructure={0};
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
static u_int16_t sound[] = {
0xfdfa, 0xff8f, 0x00ef, 0x0203, 0x02c5, 0x0329, 0x0335, 0x02b4,
0x01c0, 0x009a, 0xffe1, 0xff84, 0xfeb9, 0xfcb7, 0xf9d9, 0xf78f,
...
...
0x003d, 0x0015, 0xffe0, 0xffa4, 0xffa8, 0xffe6, 0x0043, 0x007c,
0x007b, 0x004b, 0x001e, 0x000f, 0xfdfd, 0xfdfd, 0x0000, 0x0000,
}; //28700
// TIM2_IRQHandler //
void TIM2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
uint32_t data = 0;
uint32_t pos = 0;
void TIM2_IRQHandler(void)
{
data = ((uint32_t)sound[pos] << 16) | (uint32_t)sound[pos];
PT8211_Out(data);
if(++pos >= 28700){
pos = 0;
}
TIM_ClearFlag(TIM2, TIM_FLAG_Update);//int flug reset
}
int main(void)
{
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
GPIO_PT8211_INIT();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
TIM2_Init(1000-1, 3-1 );
Interrupt_Init();//int init
SysTick->CTLR=0x05;//system counter enable
count=SysTick->CNT;//get system counter val
while(1)
{
}
}
タイマーを初期化してインタラプト関数を宣言するところは下記の記事のコードをそのまま使わせて頂きました。
CH32V003F4P6 タイマー割り込み-じいじの電子実験室
まだ細かいところは理解してないので微調整するときにまた考えます。
今やりたいのは48kHz(20.83マイクロ秒)周期でインタラプト関数が呼び出されるようにしたかっただけなので、
TM2_Initを1000回周期、プリスケーラ1/3(=48MHz)で呼び出すようにだけ変更しました。
static u_int16_t sound[] は、鳴らす音源の実データ部分です。
片チャンネル16bitなので、今回はモノラル音源のデータを作って、
両チャンネルに同じデータを鳴らすようプログラムしました。
音源データの作り方
これはマイコン側とは関係ないのですが、音源データのテキスト化について気になる方がいるかも知れないので簡単に書いておきます。
「48kHz 16bit モノラル音源」を用意します。無料で使えるAudacityを使うのがオススメです。
CH32V203のROMは64KBなので、音源データで使えるのはせいぜい50KB前後が限度です。
48k * 16 / 8 = 96k なので、だいたい0.5~0.6秒分くらいまでしか入りません。
そんなわけで0.6秒ほどのwaveデータを作成しました。
これをバイナリエディタで読み込んで、先頭のファイルヘッダ44バイト分を削除します。
それで保存したデータのファイルサイズを見ておいて、下記のような泥臭いコードでテキストに出力します。
DWORD size = 57400;
LPBYTE buf = new BYTE[size];
FILE* fp;
_tfopen_s(&fp, _T("C:\\tmp\\test.wav"), _T("rb"));
fread(buf, 1, size, fp);
fclose(fp);
_tfopen_s(&fp, _T("c:\\tmp\\test.txt"), _T("a+"));
LPWORD buf2 = (LPWORD)buf;
CString str, str2;
int pos = 0;
for (int i = 0; i < size/2; i += 8) {
str.Format(_T(""));
for (int j = 0; j < 8; j++) {
str2.Format(_T("0x%04x, "), buf2[pos++]);
str += str2;
}
_fputts(str.GetBuffer(), fp);
_fputts(_T("\n"), fp);
}
fclose(fp);
※言い訳ですがファイルヘッダを削ったりファイルサイズを取得したりするのもこのcppに含めることができますが
面倒だったのでやってないだけです。そのうちちゃんと作ります…。
まとめ
そんなわけで無事に音を鳴らすことができました。めでたしと。
CH32V203で16bitDAC制御できた。無駄に48kHz16bitで再生してる。不揮発メモリが64KBあるから0.6秒のモノラル生データ約57KBが入り切るっていう。楽しいなあ。後でQiita記事書く。 pic.twitter.com/hDn5ND4L4C
— オガワン (@Ogawan) October 12, 2024
今回ブレッドボード上で作った回路はだいたいこんな感じです。
CH32V003と違ってCH32V203ではWCH-LinkEのSWCLKも繋ぐ必要があったというところで若干ハマりましたがなんとかなりました。
※これも言い訳ですが、オペアンプの未使用ピンは入力側をGNDに繋いだり出力側はオープンにしないとかいろいろやっておいたほうが良いと思いますが面倒なのでやってませんこまけえこたいいんだよ。
次はSPI Flashメモリの読み書きを作っていければと思います。
※「PCM音源」って書いてたところを「WAV音源」に直しました。PCMはなんか違うっぽい(よく知らん)。