オーディオ/ミニアンプ0.3/設計

実在の部品を使って、要求仕様を実体化する手段を考えてます。まず、基本的な構成をコスト・安全性・工作のしやすさから決定、個々の機能の実現化を深めていき、具体的な回路や部品の選定、実際製作できるまでのレベルまで落とし込みます。成果物は「設計書」である回路図と部品表、ソフトウェアの祖です。

全体構成

基本

要件再まとめ

  • 準Hi-Fi品質(AM放送レベル)のパワーアンプ
  • スピーカ出力は常時使用する物なので小出力で良い (3W+3W)
  • 音声入力はヘッドフォン/ライン/Bluetooth
  • ミキサー(レベル調整、バランス調整、ミキシング機能)
  • 自動化(電源、入力カット)
  • 本体の大きさは横1/2DINサイズ
  • 電源は商用AC100V、省電力
  • 全系統にレベルメータ搭載
  • バージョン0.2と比べ、アナログ入力ノイズの低減
  • 安く、単純に、作りやすく

これを、以下のような機能ブロックで実現するのが一般的な考え方だと思います。

内部構成(基本)

やはり、これまでの検討でデジタルベースの構成が有利だという判断には、変りはありません。前回は、マイクロコントローラに内蔵するADCやPWMモジュールを最大限利用し、外付け部品数を相当数カットでき、そこそこの音質の実用的な小物を作り上げる事ができました。

今回の構成案

昨今、Bluetooth機能を内蔵するマイクロコントローラも流通するようになり、さらなる部品数削減、単純化、配線数を減らす事が出来そうです。

メーカー製品適用入手性(価格)
Espressif SystemsESP32dual開発ボード1,360円/秋月
Texas InstrumentsCC2640R2FLE2016/12
STMicroelectronicsSTM32WBLE2018/2
Analog DevicesMAX32665LE2018/10
ルネサスRX23WLE2021/4
Microchip TechnologyPIC32CX(WBZ45)LE2022/10
Bluetooth機能付きマイコン

残念ながら、ほとんど、Bluetooth Low Energy(LE)にしか対応しておらず、利用することはできません。LEオーディオは、まだ普及していません。前バージョンのように、USB-Bluetoothアダプタ(ドングル)を活用すれば、もっと多くのマイクロコントローラーを使えますが、コスト的には不利です。検討時点では「ESP32」が唯一使えそうなデバイスです。これは、BluetoothはA2DP(sink)をサポートしており、ADCは12bit/44.1kHz/4ch可能、PWMも10bit/44.1kHz/4ch、ポート数も問題なさそうです。何といっても値段が安いので、間違いなく前作よりは安くできるでしょう。マイコン+BTドングル=3,180円:1,360円 → 1,820円カットできる!

うまくいけば、以下のような構成で実現できるかもしれません。ESP32が、「BT付きオーディオ・ミキサー・アンプ・モジュール」となるような感じです。

入力は、RCバンドパスフィルタで可聴域外の信号をカットして内蔵ADCに入力。信号レベルなどで問題があるならばオペアンプを使います。

出力は内蔵PWMモジュールの出力を、FETブリッジを通してスピーカを駆動します。内蔵DACモジュールでアナログ音声信号も出せますので、安価で電力効率の良いD級アンプを使う手もあります。

ESP32は初めて使うデバイスであり、データシートは十分とは言えない情報量なので、各機能を検証し、どこまで実現できるのか実際に施工する方策とします。

ESP32の機能確認

ESP32は初めて扱う部品です。本当に要件通りに機能するのか確認しないと心配ですし、ソフトウェア開発環境に慣れる必要が有ります。

  • 音声データをPWM出力
  • Bluetoothから音声データの取り込み
  • ADCでの音声取り込み
  • ADC×2系統+Bluetooth→PWM出力の連携

開発環境

ESP-IDFというコマンドラインベースの統合開発ツールが、メーカから無償提供されています。主要3OSに対応していますので、使用するPCには困りません。ビルドからROM書き込み、実行まで、これ一つで済みます。基本的にコマンドラインでの操作になりますので、慣れていないとストレスが貯まるかもしれません。確かにGUIのIDEツールは使いやすいかもしれませんが、コマンドラインでの開発もいいものです。

サンプルソースコードが、多数用意されています。多くのAPIのソースコードもアクセスできるので、API説明書には記載されていない所も自力で解決できます。ESP32のドキュメントは説明不足の感があります。ESP32自体は、工作しやすい開発ボードを利用します。今回はESP32-DevKitC-VEを使いました。

PWM機能

ESP32では、PWMを生成するには、MCPWMモジュールと、LEDCモジュールの2つがあります。MCPWMは、電動機(電気モーター)を制御するのに特化した多くの機能を持ちます。対して、LEDCモジュールは、単純にLEDの照度制御などに使うような単純なPWM信号を生成するものです。どちらも80MHzクロック駆動なので、44.1kHzでの最大解像度は、(80×10^6)/(44.1×10^3)=1814となります。2の倍数で切り下げた場合、1024で10ビットでの出力となります。もっと、上げたい感じはありますが、前作と同程度なので、とりあえず良しとしましょう。

確認方法は、以下のようにします。

  • やりたい事は正弦波を出力し、実際に音を聞く事
  • 音声データは表計算ソフトで作成(sin440Hz,サンプリング44.1kHz,PCM10bit)
  • データをcソースコードに埋め込み
  • PWM出力ピンには、実際にスピーカをドライバ経由で接続する
  • ピリオド周波数は、いくつか試行してみる
テスト回路

NJM2670はフルブリッジが2組入った汎用ドライバです。ステレオでも1個で済む都合のいいデバイスなので使ってみました。バイポーラトランジスタ型なので発熱が大きいのでエネルギー効率的に最終選択にはならないと思いますし、最高周波数がデータシートでは明らかになってないので、実際に使って試してみます。

MCPWMモジュール

サンプルソース(esp-idf/examples/peripherals/mcpwm/mcpwm_servo_controol)を使って動かしてみました。実際にはサーボモータを接続せず、出力信号はオシロスコープで見て、ピリオド周波数を44.1kHzに変更したりして動作確認を行いました。

dutyの指定がfloatだったりして場違い的なAPIでしたが、致命的なのは1MHz程度の精度しかなかったので、使用はあきらめです。その後、IDFのバージョンアップ(5.1)でAPIが大幅に変わり改善されました。再確認はしていません。

LEDCモジュール

サンプルソース(esp-idf/examples/peripherals/ledc/ledc_basic)をベースに改造して動かしてみました。duty値のサンプリング周波数周期での定期的な更新については、ソフトウェアで対応する方法しかありません。STM32ではDMAを使って自動化できましたが、同じとはいかないようです。タイマ(GPTimer)を44.1kHzに設定し、タイマ割り込みでLEDCのduty値を音声データで順次書き換えるロジックを加えました。プログラムはこんな感じです。

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"
#include "driver/gptimer.h"
#include "esp_err.h"

