RIGOL (リゴル) 1.5GHzスペクトラム・アナライザ(トラッキング・ジェネレータ機能付) DSA815-TG -

4.0 / 5.0

やっぱスペアナは欲しいよなあと思いはじめて数ヶ月、決心がついたので買いました。技術力が低いので本当に金額に見合うぶんだけの活用ができるかは微妙ですが「見えないものが見えるようになる」というメリットに抗えませんでした。

スペアナといっても現状ではいくつも選択肢があります。USB 接続のものはハードのインターフェイスがいらない分をスペックにコストをかけられるので、スペック的には良さそうです。

DSA815-TG は位相雑音(ゆらぎ/時間ドメインでのジッタ)が多いというのが大きな欠点の一つのようです。高純度の信号の測定ができないことを意味しますが、この欠点がどれぐらい今後に影響してくるのかまだわかっていません。

しかしやはり欠点はあってもスタンドアロン型のもののほうが使い勝手は良さそうです。電源入れれば使えるというのは大事に感じます。USB 接続してアプリを立ちあげてという手順は地味にだるいのです。そういうところを考えると、スペックが良くても使う頻度が少なくなってしまうのは一番損になりますから、スタンドアロンのものにしました。

この機種は数年前までは15万前後で買えたみたいですが、円安進行のためか、かなり値上りしています。悔しいところですが諦めて18万ぐらいで買いました。当然トラッキングジェネレータ付きです。

所感

思ったよりも反応が良くて使いやすいです。

UIを日本語設定にすることができますが、この手の中華製品にありがちなひどい明朝フォントになったりはせず、普通に使えるレベルだと思います。とはいえ英語設定で使ってますが…

他に買ったものや欲しいもの

10dB アッテネータはマトモなやつを1つ買いました。追加でいくつか安いアッテネータを注文しています。

スペクトラム・アナライザによる高周波測定 (計測器BASIC) - 高橋 朋仁

高橋 朋仁

4.0 / 5.0

この本は買う前に読んでいました。アマゾンではなく、CQ出版社のサイトから PDF を買ってます

あとはリターンロスブリッジ (VSWR ブリッジ) が欲しいので作るかどうするか、という感じです。

方向性結合器は自分の欲しい周波数帯・耐電力のものは全く売っていないので作るしかありません。一度作っていますが、もっといいのを作りたいところです。

とはいえ50MHzまでならアンテナアナライザーもあるので、そこまで必要でもないかなという気がします。

LAN 経由でスクリーンショットをとる

まず測定画面のスクリーンショット(ハードコピー)ができないとかなり不便なので、ここから解決しました。

できたもの

プログレスを出しつつスクリーンショットをダウンロードしてこれます。ダウンロードしたあとは imagemagick の convertコマンドで png に変換し、optipng コマンドで最適化します。

ダウンロード部分にはライブラリ依存が一切ないので Ruby さえ動けば bmp ダウンロードはどこでも動きますし、LAN 経由なので一切ドライバなどが必要ないのが良いところです。

png 変換時には imagemagick コマンドと optipng コマンドが必要です。

測定器とのコミュニケーションプロトコル

DSA815 には USB 経由での GPIB 通信と、LAN ケーブル経由での VXI-11 (というよりは、LXIの一部)がサポートされています。

このへん、用語がいっぱいあるので整理しとくと

  • GPIB (IEEE488)
    • すごく古くからあるパソコンと計測器を繋ぐバス規格
    • 今は USB 経由に変換して繋ぐのが普通なようだ?
  • VXI-11
    • GPIB をイーサネット上で再現するような規格
  • LXI
    • LAN経由で計測器繋ぐというのをまとめた上位規格
    • Web インターフェイスとか、生TCPソケットとかの定義を含みつつ、VXI-11 も含む。
  • VISA
    • 計測器用プロトコルをひとまとめにした上位規格
    • VISA の裏側で GPIB とか VXI-11 とかが動いているイメージ

「というよりは、LXIの一部」と書きました。DSA815 はドキュメントには書いてないのですが、生のTCPソケット経由での通信もできるようになっていて、実はこれが一番簡単です。LXI の仕様では生TCPソケットの通信はポート5025を使うような記述がありますが、DSA815 では 5555 ポートです。

DSA815 でスクリーンショットコマンド

:PRIV:SNAP?

を送ると、11バイトのヘッダ(ファイル長を含む)とビットマップファイルが送られてきます。この :PRIV:SNAP? は隠しコマンドのようで、公式の Programming Reference にはありません。PRIV は Private の略なんでしょう。別のクライアントの通信からわかったので微妙なところです。

ref.

  1. トップ
  2. tech
  3. スペクトラムアナライザ DSA815-TG を買いました / LAN経由でスクリーンショットとれるようにしました

スペアナがあったらやってみたかったことの1つとしてコモンモードフィルタ(RFチョーク)の評価というのがあります。だいたいトロイダルコアを使って作っており、トロイダルコアは再現性が高いので計算通りにやれば実用性能はでるはずなのですが、ちゃんと作れているか?はやはりはかってみなければわかりません。

スペアナがなくてもハムバンドでダミーロードを使いつつ高周波電流を頑張って測るという方法で一応検証はできるのですが、面倒くさすぎるので結局グラフ化したりまでしたことはありません。面倒くささが勝りすぎます。

その点スペアナがあれば一瞬で…… という気持ちがありました (実際は適切な治具がないと正確に測るのはやはり難しいのですが……)

概要

大地経由での電流がコモンモード電流なので、フィルタのホットとコールドをショートして、TG のホットをフィルタを通してスペアナの入力にし、TG とスペアナのコールド側をショートさせれば計れそうです。要は単にRFチョークとして測定します。

なおコモンモードフィルタはだいたい-30dB ぐらいあれば十分な性能といえるらしいです。

測定方法

まずワニグチクリップをトラッキングジェネレータとスペアナ入力に直付けし、ホットコールドをそれぞれショートさせます。

この状態でTGの出力レベルを0dBmに設定し、ノーマライズします。(なおビビリなので外付けのアッテネータは入れてませんが内蔵アッテネータは10dB入れたままです) それなりに出力がないとノイズに測定結果が隠れてしまうので気をつけます。

ノーマライズされた状態でホット側をはずしてアイソレーションを確認します。-30dB 以上なければ測定の意味がないので、アイソレーションがとれてなさそうなら諦めてスパンを狭くして再度ノーマライズからやりなおします (意味がない結果が見えていると混乱するため)。

TG 側にワニグチクリップをつけたりして配線長を増やすと50MHz〜は容易にアイソレーションがとれなくなるので注意します。以下はオープンの状態のアイソレーションです。

この状態でフィルタに接続して測ります。フィルタの入力側(TG側)の線長が長いとこれまたアイソレーションがとれないので誤差の原因になりますが、排除するのが難しいです。シールドケースに入れて測るしかない気がしますが、今回は用意できてないので多少飛びこみがあります。

結果

今回計ったのはキットものの http://www.ddd-daishin.sakura.ne.jp/ddd/dcf/dcf-rf-29l3/kit-dcf-rf-29l3-ver2.htm これです。スペック的には 50MHz ぐらいまでは -30dB 余裕でとれるみたいなグラフがあります。

一応このように雑に測った感じでも 50MHz ぐらいまでは -30dB ぐらいになっているように見えます。(0〜100MHzのスパンなので真ん中が50MHzです)。とはいえ -40dB までとれている範囲はそれほどないです。

挿入損失

ついでに挿入損失も測りました。


メモ:挿入インピーダンスとdB

入出力インピーダンス50Ωとして、例えばインピーダンスZを直列挿入した場合、入力側にかかる電圧は出力電圧を、挿入インピーダンス と入力インピーダンスで分圧した形になる。

なので、入力電圧は になる。dB で表わすと

  1. トップ
  2. tech
  3. アマチュア無線用コモンモードフィルタの特性評価

神道の文献は、たとえ最古の古事記(712年)であっても、仏教伝来(遅くとも6世紀半ば)後に書かれたものであるので、そもそも「仏教に影響されていない神道」というものを記録している文献はない。すなわち仏教抜きで神道を考えるというというのはそもそも現代では無理。

「復古神道」は名前に反して江戸時代の新興宗教であって、儒教・仏教色を排すという意味ではかえってそれらの影響をうけている。

C++ はながいこと食わず嫌いだった。とにかく「難しい」というイメージだけ先行していた。

しかし、あくまで better C として使う限りでは難しくないし、可読性が上がるので、 C++ を使わない手はないという気持ちになった。

これまでの漠然とした C++ への不安

書いたコードと出てくるバイナリ

C を書いていると、だいたいは書いたコードがそのままバイナリに翻訳されている感があり、安心感がある。struct はメモリ上の配置そのままだし、関数だって単に処理のまとまりに名前がついてて、呼び出しもとに自動的に戻ってくるラベルなだけだ。

C++ になるとオブジェクト指向に概念が入ってきて、これが実際どのようにコンパイルされるかに不安が出てくる。クラスはメモリ上でどう表現されているのか、メソッドディスパッチはどのように行われているか?

もちろん知っている人にとっては簡単な話で

  • class は単に struct である
    • フィールド定義はそのままメモリ上の表現となる
    • メソッド定義は関数になる
    • 実際、C++ で class と書かれた定義を struct に置き換えても問題なくコンパイルされる (フィールドのスコープがデフォルトで class は private で struct が public である違いだけ)
  • メソッドディスパッチは基本的に静的である
    • レシーバによって動的にディスパッチされる関数は virtual 関数と呼ばれる
    • コンパイラがテーブルを作って動的に呼びだし関数を変えている

ということで、virtual を使わない限りでは、C++ の class は C で struct 定義と、その第一引数にその sturct ポインタをとる関数郡でしかなく、これは C でよくやるオブジェクト指向のプリミティブな実装とよく似ている。

このようなCのコードは

#include <stdio.h>

typedef struct {
	unsigned counter;
} my_counter;

void my_counter_init(my_counter* this) {
	this->counter = 0;
}

void my_counter_incr(my_counter* this) {
	this->counter++;
}

unsigned my_counter_get_count(my_counter* this) {
	return this->counter;
}

int main() {
	my_counter counter;
	my_counter_init(&counter);

	printf("%d\n", my_counter_get_count(&counter));
	my_counter_incr(&counter);

	printf("%d\n", my_counter_get_count(&counter));
}

このようなC++のコードとほとんど同じバイナリが出力される

#include <cstdio>

class my_counter {
	unsigned counter;
public:
	my_counter();
	void init();
	void incr();
	unsigned get_count();
};

my_counter::my_counter() {
	this->counter = 0;
}

void my_counter::incr() {
	this->counter++;
}

unsigned my_counter::get_count() {
	return this->counter;
}

int main() {
	my_counter counter;

	printf("%d\n", counter.get_count());
	counter.incr();

	printf("%d\n", counter.get_count());
}

テンプレート多用するとバイナリサイズ増えるんじゃないの?

テンプレートは、複数の型に対する処理を1回でまとめて書けるという仕組みなので、原理的には、扱う型が増えるほど、出力バイナリに関数本体が増えていくことになる。

しかし一方で、テンプレートをコンパイル時計算のために使うような場合は、コンパイル時に解決されるコードがほとんどになり、出力バイナリは書いたコードの見た目の多さに反してかなり少なくなることもある

ということで、テンプレートが使われているからといってバイナリサイズが肥大化するというわけではない。

最適化はどこまで信用できるか?

この書きかたで本当に最適化されたコードに出てくるの? という不安がある。これは難しくて、コンパイラが優秀でも、当然プログラマがちゃんと const を付けてコンパイラに意図を伝えないと、完全に最適化されたコードにはならない (インライン化されないとか)。

C にはない概念がある分、const の付けかたが複雑で、ベターCとして使おうと思うと一番ハマる。

一方で、テンプレートメタプログラミングでコンパイル時に計算することができるので、コンパイラが判断できないような高度な最適化を自力でやることができる。

なぜ組込みでこそなのか?

組込みのコードはどうしてもマジックナンバーが多くなりやすく、C で書くとマクロだらけになる。Cのマクロは文字列展開なので型がなく、当然マジックナンバーにも型を付与できない。組込みは実行時デバッグのコストが高いので、コンパイル時に見つけることができるエラーは全てコンパイル時に見付けたいが、Cではそこまでのことができない。

