回路図の 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 にバッテリー表示をつける

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

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

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.

https://www.st.com/content/ccc/resource/technical/document/application_note/b9/9b/16/3a/12/1e/40/0c/CD00167594.pdf/files/CD00167594.pdf/jcr:content/translations/en.CD00167594.pdf

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

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

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

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 モードに入る。

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 シリアル化を試す
  1. トップ
  2. nanovna
  3. 中華 NanoVNA の Bluetooth シリアル化を試す

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

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 のコミュニケーション方法が 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 を使ってブラウザから見る
  1. トップ
  2. nanovna
  3. NanoVNA を WebUSB を使ってブラウザから見る

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 左側の文字の意味
  1. トップ
  2. nanovna
  3. NanoVNA 左側の文字の意味

ポインタ配列の const が理解できなかったのでメモ

検証コード

const char* const foo[] = {"foo","bar","baz"};

extern void __print(const char* buf);
void main(void) {
	char* str;
	__print(foo[0]);
}

これの foo のついている2つのconstを消したりつけたりする。__print は最適化で消されないように extern してるだけで特に意味はない。

arm-none-eabi-gcc -c c.c -o a.o && arm-none-eabi-objdump -t a.o

このようにして symbol table を見て、どのセクションに配置されるかを確認する。

char* foo[]

...
00000000 g     O .data  0000000c foo
00000000 g     F .text  0000002c main
...

当然 .data に配置される。

const char* const foo[]

...
0000000c g     O .rodata        0000000c foo
00000000 g     F .text  00000028 main
...

当然 .rodata に配置される。

const char* foo[]

...
00000000 g     O .data  0000000c foo
00000000 g     F .text  0000002c main
...

.data に配置される。

これがいまいちよくわからない。ここにconstをつけても以下はコンパイルエラーにならない。何が const になっているのだろう?「ポインタの配列 foo」そのもの?

foo[0] = "piyo";

しかし以下のように「ポインタの配列 foo」そのものを更新しようとしてもエラーになるので、そもそも「ポインタの配列 foo」そのものを更新しようがない気がする。

	char* bar[] = {"xxx"};
	foo = bar; //=>  error: assignment to expression with array type

char* const foo[]

0000000c g     O .rodata        0000000c foo
00000000 g     F .text  00000028 main

.rodata に配置される。

この場合、foo[0] への代入はコンパイルエラーになる。「ポインタの配列」の「ポインタ」が const になっている?

「ポインタ配列」の場合、上記のように「ポインタの配列 foo」そのものを更新しようがないので、.rodata で良いのだろうか?

foo[0] = "piyo"; //=> error: assignment of read-only location 'foo[0]'

char* const foo と const char* const foo は全く同じバイナリが吐かれる。ポインタ配列の最初の const は無意味なのだろうか?

  1. トップ
  2. tech
  3. C言語のポインタ配列の const の効果

前につくったアッテネータを測ってみる 高周波用アッテネータを作ってみる | tech - 氾濫原

NanoVNA だと 300MHz 付近にデコボコがあるようにみえますが、これはおそらく 0.5〜900MHz でキャリブレーションした状態で、0.5〜500MHz の範囲を見ている (校正の補完を使っている) せいと思われるので、再度この範囲でキャリブレーションしなおしてみます。(300MHz を超えると高調波モードを使うので段差ができやすい)

消えました。

リターンロス (S11)

前回の結果の再掲 (TG つきスペアナ + リターンロスブリッジの結果)

挿入損失 (S21)

前回の結果 TGつきスペアナでの結果

  1. トップ
  2. tech
  3. NanoVNA の測定メモ
  1. トップ
  2. nanovna
  3. NanoVNA の測定メモ

#!/usr/bin/env ruby

require 'pp'

D = Struct.new(:sec, :size, :name)

target = ARGV.shift

sram = `arm-none-eabi-objdump -t '#{target}'`.chomp.split(/\n/).
	select {|l| /\.bss|\.data/ =~ l }.
	map {|l|
		sec, size0 = *l.split(/\t/)
		size, name = *size0.split(/\s+/) if size0
		D.new(sec, size.to_i(16), name)
	}

total = sram.map {|i| i.size }.reduce {|r,i| r + i}

sram.sort_by {|i|
	i.size
}.each { |i|
	puts "% 3d%% % 10d %s" % [i.size.to_f / total * 100, i.size, i.name]
}

puts "total: %d bytes" % total

こういうのを書いて

$ foo  build/ch.elf

と実行すると

