C0 → CAL0 の設定を読んでいる状態。
    c0 のように小文字の場合はエラー値の補完をしている状態
    (recall してから周波数範囲を変えた場合。厳密には uncal )
D → directivity エラー修正
R → refrection tracking エラー修正
S → source match エラー修正
T → transmission tracking エラー修正
X → isolation (crosstalk) エラー修正

それぞれの意味はエンハンストレスポンス校正あたりで検索する (自分はよく理解してない)

なお cal を done したときに、終わった calibruation menu のハイライトが一部消えるのは正常 (上記エラー情報を計算し終えると消える仕様)

  1. トップ
  2. tech
  3. NanoVNA 左側の文字の意味

NanoVNA の USB のコミュニケーション方法が USB-CDC で、プロトコル自体は簡単そうだ、というのを前に書きました。なぜそんなことを調べていたかといえばブラウザでUIを作りたかったからなので、作りました。

機能

  • デバイスの現在のデータの読みこみとグラフ化(スミスチャート・周波数応答)
  • 複数のトレースタイプ (clear write / freeze / minhold / maxhold / videoavg / poweravg)
    • freeze が値の比較に便利だと思う。これが欲しくてトレースタイプを実装してます。他のはオマケ
  • ウィザード形式のキャリブレーション
  • デバイス画面のキャプチャ
  • デバイスバージョンの表示 (比較的新しいファームでなければ version コマンドがありません)
  • TDR (Time Domain Reflectometry)
  • s1p s2p 出力。ただし s2p は S22 = S11, S12 = S21 として出力する。

実装

ほとんどの機能は JavaScript で直接実装しています。TypeScript も使っていません。USB Device との通信は WebUSB で、できるだけポーリングを正確にしたいため Worker 内で行っています。このあたりは HackRF One を WebUSB から操作してスペアナを作るのと同じような手順です。

グラフは chart.js をそのまま使っているので工夫したところはありません。

現状では TDR 測定のところでだけ Rust で書いた実装を wasm にしたものを呼んでいます (TDR は RL をただ ifft しているだけです)。パフォーマンスが必要なわけではないのですが、複素数の計算を JS でやるのがとにかく面倒かつ可読性を損うので、Rust 資産を流用しという魂胆です。

本当は、Rust での処理は生dump データから、デバイスでやっている計算すべてをホスト側でやるのも見越してやりはじめましたが、一通りスキャンするのにかえって時間がかかりそうなのでやるのをやめました。ヒルベルト変換は fft/ifft でできるし、すぐ実装はできそうではあります。

備考:WebUSB と USB-CDC の将来は不安

WebUSB はそもそも既存のよくあるUSBデバイスに対してアクセス権限を与えるようなコンセプトのものではなく、USB-CDC も同様にスコープに入っていません。本来はSerial APIという提案仕様があって、こちらでカバーされるはずです。だけれど現状では実装されているブラウザが存在しません。

ということでやはりWebUSB でやるしかないわけですが、OS間で取り扱いが違うので意外と面倒くさいことがわかりました。

  • Windows では CDC 用のドライバがインターフェイスを握ってしまう
    • ドライバを libusb 用のものに置き換えれば使える
  • Ubuntu では cdc_acm ドライバがインターフェイスを握ってしまう
    • udev ルールで bind 直後に即 unbind することができる。

ドライバを入れ替えると普通のシリアルポートとしてOSには(当然だけど)認識されなくなるので、必要に応じて再び入れ替えたりする必要があります。

ここで一つ問題があって、現行の NanoVNA は USB の vid/pid を STM32 の CDC デバイスのもの(?)を流用しているため、上記のようなドライバ置き換えが他の STM32 デバイスに影響を及ぼすことがあります。vid/pid の関するイシューはあるのでそのうちなんとかなるかもしれませんが現状ではこの通りです。

なお macOS は実際のデータ転送に使うインターフェイスは、実際にポートがopenされるまで排他的に確保しないようで、WebUSB からも自由にアクセスできます。逆に WebUSB で握っている間にシリアルポートを別途開こうとすると、そのタイミングで resource busy が出ます。