浮動小数点まわりについてはMCUでは非常に重い処理になるので、できるだけ事前計算した係数をつかってMCU上では整数演算にしてしまうなどの最適化をしたくなる。こういうときCのマクロだけで書くのはとても厳しい。C++ であればコンパイル時に浮動小数点計算を行って定数展開できるので、あきらかに有利になる。

浮動小数点に限らず、高いレイヤーでの最適化はコンパイラは知るよしもないので手でやる必要がある。その際C++のTMPは非常に強力に使える。

  1. トップ
  2. tech
  3. 組込みでこそ C++

測定中、10MHz 付近に急激な落ちこみが観測されることがあって気になっていました。そういうものなのかなとも思ったのですが、調べてみたら同じような問題にあたっている人がいて、まぁよく考えたらそういうものではないよなという感じです。

これは簡単に再現することができます。TGとSAを同軸で直結させて、TGを有効にし、10MHz 付近を拡大することで、変な落ちこみを見ることができます。

以下の画像がそうですが、明かに1dB 落ちこみがあるのがわかると思います。

あるいは、掲示板中にもありますが 10μH と 33pF ぐらいを直列に接続し、バンドパスフィルタを構成した上で 10MHz 付近を見ると、スカートが不自然になっているので非常に目立ちます。 冒頭の画像はこの方法での再現です。直結よりもわかりやすくおかしいことがわかります。

広いスパンで見ているときや、広いダイナミックレンジで見ているときには気付きにくいかもしれませんが、フィルタのようなものを測定するときはかなり気になる挙動だと思います。

10MHz 付近はアマチュア無線でも使う帯域ですし、基準信号付近ですから余計気になります。位相雑音がどうとかよりも致命的な問題でしょう。

現時点での解決方法

ファームウェアの 1.15 が1月22日にリリースされており、これにある程度の修正が含まれています。

最新のファームウェアは http://int.rigol.com/Support/SoftDownload/3 からダウンロードできます。

Product Series → Spectrum Analyzer → DSA815 Firmware_00.01.15.01.00 というのをダウンロードします。DSA815(DSP)update.rar というやつです。

DSA815update.sys を USB メモリのルートディレクトリにコピーして、DSA815 のフロントUSBポートに接続し、Storage の画面でこのファイルを選択したあと、Sys Update を選択すると、アップデートが開始されます。数分で終わり、再起動すると最新のファームウェアで起動します。

バッファロー USB3.0メモリ バリューモデル (8GB・ブラック)RUF3-WB8G-BK -

3.0 / 5.0

↑ 使ったUSBメモリです。自宅にUSBメモリがなく、SDカードリーダーとSDカードで代用しようと思いましたが、DSA815 では認識されませんでした。純粋な USB メモリじゃないとダメなようです。ググってみると、メーカー的にはできるだけ容量が少ないほうが信頼性が高いということになっているみたいです。とはいえ現時点で新品はもはら8GB未満は売ってません。これは USB 3.0 メモリですが、DSA815 は USB 3.0 に対応してないので、古くてもなんでもいいはずです (信頼性は別として)。

解決したか?


見ての通りです。直結の場合では不連続部分はわかりません。一方バンドパスフィルタで見たときはすこし不連続な部分が残っているようにみえます。

ということで、完全には解決しません。挙動的にはまるめ誤差みたいなのが発生しているような感じますが実装がどうなっているかはわかりません。

とはいえ、このように修正が入るので、現状の状態は Rigol のエンジニアは把握しており、直すつもりはあるようです。ファームウェアアップデートだけですめばいいんですが……

ファームウェアアップデートと保証

DSA815-TG には3年保証がついているのですが、ファームウェアアップデート時にどのような扱いになるか不安になりました。(そもそも国内販売版のものが国際版と完全に同一であるかの確証もない)

国内のリゴルジャパンへメールで確認をとったところ、すぐに返信していただけました。

  • 国内販売のものでもファームウェアは共通
  • ファームウェアアップグレードしても保証期間内では無償サポート

とのことで、アップグレードしても大丈夫そうです。

1.15 での他に気になった挙動

購入直後スクリーンショットをLAN経由でとれるようにしましたが 1.15 になって、TCPソケット経由でのコマンド解釈がすこし厳密になって動かなくなったので直しました。

あと、スクリーンショットをとるコマンドは最後にリモートモードを解除するコードを入れていたのですが、動かなくなりました。リモートからコマンドを実行すると必ずリモートモードになってしまって若干鬱陶しいのでなんとかしたいのですが、こちらの原因はよくわかってません。

どうやらスリープを挟まないとダメな模様

また、デフォルトで mDNS での DNS-SD の機能が無効になってみたいです。HTTP 経由でアクセスして設定しないと DNS-SD のクエリに応答しません。

備考

元のバージョンは00.01.12 でした。

アップグレード後は 00.01.15 です。


  1. トップ
  2. tech
  3. DSA815-TG の 10MHz 付近の奇妙なバグとファームウェアアップグレード

ebay で見つけて買ったやつシリーズです。表題のようなものを買ったので動かしました。

https://github.com/adafruit/Adafruit_LED_Backpack が割とよくできていて、これ使えばすぐに動かせました。

#include <Arduino.h>
#include <Wire.h>
// https://github.com/adafruit/Adafruit_LED_Backpack
#include "Adafruit_LEDBackpack.h"

// Adafruit_LEDBackpack matrix = Adafruit_LEDBackpack();
Adafruit_8x8matrix matrix = Adafruit_8x8matrix();


void setup() {
	Serial.begin(9600);
	Serial.println("begin");
	matrix.begin(0x70);
	matrix.setBrightness(10);
	matrix.setTextSize(1);
	matrix.setTextWrap(false);
	matrix.setRotation(1);
}


void loop() {
	char* message = const_cast<char*>("Hello, World!\n");

	int16_t x, y;
	uint16_t w, h;
	// getTextBounds は \n で終わってないと width を正しく計算しない。
	matrix.getTextBounds(message, 0, 0, &x, &y, &w, &h);
	Serial.println("getTextBounds: ");
	Serial.print("  x = "); Serial.println(x);
	Serial.print("  y = "); Serial.println(y);
	Serial.print("  w = "); Serial.println(w);
	Serial.print("  h = "); Serial.println(h);
	Serial.println("");

	for (int16_t x = 8; x >= -(int16_t)w; x--) {
		matrix.clear();
		matrix.setCursor(x, 0);
		matrix.print(message);
		matrix.writeDisplay();
		delay(50);
	}
}
  1. トップ
  2. tech
  3. I2C 8*8 LED dot Matrix module HT16K33

BMP180 搭載のモジュールを ebay で買ってみたので試しました。約$2。どの気圧計にしろ温度計が必要で内部補正には使われていたりしますがだいたい内部用で外から値がとれません。このモジュールは温度もI2C経由で測れて一石二鳥モジュールです。

こんな感じのモジュールで、BMP180 以外に実装があります。これは 3.3V レギュレータで、5V 供給しても大丈夫なようになっています。(I2C のロジックレベル変換は簡易的ですが)。なので 5V の Arduino でも使えます。

BMP085 というものと互換性があるみたいで (BMP085はディスコン) それ用のライブラリがそのまま使えます。

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BMP085.h>

// https://github.com/adafruit/Adafruit-BMP085-Library
Adafruit_BMP085 bmp;

void setup() {
	Serial.begin(9600);
	int ok = bmp.begin();
	if (!ok) {
		Serial.println("bmp.begin() failed.");
		for (;;);
	}
}

void loop() {
	Serial.print("Temperature = ");
	Serial.print(bmp.readTemperature());
	Serial.println(" *C");

	Serial.print("Pressure = ");
	Serial.print(bmp.readPressure() / 100.0);
	Serial.println(" hPa");

	Serial.println();
	delay(1000);
}
Temperature = 19.40 *C
Pressure = 1011.48 Pa

Temperature = 19.40 *C
Pressure = 1011.47 Pa
  1. トップ
  2. tech
  3. BMP180 I2C 気圧・温度計センサー

ebay で買ったシリーズです。$3 ぐらいで買いました。以下らへんのモジュールとよく似たものです。

MQ-135 のほかすこし実装 (LM393など) があります。LM393 はコンパレータで、基板上の半固定で設定した閾値でデジタル出力できるようになっています。アナログ出力は MQ-135 直結です。

MQ-135

空気の汚染度に感度があるセンサーで、ベンジン・アルコール・煙などに反応しやすいと書いてあります。CO2 にもある程度反応するみたいで、ググると poor man's CO2 センサーとしても一定の人気があるみたいです。

自分としては、換気扇ごしに外からタバコの煙が自宅に入ってくることがあってほとほと不愉快なので、どんな時間帯にそういう状況になるかをつきとめたいというモチベーションがありますが、そういう用途に使えるかはわかりません。というかテストのためにタバコを買うのも絶対に嫌なのでテスト不可能です。

仕様

MQ-135 自体がデータシートでプリヒートを24時間以上求めています。常時起動してないとダメですね。とはいえ1時間ぐらいで値はほぼ安定しているようには見えます。

MQ-135のデータシート的には10kΩ〜47kΩの負荷抵抗を想定しているみたいですが、このモジュールは 102 (1kΩ) の抵抗が負荷抵抗になっているように見えますので、普通に使うより感度が低くなりそうです。

ググると頑張って ppm 単位の絶対値出力を出そうという試みがあるのですが、素人がキャリブレーションしようがない (他の正確な絶対値センサーが必要) ので、基本的には相対的な値を範囲を一定期間モニタリングしたうえで値を把握して使うほかない気がします。

実測出力

  • 200mV / 数時間通電後の無人の部屋
  • 700mV / アルコールティッシュを近付けたとき
  • 800mV / サカムケアを近付けたとき (イソプロパノールなど)
  • 1660mV / プラモデル用接着剤を近付けたとき (アセトンなど)

意外と反応が良くて、臭いがあるとすぐ変化が出力されます。

  1. トップ
  2. tech
  3. MQ-135 Sensor Air Quality Sensor Hazardous Gas Detection LM393 MQ135 gas sensor

ebay で800円ぐらいで買ったものです。

この手のモジュールにはAD9851(源クロック6倍周波数逓倍器付き)のものとAD9850のものがあり、さらに基板のタイプが2つあります。AD9850/AD9851は制御方法も含めてほとんど互換なので、源クロックとチップだけ違うものが出回っているみたいです。

試してみたのはおそらく古いタイプのもので、ブレッドボードには直接挿すことができないので、ちょっと面倒くさいタイプです。

周波数の設定・パワーダウンモードへの移行・設定位相(2台以上のDDSでsin/cos作る場合に使う)を40bitレジスタにまとめて突っ込む形になっています。

この40bitレジスタはパラレルモードとシリアルモードで設定でき、ググると大抵の例でシリアルモードを使ってるみたいです。

ただ、リセット直後はパラレルモードで起動するので、シリアルモードに変えてやる必要があります。といっても、D0=1 D1=1 D2=0 にした状態で、W_CLK パルスを送り、すぐ FQ_UD パルスを送るとシリアルモードに入るということになっています。

このモジュールの場合、ジャンパでD0=1 D1=1 D2=0 を設定できるようになっており、デフォルトでジャンパしてあるので、W_CLK と FQ_UD パルスをマイコンから送るだけで初期化できます。

コード

ライブラリみたいにしてあるのもありましたが、結局自分で書きました。

入手したモジュールは125MHzが源クロックでした。

#include <Arduino.h>
#include <Wire.h>

template <uint32_t CLKIN>
class AD9850 {
	static constexpr double PHASE_FACTOR = 0x100000000 / (double)CLKIN;

	const uint16_t PIN_DATA;
	const uint16_t PIN_FQ_UD;
	const uint16_t PIN_W_CLK;
	const uint16_t PIN_RESET;

	void serial_write(uint32_t freq, uint8_t phase, bool powerdown) {
		// freq (delta phase)
		for (int i = 0; i < 32; i++) {
			digitalWrite(PIN_DATA, (freq>>i) & 1);
			digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
			digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);
		}

		// control bits
		digitalWrite(PIN_DATA, LOW);
		digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);

		// powerdown
		digitalWrite(PIN_DATA, powerdown ? HIGH : LOW);
		digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);

		// phase
		for (int i = 0; i < 5; i++) {
			digitalWrite(PIN_DATA, (phase>>i) & 1);
			digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
			digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);
		}

		digitalWrite(PIN_FQ_UD, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_FQ_UD, LOW); delayMicroseconds(4);
	}

