guiメモ

技術寄りの雑記(予定)

ポータブルハイレゾUSB DACの試作

概要

CH32マイコンを用いてFRISKのケースに収まる大きさのUSB DAC開発ボードを作成しました。32bit/384kHzに(のみ)対応しています。

はじめに

CH32V305/307にはUSB-HS PHYが搭載されており、これを用いるとUSB周りの外付け部品なしでUAC2.0によりハイレゾ音源を再生できるようになります。今回は秋月電子で取り扱いが開始されたCH32V305FBP6を用いたシンプルなUSB DAC開発ボードを作成しました。

USB DAC開発ボードの作成

今回はプログラムの動作確認を行うために必要最小限の機能を備えることを目的とし、CH32V305FBP6にPCM5102Aを直接接続しただけのシンプルな構成を採用しました。

今回作成したUSB DAC開発ボードの回路図
あまりにもシンプルな回路なので、持ち運びしやすいように仕上げることとしました。具体的には、FRISKのケースに収まるように基板を設計しました。
完成した開発ボードの外観
蓋を開けたときの様子
開発ボード単体の写真
ケーブル類を装着したときの様子

プログラムの準備

公式githubにUSB Audioのサンプルは執筆時点で公開されていませんが、解説記事とともにサンプルプログラムを公開されている方がいるため、こちらを参考にプログラムを改変していきます。
今回作成した回路のクロックは16MHzであるため、まずはUSB周りのクロックの設定を変更します。こちらは先に示した記事にある通りにch32v30x_usbhs_device.c内にあるUSBHS_RCC_Init()関数の内容を次のように分周比が2となるように変更します。

RCC_USBHSPLLCLKConfig( RCC_HSBHSPLLCLKSource_HSE );
RCC_USBHSConfig( RCC_USBPLL_Div2 );
RCC_USBHSPLLCKREFCLKConfig( RCC_USBHSPLLCKREFCLK_8M );
RCC_USBCLK48MConfig( RCC_USBCLK48MCLKSource_USBPHY );
RCC_USBHSPHYPLLALIVEcmd( ENABLE );
RCC_AHBPeriphClockCmd( RCC_AHBPeriph_USBHS, ENABLE );

次にI2S周りのクロックの設定を変更します。system_ch32v30x.c内にあるmyI2S_SetSysClock()関数の内容を次のように予め用意されてある16MHz向けのものに変更します。

#if 1
    //384K 32bit MCK DISABLE HSE 16M
    u32 temp=0;
    //HSE
    temp=RCC->CTLR;
    temp|=RCC_HSEON;
    RCC->CTLR = temp;
    while(RCC->CTLR & RCC_HSERDY==0);

    temp=RCC->CFGR0;
    temp&=(u32)~0x3FF0;//AHB APB2 APB1
    temp|=(u32)0x3D0002;//PLL-16 HSESource PLLSource
    RCC->CFGR0=temp;
    RCC->CFGR2=0x10000|0x40|0x0A00|0x04;//DIV2-5 PLL2-12 DIV1-5

    temp=RCC->CTLR;
    temp|=(1<<26);
    RCC->CTLR=temp;
    while( (RCC->CTLR & (1<<27)) == 0 );//PLL2READY

    temp=RCC->CTLR;
    temp |= (1 << 24);
    RCC->CTLR=temp;
    while( (RCC->CTLR & (1<<25)) == 0 );//PLLREADY
    while ((RCC->CFGR0 & (uint32_t)RCC_SWS) != (uint32_t)0x08);
#endif

#if 0
    //384K 32bit MCK DISABLE HSE 24M
    u32 temp=0;
    //HSE
    temp=RCC->CTLR;
    temp|=RCC_HSEON;
    RCC->CTLR = temp;
    while(RCC->CTLR & RCC_HSERDY==0);

    temp=RCC->CFGR0;
    temp&=(u32)~0x3FF0;//AHB APB2 APB1
    temp|=(u32)0x3D0002;//PLL-16 HSESource PLLSource
    RCC->CFGR0=temp;
    RCC->CFGR2=0x10000|0x40|0x0600|0x04;//DIV2-5 PLL2-8 DIV1-5

    temp=RCC->CTLR;
    temp|=(1<<26);
    RCC->CTLR=temp;
    while( (RCC->CTLR & (1<<27)) == 0 );//PLL2READY

    temp=RCC->CTLR;
    temp |= (1 << 24);
    RCC->CTLR=temp;
    while( (RCC->CTLR & (1<<25)) == 0 );//PLLREADY
    while ((RCC->CFGR0 & (uint32_t)RCC_SWS) != (uint32_t)0x08);
#endif

次にmain.c内にGPIOの設定を行う関数MyGPIO_Init()を作成します。

void MyGPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure={0};
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    GPIO_WriteBit(GPIOC, GPIO_Pin_6, Bit_RESET);
    GPIO_WriteBit(GPIOC, GPIO_Pin_7, Bit_RESET);
    GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET);
    GPIO_WriteBit(GPIOC, GPIO_Pin_9, Bit_RESET);
}

このとき、同じくmain.c内にあるI2S_DMA_Init()関数にてI2S周りの設定が記述されていますが、今回はMCLKを出力しないため、該当する部分をコメントアウトします。

//    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
//    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
//    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
//    GPIO_Init(GPIOC, &GPIO_InitStructure);

最後にmain.c内にあるmain関数内の内容を次のように書き換えてプログラムの準備は終了です。ここでは不意にプログラムがコケたときのために、無限ループ内に1秒周期でLEDが光るようにしています。

int main(void)
{
    NVIC_InitTypeDef  NVIC_InitStructure = {0};
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    Delay_Init();

    MyGPIO_Init();
    I2S_DMA_Init();

    USBHS_RCC_Init( );
    USBHS_Device_Init( ENABLE );

    NVIC_InitStructure.NVIC_IRQChannel = USBHS_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    SetVTFIRQ((u32)USBHS_IRQHandler,USBHS_IRQn,1,ENABLE);

    while(1)
    {
        Delay_Ms(500);
        GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET);
        Delay_Ms(500);
        GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);
    }
}

感想

基板を起こしたことを除くとサンプルプログラムをほぼそのまま動かしたような形になりましたが、あっさりとハイレゾ対応のUSB DACが作れてしまいました。主要部品が秋月電子で揃えられるという手軽さのインパクトを強烈に感じますがいかがでしょうか。
サンプルプログラムでは32bit/384kHzにのみ対応しており、また不意に不調となる場合があるため、USBの勉強をしながら適切な割り込み処理や様々なレートに対応できるようにしたいです。

補足

44.1kHz系のレートに対応できるか検討したところ逓倍率の上手い設定が無いらしく厳しいみたいです。USB-HS使用時には外部からそこそこ精度の良いクロックを供給しなければならず、これと両立させようとすると本稿で示した48kHz系のみとなってしまうようです。I2S専用のクロック入力を受け付けるようなモデルの登場を期待したいです。