現行の Android では特に何もせず、OTG コネクタさえ使えば接続することができました。ただ、中華NanoVNAはコネクタにType-Cを採用はしているものの、CC に何も接続されていないため、Type-C - Type-C ケーブルを使うとデバイスを認識しません。Type-A OTG 変換ケーブルなどを経由する必要があります。

とりあえず何が言いたいかというと、今のところ WebUSB で USB-CDC デバイスをなんとか動かす方法は存在しているけど、今後はどうなるかわからないということです。WebUSB 自体も (使われなければ) 消滅する予感のする仕様と感じています。少なくとも今の仕様 (パーミッションダイアログが危険性の割に雑) のまま広く使われることもないだろうと思います。

  1. トップ
  2. tech
  3. NanoVNA を WebUSB を使ってブラウザから見る

ヘタなの買うより安いですね。

Arduino をフレームワークにした場合これだけです。

// main.ino
#include "BluetoothSerial.h"

BluetoothSerial SerialBT;

void setup() {
	Serial.begin(115200);
	SerialBT.begin("ESP32");
}

void loop() {
	if (SerialBT.available()) {
		Serial.write(SerialBT.read());
	}
	if (Serial.available()) {
		SerialBT.write(Serial.read());
	}
}
  1. トップ
  2. tech
  3. ESP32 を BLE SPP / UART 変換器にする

NanoVNA は USB-CDC による通信をサポートしていますが、ここが無線化すると(特にアンテナ調整の場合は)便利なので、コンセプトを試してみました。無線化といってもいろいろやりようは考えられますが、今回はシリアル通信を BLE SPP にのせかえる方法としました。

シリアルポートをMCUから引き出す

USART1_TX USART1_RX を MCU (STM32F072CBT6) から引き出します。中華 NanoVNA では NC になっている、それぞれ PA9 (USART1_TX) PA10 (USART1_RX) です。LQFP48 なので PA9 は 30ピン、 PA10 は 31ピンになります。

かなり細かいので面倒ですが、このピッチならギリギリなんとかなります。あまり接続部に負担をかけたくないので、ポリイミドテープで一旦うけています。

これをそのまま BLE 変換器に繋いでもいいのですが、デバッグがしにくいので一旦安定したところに繋ぎます。JTAG のピンヘッダがある部分に曲げたピンヘッダを追加して置いて、固定しました。

シリアルポートの有効化

NanoVNA のコード側の対応も必要です。

このパッチによって有効化しています。USB を接続した状態で電源をONにした場合は USB-CDC のシェルを有効にし、そうではない場合はシリアル経由のシェルを有効にします。

BLE UART 変換

手元に ESP-WROOM-32 が余っていたので、これを利用してみました。一旦雑に配線して変換プログラムを書きこんで、不要な配線をとって実際に組みこみます。

コードはこんな感じです。あまり好ましいとは思いませんが、RF 回路の突発電流で brown out detection (低電圧検出) にひっかかることがあるので検知を切っています。

#include "BluetoothSerial.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"

BluetoothSerial SerialBT;

void setup() {
	WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
	Serial.begin(115200);
	SerialBT.begin("NanoVNA");
}

void loop() {
	if (SerialBT.available()) {
		Serial.write(SerialBT.read());
	}
	if (Serial.available()) {
		SerialBT.write(Serial.read());
	}
}

デバッグの様子です。

組み込み

ピン名がわかりやすいからという理由で、ESP-WROOM-32 のシールド側を下にしています。シールドにはポリイミドテープを貼って念のため絶縁しています。これを無理矢理 STM32 の上に両面テープで貼りつけて固定し、配線しました。

一応これで納まりました。

懸念点

ESP-WROOM-32 の消費電力が非常に多いです。突発的に100mAぐらい平気で流れるので、本体の消費とあわせると 3.3V レギュレータの定格 (200mA) を若干オーバーしているかもしれません。

データが欠落します。BLE なのと、CTS/RTS を無視しているので仕方ないですが、データの欠落が結構起こります。

やっておいてなんですが、この方法は大変な割に微妙なので、ナシかなと思いました。古いスマフォでUSBを中継したほうが楽だし確実そうです。