static int w440[] = {
0,
16,
32,
47,
 :
-36,
-20,
-4,
};
#define W440_NUM (sizeof(w440)/sizeof(int))

static int cnt;
static bool IRAM_ATTR example_timer_on_alarm_cb_v1(gptimer_handle_t t, const gptimer_alarm_event_data_t *d, void *u)
{
    cnt++;
    if (cnt >= W440_NUM) {
        cnt = 0;
    }
    ledc_set_duty(   LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, w440[cnt] + 512);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0);
    ledc_set_duty(   LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1, 512 - w440[cnt]);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1);
    return(0);
}

void app_main(void)
{
    ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_HIGH_SPEED_MODE,
        .timer_num        = LEDC_TIMER_0,
        .duty_resolution  = LEDC_TIMER_10_BIT,
        .freq_hz          = 44100 * 1,
        .clk_cfg          = LEDC_AUTO_CLK
    };

    ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_HIGH_SPEED_MODE,
        .channel        = LEDC_CHANNEL_0,
        .timer_sel      = LEDC_TIMER_0,
        .intr_type      = LEDC_INTR_DISABLE,
        .gpio_num       = 18,
        .duty           = 5,
        .hpoint         = 0
    };
    ledc_channel_config_t ledc_channel2 = {
        .speed_mode     = LEDC_HIGH_SPEED_MODE,
        .channel        = LEDC_CHANNEL_1,
        .timer_sel      = LEDC_TIMER_0,
        .intr_type      = LEDC_INTR_DISABLE,
        .gpio_num       = 19,
        .duty           = 5,
        .hpoint         = 0
    };

    gptimer_handle_t gptimer = NULL;
    gptimer_config_t timer_config = {
        .clk_src       = GPTIMER_CLK_SRC_DEFAULT,
        .direction     = GPTIMER_COUNT_UP,
        .resolution_hz = 44100,
    };
    gptimer_alarm_config_t alarm_config1 = {
        .alarm_count  = 1,
        .reload_count = 0,
        .flags.auto_reload_on_alarm = true,
    };
    gptimer_event_callbacks_t cbs = {
        .on_alarm = example_timer_on_alarm_cb_v1,
    };

    int	i = 0;

    ledc_timer_config(&ledc_timer);
    ledc_channel_config(&ledc_channel);
    ledc_channel_config(&ledc_channel2);

    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));
    ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, 0));
    ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config1));
    ESP_ERROR_CHECK(gptimer_start(gptimer));

    for(;;) {
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}

2値PWMの場合は、良好な音(ポー)が出ました。しかしながら、3値PWMではあまり良い音ではありませんでした。下図は、3値制御した時のスピーカーへの出力波形です。

やはり、ドライバICが良くなかったのかもしれないと考え、別の部品で試してみることにしました。NJW4860は、スピーカーを直接駆動するには電力効率が悪く実用にはなりませんが、ちょっとした確認には使えると考え、試行してみました。

結果は「良好」、澄み切ったポー音が聞こえました。その後、LEDCのピリオド周波数を変えて色々試行してみましたが、大きく変わらないような気がします。波形をオシロスコープで確認しました。

これは3値制御した時の様子です。波形は変わらないような気がしますが、耳で聞くとまるで違うので不思議です。LEDCモジュールは十分使えると判断します。

ADC機能

スペックは解像度12bit、サンプリングレート2Mspsなので問題ありません。また、アッテネータなし(0[dB])にすると1Vp-pの信号を受けられますので、プリアンプ不要にできる可能性を持っています。

シグナル発生器を持っていたので、これをADCでキャプチャ(連続入力)可能か検証します。結果データは画面にダンプして、表計算ソフトに取り込み、グラフ化して波形確認します。

  • ESP32は負電圧を受け付けないので、分圧抵抗でオフセットしAC 結合
  • 44.1kHz×4ch入力しメモリバッファに格納
  • データがそろったらmonitor画面に表示
  • 画面から表計算ソフトにコピー&ペーストして取り込みグラフ化

 サンプルソース(esp-idf/examples/peripherals/adc/dma_read)をベースに改造して動かしてみました。(このソースは当時のAPIバージョンなので、今はそのままでは動かないでしょう)

#include <string.h>
#include <stdio.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/adc.h"

#define TIMES              1000
#define GET_UNIT(x)        ((x>>3) & 0x1)
#define ADC_RESULT_BYTE    2

static uint16_t adc1_chan_mask = BIT(7) | BIT(6) | BIT(5)| BIT(4);
//static uint16_t adc1_chan_mask = BIT(7);
static uint16_t adc2_chan_mask = 0;
static adc_channel_t channel[] = {ADC1_CHANNEL_7, ADC1_CHANNEL_6, ADC1_CHANNEL_5, ADC1_CHANNEL_4};
//static adc_channel_t channel[] = {ADC1_CHANNEL_7};

static const char *TAG = "ADC DMA";

static void continuous_adc_init(uint16_t adc1_chan_mask, uint16_t adc2_chan_mask, adc_channel_t *channel, uint8_t channel_num)
{
    adc_digi_init_config_t adc_dma_config = {
        .max_store_buf_size = 1024,
        .conv_num_each_intr = TIMES,
        .adc1_chan_mask = adc1_chan_mask,
        .adc2_chan_mask = adc2_chan_mask,
    };
    ESP_ERROR_CHECK(adc_digi_initialize(&adc_dma_config));

    adc_digi_pattern_config_t adc_pattern[SOC_ADC_PATT_LEN_MAX];
    for (int i = 0; i < channel_num; i++) {
        uint8_t unit = GET_UNIT(channel[i]);
        uint8_t ch   = channel[i] & 0x7;
        adc_pattern[i].atten     = ADC_ATTEN_DB_0;
        adc_pattern[i].channel   = ch;
        adc_pattern[i].unit      = unit;
        adc_pattern[i].bit_width = SOC_ADC_DIGI_MAX_BITWIDTH;

        ESP_LOGI(TAG, "adc_pattern[%d].atten     = %d", i, adc_pattern[i].atten);
        ESP_LOGI(TAG, "adc_pattern[%d].channel   = %d", i, adc_pattern[i].channel);
        ESP_LOGI(TAG, "adc_pattern[%d].unit      = %d", i, adc_pattern[i].unit);
        ESP_LOGI(TAG, "adc_pattern[%d].bit_width = %d", i, adc_pattern[i].bit_width);
    }
    adc_digi_configuration_t dig_cfg = {
        .conv_limit_en  = 1,
        .conv_limit_num = 250,
        .sample_freq_hz = 44100 * 4,
//        .sample_freq_hz = 44100,
        .conv_mode      = ADC_CONV_SINGLE_UNIT_1,
        .format         = ADC_DIGI_OUTPUT_FORMAT_TYPE1,
        .pattern_num    = channel_num,
        .adc_pattern    = adc_pattern,
    };
    ESP_ERROR_CHECK(adc_digi_controller_configure(&dig_cfg));
}

