Sipeed Longan NanoでI2S DACを使う

2020年11月9日月曜日

I2S DAC Longan Nano

t f B! P L

外部DACとしてES9023を使用

USB DACとして販売されていたCovia ZEAL EDGE ZDC-205A-SGを改造して実験に使用しました。 24bit DACとしてESS Technology社 ES9023が使用されており、出力にヘッドフォンアンプも搭載されているので小型かつ高音質です。
Covia Zeal EdgeのI2S DAC化

上の写真のQFPとヘッドフォンジャックの間にある16pinのICがES9023です。


ES9023 I2S関連ピン

ピン番号ピン名備考
1BCK足を浮かせてLongan Nano I2S2_CKと接続
2LRCK足を浮かせてLongan Nano I2S2_WSと接続
3SDI足を浮かせてLongan Nano I2S2_SDと接続
13MCLKすでに供給されているのでそのままにしておく

I2Sピン設定

Longan Nano I2S関連ピン
Sipeed Longan Nanoでは、SPIとI2Sが共用になっていてSPI0~2のうちI2Sが使用できるのはSPI1, SPI2です。ただしSPI1はモジュールの基板上でmicroSDに割り当てになっているため、microSDを生かしたい場合は、SP2(I2S2)をI2S DAC用として使用することになります。

ピン名I2S1I2S2(今回使用)
I2S_CK (=SPI_SCK)PB13PB3 (JTDO)
I2S_WS (=SPI_NSS)PB12PA15 (JTDI)
I2S_SD (=SPI_MOSI)PB15PB5


I2S2ピンの設定

各ブロックに設定する前にクロックをONにする設定およびPA15, PB3, PB5ピンをGPIOではなくAlternate Function(I2S2)として使用するための設定です。PAxxはGPIOAブロックのピン、PBxxはGPIOBブロックのピンでブロック毎に別に設定する必要があります。

    rcu_periph_clock_enable(RCU_AF);
    rcu_periph_clock_enable(RCU_GPIOA);
    rcu_periph_clock_enable(RCU_GPIOB);

    gpio_pin_remap_config(GPIO_SWJ_DISABLE_REMAP, ENABLE);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
    gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3 | GPIO_PIN_5);

I2Sの初期化

I2Sのフォーマットとして、I2S_STD_PHILLIPSを指定します。I2S_CKの極性については、I2S_CKPL_LOWにしておくと送信開始前にI2S_SDに不要なエッジが出力されるようなので、I2S_CKPL_HIGHにしておきました。

I2S_FRAMEFORMAT_DT24B_CH32Bは、1chあたり32bitのデータからを実効下詰め24bitデータをMSBから出力する設定となります。I2S_AUDIOSAMPLE_44Kで、サンプリング周波数を指定します。

    rcu_periph_clock_enable(RCU_SPI2);

    i2s_init(SPI2, I2S_MODE_MASTERTX, I2S_STD_PHILLIPS, I2S_CKPL_HIGH);
    i2s_psc_config(SPI2, I2S_AUDIOSAMPLE_44K, I2S_FRAMEFORMAT_DT24B_CH32B, I2S_MCKOUT_DISABLE);
    i2s_enable(SPI2);
I2S Clock Path(ドキュメントから抜粋)

これらの設定により、I2S_CKの周波数も同時に決定されます。 

  • I2S_CK周波数 = 108 (MHz) / 38 = 2.842 (MHz)
  • サンプリング周波数 = 108 (MHz) / (64 x 38) = 44.41 (KHz)

ただし、サンプリング周波数44.1KHzに対して44.41KHzでは誤差が大きすぎるのでこのままでは問題があります。

より正確なサンプリング周波数に対応するため、クロックの使用経路を以下のように見直します。I2Sブロックのクロックとして108MHzではなく、96MHzを供給するようにします。この場合I2Sブロック内で分周比が34に設定されれば

  • I2S_CK周波数 = 96 (MHz) / 34 = 2.824 (MHz)
  • サンプリング周波数 = 96 (MHz) / (64 x 34) = 44.118 (KHz)

となるはずです。

I2S Clock Paths その2 (ドキュメントから抜粋)

そのための追加の設定は以下の通りです。
    RCU_CTL &= ~(RCU_CTL_PLL1EN | RCU_CTL_PLL2EN);
    rcu_predv1_config(RCU_PREDV1_DIV2);
    rcu_pll2_config(RCU_PLL2_MUL12);
    rcu_i2s2_clock_config(RCU_I2S2SRC_CKPLL2_MUL2);
    RCU_CTL |= (RCU_CTL_PLL1EN | RCU_CTL_PLL2EN);

DMAによるデータ転送

