秋葉原(秋月電子)で部品を揃えてラジオを作ろう

令和のラジオづくりを体験してみようと思いやってみました。

CH32V003 RISC-V マイコン

  • J4M6 8ピン SOP パッケージ

akizukidenshi.com

  • F4P6 20ピン SSOP パッケージ

akizukidenshi.com

それぞれを DIP 変換基板へ実装し大きさ比較。

akizukidenshi.com

www.wch-ic.com

開発環境

vscode + PlatformIO

Visual Studio Code の PlatformIO のプラグインをインストールし、PlatformIO の設定から ch32v のプラットフォーム拡張をインストールします。また、 WCH-LinkE へのパーミッションを udev の設定で調整します。一通りの設定手順は PlatformIO/CH32V のドキュメントに記述されています。

pio-ch32v.readthedocs.io

wlink は Rust で実装された WCH-LinkE を用いて CH32V を操作する cli tool です。WCH-LinkUtility が Windows のみなので、特に Linux で Delay_Ms(1000); はその代わりとして重宝する tool です。

github.com

プログラミング

折角のはじめての CH32V なので Arduino 互換ではなく NoneOS を使ってみました。まとまったドキュメントが見つけられませんでしたが、UART Serial の Hello World からはじめて I2C を操作するところまで動きました。 プログラミングと動作確認は 20ピンの F4P6 で行っています。J4M6 はピン数が足りずデバッグに手間取ったりするので F4P6 で一通り動作させてから J4M6 に載せると言った手順が良いと思います。

Hello World

#include <ch32v00x.h>
#include <debug.h>

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();

    USART_Printf_Init(115200);

    while(1) {
        printf("Hello world\n");
        Delay_Ms(1000);
    }        
}

I2C

データシートを見ると CH32V003 は PC1/SDA, PC2/SCL となっているので GPIOC の Pin1, Pin2 にクロックを設定し I2C1 を有効にします。

class i2c {
    I2C_TypeDef* interface;

public:
    i2c(I2C_TypeDef *i2c, uint32_t clock);
    void write(uint8_t addr, const uint8_t* data, size_t length);
    void read(uint8_t addr, uint8_t reg, uint8_t* data, size_t length);
};

i2c::i2c(I2C_TypeDef *i2c = I2C1, uint32_t clock = 400000) : interface(i2c)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure = {
        .GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2,
        .GPIO_Speed = GPIO_Speed_50MHz,
        .GPIO_Mode = GPIO_Mode_AF_OD,
    };
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    I2C_InitTypeDef I2C_InitStructure = {
        .I2C_ClockSpeed = clock,
        .I2C_Mode = I2C_Mode_I2C,
        .I2C_DutyCycle = I2C_DutyCycle_2,
        .I2C_OwnAddress1 = 0x00, 
        .I2C_Ack = I2C_Ack_Enable,
        .I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit,
    };
    I2C_Init(i2c, &I2C_InitStructure);

    I2C_Cmd(i2c, ENABLE);
    I2C_AcknowledgeConfig(i2c, ENABLE);
}

void i2c::write(uint8_t addr, const uint8_t* data, size_t length)
{
    addr <<= 1;

    while( I2C_GetFlagStatus(interface, I2C_FLAG_BUSY ) != RESET );
    I2C_GenerateSTART(interface, ENABLE);

    while( !I2C_CheckEvent( interface, I2C_EVENT_MASTER_MODE_SELECT ) );
    I2C_Send7bitAddress( interface, addr, I2C_Direction_Transmitter);

    while( !I2C_CheckEvent( interface, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) );

    I2C_SendData( interface, *data);
    while( !I2C_CheckEvent( interface, I2C_EVENT_MASTER_BYTE_TRANSMITTED ) );

    for(auto i = 1; i < length; i++) {
        I2C_SendData( interface, *(data + i));
        while( !I2C_CheckEvent( interface, I2C_EVENT_MASTER_BYTE_TRANSMITTED ) );
    }

    I2C_GenerateSTOP( interface, ENABLE);

    return;
}