void app_main(void)
{
    esp_err_t ret;
    uint32_t ret_num = 0;
    uint8_t result[TIMES] = {0};
    memset(result, 0xcc, TIMES);

    continuous_adc_init(adc1_chan_mask, adc2_chan_mask, channel, sizeof(channel) / sizeof(adc_channel_t));
    adc_digi_start();

    while(1) {
        ret = adc_digi_read_bytes(result, TIMES, &ret_num, ADC_MAX_DELAY);
        if (ret == ESP_OK || ret == ESP_ERR_INVALID_STATE) {
            if (ret == ESP_ERR_INVALID_STATE) {
		;
            }

            ESP_LOGI("TASK:", "ret is %x, ret_num is %d", ret, ret_num);
            for (int i = 0; i < ret_num; i += ADC_RESULT_BYTE) {
                adc_digi_output_data_t *p = (void*)&result[i];
                ESP_LOGI(TAG, "Unit%d, Channel%d, %4d", 1, p->type1.channel, p->type1.data);
            }
            vTaskDelay(1);
        } else if (ret == ESP_ERR_TIMEOUT) {
            ESP_LOGW(TAG, "No data, increase timeout or reduce conv_num_each_intr");
            vTaskDelay(1000);
        }

    }

    adc_digi_stop();
    ret = adc_digi_deinitialize();
    assert(ret == ESP_OK);
}

これがテスト結果です。ch7以外は無接続なので無視して下さい。センターが下限よりなのは設定通りで問題ありません。ADCの中心電圧は0.525Vに対し、テスト回路は約0.45Vなので下よりになるはずです。波形自体は多少のゴツゴツ感はありますが、まあ良しとしましょう。気になるのがデータ順で、前後入れ変わったり、チャネル毎の数が違ったりする時があります。機械らしくない動きは不思議です。

ここまで来ると、実際音出ししてみたくなります。LEDCがうまく動きましたので、これと連動させてみます。ソースコードはこんな感じです。

//
//	ADC -> PWM test
//
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"
#include "driver/gptimer.h"
#include "driver/adc.h"
#include "hal/adc_ll.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_task_wdt.h"

static const char *TAG = "LEDC3";

static int w440[1024];
#define W440_NUM (sizeof(w440)/sizeof(int))
#define CH_NUM   4
#define SAMP_NUM 5

volatile static int cnt;

static int c1,c2,c3;

//
// Timer(44.1kHz) intrrupt
//
static bool IRAM_ATTR pcm2pwm(gptimer_handle_t t, const gptimer_alarm_event_data_t *d, void *u)
{
    ledc_set_duty(   LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, w440[cnt] + 512);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0);
    ledc_set_duty(   LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1, 512 - w440[cnt]);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1);
    cnt++;
    if (cnt >= W440_NUM) {
        cnt = 0;
        c1++;
    }
    return(0);
}

//
// ADC task
//
void get_adc_data(void *dummy) {
    int ofs;
    int ofs_sum;
    int ofs_cnt;

    ofs = 372;
    ofs_sum = 0;
    ofs_cnt = 0;
    for(;;) {
        esp_err_t r;
        uint8_t   v[2 * CH_NUM * SAMP_NUM * 100];
        uint32_t  n;

        r = adc_digi_read_bytes(v, sizeof(v), &n, ADC_MAX_DELAY);
        if ((r == ESP_OK) || (r == ESP_ERR_INVALID_STATE)) {
            static int j;
            static int z;
            int i;
            int k;
            int s;
            int a;
            int b;
            adc_digi_output_data_t *p;

            k = 0;
            s = 0;
            b = ofs;
            for(i = 0; i < n; i += 2) {
                p = (void*)&v[i];
                if (p->type1.channel != 7) {
                    continue;
                }
                a = p->type1.data;
                if ((a <= 0) || (a >= 4095)) {	// error data
                    a = b;
                    c3++;
                } else {
                    b = a;		// before value
                }
                s += (a - ofs);
                ofs_sum += a;
                ofs_cnt++;

                k++;
                if (k >= SAMP_NUM) {
                    while(j == cnt);
                    w440[j] = s / SAMP_NUM / 4;     // 12bit -> 10bit
                    j++;
                    if (j >= W440_NUM) {
                        j = 0;
                        c2++;
                    }
                    k = 0;
                    s = 0;
                }
            }
            if (ofs_cnt > 44100 * SAMP_NUM) {
                ofs = ofs_sum / ofs_cnt;
                ofs_sum = 0;
                ofs_cnt = 0;
            }
        } else {
            vTaskDelay(1);
        }
    }
}