DMA初期設定
送出するデータをDMAによりSPI_DATAレジスタに転送するようにします。DMA転送を準備するための手続きのポイントをまとめると以下になります。
  • メモリからペリフェラルの転送
  • ペリフェラル側はアドレス固定: SPI_DATA(SPI2)
  • メモリ側は16bit転送、ペリフェラル側は32bit転送
  • DMA1のCH1を使用
void init_dma_i2s2(uint32_t memory_addr, uint32_t trans_number)
{
    rcu_periph_clock_enable(RCU_DMA1);

    dma_struct_para_init(&dma_param);
    dma_param.periph_addr = &SPI_DATA(SPI2);
    dma_param.periph_width = DMA_PERIPHERAL_WIDTH_32BIT;
    dma_param.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
    dma_param.memory_addr = memory_addr;
    dma_param.memory_width = DMA_MEMORY_WIDTH_16BIT;
    dma_param.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
    dma_param.direction = DMA_MEMORY_TO_PERIPHERAL;
    dma_param.number = trans_number;
    dma_param.priority = DMA_PRIORITY_HIGH;
    dma_init(DMA1, DMA_CH1, &dma_param);
}
DMA転送
I2S2の初期化とDMAの初期化及びその後の転送を行う部分をまとめると以下のようになります。転送元のバッファとしては、audio_buf0, audio_buf1配列を準備してダブルバッファとして運用します。
    #define SIZE_OF_SAMPLES 512  // samples for 2ch

    int32_t audio_buf0[SIZE_OF_SAMPLES];
    int32_t audio_buf1[SIZE_OF_SAMPLES];

    init_i2s2();
    init_dma_i2s2(audio_buf0, SIZE_OF_SAMPLES*2);

    spi_dma_enable(SPI2, SPI_DMA_TRANSMIT);
    dma_channel_enable(DMA1, DMA_CH1);

    int count = 0;
    while (1) {
        if (SET == dma_flag_get(DMA1, DMA_CH1, DMA_FLAG_FTF)) {
            dma_flag_clear(DMA1, DMA_CH1, DMA_FLAG_FTF);
            dma_channel_disable(DMA1, DMA_CH1);
            if (count % 2 == 0) {
                init_dma_i2s2(audio_buf1, SIZE_OF_SAMPLES*2);
            } else {
                init_dma_i2s2(audio_buf0, SIZE_OF_SAMPLES*2);
            }
            dma_channel_enable(DMA1, DMA_CH1);

            // 次のaudio_bufの準備をここで行う

            count++;
        }
    }
I2Sデータの並び

ここまででDMA転送によりI2Sのデータレジスタにデータを連続的に転送することができますが、メモリ上のデータの配置とI2Sからの実際の出力データの並びの関係をロジアナで確認してみると、以下のようになっていました。(リトルエンディアンです。)

DMA転送データとI2Sデータの関係

サンプルデータは各チャネルごとに24bitなので、Lデータを0番地に、Rデータを4番地に配置して、普通はそれぞれ32bitに対して上詰め(D, C, B)、または下詰め(C, B, A)に配置することになると思います。 
しかし、実際にI2S出力されるデータは、16bitごとに0番地のMSBが冒頭に出力されて、
次に2番地のMSB側8bitが出力されて、下位8bitは0が出力されます。 
つまり通常のデータの置き方をするとI2S出力では16bitごとに逆転したようなデータが出力されてしまいますので生成するデータそのものを16bitごとに入れ替えて作成する必要がありそうです。

コード例

以上を踏まえて、最後に任意周波数の三角波を生成するコード例を以下に記載します。
VS_CODE platformioのプロジェクトはここに置いてあります。

#include "gd32vf103_rcu.h"
#include "gd32vf103_gpio.h"
#include "gd32vf103_spi.h"
#include "gd32vf103_dma.h"

#define SIZE_OF_SAMPLES 512  // samples for 2ch
#define SAMPLE_RATE     (44100)
#define WAVE_FREQ_HZ    (440)
#define PI              (3.14159265)
#define SAMPLE_PER_CYCLE (SAMPLE_RATE/WAVE_FREQ_HZ)
#define DELTA 2.0*PI*WAVE_FREQ_HZ/SAMPLE_RATE

int32_t audio_buf0[SIZE_OF_SAMPLES];
int32_t audio_buf1[SIZE_OF_SAMPLES];

static double ang = 0;
static int count = 0;
static double triangle_float = 0.0;

union U {
    uint32_t i;
    uint16_t s[2];
} u;

// 16bit入れ替え
uint32_t swap16b(uint32_t in_val)
{
    u.i = in_val;
    return ((uint32_t) u.s[0] << 16) | ((uint32_t) u.s[1]);
}

double _square_wave(void)
{
    double dval;
    if (ang >= 2.0*PI) {
        ang -= 2.0*PI;
        triangle_float = -(double) pow(2, 22);
    }
    if (ang < PI) {
        dval = 1.0;
    } else {
        dval = -1.0;
    }
    return dval;
}