void i2c::read(uint8_t addr, uint8_t reg, uint8_t* data, size_t length)
{
    addr <<= 1;

    while( I2C_GetFlagStatus(interface, I2C_FLAG_BUSY ) != RESET);

    I2C_GenerateSTART(interface, ENABLE);
    while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_MODE_SELECT ));

    I2C_Send7bitAddress(interface, addr, I2C_Direction_Transmitter);
    while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ));

    I2C_SendData(interface, reg);
    while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_BYTE_TRANSMITTED ));

    I2C_GenerateSTART(interface, ENABLE);
    while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_MODE_SELECT ));

    I2C_Send7bitAddress(interface, addr, I2C_Direction_Receiver);
    while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    for(auto i = 1; i < length; i++) {
        while( !I2C_CheckEvent(interface, I2C_EVENT_MASTER_BYTE_RECEIVED));
        *(data + i)= I2C_ReceiveData(interface);
    }

    I2C_GenerateSTOP(interface, ENABLE);

    return;
}

KTMicro ソフトウェアラジオ IC KT0913, (KT0936M), KT0937-D8

今回3種類のソフトウェアラジオの IC を購入しました。すべて KTMicro 社製で KT0913, KT0936M, KY0937-D8 の三種です。

akizukidenshi.com

秋月で販売されている KT0913 と KT0937-D8 は同じ SSOP16 のパッケージなので見た目区別つきません。

3種類の IC の違いは対応する周波数帯域やマイコンからのコントロール、もしくはマイコン無しに構成可能とする設計と言った違いがあります。例えば少なくとも KT0936M はマイコンを必要とせずにラジオとして機能させる事を前提とした設計で逆にマイコンから制御するにはやや面倒になっています。逆に KT0913 や KT0937-D8 はマイコンと I2C で繋いで制御する事を前提とした設計になっているようです。

FM/AM SW LW MCU制御
KT0913
KT0936M ☓| 
KT0937-D8
  • FM 32MHz 〜110MHz / AM(MW) 500KHz 〜 1750KHz
  • SW 1.75MHz 〜 32MHz
  • LW 150KHz〜520KHz

今回は CV32V003 から制御するので KT0936M は使いません。 KT0913 と KT0937-D8 は双方 I2C でマイコンから制御可能ですが、比較して KT0913 の方が制御が簡単な印象です。例えば、KT0913 は I2C で周波数を設定してその周波数に変更が可能ですが、KT0937-D8 は周波数を設定することは出来るもののその周波数に変更することが I2C  だけでは出来ませんでした。(方法はあるのかもしれませんが私には見つけられませんでした) clock は内蔵されていないようでそれぞれいくつかの周波数の clock が使えるようですが、32.768KHz のクリスタルが共通して使えるようです。

KT0913 

ブレッドボードに配線

KT0913 と CH32V003 以外に音を鳴らすまでに少なくとも部品は KT0913 への外部クロックと音を鳴らすための何かになります。外部クロックは 32.768KHz のクリスタルと負荷としてのコンデンサ(データシートは23pFとありましたが、手持ちに 22pF があったのでそれを使いました)、音声の出力はマイクロスピーカーとしました。

加えて手持ちのパーツから、クリスタルは足が細かったのでブレッドボードに挿しづらく丸ピンソケットにはんだ付けしています。スピーカーは端子台を介するようにしています。また、ノイズがちょっとひどかったのでデカップリングに 0.1uF のコンデンサでバイパスしています。

アンテナは FM のみ壊れたジャンパー線を挿しています。AM アンテナはここでは繋いでいませんが必要に応じてバーアンテナやコイルを付け足します。

I2C での制御を前提としているので CH と VOL には何も繋いでいません。

I2C で制御

I2C アドレスは 0x35、レジスタに対する Read/Write は2バイト単位で行います。(ただし何故か Read で3バイト返してきているように見えます。なにかミスしているのかな?…)