public:
	AD9850(
			uint16_t data,
			uint16_t fq_ud,
			uint16_t w_clk,
			uint16_t reset
		) :
			PIN_DATA(data),
			PIN_FQ_UD(fq_ud),
			PIN_W_CLK(w_clk),
			PIN_RESET(reset)
	{
		pinMode(PIN_DATA, OUTPUT);
		pinMode(PIN_FQ_UD, OUTPUT);
		pinMode(PIN_W_CLK, OUTPUT);
		pinMode(PIN_RESET, OUTPUT);
	}
	
	/**
	 * W0 ... W31  -> Freq (LSB first)
	 * W32, W33    -> Control (for factory test)
	 * W34         -> Power-Down
	 * W35 ... W39 -> Phase (LSB first)
	 */

	void reset() {
		// ensure low
		digitalWrite(PIN_DATA, LOW);
		digitalWrite(PIN_FQ_UD, LOW);
		digitalWrite(PIN_W_CLK, LOW);

		// reset
		digitalWrite(PIN_RESET, HIGH); delay(1);
		digitalWrite(PIN_RESET, LOW); delay(1);

		// reset to serial mode
		// Pins of D0, D1 = HIGH, D2 = LOW for serial mode
		digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);

		digitalWrite(PIN_FQ_UD, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_FQ_UD, LOW); delayMicroseconds(4);
	}

	void set_frequency(uint32_t frequency) {
		set_frequency(frequency, 0);
	}

	void set_frequency(uint32_t frequency, uint8_t phase) {
		uint32_t deltaPhase = PHASE_FACTOR * frequency;
		serial_write(deltaPhase, phase, 0);
	}

	void powerdown() {
		serial_write(0, 0, 1);
	}
};

AD9850<125000000> ad9850(9, 10, 11, 12);

void setup() {
	Serial.begin(9600);

	ad9850.reset();
	ad9850.set_frequency(10e6);
}

void loop() {
}

出力

商品説明だと40MHz まで出せると書いてありました。たしかに出せることは出せるようです。

このモジュールの出力には7次のローパスフィルタがついています。これはAD9851のデータシート通りの定数のフィルタのようで、70MHzぐらいにカットオフ周波数ががあるみたいです。

なおローパスフィルタの後の出力インピーダンスは200Ωになっているみたいです。

AD9850自体の出力は電流出力で、12ピンについている抵抗でフルスケールの出力電流が決まることになっています。このモジュールでは3.9kΩが実装されており、10mA フルスケールの出力に設定されています。200Ωで10mW(10dBm)。実際使う場合バッファして50Ω出力とする必要はありそうです。

片方だけにローパスフィルタが入っており、もう片方の出力は200Ωで電圧変換されて直接ピンヘッダに出ています。ということで差動出力には使えません。





  1. トップ
  2. tech
  3. AD9850 DDS モジュール

AD9850 DDS モジュール に続き AD9851 で、別基板バージョンのものです。

ジャンパとかが一切ない簡略版?なのか進化版?でしょうか。

D0, D1 は 10kΩでプルアップされているので、これらは自分でプルアップする必要はありません。D2 を GND に接続するだけでシリアルモードに入れる実装になっています。

コード

#include <Arduino.h>
#include <Wire.h>

template <uint32_t CLKIN, bool MULTIPLIER>
class AD9851 {
	static constexpr double PHASE_FACTOR = 0x100000000 / (double)(CLKIN * (MULTIPLIER ? 6 : 1));

	const uint16_t PIN_DATA;
	const uint16_t PIN_FQ_UD;
	const uint16_t PIN_W_CLK;
	const uint16_t PIN_RESET;

	void serial_write(uint32_t freq, uint8_t phase, bool powerdown) {
		// freq (delta phase)
		for (int i = 0; i < 32; i++) {
			digitalWrite(PIN_DATA, (freq>>i) & 1);
			digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
			digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);
		}

		// control bits
		digitalWrite(PIN_DATA, MULTIPLIER ? HIGH : LOW);
		digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);
		digitalWrite(PIN_DATA, LOW);
		digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);

		// powerdown
		digitalWrite(PIN_DATA, powerdown ? HIGH : LOW);
		digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);

		// phase
		for (int i = 0; i < 5; i++) {
			digitalWrite(PIN_DATA, (phase>>i) & 1);
			digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
			digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);
		}

		digitalWrite(PIN_FQ_UD, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_FQ_UD, LOW); delayMicroseconds(4);
	}

public:
	AD9851(
			uint16_t data,
			uint16_t fq_ud,
			uint16_t w_clk,
			uint16_t reset
		) :
			PIN_DATA(data),
			PIN_FQ_UD(fq_ud),
			PIN_W_CLK(w_clk),
			PIN_RESET(reset)
	{
		pinMode(PIN_DATA, OUTPUT);
		pinMode(PIN_FQ_UD, OUTPUT);
		pinMode(PIN_W_CLK, OUTPUT);
		pinMode(PIN_RESET, OUTPUT);
	}
	
	/**
	 * W0 ... W31  -> Freq (LSB first)
	 * W32, W33    -> Control (for factory test)
	 * W34         -> Power-Down
	 * W35 ... W39 -> Phase (LSB first)
	 */

	void reset() {
		// ensure low
		digitalWrite(PIN_DATA, LOW);
		digitalWrite(PIN_FQ_UD, LOW);
		digitalWrite(PIN_W_CLK, LOW);

		// reset
		digitalWrite(PIN_RESET, HIGH); delay(1);
		digitalWrite(PIN_RESET, LOW); delay(1);

		// reset to serial mode
		// Pins of D0, D1 = HIGH, D2 = LOW for serial mode
		digitalWrite(PIN_W_CLK, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_W_CLK, LOW); delayMicroseconds(4);

		digitalWrite(PIN_FQ_UD, HIGH); delayMicroseconds(4);
		digitalWrite(PIN_FQ_UD, LOW); delayMicroseconds(4);
	}

	void set_frequency(uint32_t frequency) {
		set_frequency(frequency, 0);
	}

	void set_frequency(uint32_t frequency, uint8_t phase) {
		uint32_t deltaPhase = PHASE_FACTOR * frequency;
		serial_write(deltaPhase, phase, 0);
	}

	void powerdown() {
		serial_write(0, 0, 1);
	}
};

AD9851<30000000, true> ad9851(9, 10, 11, 12);

void setup() {
	Serial.begin(9600);

	ad9851.reset();
	ad9851.set_frequency(10e6);
}

void loop() {
}

AD9851 とほぼ同じですが、6倍の周波数逓倍器がの有効無効化のビットがあるので、その部分だけ実装を変えてあります。

出力

購入したモジュールは 30MHz の源発振のものなので、逓倍後は 180MHz になります。

40MHz ぐらいから出力が低くなります。

ZOUT2 (フィルタあり)



逓倍している分スプリアスが多いようです。


ZOUT1 (フィルタなし)


  1. トップ
  2. tech
  3. AD9851 DDS モジュール

なんとなく思いたったので Twitter 使うのをやめてみることにします。ここ最近のこと考えてみると、特に Twitter を使っていて得るものはないのではないか、という気がしてきました。

何か思うことがあれば日記として書こうと思います。

はてブもやめて、日記にリンクと思うところを書いたほうがいいかもしれません。そのリンクは日記に書くほどのことなのかを一旦考えてみてもいい気がします。


この日記は tech タグか photo タグ、またはアマゾンの商品リンクが含まれている場合しか Atom フィードにコンテンツを出さないようになっています。なので、クソのような日記エントリをいくら書いても誰にも迷惑になるようなことはありません。

日記はそもそも情報としての価値は低いですから、フィードリーダーで読むようなものではないという気がします。「ウェブサイトを開いて読む」というコンテキストが共有されていないと、変なことを言いだす人もいるので (メディアによって読み手が受けとるイメージが変わってしまい、フィードリーダーだと押し付けがましくなる)、このようになっています。


こういう変な仕様のブログシステムというのはASPではまずありえませんから、やはり自分でブログシステムを作っておくというは、良いと思います。

IrKit を cron から読んで朝起きたとき部屋が暖かくなるようにしていましたが、暖くなってきたので、一旦止めました。

cron の有効無効を手動でやっててアホらしいので温度センサーと連携したいところです。

定本 トランジスタ回路の設計―増幅回路技術を実験を通してやさしく解析 - 鈴木 雅臣

鈴木 雅臣

4.0 / 5.0

定本 続トランジスタ回路の設計―FET パワーMOS スイッチング回路を実験で解析 - 鈴木 雅臣

鈴木 雅臣

4.0 / 5.0

一回図書館で借りて一通り読んだのですが、トランジスタ回路の定番パターンと設計方法が一通りまとまっているのがほかになく、参照したいと思うことが多々あるので、買うことにしました。

これこそ電子書籍でPDFになっていてほしいのですが、Tech Villageにはないようです。改訂してPDFにして欲しいです。

SDカードなどを接続したとき、使ってもいない写真.appがいちいち起動してだいぶ鬱陶しいです。以下のコマンドで止めることができました。

defaults -currentHost write com.apple.ImageCapture disableHotPlug -bool YES

昔は Image Capture.app の環境設定からデフォルトの設定が変えられたはずなんですが、El Capitan ではその機能が消滅していて、GUI で変更する方法がないようです。クソですね。

  1. トップ
  2. tech
  3. El Capitan になってから、Mac の写真.appの自動起動がガチでうざい

普段からほとんど常時に「バカにされている」と感じている。基本的には特定の誰かというわけではない。ただしコンテキストによっては特定の誰かの顏が頭に浮かんでいることもある。とはいえそれも、特定の誰かが事実として常に罵倒してくるわけではない。

普通に生きていると、自分の意見というのは、ほとんど尊重されない。自分の中でこの感覚が消化された結果「バカにされている」という主観ができあがるのだろうと思う。

「尊重されなさ」はあらゆることで感じられる。

煙草、特に路上喫煙に殺意(文字通り。)が沸くのはなぜかといえば、臭い煙に困っている自分が尊重されていないと感じるからだと思う。ここで大事なのは、実際にそれをしている喫煙者が尊重してくれないということではなく、そういう喫煙者の存在を許していて、なおかつ自分の存在を許さない(と主観的に感じる)社会が尊重してくれない、ということである。

社会に自分が尊重されていないというのは、自分の存在が否定されていることと同じことです。

自分の中で全ての「尊重されなさ」は社会と通じて繋がっている。仕事で尊重されなくても、道を歩いていて尊重されなくても、それらは繋がっており、自分の中で消化され、バカにされたと感じるわけです。

1000円ぐらいで買いました。このモジュールはアンテナが分離型で、1PPS 出力がとれるという特徴があります。

ublox というメーカーのGPSチップが載っています。u-center というソフトウェアから詳細な設定ができることになっています。ただし u-center は Windows のみです。

GPS Fix に時間がかかる

窓際で受信させてみましたが、いつまで経っても GPS Fix しません。u-center でしばらく眺めていると1時間〜2時間ぐらいでようやく GPS Fix しました。

スマフォ内蔵のGPSだと窓際でもすぐGPS Fixできるので、ちょっと残念なところです。

1度3D Fixまでいけば、割と安定して受信が継続されました。

NEO-6M は GPS(アメリカ)のみに対応していて、GLONASS (ロシア)やQZSS(日本)は対応していないので、そのせいかもしれません。

1PPS

自宅でGPS使って位置を知れてもあんまり意味がありません。このGPS モジュールの最大の目的は1PPSです。

1PPS (Pulse Per Second) 出力はLED が接続されており、GPS Fix 時には1秒ごとにLEDがフラッシュします。

この1PPSはGPS衛星に搭載されている原子時計と同期しているので、とても正確です。ただし、GPSモジュール内蔵クロックが48MHzなので最大42nsぐらいのジッタはありそうです。そこそこに長い目で見ると原子時計並に正確な1Hzが得られるというものです。

スペック的には以下のようになっています(いまいち各項目の意味がわかりませんでしたが…)

モジュールのピンヘッダに出ていないので、必要なら直接引き出す必要があります。

• For best timepulse performance it is recommended to disable the SBAS subsystem. 

日本にはSBASシステムがそもそもないので完全に無効にしときましょう。

NMEA を Ruby で雑に読んでみる


特にライブラリとかを使わずに、ちゃんと動いているかだけのテストをできるようにスクリプトを書きました。動かすと以下のような出力になります。