9600bps ぐらいまでボーレートを落とせばデータ欠落はなくなるようです。が、本当に遅いのでやはり厳しいです。57600 だと少し欠落する。

BLE ではなく普通の Bluetooth SPP にすればいいんですが、普通の Bluetooth は Web Bluetooth API から呼べないのでやる気になっていません。

  1. トップ
  2. tech
  3. 中華 NanoVNA の Bluetooth シリアル化を試す

STM32 には出荷時点でブートローダーが入っていて、様々な方法ですぐ書きこめるようになっている。ブートローダーは、書き換えできない「システムメモリ」と呼ばれる領域に入っている。

ユーザーコードからでも、このシステムメモリにジャンプすればブートローダーのモードに入れる。入れるのだけど、このブートローダーに入る方法について ATN2606 にはこう書かれている

In addition to patterns described above, user can execute bootloader by performing a jump to system memory from user code. Before jumping to bootloader user must:
• Disable all peripheral clocks
• Disable used PLL
• Disable interrupts
• Clear pending interrupts
System memory boot mode can be exited by getting out from bootloader activation condition and generating hardware reset or using Go command to execute user code.

つまりいろんなものを初期状態に戻さなければいけない。これは、まぁめんどうくさい……

同様の事例をググってみると、マジックコードをメモリに書きこんで (普通のグローバル変数でも良いし、メモリを節約したいなら、どうせリセットするのだし適当なアドレスに書きこんでいい)、システムリセットを起こし、(メモリは初期化されないので) マジックコードを検出して、リセット直後のあらゆるペリフェラルが初期化される前にブートローダ(システムメモリ)へジャンプをかけるという方法がひっかかる。

リセット直後にジャンプするほうがあきらかに楽。

ChibiOS では

board.c の __early_init() というのがスタック初期化直後に ChibiOS のブートストラップコードのアセンブリから呼ばれてくるので、ここにジャンプコードを実装してやる。

以下のようになった。いろいろ試したあげく、結局ほぼstackoverflowの内容と同じだけど、どうしてもうまくいかなかったため調べていたら、どうも __enable_irq() が必要ということがわかった。

STM32F072xB_SYSTEM_MEMORY の位置には初期スタックポインタのアドレスが入っているはず、、なんだけどリセットからくると(?)ダメみたいなので、定数値を入れている。STM32F072xB_SYSTEM_MEMORY+4 が実際のジャンプ先。

// board.c
void __early_init(void) {
  if ( *((unsigned long *)BOOT_FROM_SYTEM_MEMORY_MAGIC_ADDRESS) == BOOT_FROM_SYTEM_MEMORY_MAGIC ) {
    // require irq
    __enable_irq();
    *((unsigned long *)BOOT_FROM_SYTEM_MEMORY_MAGIC_ADDRESS) = 0;
    // remap memory. unneeded for F072?
    // RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
    // SYSCFG->CFGR1 = 0x01;
    __set_MSP(SYSTEM_BOOT_MSP); 
    ( (void (*)(void)) (*((uint32_t *)(STM32F072xB_SYSTEM_MEMORY+4))) )();
  }

  //si5351_setup();
  stm32_clock_init();
}

定数はこのようになっている。STM32F072xB_SYSTEM_MEMORY は型番によって違うので調べる必要がある。リファレンスマニュアルに書いてある。

// board.h
#define STM32F072xB_SYSTEM_MEMORY 0x1FFFC800
#define BOOT_FROM_SYTEM_MEMORY_MAGIC_ADDRESS 0x20003FF0
#define BOOT_FROM_SYTEM_MEMORY_MAGIC 0xDEADBEEF
#define SYSTEM_BOOT_MSP 0x20002250

実際のジャンプするコードはこう。メモリにフラグを書いてリセットしているだけ。

  *((unsigned long *)BOOT_FROM_SYTEM_MEMORY_MAGIC_ADDRESS) = BOOT_FROM_SYTEM_MEMORY_MAGIC;
  NVIC_SystemReset();

ref

  1. トップ
  2. tech
  3. STM32F072 で、ユーザーコードから DFU モードに入る。