32.768KHz のクリスタルを使っているとして、電源 ON の default から指定する周波数の FM を受信するには

  1. STATUSA(0x12)を読み XTAL_OK(15bit) が 1 (Ready)になるまで待ちます。
  2. VOLUME(0x04)のDMUTE(13bit) を 1 (Mute disable) に設定します。
  3. TUNE(0x03)のFMCHAN(0〜11bit)を受信する周波数を 50,000 で割った値、 FMTUNE(15bit 目)を 1 (FM Tune Enable)に設定します。例えば、NHK-FM 東京の82.5MHz であれば (82500(KHz) / 50(KHz)) | 0x8000(FM Tune) = 0x8672 となります。

write to 0x35 ack data: 0x12 
read to 0x35 ack data: 0xDF 0x68 0x06
write to 0x35 ack data: 0x04 0xE0 0x80 
write to 0x35 ack data: 0x03 0x86 0x72 

これ で指定した周波数の放送を受信し音が鳴りました。

KT0937-D8

もしかしたらあるのかもしれませんが、KT0937-D8 を I2C の制御だけで周波数を設定する方法を私は見つけることが出来ませんでした。ですので CH のピンから周波数を設定するようにしています。 CH ピンから周波数を設定するには 10kΩ 可変抵抗を繋いでアナログで入力する Dial Mode

とスイッチで制御する Key Mode

があり今回は Key Mode を使っています。

ブレッドボードに配線

部品は KT0913 で使ったものに加えて Key Mode での制御での2つのスイッチとしてDIP化したレバースイッチを利用しています。また、KT0913 にあった ENABLE が無い代わりに DVDD (デジタル電源入力)があります(意味として大きく変わらないように思います)。

I2C で制御

I2C アドレスは 0x35、レジスタに対する Read/Write は1バイト単位で行います。 前述したように CH ピンからの制御を行うので、 KT0913 と異なりその制御パラメータを設定する必要があります。

32.768KHz のクリスタルを使っているとして、電源 ON の default から指定する周波数の FM を受信するには

  1. GPIOCFG2(0x51)の CH_PIN (0〜1bit)を 01 (Key Mode)に設定します。
  2. PLLCFG0(0x04)の SYS_CFGOK(7bit)を 1 (Ready)に設定します。
  3. G38KCFG0(0x1B)を読み POWERON_FINISH(1bit) が 1 (Finish)になるまで待ちます。
  4. LOW_CHAN0(0x0098) と LOW_CHAN1(0x0099) に周波数範囲の下限値を 50,000 で割った値を設定します。例えば 76MHz であれば 76000(KHz) / 50(KHz) = 0x5F0 となり、 LOW_CHAN0(0x0098) に 0x05、 LOW_CHAN1(0x0099)に 0xF0 を設定します。
  5. FMCHAN0(0x0088) と FMCHAN1(0x0089) に周波数範囲の上限値を 50,000 で割った値を設定します。例えば 95Mhz であれば 95000(kHz) / 50(kHz) = 0x76C となります。この時、FMCHAN0(0x0088)の CHANGE_BAND(7 bit) を 1 (Change band) に、AM_FM(6 bit)を 0 (FM) に設定します。つまり、FMCHAN0(0x0088) は0x07 | 0x80(Change band) & 0xBF(FM) = 0x87、FMCHAN1(0x0089) は 0x6C となります。
  6. CHAN_NUM0(0x009A) と CHAN_NUM1(0x009B) にチャンネル数を設定します。チャンネル数は周波数の上限(FM_CHAN)と下限(LOW)CHAN)の範囲をFM_SPACE(Default で 100KHz)で割った値に1を加えて設定します。例えば上限が 95MHz 、下限が 76MHz とすると (95,000 - 76,000) / 100 + 1 = 0xBF となり、CHAN_NUM0(0x009A) に 0x00、CHAN_NUM1(0x009B) に 0xBF を設定します。