2016-02-16 15:28:5.000Z VALID:YES / 3D Fix [1: 31dB*] [3: 21dB*] [7: 26dB*] [8: 0dB] [10: 0dB] [11: 15dB*] [16: 15dB*] [17: 0dB] [22: 0dB] [27: 0dB] [28: 19dB*] [30: 0dB*]
#!ruby -v
require 'serialport'

@port = SerialPort.new(
	"/dev/tty.usbserial-A50285BI",
	9600,
	8,
	1,
	0
)

cols = `tput cols`.to_i

status = {
	using: [],
	sates: {}
}

loop do
	while line = @port.gets
		type, *rest = line.chomp.split(/,/)
		next unless type[0] == '$'
		_ = rest.pop
		case type
		when "$GPRMC"
			h, m, s = *rest[0].match(/(..)(..)(.+)/).captures.map {|i| i.to_f }
			dd, mm, yy = *rest[8].match(/(..)(..)(..)/).captures.map {|i| i.to_i }
			datetime = "%04d-%02d-%02d %02d:%02d:%02.3fZ" % [yy + 2000, mm, dd, h, m, s]
			state = rest[1]
			status[:UTC] = datetime
			status[:is_valid] = state == "A"
		when "$GPGGA"
			sate_count = rest[6]
			status[:sate_count] = sate_count
		when "$GPGSA"
			mode = rest[0]
			type = rest[1] # 1 = invalid, 2 = 2d, 3 = 3d
			sate_nums = rest[2, 12]
			status[:mode] = mode
			status[:type] = type
			status[:using] = sate_nums.map {|i| i.to_i }
		when "$GPGSV"
			total = rest.shift
			current = rest.shift
			if current == 1
				status[:sates] = {}
			end
			_ = rest.shift # count
			until rest.empty?
				num = rest.shift
				e = rest.shift
				d = rest.shift
				sn = rest.shift
				status[:sates][num] = {
					num: num.to_i,
					e: e,
					d: d,
					sn: sn.to_i,
				}
			end
			if total == current
				# done
			end
		else
			# ignore
		end
		out = "%s VALID:%s / %s Fix %s" % [
			status[:UTC],
			status[:is_valid] ? "YES" : "NO",
			status[:type] == "1" ? "NO" : "#{status[:type]}D",
			status[:sates].values.sort_by {|i| i[:num] }.map {|i|
				"[%s: %ddB%s]" % [
					i[:num],
					i[:sn],
					status[:using].find_index(i[:num]) ? "*" : ""
				]
			}.join(" ")
		]
		print "%- #{cols-1}s\r" % out
		$stdout.flush
	end
end
  1. トップ
  2. tech
  3. ublox NEO-6M GPS Module

スマフォで見たとき、ファーストビューがあまりにダサすぎたのでヘッダ部分をちょっとマシにしました。

日記のタイトル部分は SVG にしてみました。そろそろ常用してもいいかなという気持ちです。


PC 側もちょっと変えていて、本文の幅を広告と一致するようにしました。なんで今までズレたまま放っていたのが謎ですがようやくすっきりした気がします……

DIP で唯一実用になる(?) ARM Cortex-M0 マイコンです。SRAM が 4KB しかないのが心許ないですが、秋月で現在180円と、性能の割に激安です。

32bit ARM なので、32bit 演算が多少出てくるような場合は AVR より圧倒的に良い選択そうです。(FPU はありませんが)。また mbed は Arduino よりもライブラリデザインがマトモという印象があります。

platformio でコンパイルして、シリアルアダプタ経由で ISP 書きこみするというのを試しました。フレームワーク(ライブラリ)としては mbed を使っていますが、mbed の開発環境は使っていません。

platformio での開発

platformio.ini を以下にようにして

[env:lpc]
platform = nxplpc
framework = mbed
board = lpc1114fn28
build_flags = -std=c++1y

main.cpp を以下にようにしました。

#include "mbed.h"

DigitalOut led(LED1);

int main() {
	while(1) {
		led = 1;
		wait(0.5);
		led = 0;
		wait(0.5);
	}
	return 0;
}

この状態で platformio run をすると .pioenvs/lpc/firmware.bin にコンパイル済みバイナリができます。

platformio は mbed の開発環境を前提としているようで、upload では単に upload_port 先のディレクトリにコピーをしようとします。今回はシリアル経由で書きこむので、platformio 経由での upload は使いませんでした。

ピンアサイン

https://developer.mbed.org/platforms/LPC1114FN28/ このページを見るのが一番良いようです。

上記コード中で指定している LED1 は左下の dp14 になります。

書きこみ

書きこみツールは lpc21isp を使うのが一番簡単なようです。展開して make するだけで OS X でも普通に動きました。

https://sourceforge.net/projects/lpc21isp/files/lpc21isp/ からダウンロードできます。

以下にような感じで書きこめました。115200 は書きこみ時のボーレート、12000 はMCUの動作周波数をkHz単位で指定するようです。内蔵RCは12MHzなので12Mhzを指定しています。が、mbed 実行時は逓倍して48MHzで動いているようなので、どっちを指定するのかよくわかりません。

lpc21isp -control -bin .pioenvs/lpc/firmware.bin /dev/tty.usbserial-A50285BI 115200 12000

ここでは -control を指定しています

         -control     for controlling RS232 lines for easier booting
                      (Reset = DTR, EnableBootLoader = RTS)

となっており、この通りにシリアル変換の DTR を nR (RESET) 、RTS を dp24 に接続することで、自動リセットしてISPモードへ移行して書きこめます。冒頭の写真の通りの配線です。

追記:シリアル通信を行う

上記の方法だと、普通にシリアル通信を行おうとするとRTS/DTRまわりの挙動によってリセットが発生してISPモードに入ってしまうことがあります。なので普通に screen などでシリアル出力を見ることができません。これは RESET の配線を切れば問題ないのですが面倒くさいところです。

実はこれもlpc21ispを使って以下にようにすると、配線を変えずにシリアルモニタができるようです。

lpc21isp -termonly -control /dev/tty.usbserial-A50285BI 115200 12000

コード例

#include "mbed.h"

Serial serial(USBTX, USBRX);
DigitalOut led(LED1);

int main() {
	serial.baud(115200);

	while(1) {
		serial.printf("Hello, World!\n");
		led = 1;
		wait(0.5);
		led = 0;
		wait(0.5);
	}
	return 0;
}

ref.

  1. トップ
  2. tech
  3. LPC1114 LPC1114FN28 / mbed 開発を platformio を使ってやる

トランジスタ技術 2016年 2月号 -

3.0 / 5.0

ここ1ヶ月ぐらい GPSDO に興味があって調べていたのですが、先月のトランジスタ技術がまさにGPSの特集だったようでした。

なんか知らずに流行りに乗ったみたいでかっこわるい…… というのはおいといて、おもしろそうなので買って読んでみました。

GPSDO については1記事だけでした。しかしMCUなしでの製作という、男らしい感じなのですが、残念ながらあんまり参考になりませんでした…… 使っているモジュールはちょっと前に買ってみたNEO-6Mで親近感がありました (というか1PPS出せて安いのをebayで探すとこれが最安なので第一候補になるわけですが)

ublox のモジュールは TIMEPULSE ピンに 1PPS に限らず安いものでも10kHzぐらいまで可変で出せるのですが、製作記事でもこの機能を使っていました (MCUを使わないことによる副作用として)。個人的にはこの機能を使うのはいまいちというか、他のGPSモジュールとの互換性がなくなるのがなあと思ってしまいます。

読んでていて疑問に思った点

  • 出力にバンドパスフィルタをつけてサイン波にしているのは?
    • 普通はサイン波で出すから?
    • 高調波成分が含まれるのが嫌?
    • 矩形波出力で立ちあがりを高速にしたほうがクロックとして有利なイメージがあるけどそうではない?

そもそも基準クロックというのに疎いので根本的なところがよくわかっておらず難しい。

出力を4分配にしているところで、バッファ+トロイダルコアを4つ使って分配しつつ直流的に?アイソレーションしているみたいでしたが、1つのバッファアンプ+ハイブリッドのほうが部品点数は減りそうだけど、うまくいかないのだろうか?とか机上の空論を考えてました。

精密なAD変換器のような非常に敏感な回路などではこの問題を避けるために、矩形波の代わりに正弦波をタイミング基準として使用する。

https://ja.wikipedia.org/wiki/%E7%9F%A9%E5%BD%A2%E6%B3%A2

Wikipedia にも書いてありましたが、クロックの高調波の影響を排除するために正弦波を使うんですね。無知でした。

その他

現在 GPS に載っている発振器のほとんどがルビジウムオシレータでびっくりしました。なんとなく全てセシウム原子時計が載っていると思い込んでいました。

AD9851 は Arduino で一度動かしてみましたが、LPC1114/mbed な環境でも動かしてみました。


DDS モジュールの定格は5Vですが、3.3Vで動かしてみています。振幅は当然減りますが、ちゃんと70MHzぐらいまでは波形が確認できました。

コード

Arduino のコードとほぼ一緒です。というか殆ど正規表現で置換しただけで動かすことができました。GPIO の操作だけなので本当に頭を一切使わずに移植できました。

GCC かつ -std=c++14 な環境なため mbed のオンラインコンパイラではコンパイルできないと思います (すこし修正すればいけるはずですが C++14 が使えない環境に興味がないので……)

また、なんとなくシリアル経由で周波数を変えるインターフェイスにしてみました。mbed のライブラリに CircularBuffer があるので簡単です。C++ の STL に circular buffer / ring buffer 相当のものがないのが不思議なんですが、いつか入る予定はあるんでしょうかね?

#include "mbed.h"
#include <CircularBuffer.h>
#include <cstdlib>
#include <string>

template <uint32_t CLKIN, bool MULTIPLIER>
class AD9851 {
	static constexpr double PHASE_FACTOR = 0x100000000 / (double)(CLKIN * (MULTIPLIER ? 6 : 1));

	DigitalOut PIN_DATA;
	DigitalOut PIN_FQ_UD;
	DigitalOut PIN_W_CLK;
	DigitalOut PIN_RESET;

	void serial_write(uint32_t freq, uint8_t phase, bool powerdown) {
		// freq (delta phase)
		for (int i = 0; i < 32; i++) {
			PIN_DATA = (freq>>i & 1);
			PIN_W_CLK = 1; wait_us(4);
			PIN_W_CLK = 0; wait_us(4);
		}

		// control bits
		PIN_DATA = MULTIPLIER ? 1 : 0;
		PIN_W_CLK = 1; wait_us(4);
		PIN_W_CLK = 0; wait_us(4);
		PIN_DATA = 0;
		PIN_W_CLK = 1; wait_us(4);
		PIN_W_CLK = 0; wait_us(4);

		// powerdown
		PIN_DATA = powerdown ? 1 : 0;
		PIN_W_CLK = 1; wait_us(4);
		PIN_W_CLK = 0; wait_us(4);

		// phase
		for (int i = 0; i < 5; i++) {
			PIN_DATA = (phase>>i & 1);
			PIN_W_CLK = 1; wait_us(4);
			PIN_W_CLK = 0; wait_us(4);
		}

		PIN_FQ_UD = 1; wait_us(4);
		PIN_FQ_UD = 0; wait_us(4);
	}

public:
	AD9851(
			PinName data,
			PinName fq_ud,
			PinName w_clk,
			PinName reset
		) :
			PIN_DATA(DigitalOut(data)),
			PIN_FQ_UD(DigitalOut(fq_ud)),
			PIN_W_CLK(DigitalOut(w_clk)),
			PIN_RESET(DigitalOut(reset))
	{
		PIN_DATA = 0;
		PIN_FQ_UD = 0;
		PIN_W_CLK = 0;
		PIN_RESET = 0;
	}

	/**
	 * W0 ... W31  -> Freq (LSB first)
	 * W32, W33    -> Control (for factory test)
	 * W34         -> Power-Down
	 * W35 ... W39 -> Phase (LSB first)
	 */

	void reset() {
		// ensure low
		PIN_DATA = 0;
		PIN_FQ_UD = 0;
		PIN_W_CLK = 0;

		// reset
		PIN_RESET = 1; wait(1);
		PIN_RESET = 0; wait(1);

		// reset to serial mode
		// Pins of D0, D1 = 1, D2 = 0 for serial mode
		PIN_W_CLK = 1; wait_us(4);
		PIN_W_CLK = 0; wait_us(4);

		PIN_FQ_UD = 1; wait_us(4);
		PIN_FQ_UD = 0; wait_us(4);
	}