// 三角波生成
void setup_triangle_sine_waves(int32_t *samples_data)
{
    unsigned int i;
    double square_float;
    double triangle_step = (double) pow(2, 23) / SAMPLE_PER_CYCLE;

    for(i = 0; i < SIZE_OF_SAMPLES/2; i++) {
        square_float = _square_wave();
        ang += DELTA;
        if (square_float >= 0) {
            triangle_float += triangle_step;
        } else {
            triangle_float -= triangle_step;
        }

        square_float *= (pow(2, 23) - 1);
        samples_data[i*2+0] = swap16b((int) square_float * 256);
        samples_data[i*2+1] = swap16b((int) triangle_float * 256);
    }
}

void prepare_audio_buf(void)
{
    setup_triangle_sine_waves(audio_buf0);
    setup_triangle_sine_waves(audio_buf1);

    init_i2s2();
    init_dma_i2s2(audio_buf0, SIZE_OF_SAMPLES*2);

    spi_dma_enable(SPI2, SPI_DMA_TRANSMIT);
    dma_channel_enable(DMA1, DMA_CH1);
    count = 0;
}

void run_audio_buf(void)
{
    if (SET == dma_flag_get(DMA1, DMA_CH1, DMA_FLAG_FTF)) {
        dma_flag_clear(DMA1, DMA_CH1, DMA_FLAG_FTF);
        dma_channel_disable(DMA1, DMA_CH1);
        if (count % 2 == 0) {
            init_dma_i2s2(audio_buf1, SIZE_OF_SAMPLES*2);
        } else {
            init_dma_i2s2(audio_buf0, SIZE_OF_SAMPLES*2);
        }
        dma_channel_enable(DMA1, DMA_CH1);
        if (count % 2 == 0) {
           setup_triangle_sine_waves(audio_buf0);
        } else {
           setup_triangle_sine_waves(audio_buf1);
        }
        count++;
    }
}

dma_parameter_struct dma_param;

void init_dma_i2s2(uint32_t memory_addr, uint32_t trans_number)
{
    rcu_periph_clock_enable(RCU_DMA1);

    dma_struct_para_init(&dma_param);
    dma_param.periph_addr = &SPI_DATA(SPI2);
    dma_param.periph_width = DMA_PERIPHERAL_WIDTH_32BIT;
    dma_param.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
    dma_param.memory_addr = memory_addr;
    dma_param.memory_width = DMA_MEMORY_WIDTH_16BIT;
    dma_param.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
    dma_param.direction = DMA_MEMORY_TO_PERIPHERAL;
    dma_param.number = trans_number;
    dma_param.priority = DMA_PRIORITY_HIGH;
    dma_init(DMA1, DMA_CH1, &dma_param);
}

void init_i2s2(void)
{
    rcu_periph_clock_enable(RCU_AF);
    rcu_periph_clock_enable(RCU_GPIOA);
    rcu_periph_clock_enable(RCU_GPIOB);
    rcu_periph_clock_enable(RCU_SPI2);

    RCU_CTL &= ~(RCU_CTL_PLL1EN | RCU_CTL_PLL2EN);
    rcu_predv1_config(RCU_PREDV1_DIV2);
    rcu_pll2_config(RCU_PLL2_MUL12);
    rcu_i2s2_clock_config(RCU_I2S2SRC_CKPLL2_MUL2);
    RCU_CTL |= (RCU_CTL_PLL1EN | RCU_CTL_PLL2EN);

    gpio_pin_remap_config(GPIO_SWJ_DISABLE_REMAP, ENABLE);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
    gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3 | GPIO_PIN_5);    

    i2s_init(SPI2, I2S_MODE_MASTERTX, I2S_STD_PHILLIPS, I2S_CKPL_HIGH);
    i2s_psc_config(SPI2, I2S_AUDIOSAMPLE_44K, I2S_FRAMEFORMAT_DT24B_CH32B, I2S_MCKOUT_DISABLE);
    i2s_enable(SPI2);
}

int main(void)
{
    rcu_periph_clock_enable(RCU_GPIOA);
    rcu_periph_clock_enable(RCU_GPIOC);
    gpio_init(GPIOC, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_13);
    gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1|GPIO_PIN_2);

    prepare_audio_buf();

    while (1) {
        run_audio_buf();
    }
    return 0;
}

自己紹介

自分の写真
電子工作&プログラミング、オーディオ・音楽

注目の投稿

Raspberry Pi Pico Wで電波時計を合わせる (JJY標準電波エミュレータ)

Raspberry Pi Pico Wのアプリケーションとして 最少の周辺部品で電波時計むけJJYエミュレータ(時刻合わせ用)を製作しました。 ※2023年6月6日: ソースコード修正の内容を反映させました。 時刻合わせ風景 概要 電波時計は電波が届くところで使...

QooQ