//
// Main
//
void app_main(void)
{
    // ---------- PWM ---------------
    ledc_timer_config_t ledc_timer = {
        .speed_mode      = LEDC_HIGH_SPEED_MODE,
        .timer_num       = LEDC_TIMER_0,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .freq_hz         = 44100 * 1,
        .clk_cfg         = LEDC_AUTO_CLK
    };

    ledc_channel_config_t ledc_channel = {
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .channel    = LEDC_CHANNEL_0,
        .timer_sel  = LEDC_TIMER_0,
        .intr_type  = LEDC_INTR_DISABLE,
        .gpio_num   = 18,
        .duty       = 5,
        .hpoint     = 0
    };
    ledc_channel_config_t ledc_channel2 = {
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .channel    = LEDC_CHANNEL_1,
        .timer_sel  = LEDC_TIMER_0,
        .intr_type  = LEDC_INTR_DISABLE,
        .gpio_num   = 19,
        .duty       = 5,
        .hpoint     = 0
    };

    // ---------- Interval timer 44.1kHz ---------------
    gptimer_handle_t gptimer = NULL;
    gptimer_config_t timer_config = {
        .clk_src       = GPTIMER_CLK_SRC_DEFAULT,
        .direction     = GPTIMER_COUNT_UP,
        .resolution_hz = 44100,
    };
    gptimer_alarm_config_t alarm_config1 = {
        .alarm_count  = 1,
        .reload_count = 0,
        .flags.auto_reload_on_alarm = true,
    };
    gptimer_event_callbacks_t cbs = {
        .on_alarm = pcm2pwm,
    };

    // ---------- ADC + DMA ---------------
    adc_digi_init_config_t	adc_dma_config = {
        .max_store_buf_size = 8192,
        .conv_num_each_intr = 2048,     // 2byte*4CH*4*samples
        .adc1_chan_mask     = BIT(7) | BIT(6) | BIT(5)| BIT(4),
        .adc2_chan_mask     = 0,
    };
    adc_digi_pattern_config_t	adc_pattern[] = {
        { .unit = 0, .channel = 7, .bit_width = 12, .atten = ADC_ATTEN_DB_0 },
        { .unit = 0, .channel = 6, .bit_width = 12, .atten = ADC_ATTEN_DB_0 },
        { .unit = 0, .channel = 5, .bit_width = 12, .atten = ADC_ATTEN_DB_0 },
        { .unit = 0, .channel = 4, .bit_width = 12, .atten = ADC_ATTEN_DB_0 },
    };
    adc_digi_configuration_t	dig_cfg = {
        .conv_limit_en  = 1,
        .conv_limit_num = 254,
//      .sample_freq_hz = 44101 * CH_NUM * SAMP_NUM,
//      .sample_freq_hz = 44450 * CH_NUM * SAMP_NUM,    // =x1, noisy
//      .sample_freq_hz = 44450 * CH_NUM * SAMP_NUM,    // =x2, noisy
//      .sample_freq_hz = 44700 * CH_NUM * SAMP_NUM,    // =x3, noisy
//      .sample_freq_hz = 44500 * CH_NUM * SAMP_NUM,    // =x4, noisy
        .sample_freq_hz = 44600 * CH_NUM * SAMP_NUM,    // =x5
//      .sample_freq_hz = 45250 * CH_NUM * SAMP_NUM,    // =x6,
//      .sample_freq_hz = 45200 * CH_NUM * SAMP_NUM,    // =x7, no good sound
//      .sample_freq_hz = 45300 * CH_NUM * SAMP_NUM,    // =x7, ch ch ...
//      .sample_freq_hz = 44600 * CH_NUM * SAMP_NUM,    // =x8, no good sound
//      .sample_freq_hz = 45300 * CH_NUM * SAMP_NUM,    // =x9, no good sound, noise mid
//      .sample_freq_hz = 45200 * CH_NUM * SAMP_NUM,    // =x9, Low noise, sound mid
//      .sample_freq_hz = 45700 * CH_NUM * SAMP_NUM,    // =x10, noise ok, sound ok
//      .sample_freq_hz = 44800 * CH_NUM * SAMP_NUM,    // =x11, ch ch ...
        .conv_mode      = ADC_CONV_SINGLE_UNIT_1,
        .format         = ADC_DIGI_OUTPUT_FORMAT_TYPE1,
        .pattern_num    = CH_NUM,
        .adc_pattern    = adc_pattern,
    };
    TaskHandle_t t = NULL;

    // ---------- Init ---------------
    ledc_timer_config(&ledc_timer);
    ledc_channel_config(&ledc_channel);
    ledc_channel_config(&ledc_channel2);

    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));
    ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, 0));
    ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config1));
    ESP_ERROR_CHECK(gptimer_start(gptimer));

    ESP_ERROR_CHECK(adc_digi_initialize(&adc_dma_config));
    ESP_ERROR_CHECK(adc_digi_controller_configure(&dig_cfg));
    ESP_ERROR_CHECK(adc_digi_start());
    ESP_ERROR_CHECK(adc_set_clk_div(2));
    adc_ll_set_sample_cycle(2);

    // ---------- Work ---------------
    ESP_ERROR_CHECK(esp_task_wdt_init(24*60*60, false));
                    xTaskCreate(get_adc_data, "get_adc_data()", 16384, NULL, 5, &t);
    ESP_ERROR_CHECK(esp_task_wdt_add(t));
    for(;;) {
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

ちょっと説明すると、初期化が終わったらADCをキャプチャする子タスクを起動し、メイン自体は無限スリープに入ってしまいます。ADCタスクでは、データが届くたびに音声バッファにデータを格納する事を無限に続けます。出力は、44.1kHzのタイミングでタイマ割り込みが入るので、音声バッファから1データを取り出し、LEDCのduty値に設定するだけです。次のような問題がありました。

  • 設定したサンプリング周波数通りにデータが出てこない
    • プチプチ音が混じるので気がついた(カウントして検証)
    • APIのソースを解析してみましたが、究明できず
    • 一定割合で遅い
    • 仕方がないので補正したサンプリング周波数値を設定
  • ノイズが多い(エラー/誤差)

結果は、それなりの音は出ましたが、ホワイトノイズが目立ちます。主観的な意見ですが、通常使用には耐えられないレベルの混入度合いです。特に、無信号時がまずいです。STM32でもホワイトノイズに悩まされました。ESP32でも通常使用レベルまで改善できるか、粘ってみます。

  • ゼロレベルの最適化
    • 仮想アナログGND値のキャプチャ
      • STM32でやったが効果薄かったので未実施
    • 計算で求める(音声データの総平均)
      • 若干効果あり
  • 多サンプリング化
    • 若干効果あり
    • ADC性能から10回/chがMAX
    • 回数を増やしすぎると、逆にノイズが増えるように感じる
    • 平均法より高度な信号処理が必要か?
  • ローパスフィルタの実装
    • ハードウェア追加
      • CRフィルター追加、効果感じられない
      • アクティブフィルタ(オペアンプ)追加、効果感じられない
    • ソフトウェア処理
  • ESP32固有の問題がないか調査
    • パスコンが有効
      • 0.1uF付加したが効果なし
    • 多サンプリング化
      • 64回平均すれば2~3カウントによせられるようだが対応は無理
    • アッテネータを有効にする
      • 効果あり

一言で書いていますが、実際はうまく動かない/不安定/APIバグ見つけた等、かなり時間を食われてしまいました。以下のデータが対応後の一番良い状態のものです。(無信号時、アッテネータ未対応)

常に20カウントぐらいのブレがあり、スパイク状の完全にミスキャプチャと思われるデータが混ざります。5回サンプリングの平均をしているのに、こんなにばらけるのはエラーが多すぎるとしか思えません。データシートでもDNL/INLが-7〜7/-12〜12カウントとはずいぶん大きいとは感じていましたが、実物もその通りなようです。精度後回しのスピード優先という感じがします。オーディオ用途では使える代物ではないのかもしれません。

なお、一番効果があったのはアッテネータを最大にすると、耳で体感できる程にホワイトノイズが減りました。トレードオフとしてプリアンプが必要になります。これでも、我慢できないノイズレベルです。

ここでの判断としては、内臓ADCに固執する事は、もう止めた方が良いのではと考えます。

外付けADC

デバイスの選択

内蔵ADCが使えない、という事に対する回避策として「外付け」のADCとの接続性を確認しておきます。まず、使えそうなデバイスを探します。 要件は

  • 1ch当たり44.1kHz以上のサンプリング
  • 解像度は10bit以上
  • 入手性の良いもの
  • 安いもの
製品スペックI/F価格
Texas Instruments PCM180824Bit, 96kHz stereoI2S250円/秋月
Analog Devices AD7367-512bit, 1Msps, 2chSPI1,650円/秋月
Microchip Technology MCP300210bit, 200ksps, 2chSPI240円/秋月
Microchip Technology MCP320412bit, 200ksps, 4chSPI350円/秋月
MAXIM MAX11188bit, 100kHz, 2chSPI200円/秋月
National Semiconductor ADC1213813bit, 71ksps,8chSPI250円/秋月

MCP3204にすると、1個で2系統入力ができ、仕様も問題なし。マイクロチップはいつもながら選択しやすい製品を用意しています。しかし、PCM1808はオーディオ向けなので、性能は保証されたようなものです。また、解像度が大きいのでプリアンプを省略できる可能性を持っています。この程度の値段で普通に入手できるので、現時点ではPCM1808がベストと考えます。

接続確認

テスト回路

テスト環境に、この部品を追加して、実際に音源を入れて音を出してみます。音源は音楽プレーヤを使いました。

PCM1808は14pin,TSSOPパッケージしかなく、0.65mmピッチなので工作が大変です。ありがたいことに、秋月電子通商からDIP変換基板バージョンが安価に売られており、早速入手しました。ESP32-DevKitCとの接続は、I2SとなりI/Oポート4本を要します。3.3V,5Vの2電源が必要ですが、DevKitCから出ている物を使います。供給能力はドキュメントにはっきり書かれていませんでしたが、レギュレータにAMS1117(800mA)を使っている事から問題ないと勝手に判断しました。(全体的にESP32のデータシートで開示されているデータは不十分だと感じます)最低限必要な部品を、ブレッドボードを使って配線します。

テスト用のプログラムは、LEDCモジュールのテストプログラムをベースに、I2Sからデータを受信するロジックを追加しました。音源I2S設定は「fs=44.1kHz,sysclk=384fs,format=24bit/standard(philips)」にしました。

//
//	ADC -> PWM test
//
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"
#include "driver/gptimer.h"
#include "driver/i2s.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_task_wdt.h"

static const char *TAG = "I2SADC";

static int w440[1024];

#define W440_NUM (sizeof(w440)/sizeof(int))

volatile static int cnt;

static int c1,c2,c3;

//
// Timer(44.1kHz) intrrupt
//
static bool IRAM_ATTR pcm2pwm(gptimer_handle_t t, const gptimer_alarm_event_data_t *d, void *u)
{
    ledc_set_duty(   LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, w440[cnt] + 512);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0);
    ledc_set_duty(   LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1, 512 - w440[cnt]);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1);
    cnt++;
    if (cnt >= W440_NUM) {
        cnt = 0;
        c1++;
    }
    return(0);
}