	void set_frequency(uint32_t frequency) {
		set_frequency(frequency, 0);
	}

	void set_frequency(uint32_t frequency, uint8_t phase) {
		uint32_t deltaPhase = PHASE_FACTOR * frequency;
		serial_write(deltaPhase, phase, 0);
	}

	void powerdown() {
		serial_write(0, 0, 1);
	}
};

AD9851<30000000, true> ad9851(/*data*/dp28, /*fq_ud*/dp26, /*w_clk*/dp25, /*reset*/dp1);

//AnalogIn adc1(dp9);
//AnalogIn adc2(dp10);
//AnalogIn adc3(dp11);
//AnalogIn adc4(dp13);

Serial serial(USBTX, USBRX);

CircularBuffer<char, 80> serial_buffer;

int main() {
	serial.baud(115200);
	serial.printf("init\n");

	ad9851.reset();
	ad9851.set_frequency(10e6);

	for (;;) {
		if (serial.readable()) {
			char c = serial.getc();
			if (c == 0x0D) continue; // ignore CR
			if (c == 0x0A) { // treat LF as line end
				std::string line(80, '\0');
				line.clear();

				char c;
				while (serial_buffer.pop(c)) {
					line += c;
				}
				serial_buffer.reset();

				serial.printf("GOT: %s\n", line.c_str());

				uint32_t new_freq = std::atoi(line.c_str()) * 1e6;
				ad9851.set_frequency(new_freq);
				continue;
			}

			serial_buffer.push(c);
		}
	}
	return 0;
}

メモ: mbed 移植正規表現

S/digitalWrite\(([^,]+), ([^)]+)\)/\1 = \2/
s/delayMicroseconds/wait_us/g
s/delay/wait/g
  1. トップ
  2. tech
  3. AD9851 DDS モジュールを LPC1114/mbed と

アマゾンプライムビデオがはじまったのはもちろん知ってはいましたが「PC で見れてもなぁ」と思ってました。PC で動画を見る習慣がないので結局見ません。

しかし PS4 で直接見れることに気付いて、ただただ、最高という気持ちに変わりました。

まぁアマゾンプライムビデオとかは些細なことで、未確認で進行形を見なおしてるんですが、最高です。最高です。小紅は最高に可愛い。

mbed の API 経由でできる以上のことをやろうとする場合、結局 mbed 側で何をやっているか (どのAPIでどのレジスタが変更されるか) は理解していないといけない。

つまり mbed のラッパーのソースコードが手元にないと、プログラムできない。しかし実際のコードがどこにあるのかわかりにくい。

基本的には mbed のライブラリ経由でできることが殆どだとは思うが、例えば高度なスリープを行いたいとか、ウォッチドッグタイマを使いたいとなると、低レベルなレジスタアクセスが必須になる。

ghq で全部落としとくぞ

Mercurial (hg) のレポジトリとして公開されている。

 ghq get https://cho45@developer.mbed.org/users/mbed_official/code/mbed-src/
 ghq get https://cho45@developer.mbed.org/users/mbed_official/code/mbed/ 

してmbed 関係のヘッダと実装を手元に置いておく

mbed LPC1114

メインのレポジトリはこっちなのですが、ヘッダファイルだけ
https://developer.mbed.org/users/mbed_official/code/mbed/file/252557024ec3/TARGET_LPC1114/TARGET_NXP/TARGET_LPC11XX_11CXX/TARGET_LPC11XX

実装は mbed-src にあった。
https://developer.mbed.org/users/mbed_official/code/mbed-src/file/a11c0372f0ba/targets/cmsis/TARGET_NXP/TARGET_LPC11XX_11CXX/TARGET_LPC11XX/system_LPC11xx.c

mbed 関係ない部分

LPC1114 のリファンレス
http://www.nxp-lpc.com/images/LPC111x_UM_Rev.00.15_Japanese.pdf

NXP による LPC の定数定義
https://developer.mbed.org/users/mbed_official/code/mbed/file/252557024ec3/TARGET_LPC1114/LPC11xx.h

  1. トップ
  2. tech
  3. mbed のフレームワークの温度感

なんとなくもう一台を作りたくなったので作っています。実は基板自体はだいぶ前(数ヶ月以上前)に作っておいたのですが、設計ミスがあったりして面倒なのでちゃんと作りはじめてませんでした。

AD8307

AD8307を使ってログアンプ検波するバージョンの SWR 計の例があることは知っていましたが、前回はそこまですることはないと思ってとりあえず簡単な方法で作りました

というのもログアンプは大変高価だからです。秋月でAD8307のDIP版が売っていますが、単価が1100円します。なお DigiKey で買うともっと高価なので、秋月は安いほうです。

(最近知りましたが中華性AD8307というセカンドソース品、というよりコピー品があり、こちらは単価が50円!!と激安のようです。SOICで、ebay で手に入ります)

方向性結合器 + 検波 + ADC

例の回路をほぼ完全にパクって実装しました。

方向性結合器は、タンデムマッチで、FT82-43 を使い、20T にしてみました (26.02dB のカップラに)。線間容量を減らして、高い周波数での特性が改善しないか?と思い巻数を減らしています。#43 は透磁率が高いので、多少巻数を減らしてもインピーダンスは高いままですが、ロスは多くなるはずです。

検波部は結合器からの出力をアッテネータ(16.1dB)を通し、インピーダンス変換を行なって AD8307 に入力しています。AD8307 のオフセットや切片調整はしていないので、25mV/dB の出力にはるはずです。

方向性結合器で 26.02dB、アッテネータで16.1dBなので、AD8307 には 42.12dB 減衰された信号が入力されます。言いかえると、AD8307の入力範囲は -72〜+16dBm ですから、-29.88〜+58.12dBm の入力範囲 (約1μW〜648W) にシフトしたことになります。

ADC は ADS1015 という 4ch + PGA 付き I2C デバイスにしてみました。ここは入手性の問題からオリジナルと違います。

実装

最初は方向性結合器の部分も基板を起こしていましたが、どうも作りにくいので立体配線に変えました。2つのトロイダルコアの間に容量結合防止のためのGNDを立てたいわけですが、プリント基板だとかえって面倒でした。

そんなこともあろうかと、基板を作る段階で途中で切って使えるような感じにしておいたので良かったです。

検証

作ったはいいのですが、どうもうまくいかず悩みました。

一つはコイルを逆付けしていてまともに方向性結合器として動いていなかったのが原因でした。

もう一つはAD8307の出力の問題で、どうしても入力電力から計算される出力電圧が出ず、未だ完全に理由がわかっていません。

AD8307 の出力

最初は 3.3V で動かしていたのですが、どうしても途中から出力電圧が上がりませんでした。定格上では 3.3V でも +10dBm までは入力可能のはずなのですが、-2dBm ぐらいから出力が上がらなくなり、全くよくわかりませんでした。

5V で動かしてみたところ、とりあえずこれは解決したように見えました。

しかし電源電圧を 5V に変えても、入力電力に応じた出力電圧にならず難儀しました。どうしても 25mV/dB の傾きになっておらず、オフセットが含まれているように見えました。切片調整もしていないし、オフセットがどこから入っているのかもわかりませんでした。

結局これはキャリブレーションと称してプログラム上で補正をかけることにしました。どっちにしろ 25mV/dB の傾きは定格上でも 23〜27mV/dB の範囲があるので調整を入れなければなりません。

ということで、適当に係数を求めたら、だいたいそれっぽい値が出るようにはなりました。

正確な電力を計る方法がないので、一定以上は調整しようがありません。

Google Spreadsheet に実測と理想を書いて比較してがんばりました。。。

方向性結合器

追記:FT82-61 を使ったバージョンに変更しました。
[tech] 自作 デジタル SWR 計(再) 2 | 方向性結合器の改善編 | Tue, Feb 23. 2016 - 氾濫原

SWR はこんな感じでした

SWR だとよくわからないので、リターンロスでみるとこんな感じです

肝心のディレクティビティなどの特性です。蓋をあけた状態で電圧をオシロで計って50Ω換算で出しています。残念ながら前回作ったものよりも良くありません。

ログアンプの効果

ダイオード検波の場合、微小電力ではダイオードの Vf を超えることができず、それはそのまま誤差になります。

ログアンプ検波することで、ダイオード検波の欠点はなくなり、微小電力から計れるようになりました。具体的には、スペアナのトラッキングジェネレータ出力を 0dBm にして計っても、多少の誤差はありますがちゃんと 1mW ぐらいの値が出てきます。

通常送信機の電力測定ではこんな微小電力まで計れる必要は特にないかと思いますが、SWR 計として反射電力を計りたい場合、SWR が低いときには反射電力も非常に小さくなりますから、ダイナミックレンジの広い測定が必要になります。ダイオード検波と通常のリニアなADCを使うことに対する最大のメリットはここだと思います。

方向性結合器部分はまだ追試が必要

SWR計としてまとめるのではなく、方向性結合器として一旦作って評価をしてから、検波回路を外付けするほうが安定したものが作れそうです。次はそのようにしたいと思います。

制御

インターフェイスはまともに作っていなくて、Arduino で I2C から値を呼んで計算させてシリアルに出力だけさせてテストしていました。今のところまだ実用にはなっていません。

  1. トップ
  2. tech
  3. 自作 デジタル SWR 計(再) | ログアンプを使いQRP〜1KWまで

FT82-43 に20ターンでやってみましたが、結果が芳しくありませんでした。

そこで、FT82-61 に 30 ターンという、おそらくこれが最善だろうと思われる組合せでやってみることにしました。昔に

FT82-61 (AL= 79) のほうが透磁率がちょうど良さそうだけど、手元になかった。

http://lowreal.net/2014/11/11/1

と書いていました。このとき実はコアを買って巻くところまではやったのですが、実際に換装するところまでやっていませんでした。1年ちょっと越しの挑戦です。

結果

SWR / リターンロス

結合器自体の SWR/RL です。

全く問題ない感じです。

方向性など

FT82-43 T20
測定周波数 kHz IN [V] OUT [V] FWD REF IN [W] OUT [W] FWD [W] REF [W] Insertion Loss Coupling Isolation Directivity
1910 51 51 2.490 0.060 52.02 52.02 0.124002 0.000072 0.00 -26.23 -58.59 -32.36
7010 49 49 2.470 0.090 48.02 48.02 0.122018 0.000162 0.00 -25.95 -54.72 -28.77
28010 52 53 2.700 0.278 54.08 56.18 0.145800 0.001546 -0.17 -25.69 -45.44 -19.75
50010 55 52 2.970 0.530 60.50 54.08 0.176418 0.005618 0.49 -25.35 -40.32 -14.97
FT82-61 T30
測定周波数 kHz IN [V] OUT [V] FWD REF IN [W] OUT [W] FWD [W] REF [W] Insertion Loss Coupling Isolation Directivity
1910 49 49 1.671 0.004 48.02 48.02 0.055845 0.000000 0.00 -29.34 -81.76 -52.42
7010 49 49 1.666 0.005 48.02 48.02 0.055511 0.000001 0.00 -29.37 -79.82 -50.45
28010 52 53 1.850 0.008 54.08 56.18 0.068450 0.000001 -0.17 -28.98 -76.26 -47.28
50010 52 53 2.132 0.013 54.08 56.18 0.090908 0.000003 -0.17 -27.74 -72.04 -44.30


下側が FT82-61 での結果です。

50MHz でも 44dB とれています。 (SWR 計としては 25dB 以上なら良い)

精度

正確な信号源があるわけではないので精度というのもあれですが、以下のようになりました。

スペアナの 0dBm 出力と、FT-450D の 40W 出力を使って

solve([2188 * a + b = 2109, 1050 * a + b = 959], [a, b]);

という連立方程式を maxima に解かせてキャリブレーション値を求めています。KX3 の表示電力と出力電力はあんまり信用していないのでキャリブレーションに入れていません。KX3 以外では誤差が10%範囲に納まっています。

仕様変更

30ターンにしたので、カップリングは -29.54dB になっています。アッテネータとあわせて 45.64dB の減衰。-26.36〜61.64dBm (2μW〜1458W)

感想

センサー部分としては十分上出来になったと思います。すくなくとも前回作ったときよりは、かなり良くなりました。

これで I2C 接続の SWR 計ができたことになるので、インターフェイスをうまいこと作ってあげたいという気持ちになりました。

  1. トップ
  2. tech
  3. 自作 デジタル SWR 計(再) 2 | 方向性結合器の改善編

