先日のKANHAM2019で頒布したディジタルVSWRメーターは、方向性結合器で得られた進行波と反射波をそれぞれショットキーダイオード1SS106で整流し平滑化したのちにPICでAD変換、計算、表示を行うわけですが、ダイオードの特性から小信号時の歪に対して補正をかけてなるべくQRPの小さい電力でもある程度の測定をしようと考えプログラムました。
ただ前々回の投稿に掲載した補正のやり方は、計算した電力値に補正をかけており適正化されていなかったため結果VSWR表示は過小評価していました。組み立てていただいた方からも同様のレポートをいただいたことから今一度補正のやり方を最初から検討しなおしました。
結局ダイオード整流電圧からAD変換した値(電圧値)をダイオードの特性に合わせるように補正をかけてから電力とVSWRそれぞれ計算するという形に変えました。つまりはダイオードの特性を測定してその結果をプロファイルとし、得られたAD変換値をそのプロファイルに沿って補正すればよいわけです。考えてみたらそのほうがすっきりというか当たり前というか・・・(汗
具体的には、対象のダイオードを下の図の回路図に倣ってブレッドボードに組み、整流前のRFピーク電圧と整流後の直流電圧値をRF出力を変えながら2現象オシロスコープで測定して結果をグラフにプロットします。プロットしたグラフを見て大まかなダイオード特性を把握します。
上の図のように実際に測定して図にプロットした特性曲線を近似的に表現できるように考えたものです。RF電圧がある程度以上であれば整流電圧とほぼ比例関係にあり1次関数で表現できそうですが、それ以下の場合は当てはまらなくなります。あまり根拠はありませんが、その領域に平方根関数を当てはめ、1次関数との接線の方程式をを用いてプロファイルを作成してみました。
接線の方程式を使えば、x1 y1を設定することによってpが導かれ補正用のプロファイルが出来上がります。
電圧が小さい領域は平方根関数ですが、8bitPICで計算させるのは実用的ではないことと、その領域の範囲が小さいのであらかじめPCのスプレッドシートで計算させた結果を1次配列にした変換テーブルを作りました。
今回はAD値1から30までが対象になるため、配列の要素は30個で済んでいます。
このプロファイルを組み込んだファームウエアをプログラムすることで大体0.5W程度からVSWRが計測できるようになりました。出力を変えると多少値にばらつきが出るので本当に完璧なモノとはまだまだ言えませんが、かなり実用的になったのではないかと思います。
QPM-01は今度のハムフェア2019で正式版を用意する予定ですが、先の評価版とファームウエアは共通になります。現時点での最新ファームウエア(ver.1.61)はこちらです。
正式版リリースのころに新たに専用ブログを設ける予定です
2019年7月31日水曜日
2019年7月29日月曜日
遅まきながらKANHAM2019に参加しました
もうすでに1週間以上経過してしまいましたが、7月20、21日大阪池田市で開催された関西アマチュア無線フェスティバル通称KANHAM2019に参加しました。
今回久しぶりに自家用車で往復しました。新東名が試験的ではありますが120km/h制限になっていたり新名神が拡大していたりで、距離的にも時間的にも近くなったという印象でした。
途中多賀SAでお風呂&仮眠しようと思いましたが、ナビゲーション設定を誤ってしまい先に会場の池田市に午前3時くらいに到着してしまいました(汗 周りに夜通し空いているスパ銭もなさそうなので、会場隣のコインパーキングに停めて8時前まで車内で仮眠をとりました。それが結果的に良かったのか、コインパーキングには夜中の3時過ぎなのにすでに数台しか空きがなく、仮眠から目覚めた7時過ぎにはもうすでに満車になっていました。
というわけで、今回はいつもの『リトルガンくらぶ』のほかに新たに結成された『アマチュアキットクリエイターズ AKC』ブースで主にキット頒布を行いました。
私はいつものVNシリーズとKeyer Mini-V2R2と新たにディジタルVSWRメーターキットQPM-01人柱版を用意、私のほかにJR6IRK岩永OM、7L4WVU原口OM、JQ1SRN武村OMの3名が各々のキットなどの頒布品を用意されました。
設営がひと段落したのも束の間、10時の開場とともに人の波が・・・
いやはや、さながらコミケットの開場直後をちょっぴり彷彿とさせる光景が。事前の宣伝効果はかなりなもので最初の1時間は対応にあたふた状態でした。そのおかげで皆さん結構頒布品が捌けていきました。
今回は参加メンバーは4名でしたが、今度のハムフェア2019ではフルメンバーの7名があの小さなブース一コマに集中するので当日の混乱をどう切り抜けるかが一つ大きな課題となりました。
ともあれ盛況で何よりでした。
1日目終了後空港近くのホテルでチェックインを済ませ、再び会場近くの石橋駅近くのお店でAKCメンバーの反省会ののちいつものリトルガンくらぶの飲み会に合流しました。
石橋駅ガードわきの小さな飲み屋さんで。地元の銘酒『呉春』をいただくのが恒例になっています。帰りに石橋の商店街の酒屋さんで一升瓶で買って帰りました。
2日目もブースにたくさんの方が足を運んでいただきました。この2日間はほとんどブースにいたので講演などは観れませんでしたが楽しく過ごさせていただきました。
関係者の皆さん、ありがとうございましたm(_ _)m
最後に近くの銭湯で汗を流し帰路につきました。
では、今回の戦利品を。
今回久しぶりに自家用車で往復しました。新東名が試験的ではありますが120km/h制限になっていたり新名神が拡大していたりで、距離的にも時間的にも近くなったという印象でした。
途中多賀SAでお風呂&仮眠しようと思いましたが、ナビゲーション設定を誤ってしまい先に会場の池田市に午前3時くらいに到着してしまいました(汗 周りに夜通し空いているスパ銭もなさそうなので、会場隣のコインパーキングに停めて8時前まで車内で仮眠をとりました。それが結果的に良かったのか、コインパーキングには夜中の3時過ぎなのにすでに数台しか空きがなく、仮眠から目覚めた7時過ぎにはもうすでに満車になっていました。
というわけで、今回はいつもの『リトルガンくらぶ』のほかに新たに結成された『アマチュアキットクリエイターズ AKC』ブースで主にキット頒布を行いました。
私はいつものVNシリーズとKeyer Mini-V2R2と新たにディジタルVSWRメーターキットQPM-01人柱版を用意、私のほかにJR6IRK岩永OM、7L4WVU原口OM、JQ1SRN武村OMの3名が各々のキットなどの頒布品を用意されました。
設営がひと段落したのも束の間、10時の開場とともに人の波が・・・
いやはや、さながらコミケットの開場直後をちょっぴり彷彿とさせる光景が。事前の宣伝効果はかなりなもので最初の1時間は対応にあたふた状態でした。そのおかげで皆さん結構頒布品が捌けていきました。
今回は参加メンバーは4名でしたが、今度のハムフェア2019ではフルメンバーの7名があの小さなブース一コマに集中するので当日の混乱をどう切り抜けるかが一つ大きな課題となりました。
ともあれ盛況で何よりでした。
1日目終了後空港近くのホテルでチェックインを済ませ、再び会場近くの石橋駅近くのお店でAKCメンバーの反省会ののちいつものリトルガンくらぶの飲み会に合流しました。
少し甘めのお酒ですが甘ったるすぎずスッキリしていて何杯でもイケます(笑) |
2日目もブースにたくさんの方が足を運んでいただきました。この2日間はほとんどブースにいたので講演などは観れませんでしたが楽しく過ごさせていただきました。
関係者の皆さん、ありがとうございましたm(_ _)m
最後に近くの銭湯で汗を流し帰路につきました。
では、今回の戦利品を。
下にある箱はなんでしょう? |
2019年7月19日金曜日
関西アマチュア無線フェスティバル(KANHAM2019)に参加します!
毎年大阪池田市で行われる恒例の関西アマチュア無線フェスティバル(KANHAM2019)に今回もはるばる1エリアから参加します。
昨年新たに結成されメンバーに加えていただいたアマチュアキットクリエイターズ(AKC)がこの関ハムでブースを持つこととなりました。いつもはリトルガンくらぶブースでキットなどの頒布を行っていましたが、混乱を避けるため実際のキット頒布はAKCブースで、完成品のデモンストレーションをリトルガンくらぶブースで行うことにしました。
各ブースは関ハムのサイトの配置図をご覧いただくとわかりますが、ほぼ向かいあった位置に配置されています。
目印はまずリトルガンくらぶの毎度おなじみ『むせんぶ』ポスター
こちらで頒布予定のキットの完成品の展示を行います。また『むせんぶ』グッズを頒布されるそうなので是非お立ち寄りください。
でもって・・・
こちらの横幕を掲げているアマチュアキットクリエイターズ(AKC)ブースにてキットの頒布を行います。
内容はこのブログの上にある『イベント頒布情報』タブを左クリックしてご参照ください。
当日会場内で黒いAKCのTシャツを着たでかいおっさんがいたら多分それは私だと思います。アイボールや自作談義など楽しみましょう!
昨年新たに結成されメンバーに加えていただいたアマチュアキットクリエイターズ(AKC)がこの関ハムでブースを持つこととなりました。いつもはリトルガンくらぶブースでキットなどの頒布を行っていましたが、混乱を避けるため実際のキット頒布はAKCブースで、完成品のデモンストレーションをリトルガンくらぶブースで行うことにしました。
各ブースは関ハムのサイトの配置図をご覧いただくとわかりますが、ほぼ向かいあった位置に配置されています。
目印はまずリトルガンくらぶの毎度おなじみ『むせんぶ』ポスター
こちらで頒布予定のキットの完成品の展示を行います。また『むせんぶ』グッズを頒布されるそうなので是非お立ち寄りください。
でもって・・・
こちらの横幕を掲げているアマチュアキットクリエイターズ(AKC)ブースにてキットの頒布を行います。
内容はこのブログの上にある『イベント頒布情報』タブを左クリックしてご参照ください。
当日会場内で黒いAKCのTシャツを着たでかいおっさんがいたら多分それは私だと思います。アイボールや自作談義など楽しみましょう!
2019年7月7日日曜日
通過型電力計の製作(その2~マイクロコントローラ編)
回路図が決まったところで、次は得られた進行波と反射波の整流電圧を測定して計算表示させるまでを担うマイクロコントローラー(マイコン)部分のプログラムを設計していきましょう。
わざわざマイコンなんて使わなくても感度の良い電流計を取り付けて校正すればいいじゃん・・・はい、ごもっともです(汗)
しかし高周波とマイコンが融合する姿に私としては非常に引き付けられるものがあるので、ここはぜひマイコンを活用しようではないか!というわけで強引に進めていきます(笑)
具体的にマイコンを選定する前に、何をマイコンにさせるかということを決めておくことが大事です。それからマイコン動作の基本を理解することです。マイコン動作の本質は『計算』です。つまり、外から入力したデータを『計算』して出力に送る、ということです。その『計算』の手順を示すものがいわゆるプログラムということになります。
今回製作した通過型電力計に当てはめてみます。まずデータの入力ですが、進行波や反射波の電圧レベルはそのままマイコンで計算することができないので、最初にアナログーディジタル変換(AD変換)によって電圧レベルをディジタル値に変換します。このディジタル値をもって初めてマイコンによる『計算』が可能になります。それから計算した結果をキャラクターLCDディスプレイで表示するため、計算結果を含めたデータをLCDに送ることによってLCDに表示させます。あとはおまけとしてLCDバックライト電源を制御する出力とファンクションボタン入力を加えています。
ということで、マイコンに必要なポートを列挙してみると・・・
1.入力
進行波電圧入力(アナログ)
反射波電圧入力(アナログ)
ファンクションボタン(ディジタル)
2.出力
LCDへの通信ポート(今回は2線シリアルI2Cバス SCLポート,SDAポート)
LCDバックライト電源ポート(ディジタル出力)
と、入出力合わせて6ポート必要になります。
ということは、8ピンPICですべてのポートを使い切るということになります。特に機能拡張をする予定はないので必要最小限の8ピンPICを使うことにしました。
8ピンのPICは12Fシリーズがポピュラーですが、その中でも上位クラスの拡張ミッドレンジコアを持っている12F1840を採用しました。値段も秋月で1個120円と非常に安価です。これでAD変換やEEPROM、いくつかの通信モジュールもひとつ小さなパッケージに内蔵されているのは驚きです。
最近の8bitPICにはほとんどがAD変換とシリアル通信、EEPROMモジュールが内蔵されていて、発振源も内蔵CR発振が選択出来て外部に水晶振動子やセラロックを接続しなくても良い設計になっています。ましてや8ピンしかないマイコンには内部発振は必須といってよいかも知れません。
またPICのピンには各々役割が複数あって、初期設定で各々のモジュールのレジスタに書き込み設定を行っています。今回のプログラムでの割り当ては赤色の枠になっています。
では処理の流れをブロックダイヤグラム風に示してみます。
実際のプログラムに落とし込みますが、最初にPICのコンフィギュレーションの記述やヘッダファイルxc.hのインクルード、初期設定(PIC各ピン(入出力、アナログ・ディジタル、プルアップなど)、モジュール(ADC、I2Cバス))を行い、メインループ内にAD変換、各電力値とVSWR値計算、計算結果表示関数を置きます。
通常表示するだけであればメインループ内で繰り返し処理を行うようにしてほぼリアルタイムに表示させていけばOKですが、進行波と反射波表示切替やバックライトの制御を加えて少し使いやすくしてみます。
通常自分がスイッチによる制御を加える場合は、タイマー割込み処理でスイッチ状態を監視してメイン処理を修飾するようにしています。
タイマー処理はTIMER0で割り込みをかけるようにしています。時間設定は1ミリ秒としてスイッチの状態をメモリにコピーし、コピーしたメモリを参照してあるパターンに一致したときにフラグを立ててメインループ内でフラグに応じた処理を修飾するという流れです。
スイッチは一つしかありませんが、押し続ける時間によって機能をいくつか持たせるようにしてピンの少なさをカバーしています。(スイッチポートをアナログにして電圧変化に応じて複数の機能を持たせる方法もあります)
次にメインの計算処理についてですが、AD変換された値(AD value)は電圧の整数値(符号なしの10bit値)です。電力はW=V^2 / Rで導かれますので、電力値に変換するにはAD valueを2乗し適当な定数を乗すれば良さそうです。この方法ではある程度信号が大きければ問題ないのですが、ダイオードによる整流のためダイオードのVf付近つまりはQRP電力の場合ダイオードのひずみにより、単純にAD valueの2乗ですべてOK!というわけにはいきません。
そこで実際の電力値とAD valueを測定、表にプロットして計算方法を探ることにしました。
縦軸は出力電力値、横軸はPICのAD変換で得られたAD value ( = det) の2乗値det^2です。
det^2値が14000超えの場合はdet^2とPowerはほぼ1次関数に収まりますが、14000以下の場合はダイオードの低VfでのVf-If特性を踏まえて上表の青色の式のように当てはめるようにしました。(2次曲線に近似しており、この領域では整流電圧と電力の関係を1次関数とみなしています。言い訳ですが(笑)、この程度の測定器であればこれ以上突き詰めても仕方ないでしょう。)
また計算途中で必要な変数の型については、AD変換で得られる値のbit桁数は10bitなので2乗としても20bitあれば事足ります。ですので計算プログラムの変数の型はunsigned longでOKです。整数同士の計算なので4MHz駆動の8bitPICでもサクサク動いてくれます。
またVSWRの計算はおなじみの
VSWR値 = (|Vf| + |Vr|) / (|Vf| - |Vr|)
をそのまま当てはめています。
最後に表示にはI2C接続の16x2キャラクタLCDを使い、LCDとのI2C通信にはMSSPモジュール使用しています。
それでは実際のコードを公開しちゃいます。
//
// VSWR_meter.c
// Copyright JL1VNQ / HARU
//
//
// ver.1.00 9 June 2019
// first release
//
// ver.1.10 12 June 2019
// change power calculation algorithm
//
#define EEPROM_SIZE 256
#define _XTAL_FREQ 4000000
#include <xc.h>
//for 12F1840 config
#pragma config FOSC = INTOSC, WDTE = OFF, PWRTE = OFF, MCLRE = OFF, CP = OFF
#pragma config CPD = OFF, BOREN = OFF, CLKOUTEN = OFF, IESO = OFF, FCMEN = OFF
#pragma config WRT = OFF, PLLEN = OFF, STVREN = ON, LVP = OFF
#define POW LATAbits.LATA5 // backlight LED control
#define FUNC PORTAbits,RA3 // function switch
#define LCD_AD 0x7C // Akiduki's I2C LCD(AQM0802, AQM1602) address
#define TMR0_set 0x83 // TMR0 1msec interval
unsigned char contrast = 5;
unsigned long forward = 0;
unsigned long reverse = 0;
unsigned long po = 0; // calculated power (x10^-1 watts)
unsigned int vswr = 0; // VSWR * 10
unsigned char for_rev = 0; // display change (0:forward, 1:reverse)
void msec_delay(unsigned short time);
void I2C_send(unsigned char data);
void lcd_cmd(unsigned char work);
void lcd_data(unsigned char work);
void lcd_init(void);
void lcd_clear(void);
void cgram_set(void);
void lcd_position(unsigned char li, unsigned char col);
void lcd_str_disp(unsigned char li, unsigned char col, const char *string);
void lcd_char_disp(unsigned char li, unsigned char col, unsigned char ascii);
void var_disp_conv(unsigned char li, unsigned char col, unsigned int val);
void pow_disp(unsigned char li, unsigned char col);
void vswr_disp(unsigned char li, unsigned char col);
void __interrupt() isr(void){
if(INTCONbits.TMR0IF){
INTCONbits.GIE = 0;
TMR0IF = 0;
static unsigned int cnt0 = 0, cnt1 = 0;
static unsigned char sw_mem = 0, sw_down = 0, dim = 0;
sw_mem <<= 1;
if(FUNC == 0) sw_mem |= 1;
if((sw_mem & 0x0F) == 0x03) sw_down = 1;
if(sw_down == 1){
if(cnt0 < 2000) cnt0 ++;
if(cnt0 < 1000){
if((sw_mem & 0x0F) == 0x00){
if(dim == 0) dim = 1;
else if(dim == 1) dim = 0;
sw_down = 0;
cnt0 = 0;
cnt1 = 20000;
}
}
else{
if(for_rev == 1) for_rev = 0;
else if(for_rev == 0) for_rev = 1;
sw_down = 0;
cnt0 = 0;
}
}
if(dim == 1){
if(forward > 10) cnt1 = 5000;
if(cnt1 > 0){
cnt1 --;
if(POW == 0) POW = 1;
}
else POW = 0;
}
else POW = 0;
TMR0 = TMR0_set;
INTCONbits.GIE = 1;
}
else if(INTCONbits.IOCIF){ // for Interrupt On Change(hang-up occur if compiling without this code)
INTCONbits.GIE = 0;
IOCAF = 0;
INTCONbits.GIE = 1;
}
}
void main(void){
OSCCON = 0x6A; // 4MHz internal OSC no PLL
PORTA = 0x00;
ANSELA = 0x11; // RA4, RA0 Analog Input
TRISA = 0x1F;
WPUA = 0x2E; // PORTA weak pull-up
OPTION_REG = 0x02; // weak pull_up, TMR0 internal clock(1us/cycle), prescaler 1:8
POW = 1; // LCD LED POW PORT on
SSP1CON1 = 0b00101000; // I2C Master mode
SSP1STAT = 0b10000000;
SSP1ADD = 9; // I2C Freq = (SSP1ADD + 1)*4/Fosc = 100kHz
ADCON1 = 0b11000000; // ADFM = 1 (right), ADCS = 100 (fosc/4), ADPREF = 00 (Vref = VDD)
ADCON0bits.ADON = 1; // ADC module enable
msec_delay(10);
lcd_init();
cgram_set();
lcd_str_disp(0,0,"VSWR Meter QPM01"); //startup splash for AQM1602
lcd_str_disp(1,0,"(c)HARU 20190612");
msec_delay(750);
POW = 0;
lcd_clear();
TMR0 = TMR0_set; // 1msec
INTCONbits.TMR0IF = 0;
INTCONbits.TMR0IE = 1;
INTCONbits.IOCIE = 1;
INTCONbits.GIE = 1;
IOCAN = 0xFF; // interrupt on change negative edge detect
IOCAF = 0;
while(1){
ADCON0 = 0b00000001; // AN0
__delay_us(10);
ADCON0bits.GO = 1;
while(!ADCON0bits.GO){
}
__delay_us(10);
forward = ((unsigned int)ADRESH << 8) + (unsigned int)ADRESL;
ADCON0 = 0b00001101; // AN3
__delay_us(10);
ADCON0bits.GO = 1;
while(!ADCON0bits.GO){
}
__delay_us(10);
reverse = ((unsigned int)ADRESH << 8) + (unsigned int)ADRESL;
if(for_rev == 1){
if(reverse < 118) po = (reverse * 5428) / 10000;
else po = ((reverse * reverse + 20000) * 19) / 10000;
po = po * 11 / 10;
}
else if(for_rev == 0){
if(forward < 118) po = (forward * 5428) / 10000;
else po = ((forward * forward + 20000) * 19) / 10000;
po = po * 11 / 10;
}
var_disp_conv(0,0,po);
pow_disp(0,10);
if(forward > 10 && forward > reverse) vswr = (forward + reverse) *10 / (forward - reverse);
else vswr = 9;
if(vswr > 9) var_disp_conv(1,0,(vswr - 10) * 30);
else var_disp_conv(1,0,0);
vswr_disp(1,10);
msec_delay(40);
}
}
void msec_delay(unsigned short time){
unsigned short i;
for(i=0;i<time;i++){
__delay_ms(1);
}
}
void lcd_init(void){
lcd_cmd(0x38);
lcd_cmd(0x39);
lcd_cmd(0x14);
lcd_cmd(0x70 + contrast);
// lcd_cmd(0x73);
lcd_cmd(0x56); // 3.3V
// lcd_cmd(0x52); // 5V
lcd_cmd(0x6C);
msec_delay(210);
lcd_cmd(0x38);
lcd_cmd(0x0C);
lcd_cmd(0x01);
msec_delay(2);
}
void I2C_send(unsigned char data){
SSP1IF = 0;
SSP1BUF = data;
while(!SSP1IF){
}
}
void lcd_cmd(unsigned char work){
SSP1CON2bits.SEN = 1;
while(SSP1CON2bits.SEN){
}
I2C_send(LCD_AD);
I2C_send(0x80); // Co=1, RS=0
I2C_send(work);
SSP1IF = 0;
SSP1CON2bits.PEN = 1;
while(SSP1CON2bits.PEN){
}
SSP1IF = 0;
__delay_us(30);
}
void lcd_data(unsigned char work){
SSP1CON2bits.SEN = 1;
while(SSP1CON2bits.SEN){
}
I2C_send(LCD_AD);
I2C_send(0xC0); // Co=1, RS=1
I2C_send(work);
SSP1IF = 0;
SSP1CON2bits.PEN = 1;
while(SSP1CON2bits.PEN){
}
SSP1IF = 0;
__delay_us(30);
}
void lcd_position(unsigned char li, unsigned char col){
lcd_cmd(0x80 | (li << 6) | col);
}
void lcd_str_disp(unsigned char li, unsigned char col, const char *string){
unsigned char i = 0;
lcd_position(li,col);
while(((col + i) < 16) && string[i]){
lcd_data(string[i]);
i++;
}
}
void lcd_char_disp(unsigned char li, unsigned char col, unsigned char ascii){
lcd_position(li,col);
lcd_data(ascii);
}
void pow_disp(unsigned char li, unsigned char col){
if(for_rev == 0) lcd_char_disp(li,col,'F');
else if(for_rev == 1) lcd_char_disp(li,col,'R');
if(po < 1000) lcd_data(' ');
else lcd_data(po / 1000 + '0');
po %= 1000;
lcd_data(po / 100 + '0');
lcd_data('.');
po %= 100;
lcd_data(po / 10 +'0');
lcd_data('W');
}
void vswr_disp(unsigned char li, unsigned char col){
lcd_str_disp(li,col,"SWR");
if(vswr < 10){
lcd_data(' ');
lcd_data(' ');
lcd_data(' ');
}
if(vswr < 100){
lcd_data(vswr / 10 + '0');
lcd_data('.');
lcd_data(vswr % 10 + '0');
}
else{
lcd_data('>');
lcd_data('1');
lcd_data('0');
}
}
void lcd_clear(void){
lcd_cmd(0x01);
msec_delay(2);
}
void cgram_set(void){ // bargraph caharacter setting
unsigned char i;
for(i=0;i<7;i++){
lcd_cmd(0x40 + i); // bar0
if(i == 0) lcd_data(0x01);
else if(i == 1) lcd_data(0x15);
else lcd_data(0x00);
lcd_cmd(0x48 + i); // bar1
if(i == 0) lcd_data(0x01);
else if(i == 1) lcd_data(0x15);
else lcd_data(0x10);
lcd_cmd(0x50 + i); // bar2
if(i == 0) lcd_data(0x01);
else if(i == 1) lcd_data(0x15);
else lcd_data(0x14);
lcd_cmd(0x58 + i); // bar3
if(i == 0) lcd_data(0x01);
else lcd_data(0x15);
lcd_cmd(0x60 + i); // bar4
if(i == 0) lcd_data(0x07);
else lcd_data(0x17);
}
lcd_cmd(0x68);
lcd_data(0x11);
lcd_cmd(0x69);
lcd_data(0x15);
lcd_cmd(0x6A);
lcd_data(0x15);
lcd_cmd(0x6B);
lcd_data(0x0A);
lcd_cmd(0x6C);
lcd_data(0x00);
lcd_cmd(0x6D);
lcd_data(0x00);
lcd_cmd(0x6E);
lcd_data(0x00);
}
void var_disp_conv(unsigned char li, unsigned char col, unsigned int val){
char value = 0;
value = (char)(val >> 5);
unsigned char col_max = 0, reg_col = 0;
col_max = value / 3;
reg_col = value % 3;
lcd_position(li, col);
if(value < 28)
{
for(unsigned char i=0;i<col_max;i++){
lcd_data(3);
}
if(col_max < 9){
lcd_data(reg_col);
for(unsigned char i=0;i<(8-col_max);i++){
lcd_data(0);
}
}
}
else{
for(unsigned char i=0;i<8;i++){
lcd_data(3);
}
lcd_data(4);
}
}
(EOF)
最新のMPLAB X IDEとXC8コンパイラ(フリー版)でコンパイル可能です。(フリー版でない場合は最適化オプションによっては動作がうまくいかない可能性があります。検証していませんが(通常版持ってないし))
次はPCBデザイン編です^^
わざわざマイコンなんて使わなくても感度の良い電流計を取り付けて校正すればいいじゃん・・・はい、ごもっともです(汗)
しかし高周波とマイコンが融合する姿に私としては非常に引き付けられるものがあるので、ここはぜひマイコンを活用しようではないか!というわけで強引に進めていきます(笑)
具体的にマイコンを選定する前に、何をマイコンにさせるかということを決めておくことが大事です。それからマイコン動作の基本を理解することです。マイコン動作の本質は『計算』です。つまり、外から入力したデータを『計算』して出力に送る、ということです。その『計算』の手順を示すものがいわゆるプログラムということになります。
今回製作した通過型電力計に当てはめてみます。まずデータの入力ですが、進行波や反射波の電圧レベルはそのままマイコンで計算することができないので、最初にアナログーディジタル変換(AD変換)によって電圧レベルをディジタル値に変換します。このディジタル値をもって初めてマイコンによる『計算』が可能になります。それから計算した結果をキャラクターLCDディスプレイで表示するため、計算結果を含めたデータをLCDに送ることによってLCDに表示させます。あとはおまけとしてLCDバックライト電源を制御する出力とファンクションボタン入力を加えています。
ということで、マイコンに必要なポートを列挙してみると・・・
1.入力
進行波電圧入力(アナログ)
反射波電圧入力(アナログ)
ファンクションボタン(ディジタル)
2.出力
LCDへの通信ポート(今回は2線シリアルI2Cバス SCLポート,SDAポート)
LCDバックライト電源ポート(ディジタル出力)
と、入出力合わせて6ポート必要になります。
ということは、8ピンPICですべてのポートを使い切るということになります。特に機能拡張をする予定はないので必要最小限の8ピンPICを使うことにしました。
8ピンのPICは12Fシリーズがポピュラーですが、その中でも上位クラスの拡張ミッドレンジコアを持っている12F1840を採用しました。値段も秋月で1個120円と非常に安価です。これでAD変換やEEPROM、いくつかの通信モジュールもひとつ小さなパッケージに内蔵されているのは驚きです。
最近の8bitPICにはほとんどがAD変換とシリアル通信、EEPROMモジュールが内蔵されていて、発振源も内蔵CR発振が選択出来て外部に水晶振動子やセラロックを接続しなくても良い設計になっています。ましてや8ピンしかないマイコンには内部発振は必須といってよいかも知れません。
またPICのピンには各々役割が複数あって、初期設定で各々のモジュールのレジスタに書き込み設定を行っています。今回のプログラムでの割り当ては赤色の枠になっています。
では処理の流れをブロックダイヤグラム風に示してみます。
実際のプログラムに落とし込みますが、最初にPICのコンフィギュレーションの記述やヘッダファイルxc.hのインクルード、初期設定(PIC各ピン(入出力、アナログ・ディジタル、プルアップなど)、モジュール(ADC、I2Cバス))を行い、メインループ内にAD変換、各電力値とVSWR値計算、計算結果表示関数を置きます。
通常表示するだけであればメインループ内で繰り返し処理を行うようにしてほぼリアルタイムに表示させていけばOKですが、進行波と反射波表示切替やバックライトの制御を加えて少し使いやすくしてみます。
通常自分がスイッチによる制御を加える場合は、タイマー割込み処理でスイッチ状態を監視してメイン処理を修飾するようにしています。
タイマー処理はTIMER0で割り込みをかけるようにしています。時間設定は1ミリ秒としてスイッチの状態をメモリにコピーし、コピーしたメモリを参照してあるパターンに一致したときにフラグを立ててメインループ内でフラグに応じた処理を修飾するという流れです。
スイッチは一つしかありませんが、押し続ける時間によって機能をいくつか持たせるようにしてピンの少なさをカバーしています。(スイッチポートをアナログにして電圧変化に応じて複数の機能を持たせる方法もあります)
次にメインの計算処理についてですが、AD変換された値(AD value)は電圧の整数値(符号なしの10bit値)です。電力はW=V^2 / Rで導かれますので、電力値に変換するにはAD valueを2乗し適当な定数を乗すれば良さそうです。この方法ではある程度信号が大きければ問題ないのですが、ダイオードによる整流のためダイオードのVf付近つまりはQRP電力の場合ダイオードのひずみにより、単純にAD valueの2乗ですべてOK!というわけにはいきません。
そこで実際の電力値とAD valueを測定、表にプロットして計算方法を探ることにしました。
縦軸は出力電力値、横軸はPICのAD変換で得られたAD value ( = det) の2乗値det^2です。
det^2値が14000超えの場合はdet^2とPowerはほぼ1次関数に収まりますが、14000以下の場合はダイオードの低VfでのVf-If特性を踏まえて上表の青色の式のように当てはめるようにしました。(2次曲線に近似しており、この領域では整流電圧と電力の関係を1次関数とみなしています。言い訳ですが(笑)、この程度の測定器であればこれ以上突き詰めても仕方ないでしょう。)
また計算途中で必要な変数の型については、AD変換で得られる値のbit桁数は10bitなので2乗としても20bitあれば事足ります。ですので計算プログラムの変数の型はunsigned longでOKです。整数同士の計算なので4MHz駆動の8bitPICでもサクサク動いてくれます。
またVSWRの計算はおなじみの
VSWR値 = (|Vf| + |Vr|) / (|Vf| - |Vr|)
をそのまま当てはめています。
最後に表示にはI2C接続の16x2キャラクタLCDを使い、LCDとのI2C通信にはMSSPモジュール使用しています。
それでは実際のコードを公開しちゃいます。
//
// VSWR_meter.c
// Copyright JL1VNQ / HARU
//
//
// ver.1.00 9 June 2019
// first release
//
// ver.1.10 12 June 2019
// change power calculation algorithm
//
#define EEPROM_SIZE 256
#define _XTAL_FREQ 4000000
#include <xc.h>
//for 12F1840 config
#pragma config FOSC = INTOSC, WDTE = OFF, PWRTE = OFF, MCLRE = OFF, CP = OFF
#pragma config CPD = OFF, BOREN = OFF, CLKOUTEN = OFF, IESO = OFF, FCMEN = OFF
#pragma config WRT = OFF, PLLEN = OFF, STVREN = ON, LVP = OFF
#define POW LATAbits.LATA5 // backlight LED control
#define FUNC PORTAbits,RA3 // function switch
#define LCD_AD 0x7C // Akiduki's I2C LCD(AQM0802, AQM1602) address
#define TMR0_set 0x83 // TMR0 1msec interval
unsigned char contrast = 5;
unsigned long forward = 0;
unsigned long reverse = 0;
unsigned long po = 0; // calculated power (x10^-1 watts)
unsigned int vswr = 0; // VSWR * 10
unsigned char for_rev = 0; // display change (0:forward, 1:reverse)
void msec_delay(unsigned short time);
void I2C_send(unsigned char data);
void lcd_cmd(unsigned char work);
void lcd_data(unsigned char work);
void lcd_init(void);
void lcd_clear(void);
void cgram_set(void);
void lcd_position(unsigned char li, unsigned char col);
void lcd_str_disp(unsigned char li, unsigned char col, const char *string);
void lcd_char_disp(unsigned char li, unsigned char col, unsigned char ascii);
void var_disp_conv(unsigned char li, unsigned char col, unsigned int val);
void pow_disp(unsigned char li, unsigned char col);
void vswr_disp(unsigned char li, unsigned char col);
void __interrupt() isr(void){
if(INTCONbits.TMR0IF){
INTCONbits.GIE = 0;
TMR0IF = 0;
static unsigned int cnt0 = 0, cnt1 = 0;
static unsigned char sw_mem = 0, sw_down = 0, dim = 0;
sw_mem <<= 1;
if(FUNC == 0) sw_mem |= 1;
if((sw_mem & 0x0F) == 0x03) sw_down = 1;
if(sw_down == 1){
if(cnt0 < 2000) cnt0 ++;
if(cnt0 < 1000){
if((sw_mem & 0x0F) == 0x00){
if(dim == 0) dim = 1;
else if(dim == 1) dim = 0;
sw_down = 0;
cnt0 = 0;
cnt1 = 20000;
}
}
else{
if(for_rev == 1) for_rev = 0;
else if(for_rev == 0) for_rev = 1;
sw_down = 0;
cnt0 = 0;
}
}
if(dim == 1){
if(forward > 10) cnt1 = 5000;
if(cnt1 > 0){
cnt1 --;
if(POW == 0) POW = 1;
}
else POW = 0;
}
else POW = 0;
TMR0 = TMR0_set;
INTCONbits.GIE = 1;
}
else if(INTCONbits.IOCIF){ // for Interrupt On Change(hang-up occur if compiling without this code)
INTCONbits.GIE = 0;
IOCAF = 0;
INTCONbits.GIE = 1;
}
}
void main(void){
OSCCON = 0x6A; // 4MHz internal OSC no PLL
PORTA = 0x00;
ANSELA = 0x11; // RA4, RA0 Analog Input
TRISA = 0x1F;
WPUA = 0x2E; // PORTA weak pull-up
OPTION_REG = 0x02; // weak pull_up, TMR0 internal clock(1us/cycle), prescaler 1:8
POW = 1; // LCD LED POW PORT on
SSP1CON1 = 0b00101000; // I2C Master mode
SSP1STAT = 0b10000000;
SSP1ADD = 9; // I2C Freq = (SSP1ADD + 1)*4/Fosc = 100kHz
ADCON1 = 0b11000000; // ADFM = 1 (right), ADCS = 100 (fosc/4), ADPREF = 00 (Vref = VDD)
ADCON0bits.ADON = 1; // ADC module enable
msec_delay(10);
lcd_init();
cgram_set();
lcd_str_disp(0,0,"VSWR Meter QPM01"); //startup splash for AQM1602
lcd_str_disp(1,0,"(c)HARU 20190612");
msec_delay(750);
POW = 0;
lcd_clear();
TMR0 = TMR0_set; // 1msec
INTCONbits.TMR0IF = 0;
INTCONbits.TMR0IE = 1;
INTCONbits.IOCIE = 1;
INTCONbits.GIE = 1;
IOCAN = 0xFF; // interrupt on change negative edge detect
IOCAF = 0;
while(1){
ADCON0 = 0b00000001; // AN0
__delay_us(10);
ADCON0bits.GO = 1;
while(!ADCON0bits.GO){
}
__delay_us(10);
forward = ((unsigned int)ADRESH << 8) + (unsigned int)ADRESL;
ADCON0 = 0b00001101; // AN3
__delay_us(10);
ADCON0bits.GO = 1;
while(!ADCON0bits.GO){
}
__delay_us(10);
reverse = ((unsigned int)ADRESH << 8) + (unsigned int)ADRESL;
if(for_rev == 1){
if(reverse < 118) po = (reverse * 5428) / 10000;
else po = ((reverse * reverse + 20000) * 19) / 10000;
po = po * 11 / 10;
}
else if(for_rev == 0){
if(forward < 118) po = (forward * 5428) / 10000;
else po = ((forward * forward + 20000) * 19) / 10000;
po = po * 11 / 10;
}
var_disp_conv(0,0,po);
pow_disp(0,10);
if(forward > 10 && forward > reverse) vswr = (forward + reverse) *10 / (forward - reverse);
else vswr = 9;
if(vswr > 9) var_disp_conv(1,0,(vswr - 10) * 30);
else var_disp_conv(1,0,0);
vswr_disp(1,10);
msec_delay(40);
}
}
void msec_delay(unsigned short time){
unsigned short i;
for(i=0;i<time;i++){
__delay_ms(1);
}
}
void lcd_init(void){
lcd_cmd(0x38);
lcd_cmd(0x39);
lcd_cmd(0x14);
lcd_cmd(0x70 + contrast);
// lcd_cmd(0x73);
lcd_cmd(0x56); // 3.3V
// lcd_cmd(0x52); // 5V
lcd_cmd(0x6C);
msec_delay(210);
lcd_cmd(0x38);
lcd_cmd(0x0C);
lcd_cmd(0x01);
msec_delay(2);
}
void I2C_send(unsigned char data){
SSP1IF = 0;
SSP1BUF = data;
while(!SSP1IF){
}
}
void lcd_cmd(unsigned char work){
SSP1CON2bits.SEN = 1;
while(SSP1CON2bits.SEN){
}
I2C_send(LCD_AD);
I2C_send(0x80); // Co=1, RS=0
I2C_send(work);
SSP1IF = 0;
SSP1CON2bits.PEN = 1;
while(SSP1CON2bits.PEN){
}
SSP1IF = 0;
__delay_us(30);
}
void lcd_data(unsigned char work){
SSP1CON2bits.SEN = 1;
while(SSP1CON2bits.SEN){
}
I2C_send(LCD_AD);
I2C_send(0xC0); // Co=1, RS=1
I2C_send(work);
SSP1IF = 0;
SSP1CON2bits.PEN = 1;
while(SSP1CON2bits.PEN){
}
SSP1IF = 0;
__delay_us(30);
}
void lcd_position(unsigned char li, unsigned char col){
lcd_cmd(0x80 | (li << 6) | col);
}
void lcd_str_disp(unsigned char li, unsigned char col, const char *string){
unsigned char i = 0;
lcd_position(li,col);
while(((col + i) < 16) && string[i]){
lcd_data(string[i]);
i++;
}
}
void lcd_char_disp(unsigned char li, unsigned char col, unsigned char ascii){
lcd_position(li,col);
lcd_data(ascii);
}
void pow_disp(unsigned char li, unsigned char col){
if(for_rev == 0) lcd_char_disp(li,col,'F');
else if(for_rev == 1) lcd_char_disp(li,col,'R');
if(po < 1000) lcd_data(' ');
else lcd_data(po / 1000 + '0');
po %= 1000;
lcd_data(po / 100 + '0');
lcd_data('.');
po %= 100;
lcd_data(po / 10 +'0');
lcd_data('W');
}
void vswr_disp(unsigned char li, unsigned char col){
lcd_str_disp(li,col,"SWR");
if(vswr < 10){
lcd_data(' ');
lcd_data(' ');
lcd_data(' ');
}
if(vswr < 100){
lcd_data(vswr / 10 + '0');
lcd_data('.');
lcd_data(vswr % 10 + '0');
}
else{
lcd_data('>');
lcd_data('1');
lcd_data('0');
}
}
void lcd_clear(void){
lcd_cmd(0x01);
msec_delay(2);
}
void cgram_set(void){ // bargraph caharacter setting
unsigned char i;
for(i=0;i<7;i++){
lcd_cmd(0x40 + i); // bar0
if(i == 0) lcd_data(0x01);
else if(i == 1) lcd_data(0x15);
else lcd_data(0x00);
lcd_cmd(0x48 + i); // bar1
if(i == 0) lcd_data(0x01);
else if(i == 1) lcd_data(0x15);
else lcd_data(0x10);
lcd_cmd(0x50 + i); // bar2
if(i == 0) lcd_data(0x01);
else if(i == 1) lcd_data(0x15);
else lcd_data(0x14);
lcd_cmd(0x58 + i); // bar3
if(i == 0) lcd_data(0x01);
else lcd_data(0x15);
lcd_cmd(0x60 + i); // bar4
if(i == 0) lcd_data(0x07);
else lcd_data(0x17);
}
lcd_cmd(0x68);
lcd_data(0x11);
lcd_cmd(0x69);
lcd_data(0x15);
lcd_cmd(0x6A);
lcd_data(0x15);
lcd_cmd(0x6B);
lcd_data(0x0A);
lcd_cmd(0x6C);
lcd_data(0x00);
lcd_cmd(0x6D);
lcd_data(0x00);
lcd_cmd(0x6E);
lcd_data(0x00);
}
void var_disp_conv(unsigned char li, unsigned char col, unsigned int val){
char value = 0;
value = (char)(val >> 5);
unsigned char col_max = 0, reg_col = 0;
col_max = value / 3;
reg_col = value % 3;
lcd_position(li, col);
if(value < 28)
{
for(unsigned char i=0;i<col_max;i++){
lcd_data(3);
}
if(col_max < 9){
lcd_data(reg_col);
for(unsigned char i=0;i<(8-col_max);i++){
lcd_data(0);
}
}
}
else{
for(unsigned char i=0;i<8;i++){
lcd_data(3);
}
lcd_data(4);
}
}
(EOF)
最新のMPLAB X IDEとXC8コンパイラ(フリー版)でコンパイル可能です。(フリー版でない場合は最適化オプションによっては動作がうまくいかない可能性があります。検証していませんが(通常版持ってないし))
次はPCBデザイン編です^^
秋月Cタイプユニバーサル基板に実装テスト 1.8~50MHz帯、20Wまで使えそうです |
2019年7月2日火曜日
通過型電力計の製作(その1~回路編)
送信しても呼ばれないなぁとかなんとなく感度が悪いなぁと思っていたら、極端な話エレメントが切れていたなんてこともない話ではありません。
というわけで運用中のモニタリングとして通過型電力計の必要性を感じました。メーカー製のしっかりしたものもありますがここはやはり自作で、ということで製作を試みました。
それでは最初に回路を吟味していきましょう。通過型電力計は方向性結合器と整流部、それに表示部で構成されています。HF~VHF帯で使用されている回路は、方向性結合器としてCM結合のものが多く、整流にはVfの低いゲルマダイオードやショットキーダイオード、表示部はメカニカルなメーター(パネルメーターやラジケーター)といったところが通常でしょうか。
まずは方向性結合器ですが、先ほど述べた通り自作の通過型電力計として好んで採用されている回路のほとんどがCM型と呼ばれる回路です。
CM型は図のように電流検出用としてトロイダルトランスなどを用い、コンデンサにより電圧検出を行っています。この方式は比較的単純ですが、素子によるばらつきや周波数による結合度の変化など不安定要素もあり、なるべく調整箇所を少なくしたいことから次の図のように電圧検出もトランスを用いた回路を採用してみました。(今回mcHFに採用されている回路を参考にしました)
これってMM型っていうのでしょうか?タンデム型と呼んでいる文献も見かけたことがありますが、正式には何と呼んでいるのかは分かりません。いずれにせよトランスの巻き数比によって計算通りにインピーダンス変換することが可能です。
それから各トランスの巻き数比については大きくとることで電力計挿入による影響(損失や不整合、高調波の発生など)を少なくする事が出来ますが、検出した信号をダイオードで整流する場合信号が小さい、特にダイオードのVfを下回る時には電流はほとんど流れません。したがってこの場合例えば出力が小さいQRPの場合VSWRの値は過小評価になってしまいます。
色々な製作例などを見てみると巻き数比10:1として20dBカップラーとしているものがほとんどですが、これはダイオード整流の場合の妥協点なのかなと勝手に想像しています。実際今回のプロジェクトでは5W前後の出力を対象にするつもりなのでこの巻き数比で進めることにします。
もしQRPp(~27dBm)から100W級(+50dBm~)を一台でカバーする電力計を作るとするならばトランスの巻き数比は33:1にして、アッテネーターを加え十分レベルを下げたところで整流(レベル検出)にダイナミックレンジの広いログアンプを使うべきでしょう。
整流には定番のゲルマニウムダイオード1N60よりもVfが低く高周波特性の良い1SS106というショットキーダイオードを採用しました。2019年7月現在秋月でセカンドソース品が販売されています。負荷抵抗はやや高めの10kΩとしてその後にPICマイコンのアナログ入力ポートに接続します。PICの入力ポート保護のためにツエナーダイオードなどによるクランプやオペアンプのボルテージフォロワを前置したほうが安全ですが、実測20Wまでなら整流電圧がPICの電源電圧を超えないことを確認したので、5W前後での使用であれば問題なさそうということでシンプルさを優先して省略しました。
進行波、反射波それぞれの整流電圧を8ピンPICのPIC12F1840の対応するアナログポートに接続しPIC内でAD変換、数値演算とLCD表示制御を行います。このPICは8ピンPICの中でもプログラムメモリやRAMサイズも大きく、今回のプログラムを純正のXC8コンパイラフリー版でコンパイルしてもプログラムエリアの50%弱に収まっています。しかも1個120円(秋月)と安いので使い勝手、コストパフォーマンスが非常に良いです。その代わりピン数が少なく今回の製作ではすべてのピンを使い切っています。でも逆に制限があるほうがいろいろと工夫を考えるのでこれはこれで良いのかもしれません。
というわけで下に固まった回路図と、プロトタイプ版の画像を上げておきます。
ディジタル通過型電力・VSWR計回路図 |
秋月基板Cタイプに方向性結合器と整流部を実装し動作テスト |
~次はプログラム編に移ります。
登録:
投稿 (Atom)