#!/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 コマンドを追加してみる

HackRF One 用の sweep mode を利用するスペクトラムアナライザーで、いまいち安定して動くものが見つからなかった。WebUSB で書いてブラウザだけあれば動くようにしたら便利かと思い書いてみた。

余計なものをインストールせず、ブラウザで上のURLを開くだけで使えるので普通に便利。だけど WebUSB は今のところ Google Chrome でしか使えない。

実装

HackRF との通信

元々 libhackrf (Cのライブラリ) は libusb を使って実装されている。WebUSB も libusb のほぼそのままの API のラッパーなので、読みかえて実装すれば簡単に移植できる。面倒なのは実際に transferIn() するところぐらい。

USB 帯域を使いきる

20MHz サンプルで 8bit I/Q 信号を得るので、コンスタントに40MB/sec の転送帯域を使用する。これは USB 2.0 の実効速度のほぼ上限にあたるため、気をつけて実装しないと抜けが発生する。

追記:20MB/sec 使いきるのは通常のサンプリングの場合で、スイープモードの場合はブロックサイズの関係上 13.3MB/sec ぐらいになる。

WebUSB だと transferIn() を使って転送するが、素朴に await transferIn() を繰替えすと必ず取りこぼしが発生した。このため、2つの await transferIn() を同時にループさせて、いずれかの IO で常にデータを受信できるようにしてほぼ解決した。

これでもマシン負荷によっては時々とりこぼす (とりこぼしたところは白くなる)

WebWorker で WebUSB を使う

WebWorker 内で WebUSB は使える。使えるけど、事前に requestDevice() してペアリング (USBにはない概念) しておく必要がある。requestDevice() はメインスレッドでユーザインタラクションがないと呼べない。

  1. メインスレッドで requestDevice() して device を取得してペアリングする
  2. 取得した device 情報を worker に postMessage (device を直接 postMessage することはできない)
  3. ワーカースレッドで getDevices() してペアリング済み device 一覧を取得して、マッチするものを選ぶ

という手順が必要だった。

FFT する

FFT は RustFFT の実装を WebAssembly で呼んでいる。ComplexAnalyserNode (WebAudio) を作った (IQ信号のFFT) | tech - 氾濫原 とほぼ似たような実装。

ウォーターフォール表示

2D 描画でも WebGL を使うべきか? スペクトラムウォーターフォール最速決定戦 | tech - 氾濫原 というのを前に書いたが、現時点だと WebGL 実装が一番負荷が少なかったので、WebGL を使って実装した。

一応 WebGL 使わないバージョンと切り替えて使えるようにしている。

  1. トップ
  2. tech
  3. WebUSB HackRF One スペクトラムアナライザー