write to 0x35 ack data: 0x51 0x01 
write to 0x35 ack data: 0x04 0x80 
write to 0x35 ack data: 0x1B 
read to 0x35 ack data: 0x84 0x00
write to 0x35 ack data: 0x98 0x05 
write to 0x35 ack data: 0x99 0xF0 
write to 0x35 ack data: 0x89 0x6C 
write to 0x35 ack data: 0x88 0x87 
write to 0x35 ack data: 0x9A 0x00 
write to 0x35 ack data: 0x9B 0xBE 

これで受信が開始されレバーボタンを操作することにより 100kHz 単位で周波数を増減し設定する事が出来ます。設定された周波数は STATUS6(0x00E4) と STATUS7(0x00E5) から読み取り (STATUS6 << 8) | STATUS7 で算出出来ます。

8ピン CH32V003 J4M6

おまけですが、8ピンの CH32V003 J4M6 で動かしてみます。と言ってもピンは I2C しか使っておらずプログラムはほとんど同じ、気をつけるのは デバッグなどで UART を使っている場合、プログラムを書き込む SWIO と衝突する事程度です。必要であれば書き換え書き込めば F4P6 同様動作すると思います。配線するととても小さくまとまり音がなると個人的にちょっと感動してしまいました。

はまりどころ

WCH-LinkE の動作モード

WCH-LinkE はCH32V に対応する RISC-V モードと対応しない ARM モードという2つの Work Mode があります。WCH-LinkE のボードには赤と青の2つの LED が載っており、ARM モードでは赤青両方が点灯し、RISC-V モードでは赤のみが点灯します。また PC から見える USB の product ID が RISC-V モードでは 0x8010、ARM モードでは 0x8012 となっているようです。

~$ lsusb | grep WCH-Link
Bus 001 Device 005: ID 1a86:8010 QinHeng Electronics WCH-Link
~$ lsusb | grep WCH-Link
Bus 001 Device 011: ID 1a86:8012 QinHeng Electronics WCH-Link

切り替えは

  • 透明ケースを外し Mode ボタンを押す
  • wlink を使う (exprimental)
$ wlink mode-switch --rv
<WCH-Link#0 libusb device> Bus 001 Device 011 ID 1a86:8012(USB-FS 12 Mbps) (DAP mode)
09:26:33 [WARN] This is an experimental feature, better use the WCH-LinkUtility!
09:26:33 [INFO] Switch mode WCH-LinkDAP 1a86:8012 #0
$ 
  • WCH-LinkUtility を使う (Windows のみ)

のいずれかで出来ます。

WCH-LinkE のバージョン

新規インストールなど、最新の開発環境を用いる場合 WCH-LinkE の version は 2.10 以上でなければなりません。(2.9 以下では書き込みなどでエラーが発生します)

github.com

$ wlink status
10:10:32 [INFO] Connected to WCH-Link v2.11(v31) (WCH-LinkE-CH32V305)

メーカーオフィシャルの開発環境の MounRiver Studio を利用してバージョンアップできます。また Windows であれば WCH-LinkUtility でも可能なようです。

8ピン CH32V003 J4M6 の UART TX 衝突問題

開発は 20ピンの F4P6 使いましょうとしている根拠でもあるのですが、データーシートで確認できるように 8 ピンの J4M6 は書き込みやデバッグで使う SWIO と UART TX が共通です。つまり、うかつにUSART_Printf_Init() とかして U(S)ART を Enable にするようなプログラムを書き込むと SWIO が使えなくなりデバッグや書き込みが出来なくなってしまいます。

Windows であれば WCH-LinkUtility で消去できるようです。Linux は(おそらくは Mac も?) wlink を使って書き込んだプログラムを消去することが出来ます。

$ wlink erase --chip CH32V003 --method power-off
20:09:53 [INFO] Connected to WCH-Link v2.11(v31) (WCH-LinkE-CH32V305)
20:09:53 [INFO] Erase chip by PowerOff
$