//
// ADC task
//
void get_adc_data(void *dummy) {
    int ofs;
    int ofs_sum;
    int ofs_cnt;

    ofs = 372;
    ofs_sum = 0;
    ofs_cnt = 0;
    for(;;) {
        esp_err_t r;
        uint8_t   v[2 * 3 * 100];
        size_t  n;

        r = i2s_read(I2S_NUM_0, v, sizeof(v), &n, 1);
        if ((r == ESP_OK) || (r == ESP_ERR_INVALID_STATE)) {
            static int j;
            static int z;
            int i;
            int a,b;

            for(i = 0; i < n; i += 8) {
                a = (v[i+3] << 24) | (v[i+2] << 16) | (v[i+1] << 8) | v[i+0];   // L
                b = (v[i+7] << 24) | (v[i+6] << 16) | (v[i+5] << 8) | v[i+4];   // R
                w440[j] = (a - ofs) >> 22;      // 32bit -> 10bit

                ofs_sum += a;
                ofs_cnt++;

                j++;
                if (j >= W440_NUM) {
                    j = 0;
                    c2++;
                }
            }
            if (ofs_cnt > 44100) {
                ofs = ofs_sum / ofs_cnt;
                ofs_sum = 0;
                ofs_cnt = 0;
            }
        } else {
            vTaskDelay(1);
        }
    }
}

