Spresense活用コンテストの作品としてモジュールをつないで機能を拡張できるオーディオコンポを作ってみました。この記事では製作物の設計や開発の上で思ったことをまとめようと思います。
このコンテストでSpresense本体(メインボード)と拡張ボードをモニター提供していただきました。ソニーセミコンダクターソリューションズを始めとする関係者の方々にこの場を借りてお礼申し上げます。
やりたいこと
Spresenseといえば音声信号処理ということで、かねてから作ろうと思っていたPC用のヘッドホンアンプのプロジェクトを拡張する形で製作を行いました。もともとはPCデスクにおいたままにして運用するつもりで構成を考えていましたが、今回のコンテストに向けてアイディアを考える中でブロックのおもちゃのようにユニットを組み合わせて場面に合わせた使い方をできるようにしたいと思ったので持ち運び可能にし、アンプ以外の機能(ミキサーやラジオなど)も持たせられるように設計することにしました。
目標
- 据え置き(USB +5V or 12V DC供給)またはLi-Po充電池での駆動ができるようにする
- 機能拡張がやりやすいモジュール化機能と挿抜検出
ハードウェア
モジュールごとの機能
部分ごとに設計や制作過程を紹介する前に各サブモジュールなどの機能を軽く説明します。
- メインモジュール: 制御用のSpresenseと一部音声信号処理回路(ミキサなど)、電源を搭載するモジュール。モノラルスピーカーも搭載しているので出力モジュールが接続されていなくても音を鳴らすことができる。
- ライン入力モジュール: 音声信号の入力を担当するモジュール
- ヘッドホンアンプモジュール: ヘッドホンから音を鳴らすためのモジュール
メインモジュール
電源
NOTE: 以降BOM(部品表)からはICソケットや導線、ユニバーサル基板などの選択の幅があるものや自明なパーツなどは除外している場合があります。参考にされる場合はご注意ください。
BOM:
品名 | 個数 |
---|---|
Adafruit 小型リチウムイオン電池充電器(USB Type -Cコネクタ搭載) ADA-4410 | 1 |
TDK-Lambda CC6-1205SF-E 絶縁型DC-DCコンバータ(12Vを5Vにするものなら何でもOK) | 1 |
秋月電子通商 昇圧DC-DCコンバータモジュール AE-TPS61088 | 1 |
秋月電子通商 5V -> ±12V DC-DCコンバータモジュール AE-TPS65131 | 1 |
リチウムポリマー充電池(500mAh程度) | 1 |
ショットキーバリアーダイオード 11EQS04 | 5 |
今回はまだ12V DCをDCジャックから受電するモジュール(パワーアンプ)を作らなかったので実質2種類ですが、USBバスパワーと内部の充電池、ACアダプタからの12V DCの3系統の電源を電力の逆流が起こらないように上手く調停して、さらにデジタル回路用の5V DC、3.3V DC、アナログ回路用の±12Vを作る必要があります。幸い充電池の充電回路やちょうど要求を満たすDC-DCコンバータが市販されていたので逆流阻止だけ考えればOKでしたが、決め打ちで接続して燃やしてしまわないように少しブレッドボードでチェックしながら回路を作りました(写真を撮っていなかったのに締め切り当日に気づきました)。最終的な回路は以下の写真の右上の回路図のとおりです。
USBバスパワーと12V -> 5Vステップダウンコンバータの出力部だけダイオードが2つ直列になっていますが、このようにしたほうが最終的にダイオードで低下した電圧を補償する昇圧回路(秋月step-upコンの部分)での出力が安定したのでこうしています。ダイオード1つで統一していた場合はダイオードを少し指でつまんでいると電圧が小刻みに変動(4.9V->5.1Vなど)していたので、それぞれの電源入力の電圧差の下限でダイオードがon / offしてしまっていたのかもしれません(ダイオードをつまむなどして素子の温度を変えると少し特性が変わってon / offしているような気がする。多分)。
フロントパネル部分
BOM:
品名 | 個数 |
---|---|
秋月電子通商 TFT LCD評価キット (K-14032) | 1 |
Bサイズ0.1inピッチユニバーサル基板 | 1 |
キースイッチとDIP化基板 (aitendoで購入) | 3 |
ロータリーエンコーダーとツマミ | 1 |
ピンヘッダ1x15 | 1 |
操作用パネルとして液晶画面と各種インタフェースをつけることにしました。この部分で特に重要な回路はなく、スイッチ類のプルアップ+プルダウンとLCDのピンの引き出しだけです。
LCDバックライト制御部
BOM:
品名 | 個数 |
---|---|
Nch MOSFET INKA114AS1 | 2 |
デュアルフォトMOSリレーTLP4227G-2 | 1 |
100Ω 1/4W 5%炭素皮膜抵抗 | 3 |
1kΩ 1/4W 5%炭素皮膜抵抗 | 1 |
バックライトの明るさ制御には(個人的にチカチカするのが嫌いで)抵抗による電流制御を用いました。バックライトoff以外に位置段階薄暗い状態にできるようにあとのサブユニットを作る際に余っていたフォトMOSリレーを使ってちょっとした抵抗値切り替え回路を作りました(図参照)。MOSFETをonにする数で電流が変化します。
ところで今回紹介する回路には何回かINKA114AS1が登場します。このMOSFETはかなり便利でゲート電流制限抵抗やゲートソース間抵抗が内蔵されているので、スイッチング速度などの要求が特にないスイッチング用途では重宝します。MCUでこのMOSFETを制御したい場合も大抵はゲートとGPIOを直結するだけでOKなはずです。
音声ミキサ回路
BOM:
品名 | 個数 |
---|---|
デュアルオペアンプ NJM4580DD | 1 |
10kΩ 1/4W 5%炭素皮膜抵抗 | 6 |
0.1uF 積層セラミックコンデンサ(バイパスコンデンサとして) | 2 |
1uF 積層セラミックコンデンサ | 4 |
本来はSpresenseのマイク入力端子にサブモジュールからの信号を入力して、Spresense内部でミキシングや音量調整などをする予定でしたが、スマートフォンのヘッドホン端子からの音声出力などマイク入力以外の音声信号をきれいに録音・パススルーする方法が見つからなかったので急遽オペアンプでサブモジュールからの入力を加算することにしました。
メインモジュール - サブモジュールの接続
メインモジュールとサブモジュールの接続は音声信号のレーンと挿抜検出機能付きのバスを束ねる形で実現しました。インターフェースのピン数は15で信号の名前だけ上げると次のようになります。
ピン番号 | 名称 | 機能 |
---|---|---|
1 | +12V | 音声信号用+12Vレール |
2 | -12V | 音声信号用-12Vレール |
3 | +5V | 電源用5V |
4 | +3V3 | 電源用3.3V |
5 | GND | 接地 |
6 | DET | サブモジュール検出 |
7 | SDA | I2C SDA |
8 | SCL | I2C SCL |
9 | In1 L | 入力レーン1左 |
10 | In1 R | 入力レーン1右 |
11 | In2 L | 入力レーン2左 |
12 | In2 R | 入力レーン2右 |
13 | Out L | 出力モジュールへの入力左 |
14 | Out R | 出力モジュールへの入力左 |
15 | Vin | 外部からの+12V入力 |
入力系のサブモジュールは入力された音声信号をIn1かIn2に流します。各モジュールはどのレーンに信号を流すかを切り替えるためのデマルチプレクサ回路を持っており、メインモジュールと協調してぶつからないように出力先を選びます。
入力された信号は(本来はSpresenseに入力するつもりだったが)メインモジュールのミキサ回路で合成されOut L, Rに出力され、出力モジュールがその信号を増幅してスピーカーを駆動します。
メインモジュールとサブモジュールとの通信は主にI2Cバスを使って行いますが、サブモジュール脱着時にそれがわかるようにサブモジュール検出信号を使っています。この仕組みについて説明します。
メインモジュールとサブモジュールをつなぐトポロジーとして今回採用したのはバス型です。さらにメインモジュールが必ず片方の端にあるようなチェーン状のつなぎ方をする場合に限定することにした(モジュールを積み重ねて接続していく形を想定した)ので以下のような方法でモジュールの挿抜を検出できます。
メインモジュールでは検出信号の端子が抵抗でプルアップされています。モジュールがここに追加されるときはまずサブモジュールが検出信号を強くプルダウンし電圧を下げることでメインモジュールに通知します。メインモジュールがモジュールを検出するとI2Cバスを使って接続されたモジュールの種類を検出しプルダウンを解除させます。このとき一番最後にある(次のモジュールを接続するポートが埋まっていない)サブモジュールで弱くプルダウンしておくことで、このユニットが取り外された場合と新たにモジュールが接続された場合のどちらも検出信号の電圧を監視することで検出することができます。
サブモジュール
共通する部分
BOM(サブモジュールごとに):
品名 | 個数 |
---|---|
LPC812M101J (SO20) | 1 |
0.1uF 積層セラミックコンデンサ(バイパスコンデンサとして) | 1 |
10kΩ 1/4W 5%炭素皮膜抵抗 | 2 |
4.7kΩ 1/4W 5%炭素皮膜抵抗 | 1 |
先程述べた挿抜検出を実現するためにすべてのサブモジュールにLPC812を使った制御部分を用意しました。
音声信号のデマルチプレクサ
BOM(1サブモジュールごと):
品名 | 個数 |
---|---|
Nch MOSFET INKA114AS1 | 2 |
デュアルフォトMOSリレーTLP4227G-2 | 1 |
941H-2C-5D 5V 2回路C接点リレー | 1 |
100Ω 1/4W 5%炭素皮膜抵抗 | 2 |
1kΩ 1/4W 5%炭素皮膜抵抗 | 1 |
Nch MOSFET INKA114AS1 | 2 |
ある入力モジュールの音声信号をIn1レーンに入れるかIn2に入れるかを選択するデマルチプレクサはリレーを使って作りました。1チャンネルごとの回路図を以下に示します。
このように1チャンネルの信号をIn1、In2の対応するチャンネルに振り分けるか切り離すかどうかを決められるようにすることで実現しました。この図ではLチャンネルのものだけを示していますがRチャンネルの信号もある場合はその信号もL信号を振り分けている側の入力レーンに振り分けることになるので(In1 L, In1 R)、(In2 L, In2 R)のペアとして2回路リレーに機能をくくりだすことができ、2つのリレーとそのドライバー回路を用意するだけで実装できます。
各モジュールごとの部品
その他モジュールの機能ごとに現在選択している入力レーンの表示やヘッドホンの駆動のために以下の部品を使いました。LEDやボタンなどの操作インタフェース系の部品はすべてLPC812に制御させています。ヘッドホンアンプはきっととして市販されていたものをそのまま流用しました。
ライン入力
BOM:
品名 | 個数 |
---|---|
黄色LED | 2 |
赤色LED | 1 |
キースイッチとDIP化基板 (aitendoで購入) | 1 |
1kΩ 1/4W 5%炭素皮膜抵抗 | 3 |
6.3mmフォンジャック | 2 |
ヘッドホンアンプ
BOM:
品名 | 個数 |
---|---|
秋月電子通商 NJM4580使用ヘッドホンアンプキット | 1 |
6.3mmステレオフォンジャック | 1 |
ソフトウェア
Spresenseのソフトウェア開発はArduino IDE、サブモジュールのLPC812のソフトウェア開発にはWebから利用できるKeil Studio(mbed Compilerの後継らしい)を使いました。期間内までにある程度形にすることができたサブモジュールがあまり多くなかったのでモジュールの挿抜機能のチェックや入力レーンの切り替えのみの実装となりました(もう少しアナログ回路部分も含めて検討できたら良かったのだが)。
LPC812へのファームウェアの書き込み
LPC812はUART経由でファームウェアを書き込む機能を持っているのでAdafruit FT232H Breakout Boardなどを使って書き込むことになります。Keil Studioはコンパイル結果を生のフラッシュイメージとして送ってくれるのでこれをlpc21isp ( https://sourceforge.net/projects/lpc21isp/ ) などのDFUダウンローダを使って書き込みます。
SpresenseとLPC812の間の通信
Arduinoとmbedのハードウェア隠蔽力はかなり強く、一般的なマイコンボード同士で通信するのと変わらない形で実装ができました。mbedとArduinoではI2Cアドレスの表記の仕方が異なるのでそれだけ注意する必要があります(mbedは8bit表記だから、例えばArduinoのWireライブラリで同様のアドレスを示す場合は2で割る必要がある)。
通信形式
I2Cバスを通して独自のコマンドを発行することでサブモジュールを制御するようにしました。
ソースコード
Spresense上のソフトウェア
// Spresenseでオーディオコンポ メインボードファームウェア v0.1.0
// 2022 (C) ikubaku <hide4d51 at gmail.com>
// Published under The MIT License
#include <lcdgfx.h>
#include <lcdgfx_gui.h>
#include <nano_engine_v2.h>
#include <nano_gfx_types.h>
#include <Wire.h>
// IOマッピング
#define N_SPK_SHDN 2
//#define N_LCD_RESET 3
//#define LCD_CS 4
//#define LCD_DC 5
#define N_LCD_BACK_LIGHT_1 6
#define N_LCD_BACK_LIGHT_2 7
#define N_SWITCH_0 8
#define N_SWITCH_1 9
#define N_SWITCH_2 10
//#define LCD_SDA 11
//#define LCD_SCK 13
//#define BUS_SDA SDA
//#define BUS_SCL SCL
#define ROTARY_A D20
#define ROTARY_B D21
#define DETECT A0
unsigned long const BUTTON_CHECK_INTERVAL_MS = 10;
int const DETECT_VOLTAGE_TH = 200;
int const DETECT_VOLTAGE_TH_OPEN = 600;
// mbedのものに対して1/2になっているので注意
uint8_t const HPA_MODULE_ADDR = 0x40;
uint8_t const LINE_IN_MODULE_ADDR = 0x41;
// サブユニットのコマンド
uint8_t const CMD_HANDSHAKE = 0x00;
uint8_t const CMD_SET_SENTINEL = 0x01;
uint8_t const CMD_UNMUTE = 0x10;
uint8_t const CMD_MUTE = 0x11;
uint8_t const CMD_SET_DEMUX_0 = 0x20;
uint8_t const CMD_SET_DEMUX_1 = 0x21;
unsigned long last_button_check_timestamp;
int last_switch0_state;
int last_switch1_state;
int last_switch2_state;
// 大きな数字のほうが最後。-1なら繋がっていない
int hpa_module_order = -1;
int line_in_module_order = -1;
int last_order_num = -1;
int current_input_num = 1;
bool mono_speaker_enabled;
// 液晶ディスプレイモジュール
DisplayIL9163_128x128x16_SPI display(3,{-1, 4, 5, 0,-1,-1});
void probe_submodules(void);
void setup() {
// GPIO初期化
pinMode(N_SPK_SHDN, OUTPUT);
pinMode(N_LCD_BACK_LIGHT_1, OUTPUT);
pinMode(N_LCD_BACK_LIGHT_2, OUTPUT);
pinMode(N_SWITCH_0, INPUT_PULLUP);
pinMode(N_SWITCH_1, INPUT_PULLUP);
pinMode(N_SWITCH_2, INPUT_PULLUP);
// 内蔵モノラルスピーカー停止
digitalWrite(N_SPK_SHDN, LOW);
// バックライトをつける
digitalWrite(N_LCD_BACK_LIGHT_1, LOW);
digitalWrite(N_LCD_BACK_LIGHT_2, LOW);
// I2Cバス初期化
Wire.begin();
display.begin();
display.setFixedFont(ssd1306xled_font6x8);
display.getInterface().setRotation(1 & 0x03);
display.clear();
display.fill( 0x00 );
display.setColor(RGB_COLOR16(255,255,0));
display.printFixed(0, 8, "Spresense Combo", STYLE_NORMAL);
lcd_delay(3000);
}
void loop() {
// サブユニット検出・切り離し処理
int det_val = analogRead(DETECT);
if (det_val < DETECT_VOLTAGE_TH || DETECT_VOLTAGE_TH_OPEN < det_val) {
probe_submodules();
}
// UIの処理
unsigned long current_time = millis();
if (current_time - last_button_check_timestamp > BUTTON_CHECK_INTERVAL_MS) {
// ボタンの入力チェック
int sw0 = digitalRead(N_SWITCH_0);
int sw1 = digitalRead(N_SWITCH_1);
int sw2 = digitalRead(N_SWITCH_2);
//debug
// 入力切り替えチェック
if (sw0 == LOW && last_switch0_state == HIGH) {
if (line_in_module_order != -1) {
Wire.beginTransmission(LINE_IN_MODULE_ADDR);
if (current_input_num == 1) {
Wire.write(CMD_SET_DEMUX_1);
current_input_num = 0;
} else {
Wire.write(CMD_SET_DEMUX_0);
current_input_num = 1;
}
Wire.endTransmission();
}
}
// モノラルスピーカーon / off
if (sw2 == LOW && last_switch2_state == HIGH) {
if (mono_speaker_enabled) {
digitalWrite(N_SPK_SHDN, LOW);
mono_speaker_enabled = false;
} else {
digitalWrite(N_SPK_SHDN, HIGH);
mono_speaker_enabled = true;
}
}
last_button_check_timestamp = current_time;
last_switch0_state = sw0;
last_switch1_state = sw1;
last_switch2_state = sw2;
}
Wire.beginTransmission(0x50);
Wire.write(0x01);
Wire.endTransmission();
delay(500);
Wire.beginTransmission(0x50);
Wire.write(0x00);
Wire.endTransmission();
delay(500);
}
// 常に1つのサブモジュールが末尾に追加 or 末尾から取り外されることを前提にしている
void probe_submodules() {
// HPA
Wire.beginTransmission(HPA_MODULE_ADDR);
if (!Wire.endTransmission()) {
// HPAと疎通
if (hpa_module_order == -1) {
hpa_module_order = last_order_num + 1;
last_order_num++;
display.clear();
display.setColor(RGB_COLOR16(255,255,0));
display.printFixed(0, 8, "HPA Added", STYLE_NORMAL);
}
} else {
if (hpa_module_order != -1) {
hpa_module_order = -1;
last_order_num--;
display.clear();
display.setColor(RGB_COLOR16(255,255,0));
display.printFixed(0, 8, "HPA Removed", STYLE_NORMAL);
}
}
// Line In
Wire.beginTransmission(LINE_IN_MODULE_ADDR);
if (!Wire.endTransmission()) {
//Line Inと疎通
if (line_in_module_order == -1) {
line_in_module_order = last_order_num + 1;
last_order_num++;
display.clear();
display.setColor(RGB_COLOR16(255,255,0));
display.printFixed(0, 8, "Line In Added", STYLE_NORMAL);
}
} else {
if (line_in_module_order != -1) {
line_in_module_order = -1;
last_order_num--;
display.clear();
display.setColor(RGB_COLOR16(255,255,0));
display.printFixed(0, 8, "Line In Removed", STYLE_NORMAL);
}
}
// handshakeと適宜set_sentinel
if (hpa_module_order != -1) {
Wire.beginTransmission(HPA_MODULE_ADDR);
if (hpa_module_order == last_order_num) {
Wire.write(CMD_SET_SENTINEL);
} else {
Wire.write(CMD_HANDSHAKE);
}
Wire.endTransmission();
}
if (line_in_module_order != -1) {
Wire.beginTransmission(LINE_IN_MODULE_ADDR);
if (line_in_module_order == last_order_num) {
Wire.write(CMD_SET_SENTINEL);
} else {
Wire.write(CMD_HANDSHAKE);
}
Wire.endTransmission();
}
}
サブモジュールのファームウェア(ライン入力モジュールの場合)
// Line Inサブモジュールファームウェア v0.1.0
// 2022 (C) ikubaku <hide4d51 at gmail.com>
// Published under The MIT License
#include <mbed.h>
#include "millis.h"
size_t const BUS_COMMAND_BUFFER_LENGTH = 1;
size_t const BUTTON_CHECK_INTERVAL_MS = 200;
enum AudioInputChannel {
None,
In1,
In2,
};
bool is_muted;
enum AudioInputChannel active_input;
unsigned long last_button_check_timestamp;
int last_mute_button_state;
// 各種表示
DigitalOut mute_led(P0_15);
DigitalOut in1_on_led(P0_8);
DigitalOut in2_on_led(P0_9);
// 挿抜検出用
DigitalInOut detection_strong_pulldown(P0_16);
DigitalInOut detection_weak_pulldown(P0_17);
// 音声DEMUX制御
DigitalOut in1_enable(P0_6);
DigitalOut in2_enable(P0_7);
// ミュートボタン
DigitalInOut mute_btn(P0_15);
I2CSlave i2c_bus(P0_10, P0_11);
void process_bus_command(uint8_t);
void mute(void);
void unmute(void);
int main() {
char buf[1];
// millisライブラリの初期化
millisStart();
// IOの初期化
detection_strong_pulldown.output();
detection_strong_pulldown.mode(OpenDrain);
detection_weak_pulldown.output();
detection_weak_pulldown.mode(OpenDrain);
mute_btn.input();
mute_btn.mode(PullUp);
// 表示リセット
mute_led = 0;
in1_on_led = 1;
in2_on_led = 1;
// 最初はミュート+出力先入力チャンネル未選択状態
is_muted = true;
in1_enable = 0;
in2_enable = 0;
active_input = None;
// Line Inモジュール = 0x82 (8bit) == 0x41 (7bit)
i2c_bus.address(0x82);
// 検出要求
detection_weak_pulldown = 0;
// メインループ
while (1) {
int i = i2c_bus.receive();
switch (i) {
case I2CSlave::ReadAddressed:
// nop
break;
case I2CSlave::WriteAddressed:
i2c_bus.read(buf, BUS_COMMAND_BUFFER_LENGTH);
process_bus_command(buf[0]);
break;
}
for(size_t i = 0; i < BUS_COMMAND_BUFFER_LENGTH; i++) buf[i] = 0; // Clear buffer
unsigned long current_timestamp = millis();
if (current_timestamp - last_button_check_timestamp > BUTTON_CHECK_INTERVAL_MS) {
last_button_check_timestamp = current_timestamp;
int button_state = mute_btn;
if (button_state && !last_mute_button_state) {
if (is_muted) {
unmute();
} else {
mute();
}
}
last_mute_button_state = button_state;
}
}
}
void process_bus_command(uint8_t cmd) {
switch (cmd) {
case 0x00: // handshake
// pull-downを解除
detection_strong_pulldown = 1;
detection_weak_pulldown = 1;
break;
case 0x01: // set_sentinel
// 自分が最後のサブモジュールなので弱くpull-down
detection_strong_pulldown = 1;
detection_weak_pulldown = 0;
break;
case 0x10: // unmute
unmute();
break;
case 0x11: // mute
mute();
break;
case 0x20: // set_demux_0
if (!is_muted) {
in2_enable = 0;
in1_enable = 1;
}
in1_on_led = 0;
in2_on_led = 1;
break;
case 0x21: // set_demux_1
if (!is_muted) {
in1_enable = 0;
in2_enable = 1;
}
in1_on_led = 1;
in2_on_led = 0;
break;
}
}
void mute() {
is_muted = true;
mute_led = 0;
in1_enable = 0;
in2_enable = 0;
}
void unmute() {
is_muted = false;
mute_led = 1;
switch (active_input) {
case In1:
in1_enable = 1;
in2_enable = 0;
break;
case In2:
in1_enable = 0;
in2_enable = 1;
break;
case None:
default:
break;
}
}
おわりに
Spresenseを活用した電子工作というテーマでのコンテストでしたがSpresenseのパワーを応用する前に音声信号を扱う回路の扱いなどで戸惑うことが多く(実際この記事を書いた段階でもヘッドホンからの出力に大ぷんノイズが乗っている)少し心残りです。一方でモジュールを組み合わせて機能させる部分を大体実装できたことや、Spresenseを使ってスマートフォンやオペアンプからの信号を受け取ったときのthroughout出力や入力時点での信号の変化の観察などを通していろいろな気づきも得られたので、また近いうちに音声信号処理を交えた電子工作に挑戦してみようと思います。
投稿者の人気記事
-
ikubaku
さんが
2022/09/26
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する