iRobot Roomba 自動掃除機 ルンバ 770 -

5.0 / 5.0

2013年の3月に買ったルンバをオフィシャルメンテナンスに出しました。約3年ぐらい、平日は毎日稼動させていました。

最近どうもガタガタ、ガタガタと言うときがあり、調べてみると後退したり旋回するときに、片方のモーターが動いていないような挙動でした。前進のときは問題なく、特定の場合だけなります。

一応一通り分解して清掃はしてみましたが、おそらく一番の原因であろうタイヤユニットまわりは中まで分解できるようになっておらず、他全てを綺麗にしても直りませんでした。

この時点では2つの選択肢がありました。

  • タイヤユニットだけを買って交換する 約6000円 (ただし直るかわからない)
  • オフィシャルメンテナンスに出す 定額だと約13000円〜 (直る)

前進は大丈夫で後退だけがダメ、なおかつ片方だけという状態だったので、なんとなく内部のDCモータードライバ回路の一部がおかしいのではないかという疑いもありました。もしそうならタイヤユニットを買っても無駄になってしまいます。

ということで自力で頑張るのは早々に諦めてオフィシャルメンテナンスに出しました。

結果

(途中で「バッテリーもヘタっているから変えるか? 変えるなら18000円のプランになるが良いか?」という趣旨の電話がありました。差額が5000円なので、まぁどうせだからと思い18000円のプランに変えました。オフィシャルバッテリはそもそも10000円ぐらいするので差額5000円なら互換品なみに安い)

返ってきたルンバは全体的に綺麗になっていました。メンテナンスの結果のレポートによると

  • 内部基板ユニットの交換
  • タイヤユニットの交換
  • エッジクリーニングブラシユノットの交換
  • メインブラシユニットの交換
  • フィルタの交換
  • バッテリーの交換(新型のXLifeバッテリーになった)

という感じで、ダストボックスと本体以外ほとんど全部変わってました。内部基板ユニットの不具合がやはりあったみたいで(モータードライバかどうかはわかりませんが)、自力でどうにかできる範囲ではなかったので今回に関しては判断は正しかったようです。

バッテリの寿命も伸びたみたいなので、とりあえずこれであと2年ぐらいは元気に動いてくれることを期待します。

オフィシャルメンテナンスは高いか?

メンテナンス費用には送料も含まれています。だいたい往復で3000円ぐらいかかるとすると、約10000円(ブラシパック)/15000円(サービスパック)が実際の工費になります。

700シリーズ メインブラシ+フレキシブルブラシ + エッジブラシ1本 -

3.0 / 5.0

メインブラシ及びエッジクリーニングブラシが約1500円

ルンバ(Roomba)iRobot(アイロボット) 700シリーズ専用フィルター2セット(4個) -

3.0 / 5.0

フィルタが1セットあたり約600円

はブラシパックの場合必ず交換、サービスパックの場合は必要に応じて交換

Roomba ルンバ エンハンスドクリーニングヘッド 【並行輸入品】 -

3.0 / 5.0

もし壊れているなら上のようなモジュール(6000円〜)が交換

ルンバ用バッテリー 長寿命3年 長時間稼動 500・600・700・800シリーズ対応 【1年保証付き Orange Line 】【互換品】 -

3.0 / 5.0

サービスパックの場合バッテリーが必ず交換 (↑これは互換品ですが)


ということで、単体として高いかというとそうでもなく、点検修理の工数を考えるとむしろお得ぐらいの価格設定です。


ただ、修理の場合実際あたまに浮かぶのは買い替えでしょう。別のルンバに買い替えるなら修理のほうが当然安くすみます。他社製品は今回検討しませんでしたが、もし安くていいのがあれば修理と競合しそうです。

any() 系のマッチャは常に「あらゆるオブジェクト」にマッチします。この挙動は Javadoc にも書いてあって、もしかすると将来的に変更するかも、みたいなことが書いてあります。

じゃあなんで any(Class) とか anyString() とかあるのか?という気持ちになるわけですが、これはたぶんオーバーロードで複数の選択肢がある場合に、ある特定のメソッドを確定するときに便利だからだ、と思います。

なお、あるクラスのインスタンスかどうかをチェックするマッチャは isA(Class clazz) のようです。

  1. トップ
  2. tech
  3. Mockito の any(Class<?> clazz) や anyString() や他の any ナンチャラは型チェックはしない

一定時間で何かをする、といえばタイマーの割込みを使うことでしょうが、タイマーを使いたくないないし使えないということもあると思います。

そういうときループをぶんまわしながら、時刻などを比較して一定時間ごとに処理をするという方法をとったりすることがあります。今回はそれを簡単に書けるスニペットを考えたという話です。

他のライブラリでどうにかする

こういうことをするライブラリを調べてみると、Metro というのがあります。これは以下のようにして使うライブラリのようです。

Metro interval250 = Metro(250); 

void loop() {
    if (interval250.check()) {
        // ここで 250msごとの処理
    }
}

もちろん悪くはないのですが、変数宣言と実際の処理が分かれており、余計な変数を宣言する必要があってなんか嫌です。(変数名をいちいち考えたくないという意味です)

loop() でいきなり使える定期実行

そこで以下のようなスニペットを考えました。interval class が本体です。

template <uint16_t time>
class interval {
	uint32_t next_run = 0;

	template <class T>
	void _run(T func) {
		uint32_t now = millis();
		if (next_run < now) {
			func();
			next_run = now + time;
		}
	}

	interval() {}
public:

	template <class T>
	static void run(T func) {
		static interval<time> instance;
		instance._run(func);
	}
};

void setup() {
	Serial.begin(9600);
}

void loop() {
	interval<250>::run([]{
		Serial.println("250ms 1");
	});

	interval<250>::run([]{
		Serial.println("250ms 2");
	});

	interval<1000>::run([]{
		Serial.println("1000ms");
	});

}

loop() 内でいきなり適当な書きかたをするだけですみます。

interval クラス

事前準備 (グローバル変数やstatic変数を自力で宣言したり) をせず、いきなり静的な関数ないしメソッドを読んで使うことができるようにするため、以下のような感じになっています。

まず、interval クラスは time をテンプレート引数にとっているので、time ごとに別のクラスが作られます。

各 time を持つ interval クラスには、さらに引数 func の型をテンプレート引数にとる run() という static メソッドを持っています。

run() は内部で static 変数として interval

func には lambda 式を書くことを想定しています。lambda 式の型は書く度に違うものになるので、同じ time を持つ処理も複数書けます。

つまり、テンプレート引数を使うことで、グローバルな状態を複数作っています。自分で余計な変数を宣言せずに「状態」を持つためにはこのような小細工が必要なようです。

これにより、各 time と引数 func ごとに interval

  1. トップ
  2. tech
  3. Arduino で一定時間ごとに何かをする interval クラス

ebay で注文した OCXO が届いたのでテストしてみました。このぐらいの小さな OCXO です。

MORION MV102 10MHz +12V OCXO

出力は HCMOS 矩形波 (10kΩ 39pF 負荷) となっています。正弦波出力が欲しかったのですが間違えて買ってしまいました。バッファしてフィルタすればいい気はします。とりあえずデジタル回路に直結でき、軽くテストするには便利なのでこれはこれで良いことにします。

GND          Us

RF   Uref   Uin

というピンアサインになっています。Us は +12V入力、RF は出力、Uref はリファレンス電圧(5V)、Uin はVCOの入力のようです。

Uin には 0〜5V で入力し、これにより±4e-7 (±4Hz) 調整できるというスペックです。

とりあえず 12V 加えるだけで 10MHz の出力が得られます。約 200mA ぐらい (2.5W程度) の消費電力があります。2時間ぐらい通電してみましたが特に下がりませんでした。クソ寒い部屋でやっているせいかもしれませんが…

OCXO なので結構熱くなります。しばらく触っていられるぐらいなので45〜55℃ぐらいでしょうか…

GPS の 1PPS で周波数カウントしてみる

GPSモジュールはこの前試した NEO-6Mです。3pin から線を別途引き出して使っています。窓際でやっているので、あまり安定した受信とはいえませんが、3D Fix したタイミングで計ってみました。

MPU として LPC1114FN28 を使って、以下のようなコードを書きました。ハードウェアカウンタは mbed の API からは使えるようになっていないので、データシートを読む必要があります。

本当は、ハードウェアでカウントしつつ、1PPS シグナルのキャプチャ信号でカウンタをキャプチャレジスタに自動的に入れて、同時に割込みを発生させてその値を読み出すみたいにしたかったのですが、少なくとも LPC1114 においてはそういうことはできないようでした (2つ以上のキャプチャピンを持っていない・外部入力カウンタとして使う場合、キャプチャレジスタを使用することはできない)

なので、実際のコードでは 32bit のカウンタを外部入力(10MHz)でフリーランカウントさせ、GPS の1PPSのタイミングでカウンタの値を読みこんで、前回との差分をとって周波数を求めています。

1PPS ごとの表示なので、簡単なコードですが、GPS同期の正確なゲート時間1秒周波数カウンタとして動いています。

#include "mbed.h"
#include <stdint.h>

constexpr uint32_t CLOCK = 10000000;
constexpr uint16_t HISTORY = 1000;

int16_t errors[HISTORY];
uint16_t error_index = 0;
uint16_t error_count = 0;

volatile bool updated = 0;
Serial serial(USBTX, USBRX);

InterruptIn gps1pps(dp25);

int main() {
	// enable 32bit counter
	LPC_SYSCON->SYSAHBCLKCTRL |= (1<<9/*CT32B0 32bit counter clock*/);

	// Capture pin dp14
	LPC_IOCON->PIO1_5         |= (0b010<<0/*FUNC=CT32B0_CAP0*/);

	// Match output (not used)
	//	LPC_IOCON->PIO1_6         |= (0b010<<0/*FUNC=CT32B0_MAT0*/);
	//	LPC_IOCON->PIO1_7         |= (0b010<<0/*FUNC=CT32B0_MAT1*/);
	//	LPC_IOCON->PIO0_1         |= (0b010<<0/*FUNC=CT32B0_MAT2*/);
	//	LPC_IOCON->R_PIO0_11      |= (0b011<<0/*FUNC=CT32B0_MAT3*/);

	LPC_TMR32B0->PR  = 0;
	LPC_TMR32B0->CCR = 0;
	LPC_TMR32B0->CTCR =
		(0b01<<0/*Counter/Timer Mode=Counter, Rising Edge*/) |
		(0b00<<2/*Count Input Select*/);
	LPC_TMR32B0->TCR = (1<<0/*Counter Enable*/);

	gps1pps.rise([]{
		static uint32_t prev = 0;

		uint32_t count = LPC_TMR32B0->TC;
		uint32_t pps_counter;
		if (prev < count) {
			pps_counter = count - prev;
		} else {
			// overflowed
			pps_counter = (0xffffff - prev) + count + 1;
		}
		prev = count;
		serial.printf("pps_counter: %lu\n", pps_counter);

		int16_t error = static_cast<int32_t>(CLOCK) - static_cast<int32_t>(pps_counter);
		error_index = (error_index + 1) % HISTORY;
		errors[error_index] = error;
		if (error_count < HISTORY) {
			error_count++;
		}
		updated = 1;
	});

	// NVIC_SetPriority();

	serial.baud(115200);
	for (;;) {
		if (updated) {
			updated = 0;
//			serial.printf("last: %d\n", errors[error_index]);
//
//			uint32_t sum = 0;
//			for (int i = 0; i < error_count; i++) {
//				sum += errors[ (error_index + HISTORY - i) % HISTORY ];
//			}
//			uint32_t error = sum * 1000 / error_count;
//			serial.printf("sum(%ds): %d\n", error_count, error);
		}
	}
}

結果

VFC 端子を 0V〜5V まで可変させて周波数を読んでみました。仕様上は の調整範囲 (±4Hz) があると書いてありました。実際やってみると以下のように ±5Hz ぐらいの可変ができるようです。

0〜5V の範囲で10Hz可変なので、2 Hz/V、2mHz/mV です。DAC で 1mHz 単位ぐらいで電圧を設定するなら、最低でも14bit程度の精度が必要そうです。

  1. トップ
  2. tech
  3. OCXO の VFC ピンでの可変範囲を GSP の 1PPS を使って調べた

答: デフォルトではしない