//
// Main
//
void app_main(void)
{
    // ---------- PWM ---------------
    ledc_timer_config_t ledc_timer = {
        .speed_mode      = LEDC_HIGH_SPEED_MODE,
        .timer_num       = LEDC_TIMER_0,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .freq_hz         = 44100 * 1,
        .clk_cfg         = LEDC_AUTO_CLK
    };

    ledc_channel_config_t ledc_channel = {
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .channel    = LEDC_CHANNEL_0,
        .timer_sel  = LEDC_TIMER_0,
        .intr_type  = LEDC_INTR_DISABLE,
        .gpio_num   = 18,
        .duty       = 5,
        .hpoint     = 0
    };
    ledc_channel_config_t ledc_channel2 = {
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .channel    = LEDC_CHANNEL_1,
        .timer_sel  = LEDC_TIMER_0,
        .intr_type  = LEDC_INTR_DISABLE,
        .gpio_num   = 19,
        .duty       = 5,
        .hpoint     = 0
    };

    // ---------- Interval timer 44.1kHz ---------------
    gptimer_handle_t gptimer = NULL;
    gptimer_config_t timer_config = {
        .clk_src       = GPTIMER_CLK_SRC_DEFAULT,
        .direction     = GPTIMER_COUNT_UP,
        .resolution_hz = 44100,
    };
    gptimer_alarm_config_t alarm_config1 = {
        .alarm_count  = 1,
        .reload_count = 0,
        .flags.auto_reload_on_alarm = true,
    };
    gptimer_event_callbacks_t cbs = {
        .on_alarm = pcm2pwm,
    };

    // ---------- I2S ---------------
    i2s_config_t i2s_config = {
        .mode                 = I2S_MODE_MASTER | I2S_MODE_RX,
        .sample_rate          = 44100,
        .bits_per_sample      = I2S_BITS_PER_SAMPLE_24BIT,
        .channel_format       = I2S_CHANNEL_FMT_RIGHT_LEFT,
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,
        .mclk_multiple        = I2S_MCLK_MULTIPLE_384,
        .bits_per_chan        = I2S_BITS_PER_CHAN_24BIT,
        .dma_desc_num         = 8,
        .dma_frame_num        = 64,
        .use_apll             = false,
        .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    };

    static const i2s_pin_config_t pin_config = {
        .mck_io_num           = 0,
        .bck_io_num           = 4,
        .ws_io_num            = 5,
        .data_in_num          = 21,
        .data_out_num         = -1,
    };
    TaskHandle_t t = NULL;

    // ---------- Init ---------------
    ledc_timer_config(&ledc_timer);
    ledc_channel_config(&ledc_channel);
    ledc_channel_config(&ledc_channel2);

    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));
    ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, 0));
    ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config1));
    ESP_ERROR_CHECK(gptimer_start(gptimer));

    ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL));
    ESP_ERROR_CHECK(i2s_set_pin(I2S_NUM_0, &pin_config));

    // ---------- Work ---------------
    ESP_ERROR_CHECK(esp_task_wdt_init(24*60*60, false));
                    xTaskCreate(get_adc_data, "get_adc_data()", 16384, NULL, 5, &t);
    ESP_ERROR_CHECK(esp_task_wdt_add(t));
    for(;;) {
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

結果は良好。もうちょっとマシなスピーカーに交換すると、もっとよくわかり、全く問題はありません。もう、ミニアンプが出来上がったような気分です。やはり、専用の部品は素晴らしいです。

Bluetooth機能

ESP32の基本機能なので、動くのは当たり前だとは思いますが、一応確認しておきます。サンプルコードを動かしてみます。
(esp/esp-idf/examples/bluetooth/bluedroid/classic_bt/a2dp_sink)これは、Bluetoothで受信した音声データを、そのままI2Sに送り込むものです。I2S入力のオーディオアンプを接続すればBluetoothスピーカーになります。I2S入力のオーディオアンプなんてあるのかと調べたら、ありました。

  • YDA142/YAMAHA – 300円(ステレオ9W、52pin SSOP使いずらい)
  • MAX98357/MAXIM – 1166円(モジュール)
  • PCM5102/TI – 700円(DAC、アナログアンプ要)

YDA142を使えば、本格的なBluetoothスピーカーが即完成です。ESP-IDFのサンプルプログラムは良く出来ていて、内臓DACでも出力できるコンフィギュレーションが用意されています。イヤホンをつなげれば簡単に音を聞けるでしょうし、アナログアンプをつないでもいいでしょう。出力先をI2SにするかDACにするかの指定は、menuconfigで画面で選択してビルドするだけです。

今回は、何も接続せず動かしてみただけです。実行すると、スマホやPCのBluetooth設定画面に「ESP_SPEAKER」というデバイスが出てきます。これをペアリングすると、スマホやPCで鳴らす音が、ESP32に送られます。動作ログがmonitor画面に逐一出てくるので分かりやすいです。基本的に、このサンプルプログラムをベースとして機能追加していけば良いでしょう。

全機能連動確認

全モジュールを通してつなげて、リソース的な不足がないかを確認しておく必要があります。ハードウェアが出来上がってから、ソフト上の問題で実現できなくなるのは嫌なものです。

ちょっと、開発に間があいてしまった(半年以上中断した)ので、再度機能確認をなぞりながら進めていくことにしました。

ステップ1(BTのみ)

 すでに完了。

ステップ2(BT+PWM)

Bluetoothで受けた音声データを、LEDCモジュールに送り込んで、実際に音出しします。LEDCの確認時に使ったテスト回路を、ブレッドボード上にもう一度組みます。数本繋ぐだけの簡単な準備です。

元々のサンプルプログラムは、Bluetooth audio API(A2DP+AVRCP)から音声データ(16bit PCM x2)をリングバッファに送り込むことを繰り返しています。また別タスクで、リングバッファからデータを取り出しI2Sに転送依頼を繰り返しています。I2Sの代わりにLEDCにすれば良いのですから簡単です。

実際は、LEDCの場合44.1kHz毎に1データづつ送り込む必要があるため、タイマ割込と組み合わせて機能を実現します。この辺りは、既に実施しているので、ソースコードをコピーしてくれば良いのです。とは言え、次のような問題がありました。

  • LEDCのduty設定APIが重くてステレオで44.1kHzに追いつけない
    • モノラルでは問題はなかった、パニックしたのですぐわかった
    • 直接レジスタを書き換えるようにした
      • LEDC APIのソースをコピペし、最低限必要な部分だけ実行

ステップ3(BT+I2S+PWM)

次に、I2S経由の外部ADC(PCM1808)を接続します。既に一度やっているので、同じ配線を追加していきます。

プログラムは、I2S取り込みタスクを追加し、LEDCへの出力時にBTとI2Sからのデータを加算(ミキシング)するように改造します。次のような問題と対応を行いました。

  • OSのリングバッファ機能が重い
    • 単純な配列&ポインタ式に改めた
  • BTコネクトすると音質低下
    • PWM用の44.1kHz割り込みが1%ぐらい来ない
    • 原因不明、割込優先度の問題か?
    • 対処的対応でタイマを再セットアップするようにして逃げた
  • バッファオーバフロー/アンダーフローがどうしても発生する
    • 捨て/待ちロジックの追加
    • 音が不自然になると思うが、仕方なし
    • スムーズな変化にして、目立たせなくする制御が必要かもしれない

 最終的には良好な状態になりました。普通に聞いていられるレベルです。

ステップ4(BT+I2S+ADC+PWM)

 一応、内臓ADC取り込みを追加してみます。

I2S取り込みタスクに加え、ADCタスクを追加します。また、LEDC出力時に。以下の問題と対応を行いました。

  • I2Sの初期化エラーとなる
    • ESP32の制約、ADC0とI2S0は排他使用
    • 使用するモジュールをI2S1に変更
  • 実用にならないほどホワイトノイズが大きい
    • 既に分かっていたが、再度、対策に励む

それでも、実用レベルにはならない。本当に、こんなに誤差が大きいデバイスなのだろうか?やっぱり使えない。

内部構成

 調査結果を踏まえて、構成を固めていきます。

オーディオ入力

Bluetooth入力は決定事項。面倒な事態となったのは2系統のアナログ入力。内臓ADCは実用レベルにはならないという判断なので、外付ADCを2個つける方法になります。しかし、もう少し内臓ADCの低雑音化にチャレンジしたいと思うし、実際の使用状況として1つが常用、1つは予備として使っているので、結果として内臓ADCが物にならなかったとしても、そんなに困らないのです。コスト的にも、前段にオペアンプのローパスフィルタを入れたとしても+100円ぐらいなので支障ないと考えます。

よって、内臓ADCx1+外付ADCx1の形を取りたいと思います。

オーディオ出力

DAC+アナログアンプでという選択肢はつまらない。PWM信号でスピーカーを駆動するのは、当初の想定通り。

操作系

基本的には、前回と同じにしたいです。グラフィックディスプレイにを軸に、オーディオ入力系統毎のレベル/バランス調整は、切り替え式のボリュームで操作する方式です。つまり、数個のスイッチと1つのボリューム操作で行います。改善するとしたら、グラフィックLCDモジュールは入手しやすい物にしたいところです。

  • 1.8インチクラスのグラフィックLCDモジュール
  • 回転つまみ
  • プッシュボタン

入力部

外付ADC

デバイスの選定

その後、PCM1808以上にC/Pの良いデバイスは見つけられませんでしたので、これを選定します。性能に関しても、確認済みです。

適合回路

データシートの推奨回路で、特に問題ないかと思います。

入力レベル

データシート上、Vp-pは3.0Vです。一般的にラインレベルと呼ばれるものは1.0Vp-pなので、プリアンプが必要かもしれませんが、24bit精度の性能を持っていますので、1〜2bit分捨てても全く支障がありません。ハードウエアの対応はなくとも大丈夫でしょう。

ローパスフィルター

IC内に内臓されています。楽ちんです。

ノイズ対策

特に、必要ないレベルである事は確認済みです。ただ、アナログ電源Vccは5Vで、ACアダプタからの電源から直接供給されると、ノイズ成分が多く含まれる可能性が高いので、対策が必要かもしれません。詳しくは、電源設計で検討します。

ホストインタフェース

I2SですがSystemClock(master clock)を含む4線式です。ESP32ではMLCKはGPIO-0番に固定されていますので、ピン割り当てでは要注意です。

部分回路図

これまでの物と変わりませんので省略します。

内臓ADC

入力レベル

ESP32のADC入力電圧範囲は、0.150V~2.45mV(アッテネーター=-11dB)、ラインレベル1Vp-pに合わせるには、2倍のプリアンプがあった方が良いでしょう。サンプルエラーを目立たせなくするためにも意味があります。

ローパスフィルター

経験的に(これまでのBTSP開発)つけた方が、対外機器との親和性が向上します。ようは、可聴域外信号が混ざった機器でも、へんなノイズが聞こえないようにします。

入力レベルの項目と合わせて、オペアンプによるアクティブフィルターがコスト的に最適だと思われます。大体1系統当たり100円ぐらいのコスト増ですみます。

オペアンプで、2倍ゲイン、100〜20kHzフィルターを使うこととします。150mV〜の信号を出力しなければなりませんので、フルスイング、もしくは0V近くまで出力できる品種である必要があります。電源は2.5V〜です。

ノイズ対策

かなりノイジーなADCである事がわかっていますので、できるだけノイズを減らす工夫をしなければなりません。

  • 電源由来のノイズ
  • VREF系のノイズ
  • デバイス内ノイズ
  • 配線、電磁誘導

経験的に、電源が一番のノイズ源と考えます。アナログ用の電源は、できるだけ静寂になるようにするべきでしょう。外部ADC(PCM1808)やオペアンプ用の電源を、分離するのは難しくはないのですが、どうにもならないのがESP32モジュール(ESP32-WROOM/WROVER)で、電源はデジタル/アナログの区別なく3.3Vの1本となっており、手が出せません。モジュールのデータシートで内部回路が示されており、電源周りの取り回しでパスコンで盛られているので、問題ないのでしょう。そう、思いたいです。と言うことで、ESP32は特に対策なしとします。

VREFと言っているのは、ADC入力の中心電圧(仮想アナロググラウンド)の事で、基準電圧を指します。当然、正確にするべきものですが、数値計測をする訳ではないので、単純な分圧抵抗器で良いと考えました。電源ラインのノイズが、もろ入って来ますので、できるだけ安定した静かな電源にするべきです。

オペアンプ内で発生するノイズは無視できるほどだと思います。データシート上には数値として乗ってはいますが、実感は湧きません。とりあえずパスします。余裕があれば、デバイスを交換したりして比較/実感してみようかと思います。

配線で隣接しているなどしたら、当然混入する事はあり得ると思いますので、部品配置や配線経路を考慮するべきだとは思います。この辺は、設計努力で、確たるものは何もありません。

ほぼ、電源での解決になりますので、具体的には電源の設計で詳細を詰めたいと思います。

部分回路図

詳しくは、0.2版で説明していますので、そちらをご覧ください。VREF(ADCのVrefではなく仮想のアナログ系のグラウンドの意味)は、ADCの入力範囲の中央((0.15V+2.45V)/2=1.3V)とします。抵抗R,コンデンサCの定数は計算値ですが、実部品は近いもの(カッコ内)となります。ステレオなので、これが2セットとなります。

オペアンプの品種については、安い定番品の2回路入ったLM324にします。高音質オーディオ用と称して10倍も高価なオペアンプもありますが、そういう品種のデータシートを見ると±15V電源でないと実力を出せないようですし、何か使いこなせないと思います。

出力部

PWM信号で、スピーカーに電力を送り込む回路を考えます。高速なスイッチ素子としては、バイポーラトランジスタ、パワーMOS-FETが現実的な選択肢です。電力効率を考えれば、MOS-FETが有利です。大きな電力領域では、トランジスタは熱になってしまうので効率が低下します。

プッシュプル回路で、電圧をコントロールする事になります。これを、2つ組み合わせてHブリッジ構成にすると、低い電圧の電源でも、大きな出力を出す事ができます。2倍のコストとなりますので、電源回路とのトレードオフでの判断が必要です。0.2版での経験からHブリッジ(フルブリッジ)でも、大きなコストアップにならないようですので、これを採用します。

MOS-FETでのHブリッジ回路を具体的に考えます。仮に5V電源、8Ωスピーカーとした場合、乱暴ですが0.6Aの電流が流れます。ザクっとドレイン電流(ID)=1A以上のFETが必要です。また、スイッチング周波数は100kHz程度。せいぜい10V位の電圧での使用となります。

大きな電力を扱うFETを高速でスイッチングする場合、マイクロコントローラのI/Oでは、十分なゲート駆動ができません。そのため、通常はFETをカスケードして駆動する回路構成となります。現実的には、以下のような方法があるかと思います。

  • ESP32で直接FETブリッジのゲートをドライブ
    • 傾向としてID=1A以下だと直駆動できる
    • 使えるFETの品種が少ない、入手性悪い、設計面倒
    • ESP32に十分なドライブ能力があるのか不明
    • 今のデータシートでは判別も推定もできない
  • ゲートドライバICと、FETブリッジを組み合わせる
    • 一般的な構成で、大出力まで対応可能
    • 電力効率が高い(発熱が少ない)
    • コストやや大きい
    • 入手性の良い部品を選べる
    • ドライバの各種保護機能の恩恵がある(ないのもある)
    • ショート保護必要
  • ゲートドライバICで直接スピーカーを駆動
    • ゲートドライバICによっては、直接スピーカーを鳴らせる
    • コスト大(例:IR2110で450円x2=900円)
  • スピーカードライバICを使用
    • 探すとそういうICはあります、一番楽に実現(IRS2092など)
    • コストも入手性も悪い(残念)
    • モータードライバが使える場合もあるが、探すのが大変
    • 用途外使用なので、使ってみないと分からない事がある
    • 各種保護機能が充実している

前回は、1番目と4番目を試してきました。やはり、作りやすさとか、入手性が良いのが大切だと思うようになったので、2番目のゲートドライバIC+FETブリッジを使う事にします。

MOS-FETゲートドライバ

品種選定

ゲートドライバスペックコスト(L+R)
IR4427PBF1.5A, 6-20V, Nch+Nch300×2=600
NJW4810A1A, 8-40V, HSOP8,TSD/OC/UVLO160×2=320
NJW48601A, 4-20V, HSOP8, TSD/UVLO120×2=240
MCP14A09019A, 4.5-18V, 1ch, SOP8110×4=440
MCP1401T0.5A, 4.5-18V, 1ch, SOT2360×4=240
MCP14700T2A, 5V(36V), SOIC8, TSD/UVLO200×2=400
LT1160CN1.5A, 10~15V(60V), Nch+Nch, UVLO430×2=860

その時々で最適な部品は変わります。安くて入手しやすい物をリストアップしてみました。TSD/OC/UVLOは温度/過電流/低電圧保護機能を示します。今回は、コストと保護機能の有無から「NJW4860」を選択します。

最終段MOS-FET

品種選定

必要とするパワーMOS-FETのスペックは

  • ID > 1A
  • VDS > 10V
  • Pch-Nchのコンビネーション

 この条件だと、使えるFETの品種は数多ありますので、かなり主観的なリストアップです。

パワーMOS-FETスペックコスト(L+R)
TPC840730V, 9/7.4A, 14/18mΩ, SOP8150×2=300
FDS455960V, 4.5/3.5A, 42/82mΩ, SO870×4=280+
Si6544DQ30V, 4.0/3.5A, 27/38mΩ, TSSOP8200+
AUIRF7343Q55V, 3.4/4.7A, 95/43mΩ, SOP8100×4=400+

+はDIP変換基板などのコストが加わることを示します。どれも、似たり寄ったりと言うことで、工作しやすい変換基板が付いた「TPC8407」を選択します。

保護機能

過熱保護

MOS-FETにかかる電力は、2A流れたとして、RD=14mΩ+18mΩなので、V=I*R, P=V*I, P=(I*R)*I=I^2*R=2A^2*0.032Ω=0.128Wと、温まりを感じない程度です。なので過熱する事態を心配するのは大袈裟かもしれません。あった方が良いに決まっているので、とりあえず検討してみます。

  • 温度センサで監視
    • マイコン内プログラムで常時チェック/割込み
    • 検出したら、ボリュームで出力カット、するか小さくする
    • センサは安いサーミスタx4箇所
    • ADCで読み取るか、デジタル入力(閾値は回路決定)
  • 温度ヒューズ
    • 相当高温(100℃以上)にならないと切れない
    • 最近は入手しづらい部品
  • ゲートドライバの熱保護を利用
    • アルミ板でゲートドライバとMOS-FETを熱的に接続
    • ゲートドライバ内のサーマルシャットダウン機能を誘発させる
    • ローテクだが安く済む

コストがかかるようなら、対策するほどでもないかと思いましたが、3番目の方法がありましたの対応する事とします。アルミ板などで、パッケージ間をブリッジするだけです。

ショート保護

スピーカー端子の誤接続、配線の接触、ショートモードでの故障を想定しています。過電流があった場合にカットする機能となります。

  • スピーカー電流監視
    • 電流センサを挿入し、マイコン内のプログラムで監視
    • 電流センサは、微小値の直列抵抗値間の電圧を計測するのが安上がり
    • RGを利用する方法もあるか?
    • センサ位置x2箇所
    • 技術的なハードルとコスト高の懸念
  • 電源ヒューズ
    • スピーカー駆動用の系統に電流ヒューズを入れる
    • 昔ながらの直管ヒューズ
    • リセッタブルヒューズが意外と安いし、交換不要

リセッタブルヒューズ(ポリスイッチ)が、30円位で済むので、対応が簡単です。電流によって発熱し、スイッチが切れ、冷めると復帰する動作となるので、扱いやすそうです。今回は、これを使う事にします。

表示・操作部

グラフィックディスプレイ

デバイス選択

基本、0.2版と同じ外装なので、1.8インチカラーグラフィックディスプレイです。前回とは異なり、比較的安定して入手できる秋月電子の「ATM0177B3A」が良いのではないかと考えました。価格もそこそこ、問題ないです。インタフェースは4線式SPIです。

対応ソフトウェア

グラフィックディスプレイは、何もないところから制御するソフトウェアを作るのは大変な作業です。ESP32にソフトウェアライブラリがあるようですが、自家製のライブラリを持っているので、ESP32にも対応できるように改良すればOKです。メインのソースコードも、多く利用可能でしょう。

ボリューム&スイッチ

回転ボリューム+プッシュボタンも0.2版と同じです。通常は、回転ボリュームでメインの音量を操作し、プッシュボタンを押すと、回転ボリュームが細かい設定値(入力レベル/バランスなど)の入力モードに変わります。この操作方式は、操作パネルの部品を最小化するための工夫です。

回転ボリュームは、今回もロータリーエンコーダとしました。ESP32にはPCNTモジュールがあり、ソフトウェア介入なしにカウントが更新されます。試したところ、カウント値が上限に達するとストップせず0に戻るので、ボリュームコントロールには使えない事が判明。ソフトウェアで実装しても、たいした事はないと思ったのですが、実際やってみると、うまくいきません。そこで、PCNTモジュールに回帰し、0戻りの件を回避するソフトウェアで対応する事にしました。

スイッチの検出は特に問題になりません。

電源部

 供給が必要な部位は以下の通り。

  • スピーカードライバ <5V2A>
  • ESP32-Devkit <5V500mA:max>
  • 外付けADC <5V10mA, 3.3V8mA, 低雑音>
  • プリアンプ(オペアンプ)<5V1mA, 低雑音>
  • LCDモジュール <5V40mA, 3.3V1mA>

5Vと3.3Vの2電源が必要ですが、ESP32-Devkit上には3.3Vボルテージレギュレータが乗っており、外部にも取り出し可能なので、これを利用する事も出来ます。3.3Vの消費電流はトータル10mA程度なので、全く問題ありません。

と言う事は、5Vを用意するだけで済みますので、どこにでもあるUSB用のACアダプタなどで、簡単に用意できますし、応用性も高まります。

しかし、オペアンプとADCに与える電源は、ノイズのない(少ない)ものが要求されています。電源ラインのノイズ対策は、

  • バイパスコンデンサ
  • ラインフィルタ
  • ボルテージレギュレータのリップル除去能力

以前の開発では、パスコンを入れると劇的にノイズが少なくなった事はまれで、ちょっとマシになったかなあ、と感じる程度でした。ラインフィルタも同じような感じですが、設計次第なのかもしれません。

ボルテージレギュレータは、能動的にノイズを減らす作用がありますので、利用すべきだと考えます。使うには、レギュレータの最低電圧差という制約があるため、+1V程度の電源に変更しなければなりません。ADCに4.7V供給にして、電圧差が極めて少ないレギュレータ(LDO型)を使った設計も可能かと検討しましたが、やはりギリギリな設計は、使える部品を狭くしたり、安定動作しないかもしれません。大きな判断となりますが、6VのAC-DC電源アダプタを使うものとします。

今回は、ボルテージレギュレータを付けるものとしますが、パスコン/ラインフィルタは、実際のノイズを聴いてみて、カットアンドトライ的な対応で考えます。また、スピーカードライバで決めた、リセッタブルヒューズを装備します。

回路図まとめ

 これまでの、個別の設計回路を1つにまとめます。

基板

一品ものなのでユニバーサル基板(蛇の目基板)上に、回路を組み上げるものとします。

ソフトウェア

各機能の確認テストで行ったコードをまとめて、基本機能を実装しました。その後、表示/操作関係のコードを、0.2版のものから取り込みます。

外装

外形

 0.2版と全く同じものを、製作して対応します。大きさは、ハーフDINサイズ(89x50mm)で、前面で表示・操作を行う形です。前バージョンでは、スイッチ操作は上面パネルを開ける必要がありましたが、今回は回転つまみをがプッシュスイッチ機能を持っている部品を使う事で、使いづらさを改善します。

素材、仕上げ

これも、前バージョンと全く同じにします。3mm厚のアクリルシートを折り曲げて四角いケースを作ります。ただし、つまみがシルバー(アルミ削り出し)しかなかったので、合わせてシルバーでの塗装に変更します。

Copyright©2023-2024 Toyohiko TOGASHI


投稿日

カテゴリー:

, ,

投稿者:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です