回路図の D2 はバッテリーから MCU の VBAT に接続する経路ですが、自分の入手した固体だと未実装でした。せっかくなので、手元にあった適当なダイオードをひとまず半田付けして、ファームウェア側の実装を試してみました。使ったのは汎用小信号用ダイオードの 1N4148 の SMD 版 (SOD-323) ですが、本来はショットキーのほうが良いはずです。

パッチの全体

D2 の実装が必要なことと、中華版のバッテリーが載っているモデルにしか適用できないので (一応 VBAT を見た結果、バッテリが載っていないあるいは D2 がない場合には無視するようにはしてありますが)、オリジナルの master には入れることができないパッチです。自分でパッチを管理する必要があります。

master に入れてもらったので、自分でパッチ管理する必要はなくなりました。ありがたや。D2 を実装したうえで最新ファームを書くだけで有効になります。

adc_vbat_read()

int16_t adc_vbat_read(ADC_TypeDef *adc)
{
#define ADC_FULL_SCALE 3300
#define VBAT_DIODE_VF 500
#define VREFINT_CAL (*((uint16_t*)0x1FFFF7BA))
	float vbat = 0;
	float vrefint = 0;

	ADC->CCR |= ADC_CCR_VREFEN | ADC_CCR_VBATEN;
	// VREFINT == ADC_IN17
	vrefint = adc_single_read(adc, ADC_CHSELR_CHSEL17);
	// VBAT == ADC_IN18
	// VBATEN enables resiter devider circuit. It consume vbat power.
	vbat = adc_single_read(adc, ADC_CHSELR_CHSEL18);
	ADC->CCR &= ~(ADC_CCR_VREFEN | ADC_CCR_VBATEN);

	uint16_t vbat_raw = (ADC_FULL_SCALE * VREFINT_CAL * vbat * 2 / (vrefint * ((1<<12)-1)));
	if (vbat_raw < 100) {
		// maybe D2 is not installed
		return -1;
	}
	
	return vbat_raw + VBAT_DIODE_VF;

}

一応 VREFINT_CAL を使って補正をかけています。面倒なので計算に float 使ってますが、返り値は mV 単位の int16_t です。

ADCサイクル数

既存の adc_single_read() では ADC のサンプリングサイクルが最小になっているのですが、これだと VBAT から内部キャパシタを十分に充電できないようで、ADC で取得できる値がやたら低くく、うまくいきませんでした。ということで ADC_SMPR_SMP_239P5 までサイクル数を増やしました。

どこでバッテリ残量をとるか

そんなに頻繁に残量をとってもしかたないのですが、面倒なので sweep 直後に毎回取得するようにました。

      if (vbat != -1) {
          adc_stop(ADC1);
          vbat = adc_vbat_read(ADC1);
          touch_start_watchdog();
          draw_battery_status();
      }

表示

5x7 のフォントとして残量アイコンを実装しようと最初は考えていましたが、小さすぎるので、自力でモリモリ描くことにしました。

void
draw_battery_status(void)
{
    int w = 10, h = 14;
    int x = 0, y = 0;
    int i, c;
    uint16_t *buf = spi_buffer;
    uint8_t vbati = vbat2bati(vbat);
    uint16_t col = vbati == 0 ? RGB565(0, 255, 0) : RGB565(0, 0, 240);
    memset(spi_buffer, 0, w * h * 2);

    // battery head
    x = 3;
    buf[y * w + x++] = col;
    buf[y * w + x++] = col;
    buf[y * w + x++] = col;
    buf[y * w + x++] = col;

    y++;
    x = 3;
    buf[y * w + x++] = col;
    x++; x++;
    buf[y * w + x++] = col;

    y++;
    x = 1;
    for (i = 0; i < 8; i++)
        buf[y * w + x++] = col;

    for (c = 0; c < 3; c++) {
        y++;
        x = 1;
        buf[y * w + x++] = col;
        x++; x++; x++; x++; x++; x++;
        buf[y * w + x++] = col;

        y++;
        x = 1;
        buf[y * w + x++] = col;
        x++;
        for (i = 0; i < 4; i++)
            buf[y * w + x++] = ( ((c+1) * 25) >= (100 - vbati)) ? col : 0;
        x++;
        buf[y * w + x++] = col;

        y++;
        x = 1;
        buf[y * w + x++] = col;
        x++;
        for (i = 0; i < 4; i++)
            buf[y * w + x++] = ( ((c+1) * 25) >= (100 - vbati)) ? col : 0;
        x++;
        buf[y * w + x++] = col;
    }

    // battery foot
    y++;
    x = 1;
    buf[y * w + x++] = col;
    x++; x++; x++; x++; x++; x++;
    buf[y * w + x++] = col;

    y++;
    x = 1;
    for (i = 0; i < 8; i++)
        buf[y * w + x++] = col;

    ili9341_bulk(0, 1, w, h);
}