ARMのNVIC (Nested Vector Interrupt Controller) は名前の通りネスト可能ですが、実行中の処理よりも優先順位が高いものだけがネストします(ネストとは割り込み処理中にさらに割り込みが発生すること)。

mbed の場合、優先度を設定するAPIはないため、全割り込みは同一優先順位となり、他の割り込みがネストすることはありません (ライブラリ側でやってる場合を除く)。

優先度をつけるには

もし優先順位をつけてネスト可能にする場合、CMSIS(ARM規定のAPI群)のAPI (NVIC_SetPriority()) を使えば可能です。ただ、このAPIを使うためには、mbedのどのAPIでどんな割り込みを設定しているかを把握している必要があります (mbed 側のコードを全部読む必要がある)。

Cortex-M0 の NVIC の割り込みの優先の仕組み

優先順位は0から3までの4段階あり、0が最も優先です。

デフォルトでは全ての割り込みが0に設定されているので、相互に割り込みがネストすることはなく、通常処理が動いている間だけ割り込みが発生します。

また、通常処理についても、__disable_irq() を呼ぶと一時的に優先度0のコードになり、割り込みが発生しなくなります。__enable_irq() で元の優先度に戻ります。

外部ピン変化割り込みで情報が失われるケース

割り込み発生から、次の割り込みまでの間に2回ピン変化が起こると、2つめの変化による割り込み情報は失われます。

ある割り込みを優先した場合、その割り込みと同じ優先度の割り込み(その割り込み自身を含む)の処理時間だけ考えればいいことになります。

  1. トップ
  2. tech
  3. mbed InterruptIn はネストするか

[tech] OCXO の VFC ピンでの可変範囲を GSP の 1PPS を使って調べた | Fri, Feb 26. 2016 - 氾濫原 でとりあえず可変範囲ぐらいはわかったので、半固定抵抗を使ってだいたい10MHzぐらいにあわせてみたいと思います。

でもって、回路やコードの構成を変えました。

MPU のクロック自体を OCXO の 10MHz とする

前回は mbed デフォルト (内蔵の12MHz CRクロックを逓倍した48MHz) でやっていました。別にこれでもいいのですが、せっかく高精度な 10MHz があるのに、これをクロックにしないのももったいないなという感じになりました。

それに、LPC1114 自体のクロック源をOCXOにすれば、内蔵カウンタでフリーランさせた値がそのまま周波数カウントになるので、GPIOを1ピン節約できます。

つまり 10MHz 5逓倍で 50MHz にして LPC1114 最大クロックで動かしてみたいと思います。OCXO の周波数カウントは5分周すれば元の10MHzになりそうです。

システムクロックとして外部クロック(水晶発振器)を使うには

用語ですが「システムオシレータ」と「システムクロック」に区別があって、システムオシレータは外部水晶発振子を使うことを想定した内蔵発振回路、システムクロックはシステムオシレータまたはBYPASSされた外部クロックを指しています。

まず OCXO は発振器なので、内蔵システムオシレータは使わないようにします。

ドキュメントを見るわけですが、システムオシレータコントロールレジスタ (SYSOSCCTRL) の BYPASS が日本語版だと間違っているので原文を見る必要があります。

Bypass enabled. PLL input (sys_osc_clk) is fed directly from the XTALIN pin bypassing the oscillator. Use this mode when using an external clock source instead of the crystal oscillator.

となっていて、XTALIN から直接クロックを入力する場合には、内蔵のシステムオシレータをバイパスするという意味になります。

まぁとにかく XTALIN には外部クロックを直接入れれば良いのですが (XTALOUT はフロート)、ここで罠があります。XTALIN は他のピンと違って 1.8V 以上入力してはならないことになっています。

5V クロックなら 5.6k / 2.4k、3.3V クロックなら 1.2k / 1k あたりの分圧が必要です。

ただ、どうも OCXO の出力そのままだとうまく分圧できなかったので、一旦バッファ (74HCU04しかなかったのでこれで…) で受けてからクロック入力に使いました。

外部クロック設定コード

ぶっちゃけかなりハマりました。デバッグするためには CLKOUT を有効にして、どの段階までちゃんと出力が出ているかを確認する必要があります。

レジスタ自体はデータシート通りに記述すれば良いのですが、関連レジスタがかなり多いのでたいへんです……

ちょっとハマったのは SYSPLLCTRL の MSEL が実際の分周数-1を設定する必要がありました。(データシートにももちろん書いてありますが…)

やってることは

  1. (mbed の初期化では IRC=12MHz を PLL で 48MHz にされています)
  2. PLL を設定しなおすので、メインクロックソースを IRC にしておく (12MHz動作に)
  3. 一旦 PLL 回路の電源を切る (ついでのシステムオシレータ回路も確実に切っておく)
  4. システムオシレータをバイパスする設定をする
  5. PLL 関連レジスタを適当に設定して 5 逓倍できるようにする
  6. PLL のクロックソースをシステムクロックに
  7. PLL 回路の電源を入れ、PLL のロックを待つ
  8. メインクロックを PLL 出力に変える (この時点で 50MHz 動作に)
  9. IRC の電源を切る (切らなくてもいいんですが)
  10. SystemCoreClock を50MHzに更新しておく

という感じです。

void set_system_clock_to_external_10mhz() {
#ifdef DEBUG_CLOCK
	// XXX: set CLKOUT to debug (dp24)
	LPC_IOCON->PIO0_1 = (0b001<<0/*FUNC=CLKOUT*/);
	LPC_SYSCON->CLKOUTCLKSEL = 0b11; /*0b00=IRC, 0b01=SysOsc, 0b10=WDT, 0b11=Main*/
	LPC_SYSCON->CLKOUTDIV = 1;
	LPC_SYSCON->CLKOUTUEN = 0;
	LPC_SYSCON->CLKOUTUEN = 1;
#endif

	// Power down config (irc=on, sysosc=on)
	LPC_SYSCON->PDRUNCFG     &= ~(
		(1<<0/*IRCOUT_PD*/) |
		(1<<1/*IRC_PD*/)
	);

	// Ensure clock source to internal in temporary
	LPC_SYSCON->MAINCLKSEL = 0b00;
	LPC_SYSCON->MAINCLKUEN = 0x00;
	LPC_SYSCON->MAINCLKUEN = 0x01;
	while ( (LPC_SYSCON->MAINCLKUEN & 0b1) == 0);

	// Power down config (syspll=off, sysosc=off)
	LPC_SYSCON->PDRUNCFG     |= (
		(1<<5/*SYSOSC_PD*/) |
		(1<<7/*SYSPLL_PD*/)
	);

	// Set System OSC = BYPASS (clock fed to XTALIN directly 1.8V / XTALOUT must be floating)
	LPC_SYSCON->SYSOSCCTRL = (1<<0/*BYPASS*/);

	// PLL Clock Source = System OSC
	LPC_SYSCON->SYSPLLCLKSEL = (0b01<<0/*SEL*/);

	// Update PLL Source
	LPC_SYSCON->SYSPLLCLKUEN = 0;
	LPC_SYSCON->SYSPLLCLKUEN = 1;
	while ( (LPC_SYSCON->SYSPLLCLKUEN & 0b1) == 0 ); 

	// Set PLL
	// M = F_clkout / F_clkin
	// FCCO = 2 * P * F_clkout (P = {1, 2, 4, 8}) (FCCO=156-320MHz)
	// F_clkout = 50MHz / M = 5 / P = 2 / FCCO = 200MHz
	LPC_SYSCON->SYSPLLCTRL = ( (5 - 1)<<0/*MSEL*/) | (0b01<<5/*PSEL*/);

	// Power down config (syspll=on)
	LPC_SYSCON->PDRUNCFG     &= ~(
		(1<<7/*SYSPLL_PD*/)
	);
	// Wait for PLL Lock
	while ( (LPC_SYSCON->SYSPLLSTAT & 0b1) == 0 );

	// Update Main Clock to PLL Output
	LPC_SYSCON->MAINCLKSEL = 0b11;
	LPC_SYSCON->MAINCLKUEN = 0x00;
	LPC_SYSCON->MAINCLKUEN = 0x01;
	while ( (LPC_SYSCON->MAINCLKUEN & 0b1) == 0); 

	// Power down config (irc=off)
	LPC_SYSCON->PDRUNCFG     |= (
		(1<<0/*IRCOUT_PD*/) |
		(1<<1/*IRC_PD*/)
	);

	SystemCoreClock = 50000000;
}


周波数カウント部分の変更

タイマーをタイマーとして使い、システムクロックをそのままカウントします。キャプチャピンはGPS1PPSをキャプチャするようにし、キャプチャのタイミングでキャプチャレジスタにタイマカウンタをコピー(ハードウェア動作)させて割込みを生成します。GPIO のピンチェンジ割込みは使いません。

レジスタの設定は問題ないのですが、割込み周りでだいぶハマりました。

  • mbed の InterruptManager で TIMER_32_0_IRQn の割込みを設定しようとしたがうまくいかなかった
  • NVIC_EnableIRQ しないとダメだった
  • extern "C" を忘れてて割込みが呼ばれずしばらくはまった

また、前回よりも精度を高めるため、10秒、100秒、1000秒のゲートでの誤差を表示するようにしました。dHz (デシヘルツ) cHz (センチヘルツ) mHz (ミリヘルツ) 単位で表示しているのが誤差の項目です。

±1カウントエラーがあるので、多少 +1/-1 がでることがあります。

1000秒単位での調整は面倒なので、10秒単位の調整しかしてませんが、このように手で雑に調整してもこのぐらいの精度が出ていました。

コード

LPC1114 はSRAMが4KBと少ないため、カウントされた値をそのまま持つのではなく、誤差を signed で持つようにしています。そもそもOCXOの可変範囲が±5Hz程度しかないので、それ以上の情報を持つ必要がありません。ということで int8_t の配列で履歴を持って、直近のn件を合計して見る形になっています。

これにより、例えばカウンタをそのまま持つには10Mは16bitに納まらないので32bitの配列にしなければなりませんが、誤差だけを保持すれば4分の1の容量ですみます。というか uint32_t だとLPC1114では1000個の履歴を持てません。

constexpr uint32_t CLOCK = 10000000;
constexpr uint16_t HISTORY = 1000;

int8_t errors[HISTORY];
uint16_t error_index = 0;
uint16_t error_count = 0;

volatile bool updated = 0;
Serial serial(USBTX, USBRX);

extern "C" void TIMER32_0_IRQHandler (void) {
	static uint32_t prev = 0;
	uint32_t count = LPC_TMR32B0->CR0;
	uint32_t pps_counter;

	if (prev < count) {
		pps_counter = count - prev;
	} else {
		// overflowed
		pps_counter = (0xffffff - prev) + count + 1;
	}
	prev = count;

	int16_t error = static_cast<int32_t>(CLOCK) - static_cast<int32_t>(pps_counter);
	if (abs(error) < 15) {
		error_index = (error_index + 1) % HISTORY;
		errors[error_index] = error;
		if (error_count < HISTORY) {
			error_count++;
		}
		updated = 1;
	}

	LPC_TMR32B0->IR = (1<<4/*CR0 Interrupt*/);
}

int main() {
	set_system_clock_to_external_10mhz();
	NVIC_EnableIRQ(TIMER_32_0_IRQn);

	// enable 32bit counter
	LPC_SYSCON->SYSAHBCLKCTRL |= (1<<9/*CT32B0 32bit counter clock*/);

	// Capture pin dp14 (for gps 1pps)
	LPC_IOCON->PIO1_5         |= (0b010<<0/*FUNC=CT32B0_CAP0*/);

	// Match output (not used)
	//	LPC_IOCON->PIO1_6         |= (0b010<<0/*FUNC=CT32B0_MAT0*/);
	//	LPC_IOCON->PIO1_7         |= (0b010<<0/*FUNC=CT32B0_MAT1*/);
	//	LPC_IOCON->PIO0_1         |= (0b010<<0/*FUNC=CT32B0_MAT2*/);
	//	LPC_IOCON->R_PIO0_11      |= (0b011<<0/*FUNC=CT32B0_MAT3*/);

	// Prescaler
	LPC_TMR32B0->PR  = 4;
	// Capture on CAP0 Rising Edge (to CR0) and Enable Interrupt
	LPC_TMR32B0->CCR =
		(1<<0/*CAP0RE*/) |
		(1<<2/*CAP0I*/);
	LPC_TMR32B0->MCR = 0;
	LPC_TMR32B0->CTCR = (0b00<<0/*Counter/Timer Mode=Timer*/);
	LPC_TMR32B0->TCR = (1<<0/*Counter Enable*/);

	serial.baud(115200);
	for (;;) {
		if (updated) {
			updated = 0;
			serial.printf("[%d] last: %+d\n", error_count, errors[error_index]);

			if (error_count >= 10) {
				int32_t sum = 0;
				for (int i = 0; i < 10; i++) {
					sum += errors[ (error_index + HISTORY - i) % HISTORY ];
				}
				serial.printf("[%d] %+ddHz\n", error_count, sum);
			}
			if (error_count >= 100) {
				int32_t sum = 0;
				for (int i = 0; i < 100; i++) {
					sum += errors[ (error_index + HISTORY - i) % HISTORY ];
				}
				serial.printf("[%d] %+dcHz\n", error_count, sum);
			}
			if (error_count >= 1000) {
				int32_t sum = 0;
				for (int i = 0; i < 1000; i++) {
					sum += errors[ (error_index + HISTORY - i) % HISTORY ];
				}
				// 1000_0000000
				serial.printf("[%d] %+dmHz\n", error_count, sum);
			}
		}
	}
}
  1. トップ
  2. tech
  3. OCXO と GPS 1PPS その2