...
  0%        124 SD1
  0%        132 USBD1
  1%        192 dump_buffer
  1%        208 ch_idle_thread_wa
  2%        384 SDU1
  2%        384 rx_buffer
  4%        640 waThread2
  6%        960 waThread1
  7%       1064 impure_data
 10%       1616 measured
 10%       1616 trace_index
 13%       2048 spi_buffer
 30%       4552 current_props
total: 15081 bytes

こういう感じである程度わかる。

  1. トップ
  2. tech
  3. SRAM 使用量のカウント

avstack.pl である程度できる。

avstack.pl の $objdump を適当にアーキテクチャをあわせて変えておく。

my $objdump = "arm-none-eabi-objdump";

このうえで、GCC のコンパイルフラグに -fstack-usage をつける。そうすると .o の同じ名前で .su というファイルができる。

asm から変換されたファイルとかでは .su は作られないので、.su がない .o を除外して、avstack.pl の引数にすべて与える。

$ ./avstack.pl build/obj/Font5x7.o build/obj/adc.o ...
  Func                               Cost    Frame   Height
------------------------------------------------------------------------
> Thread1                             792       20       16
  sweep                               772       84       15
  ui_process                          688       12       14
  ui_process_touch                    676       12       13
  touch_pickup_marker                 664       52       12
  drag_marker                         612       44       11
  ui_process_lever                    608       12       12
  ui_process_normal                   596       28       11
> menu_marker_sel_cb                  580       12       11
...

すると、使用率順に表示してくれる。> マークはどこからもそれが呼ばれていない関数、つまり呼び出し元と思われるもの。

Cost は最大スタック利用量。Frame は該当関数のスタック利用量 (コールコストを含む)。Height は最大コールスタック数。

Height * コールコストが意外とでかい。

  1. トップ
  2. tech
  3. スタック使用量の静的解析


NanoVNA という非常に小型で低コストのスタンドアロン VNA (ベクターネットワークアナライザ) がにわかにグローバルで流行している。VNA は高周波回路設計・実装に必須の測定器のひとつだが、もともと非常に高価なため、個人でちゃんとしたメーカーものを所有することはまずない。たとえ低価格と言われるものでも数万円〜だった。それがさらに低コストで使える性能であると評判なので、VNA の民主化だというおもむき。

NanoVNA の設計のオリジナルはTT (ttrftech) さんという日本語でブログも書かれているかたの設計のもの。自分もハムフェアかMaker Faireかで実機をちらっと見たことがある気がする (もしかするとVNAではなく後続プロジェクトのSDRのほうだったかもしれないが)。少量のキット化までされていたが、これを hugen79 さんが手を加えてしPCBアートワークなどをやりなおして売っているようだ (もともとPCBの設計はソースに含まれてない)。ファームにも独自のコードが入っている。

aliexpress などでは、hugen79 版を元にした (おそらく) さらにこれのクローンと思われるものが非常に安価に売っている。hugen79 レポジトリの README では、粗悪なものがあって性能が出ないことがある (ミキサーまわりに齟齬があるらしい? シールドの有無など違いもある) けど、本来の性能じゃないから買う側でちゃんと調べろや、ということが書いてある。

自分には技術レベル的にVNAは必要ないが、VNAとして使わなくても、これはそのままアンテナアナライザーにもなるわけで、もはや自作するより良さそうだ。

ちなみに2ポートのうちCH0だけ反射を計測できるので、繋ぎかえて測定してマージしないと s2p 相当の結果を得られない。(S11 S21 を得て、繋ぎかえて S11、S21をS22 S12 に読み替える) 入出力が全く同じで対称の受動コンポーネントなら S22=S11 S12=S21 としてもまぁいいのかも (VNA使ったことないので温度感がわからない)。

基本的な使いかた

  • 周波数の範囲を決める (範囲内の固定 101 ポイントを測定する)
  • キャリブレーションする
    • RESET してから、CAL を順番に実行していく
  • 被測定物を繋ぐ

特定のよく使う周波数範囲に関してはキャリブレーション済みの状態を保存できる。

PCソフトウェア

オリジナル版だと python による実装があり Jupyter Notebook から呼びだすような例が書いてある。

それとは別に hugen79 開発の C# ソフトウェアが存在している。ただしこれはOSSではない。

デバイスとの通信は USB CDC によるもので特に難しさはなさそう。main.c を見れば実装はわかるし、python 実装を読めばホストインターフェイスは容易に書けそう。ただ細かいことをやろうとするとホスト側で信号処理をやらないといけない。

逆に USB CDC しかないので雑に BLE シリアル化みたいなのはちょっと面倒そう。MCU から使ってないピンを引き出してUARTに割り当てたらいいのかも。

ファームウェア