vbat2bati は以下のような inline 関数です。閾値がいまいちわからないので、結構雑に設定してます。

// convert vbat [mV] to battery indicator
static inline uint8_t vbat2bati(int16_t vbat)
{
	if (vbat < 3200) return 0;
	if (vbat < 3450) return 25;
	if (vbat < 3700) return 50;
	if (vbat < 3950) return 75;
	return 100;
}
  1. トップ
  2. tech
  3. 中華 NanoVNA にバッテリー表示をつける

マイクロソフト ワイヤレス マウス 人間工学 高精細読み取りセンサー Sculpt Ergonomic Mouse (ブルートラック) L6V-00013 -

4.0 / 5.0

【国内正規品】SteelSeries QcK mini マウスパッド 63005 -

5.0 / 5.0

会社のマウスとマウスパッドを買い替えた。前まで SteelSeries QcK (mini じゃないやつ) を使ってたけど、普段全面を使うことはまずないので mini にしてみた。

PCB上にピンヘッダ(未実装)がある。

  • SWDIO
  • SWCLK
  • GND
  • NRST
  • VCC (使わない)

SWD (Serial Wire Debug) 用のもの。ここに ST-Link を繋ぐ。ST-Link といっても中華 ST-Link だけど、こういう感じになる。

VCC 以外を接続する。VCCは普通に電源をオンにして供給する。

事前条件

普通に make して build できる環境にしておく。arm-none-eabi-gcc が入っていればよい。

open-ocd は brew で入るデフォルトではなく、head を入れる必要がある。なぜか texinfo が要求されて死んだので前もって入れたほうがよさそう。

brew install texinfo
brew install open-ocd --HEAD

VSCode

普段は vim を使っているが、CUI デバッガは個人的にはつらいので、こういうときは VSCode を使う。

VSCode を入れたのち Cortex-Debug extension を入れて使う。VSCode を開いて、Extensions から検索して Install するのが最速。

tasks.jsonを書く

make を呼ぶようにしておく

{
    "tasks": [
        {
            "type": "shell",
            "label": "build",
            "command": "make",
            "args": [
            ],
            "options": {
                "cwd": "${workspaceRoot}"
            }
        }
    ],
    "version": "2.0.0"
}

launch.json を書く

stlink を使って stm32f0x をデバッグするので以下のようにする。また、デバッグ前に build するようにする。

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "cortex-debug",
            "servertype": "openocd",
            "request": "launch",
            "name": "OpenOCD-Debug",
            "executable": "build/ch.elf",
            "configFiles": [
                "interface/stlink.cfg",
                "target/stm32f0x.cfg"
            ],
            "cwd": "${workspaceRoot}",
            "preLaunchTask": "build",
        }
    ]
}

デバッグ開始する

Debug を開いて Start Debugging (F5) をする。ビルドしたのち、しばらくする (デバイス側にビルドしたファームが転送される) とデバイス側の画面は白くなり、リセットハンドラでブレークするので、適当な場所にブレークポイントを置いて resume する。

備考

OpenOCD の cfg の場所

/usr/local/share/openocd/scripts/

にある。結構 deprecated になっているものも置いたままだったりする。stlink.cfg は ST-Link のバージョンに関係なく共通で使えるものになっている。

リソース

ref

  1. トップ
  2. tech
  3. NanoVNA を VSCode + ST-Link + OpenOCD でオンチップデバッグ