VFC を PWM によるデジタルコントロールにしてみました。

前回は多回転ボリュームを使って手動で調整をしました。これだと10秒単位ぐらいならともかく、1000秒単位の調整は面倒くさすぎてやっていられません。のでここをMCUで自動化します。

PWM による VFC のコントロール

OCXO から出ている Vref を PWM でスイッチングしてローパスフィルタにかけてコントロール電圧を生成します。DAC デバイスの電源電圧を Vref にするという形でもよさそうですが手元に DAC デバイスがないので PWM でやっています。

PWM の周波数は約30Hzです。これは mbed の PwmOut の period_us() に int の最大値 (1<<15) を設定した値になります。(なんでこのAPIはintなんでしょうかね?)

これにより、int16_t 変数でパルス幅を保持して、pulsewidth_us() が最大の分解能を持つようにしています。

PWMの周波数が30Hzぐらいなので、外部フィルタとしてかなり低い周波数のローパスフィルタを使っています。応答性が高くてもあまり意味がないないし困るので1Hzぐらいのローパスフィルタでも良さそうです。

また、PWM 周波数を変えた直後はグリッチが発生するので、2秒間のGPS1PPS割込みを無視するようなコードを加えました。

制御

PID制御などではなく、まずは適当にインクリメント・デクリメントし続けるような実装にしました。

当然 PID 制御したほうがいいのですが、とりあえず頭を使いたくなかったので…

結果

シリアルに出している直近1000秒ごとの誤差合計を、雑なワンライナーで整形して Google Spreadsheet でグラフ化してみました。主にソフトウェア的要因と思われる過補正を繰替えしている雰囲気を感じられますが、概ね±10mHz(±1ppb)におさまっています。

GPSDO

調整を自動化したので、これはもはやGPSDOと呼べそうです。PID制御にするなど、ソフトウェア的な制御改良の余地はありますが、GPSDOの大枠が実装できました。

どちらかといえばあとはアナログ回路の要素が多くなります。といっても以下らへんを実装すれば実用的になりそうです。

  • 出力が矩形波なので高次のローパスフィルタないしバンドパスフィルタを設計実装する
  • 波形整形するバッファを使っていないのでバッファのしかたを変える
  • GPSのアンテナ入力を外部入力に変える
  • GPS FIx されず、1PPS 信号がきてないときのタイムアウト処理を追加
  • ケースに入れる

アナログまわりが勉強中なので現状の技術力だと難しい面もありますが、製作例もあるのでなんとかはなりそうです。とはいえ、作っても現状だと応用先がスペアナぐらいしかないので、あんまりやる気がありません。

かなり簡単な構成でppbオーダーの高精度なクロックが得られるのは面白いです。もうちょっとアナログ回路の技術力があればさくっと作りあげられそうです。

コード

#include "mbed.h"
#include <stdint.h>
#include <inttypes.h>

void set_system_clock_to_external_10mhz() {
#ifdef DEBUG_CLOCK
	// XXX: set CLKOUT to debug (dp24)
	LPC_IOCON->PIO0_1 = (0b001<<0/*FUNC=CLKOUT*/);
	LPC_SYSCON->CLKOUTCLKSEL = 0b11; /*0b00=IRC, 0b01=SysOsc, 0b10=WDT, 0b11=Main*/
	LPC_SYSCON->CLKOUTDIV = 1;
	LPC_SYSCON->CLKOUTUEN = 0;
	LPC_SYSCON->CLKOUTUEN = 1;
#endif

	// Power down config (irc=on, sysosc=on)
	LPC_SYSCON->PDRUNCFG     &= ~(
		(1<<0/*IRCOUT_PD*/) |
		(1<<1/*IRC_PD*/)
	);

	// Ensure clock source to internal in temporary
	LPC_SYSCON->MAINCLKSEL = 0b00;
	LPC_SYSCON->MAINCLKUEN = 0x00;
	LPC_SYSCON->MAINCLKUEN = 0x01;
	while ( (LPC_SYSCON->MAINCLKUEN & 0b1) == 0);

	// Power down config (syspll=off, sysosc=off)
	LPC_SYSCON->PDRUNCFG     |= (
		(1<<5/*SYSOSC_PD*/) |
		(1<<7/*SYSPLL_PD*/)
	);

	// Set System OSC = BYPASS (clock fed to XTALIN directly 1.8V / XTALOUT must be floating)
	LPC_SYSCON->SYSOSCCTRL = (1<<0/*BYPASS*/);

	// PLL Clock Source = System OSC
	LPC_SYSCON->SYSPLLCLKSEL = (0b01<<0/*SEL*/);

	// Update PLL Source
	LPC_SYSCON->SYSPLLCLKUEN = 0;
	LPC_SYSCON->SYSPLLCLKUEN = 1;
	while ( (LPC_SYSCON->SYSPLLCLKUEN & 0b1) == 0 ); 

	// Set PLL
	// M = F_clkout / F_clkin
	// FCCO = 2 * P * F_clkout (P = {1, 2, 4, 8}) (FCCO=156-320MHz)
	// F_clkout = 50MHz / M = 5 / P = 2 / FCCO = 200MHz
	LPC_SYSCON->SYSPLLCTRL = ( (5 - 1)<<0/*MSEL*/) | (0b01<<5/*PSEL*/);

	// Power down config (syspll=on)
	LPC_SYSCON->PDRUNCFG     &= ~(
		(1<<7/*SYSPLL_PD*/)
	);
	// Wait for PLL Lock
	while ( (LPC_SYSCON->SYSPLLSTAT & 0b1) == 0 );

	// Update Main Clock to PLL Output
	LPC_SYSCON->MAINCLKSEL = 0b11;
	LPC_SYSCON->MAINCLKUEN = 0x00;
	LPC_SYSCON->MAINCLKUEN = 0x01;
	while ( (LPC_SYSCON->MAINCLKUEN & 0b1) == 0); 

	// Power down config (irc=off)
	LPC_SYSCON->PDRUNCFG     |= (
		(1<<0/*IRCOUT_PD*/) |
		(1<<1/*IRC_PD*/)
	);

	SystemCoreClock = 50000000;
}

constexpr uint32_t CLOCK = 10000000;
constexpr uint16_t HISTORY = 1000;

int8_t errors[HISTORY];
uint16_t error_index = 0;
uint16_t error_count = 0;

volatile bool updated = 0;
volatile uint8_t skip_second = 0;
Serial serial(USBTX, USBRX);
PwmOut osc_control(dp1);

extern "C" void TIMER32_0_IRQHandler (void) {
	static uint32_t prev = 0;
	LPC_TMR32B0->IR = (1<<4/*CR0 Interrupt*/);

	uint32_t count = LPC_TMR32B0->CR0;
	uint32_t pps_counter;

	if (prev < count) {
		pps_counter = count - prev;
	} else {
		// overflowed
		pps_counter = (0xffffff - prev) + count + 1;
	}
	prev = count;

	if (skip_second > 0) {
		skip_second--;
		return;
	}

	int16_t error = static_cast<int32_t>(pps_counter) - static_cast<int32_t>(CLOCK);
	if (abs(error) < 15) {
		error_index = (error_index + 1) % HISTORY;
		errors[error_index] = error;
		if (error_count < HISTORY) {
			error_count++;
		}
		updated = 1;
	}
}

int16_t pulsewidth = 16500;

void vcf_plus(uint8_t count) {
	pulsewidth -= count;
	osc_control.pulsewidth_us(pulsewidth);
	skip_second = 2;
	serial.printf("osc_control = %d\n", pulsewidth);
}

void vcf_minus(uint8_t count) {
	pulsewidth += count;
	osc_control.pulsewidth_us(pulsewidth);
	skip_second = 2;
	serial.printf("osc_control = %d\n", pulsewidth);
}

int main() {
	set_system_clock_to_external_10mhz();
	NVIC_EnableIRQ(TIMER_32_0_IRQn);

	osc_control.period_us(1<<15);
	osc_control.pulsewidth_us(pulsewidth);

	// enable 32bit counter
	LPC_SYSCON->SYSAHBCLKCTRL |= (1<<9/*CT32B0 32bit counter clock*/);

	// Capture pin dp14 (for gps 1pps)
	LPC_IOCON->PIO1_5         |= (0b010<<0/*FUNC=CT32B0_CAP0*/);

	// Match output (not used)
	//	LPC_IOCON->PIO1_6         |= (0b010<<0/*FUNC=CT32B0_MAT0*/);
	//	LPC_IOCON->PIO1_7         |= (0b010<<0/*FUNC=CT32B0_MAT1*/);
	//	LPC_IOCON->PIO0_1         |= (0b010<<0/*FUNC=CT32B0_MAT2*/);
	//	LPC_IOCON->R_PIO0_11      |= (0b011<<0/*FUNC=CT32B0_MAT3*/);

	// Prescaler
	LPC_TMR32B0->PR  = 4;
	// Capture on CAP0 Rising Edge (to CR0) and Enable Interrupt
	LPC_TMR32B0->CCR =
		(1<<0/*CAP0RE*/) |
		(1<<2/*CAP0I*/);
	LPC_TMR32B0->MCR = 0;
	LPC_TMR32B0->CTCR = (0b00<<0/*Counter/Timer Mode=Timer*/);
	LPC_TMR32B0->TCR = (1<<0/*Counter Enable*/);

	serial.baud(115200);
	for (;;) {
		if (serial.readable()) {
			char c = serial.getc();
			serial.putc(c);
			serial.printf(" %d\n", c);
			if (c == '+' || c == 'p') {
				vcf_plus(100);
			} else
			if (c == '-' || c == 'm') {
				vcf_minus(100);
			}
		}

		if (updated) {
			updated = 0;
			int8_t last = errors[error_index];
			serial.printf("[%d] last: %+d\n", error_count, last);
			if (last >= 2) {
				vcf_minus(1);
			} else
			if (last <= -2) {
				vcf_plus(1);
			}

			if (error_count >= 10) {
				int32_t sum = 0;
				for (int i = 0; i < 10; i++) {
					sum += errors[ (error_index + HISTORY - i) % HISTORY ];
				}
				serial.printf("[%d] %+ddHz\n", error_count, sum);

				if (sum >= 2) {
					vcf_minus(1);
				} else
				if (sum <= -2) {
					vcf_plus(1);
				}

			}
			if (error_count >= 100) {
				int32_t sum = 0;
				for (int i = 0; i < 100; i++) {
					sum += errors[ (error_index + HISTORY - i) % HISTORY ];
				}
				serial.printf("[%d] %+dcHz\n", error_count, sum);

				if (sum >= 2) {
					vcf_minus(1);
				} else
				if (sum <= -2) {
					vcf_plus(1);
				}
			}
			if (error_count >= 1000) {
				int32_t sum = 0;
				for (int i = 0; i < 1000; i++) {
					sum += errors[ (error_index + HISTORY - i) % HISTORY ];
				}
				// 1000_0000000
				serial.printf("[%d] %+dmHz\n", error_count, sum);

				if (sum >= 2) {
					vcf_minus(1);
				} else
				if (sum <= -2) {
					vcf_plus(1);
				}
			}
		}
	}
}
  1. トップ
  2. tech
  3. OCXO と GPS 1PPS その3