販売されているものに入っているファームウェアは店によって違うっぽい。hugen79 ファームが入っているようだが、これにはいくつか種類がある。上限周波数とアンテナアライザーに特化しているかどうかの違いらしい。いまいち違いがわからないのでコードを読んでみたが、上記レポジトリには上限周波数のフラグはあってもアンテナアライザー特化版フラグが入ってなさそう。謎。

ttrftech ファームも割と最近には上限を900MHz までとする高調波拡張が入っているみたい。(ブランチには1.5GHzまでの拡張もある)


ということで何らかの変更を入れる場合どっちからフォークするかは悩ましい。github を検索すると FreeRTOS バージョンのコードもあったりする。hugen79 版はオリジナルから定期的にコードを手動でマージしているみたいなので、特に必要がなければオリジナルにPRを作ったほうが良さそうかな。

SRAM がギリギリなのであまり高級な機能は入れにくい。可能なら宣言済みのバッファをうまく利用したい。

ファームウェアの歩きかた

ブロックダイアグラムをまず見とく。全体でやってることは難しくなくて、クロックジェネレータ・ミキサ・ステレオオーディコーデック・MCU をうまく組み合わせてあまり部品数を増やさずに構成されている。

MCU は STM32F072C8T6 (Coretex-M0 48MHz / 16KB SRAM / 64KB Flash)


STM32F072CBT6 (Coretex-M0 48MHz / 16KB SRAM / 128KB Flash) でした。

ファームウェア側は RTOS として ChibiOS が採用されている。

シェルまわり

USB CDC のシェルは ChibiOS の shell 機能で実装されている。main.c に定義がある。

すべての機能がシェルから呼べるようになっているので、機能から見るなら基本は main.c を起点にして該当コードを探すのが読みやすいと思う。

信号入力まわり
  1. TLV320AIC3204 (ADC) を初期化
  2. STM32 の I2S は常時起動しており i2s_end_callback() が呼ばれている
  3. dsp_process() で現在の値をある程度計算してグローバルに保持する

があったうえで、別スレッドで

  1. オシレータの周波数を設定
  2. ADC のチャンネルを選択して DSP が安定するのを待つ
  3. calculate_gamma() でΓを求める

がスイープにあわせて実行されている。

レンダリングまわり

ベクトルネットワークアナライザNanoVNAの液晶画面を実装する このエントリにおおまかな実装概要が書いてある。コードは plot.c だがメモリが少ないのを工夫して実装してあるので難しい。ちゃんと読んでない。トレース結果とかの状態をグローバルに保持しつつ、再描画が必要な領域に dirty フラグ (mark) を立てて、draw_cell() が実際に特定の領域のピクセル情報をつくってディスプレイに送っているみたい。

ref

  1. トップ
  2. tech
  3. 中華 NanoVNA ってなんなのか? またはファームの歩きかた
  1. トップ
  2. nanovna
  3. 中華 NanoVNA ってなんなのか? またはファームの歩きかた

NanoVNA の実装を軽く読んだ感じでは画面のキャプチャをとる機能がなさそうだったので、実装を書いてみた。測定器はとにかくキャプチャをとりたいし、せっかく綺麗にレンダリングされているので、できれば保存したい。

中華 NanoVNA ってなんなのか? またはファームの歩きかた | tech | nanovna - 氾濫原 にも少し書いたけど、画面描画はメモリ消費を抑える実装になっており、MCU 側で画面バッファを全て持っているわけではない。

そこで、使っているLCDドライバの ILI9341 のデータシートを見てみたところ、ドライバ側で持っているメモリ内容を SPI 経由で読み出せそうであったので、そちらを利用した。

メモリ内容を読めるといっても、MCU 側のメモリ容量的に全てを一気に読むことはできないので、一部読んではUSBに流し、一部読んではUSBに流すというのを繰り返す実装にした。

ハマったところ

SPIを受信するコードから書く必要があったけど、STM32 に慣れていないせいでかなりハマってしまった。

まず FRXTH フラグを適切にセットしていないと RXNE フラグがセットされないので RXNE を見て無限ループさせるとそこでスタックする。

あとは (おそらく) 受信バッファ溢れ (オーバーフロー OVR )の場合で、受信バッファが溢れた場合、あとからきたデータで上書きされるのではなく、単に全て捨てられるようなので、執拗に OVR をクリアするような実装を書く必要があった。もっとスマートに書けるのかもしれない。

  1. トップ
  2. tech
  3. NanoVNA に capture コマンドを追加してみる
  1. トップ
  2. nanovna
  3. NanoVNA に capture コマンドを追加してみる