ラズパイ日記

2023年1月15日 (日)

NEO-7M GPSモジュールが中国から届いた

アマゾンでオーダーしたNEO-7M GPSモジュールが中国郵便ePacketで届いた。PICO-TNCのGPSモジュールとして使えるか確認する。

届いたのはモジュール基板とアンテナモジュール、ピンヘッダー。
Img03600_small

ピンヘッダーを半田付けして、RS232Cレベル変換モジュールを通してPCのコムポートに接続してGPSモジュールの出力を見てみた。
Img03622_small

GPSモジュールは以下のテキストデータを1秒間隔で出力し続けている。

$GPRMC,013837.00,A,3540.78920,N,13738.11534,E,0.134,,150123,,,A*76
$GPVTG,,T,,M,0.134,N,0.248,K,A*2B
$GPGGA,013837.00,3540.78920,N,13738.11534,E,1,06,1.91,540.5,M,36.5,M,,*53
$GPGSA,A,3,25,32,24,10,23,12,,,,,,,3.20,1.91,2.57*09
$GPGSV,3,1,09,10,47,226,29,12,51,054,26,22,29,312,10,23,26,190,12*7B
$GPGSV,3,2,09,24,22,071,09,25,79,103,30,29,12,146,08,31,26,273,26*77
$GPGSV,3,3,09,32,61,324,29*48
$GPGLL,3540.78920,N,13738.11534,E,013837.00,A,A*6D

この出力内容の内、$GPGGAはプロトタイプで使っていたGPSモジュールが出力していた$GNGGAとフォーマットが同じであることが確認できたので、この出力をそのままPICO-TNCのコードに読み取らせた。

結果は良好で、PICO-TNCはビーコンを発生することができた。なお、NEO-7Mモジュール基板はGPSデータ出力時にLEDを点灯する。よって、このLEDの点灯でGPSデータ受信が行われていることが分かる。
Img03644_small

NEO-7Mモジュールをプラケースにねじ止め固定した。
Img03647_small

蓋を閉じる前の様子はこんな感じ。ケース内のスペースにはまだ若干の余裕がある。
Img03646_small

蓋を閉じるとこんな感じになる。FT-70Dよりも一回りちょっと大きなケースとなるが、プロトタイプ1号機としてはまずまずの出来だと思う。
Img03648_small

PICO-TNCとFT-70Dで近所を軽トラ(超低速)で走ってみた。Beaconインターバルは1分。プロットはいずれも移動経路上にあり、GPSとしては使いそうと判断した。
20230115neo7mtracking

このNEO-7Mモジュール+アンテナモジュールはアマゾンで850円程。送料が800円かかる。とりあえず追加で3個をオーダーした。

追記

このGPSモジュールは捕捉できるGPS衛星の数が5から8個程度のようだ。秋月で購入したGPSモジュールは、みちびきも捕捉できるので12個だった。つまりこのGPSもジュールはそれなりの誤差があるということだ。

$GPGGA,013837.00,3540.78920,N,13738.11534,E,1,06,1.91,540.5,M,36.5,M,,*53
$GPGGA,202824.00,3540.79166,N,13738.11552,E,1,07,1.55,510.8,M,36.5,M,,*59
$GPGGA,221438.00,3540.78683,N,13738.11710,E,1,08,1.10,513.7,M,36.5,M,,*52

実験をしている作業場所をGoogle Mapでみてみると35.67982(3540.7892)、137.63527(13738.1162)とでてくる。標高も30メートルは前後する様子だ。

2023年1月 9日 (月)

TNCアプリにおけるDemoduatorの動作確認

PICO TNCのdemodulatorにおける信号処理を調べてみた。

demodulatorはADCデータを1バイト読み込み、ある値sumを出す。sumを算出するに当たってdemodulatorは以下の信号処理を行っている。

  • Bandpass Filter 入力:adc  出力:val
  • Digital Correlator (Delay) 入力:val  出力:m
  • Lowpass Filter  入力:m  出力:sum

それぞれの処理後の値をプロットしてみた。

まずは全体像。それぞれの値に大きな差があるため、わかりにくくなっているが、mとsumの関係はわかる。
Graphtotal

ADCデータ列
Graph1

Bandpass Filterの出力。通過周波数は900Hzから2500Hz。これによってOffset成分がなくなる。
Graph2

Delayの出力。DelayはMarkとSpaceのTone差を最大化することを目的としている。これにより、両Toneの分別がしやすくなる。ここでは446usのDelay値を積算することで1200Hzは正値、2200Hzは負値となるようにしている。一般的なDelayはDelay値を加算しているが、ここでは積算している。
  m=val[t] * val[t-446us]    なお、Sampling Rate =1200x11では6サンプル分遅延した値を積算している。
Graph3
6サンプリング前の値を取り出す方法として、6エレメント(6サンプル)のリングバッファを使っている。リングバッファの場合、現在のポインタをtとすると、t+1は現在よりも5サンプリング前のデータが格納されている。t+2は4サンプリング前、、、、といった具合。なのでtは今から6サンプリング前のデータ格納されている訳で、その値を取り出してから、tに新しくサンプリングしたデータを格納する。こうすることで、過去のサンプリング値を取り出したあと、そこに新しいサンプリングデータを格納し、ポインターを一つ進める、という動作を繰り返すことで常に6サンプリング前のデータを取り出すことができる。

Lowpass Filterの出力結果。カットオフ周波数は1200Hz。プラス部分がSpace(2200Hz)、マイナス部分がTone(1200Hz)となってる。逆に言えば、この波形を取得するためにDelayを使っている。
Graph4

レベル識別の閾値は以下のとおり

  • sum <  - 4096 : bit=1 (Mark)
  • sum >=  4096 : bit=0 (Space)

DIGITAL PLL

Ditigal PLLは11回で1 Time Domainを形成している。

  • PLL Counter Step = 390,451,572
  • PLL Counterは正値5回負値6回、または正値6回負値5回の合計11回で1 time domainとなる。
    • PLL Step 11回でbit decode
  • 入力bitに遷移が発生した時にPLL Counterを25%調整する。
    • PLL Counter値が負値の場合は25%加算し、正値の場合は25%減算する。

このPLL Counter値の正負で25%加算減算するのがミソで、bit遷移が発生した時点でのPLL Counter値からPLL Counter値自体を調整し、遷移点とTime Domainの位置関係を保っている。

実験結果

PICO_TNCが発生するAFSK信号をPCに取り込み、サンプルコードでDelay(積算)とLowpass Filterを通してみた。

Beacon Delay Lpf

Beaconデータ:ダウンロード - mice7mono.wav

Delayコード:ダウンロード - dd_psude_stereo2.c

Delay後データ:ダウンロード - mice7mono_output.wav

LPFコード:ダウンロード - dd_lpf2.c

LPF後データ:ダウンロード - mice7mono_output_output.wav

 

2023年1月 4日 (水)

AFSKでの復調について

AFSKの復調について大変参考になる情報があったので、その要約も含めて備忘録。

 

復調における課題:

  • Low SNR
  • Hight Twist -  Mark信号とSpace信号の大きな振幅差
  • Phase distortion - Mark信号とSpace信号が符号境界に到達しない
  • Inter-Symbol interference - Mark信号とSpace信号がPhase Distortionにより重なる
  • Frequency Distortion - Tone周波数とData Rateとの大きな差
  • No error correction - 1ビットエラーで破棄

Hight Twist対策

Digital Correlator

Comb Filter(自らの遅延信号によりその信号自体を増幅する手法)の利用。

Comb

Delayを最適化することでMarkとSpaceのTone差を最大化することができる。
Zero Cross Detectorを使ってアナログ信号をデジタル変換し、同じDelayをComb Filterに適用する。

テストでの入力信号は以下。ジッターを発生させる低周波成分が含まれていてこのままではZero Cross Detectorにかけられない。
Wave1

1200Hz-2200Hzを通過させるBandpass Filterを通すと、低周波成分が除去できる、
Wave2

Bandpass Filterを通した波形をZero Cross Detectorにかける。

Wave3

Digital Comb Filterを適用する。Digital Comb Filterでは以下を実行する。

  1. Zero Cross Detector出力をDelayさせた信号とXORを取る
  2. 得られた信号に対してLow Pass Filterをかける。
  3. 得られた波形に対してZero Cross Detectorをかける。

XORを取った信号波形

Wave4

Cutoff 760HzでLowpass Filterをとおした波形

Wave5

Lowpass Filterを通した波形に対してZero Cross Detectorをかける。

Wave6

Clock Recovery

送信信号に対して、受信側のクロックを同期させる必要がある。そうしないと上記Bit Periodが得られない。

この為にTX Dlayがある。TX Delayで送られる信号でこのClock同期をとる。よってTX Delayはそれなりの長さが必要となる。

この過程においてCarrierシグナルからビット周期(パルス幅)を測定する。

Wave7

クロック同期をとるためにDigital PLLをつかう。Digital PLLはジッター量を計り、そのジッター量により、Data Carrierに同期しているかを判断する。

ここでは、シンボル周期をサンプリング数22で測定している。この範囲で上記矩形幅によってPLLを遅らせたり、進めたりする。

ロック状態と判断するには、ジッター幅が1.1サンプル以内になった時、アンロックと判断するのはジッター量が4.4サンプル以上となった時。

2023年1月 2日 (月)

APRSでのNRZIについて備忘録

APRSでのNRZI方式について備忘録

APRSはHDLCフレームをAX.25で転送するとの理解で考えると、APRSのTNCはNRZ-SpaceでEncode/Decodeしていると解釈される。

NRZ-Spaceはレベル遷移をゼロで行う。一般的なNRZIは1でレベル遷移実行となっているからここが大きな違いだ。

これについてはWikiwandに書かれている。以下がNRZ-Space部分の抜粋。

Non-return-to-zero space

Non-return-to-zero space
Encoder for NRZS, toggle on zero

"One" is represented by no change in physical level, while "zero" is represented by a change in physical level. In clock language, the level transitions on the trailing clock edge of the previous bit to represent a "zero".

This "change-on-zero" is used by High-Level Data Link Control and USB. They both avoid long periods of no transitions (even when the data contains long sequences of 1 bits) by using zero-bit insertion. HDLC transmitters insert a 0 bit after 5 contiguous 1 bits (except when transmitting the frame delimiter "01111110"). USB transmitters insert a 0 bit after 6 consecutive 1 bits. The receiver at the far end uses every transition — both from 0 bits in the data and these extra non-data 0 bits — to maintain clock synchronization. The receiver otherwise ignores these non-data 0 bits.

更に重要なのは赤字部分。1が5つ続いたら0を挿入すること。これを知らないとデコードが出来ない。

2022年12月31日 (土)

pico_tnc 2200Hz(Space)でPhaseをずらす件の備忘録

GitHubからダウンロードしたpico_tncのsend.cのsend_byte()についての考察記録

なぜ、mark_tabのサイズは121なのか,、そしてspace_tabのサイズは96なのか。

wave_table.h
#define PHASE_CYCLE 6
#define MARK_TAB_LEN 121 // 66 * 5/6 + 66
#define SPACE_TAB_LEN 96 // 36 * 5/6 + 66
mark_tab[MARK_TAB_LEN]={.....  1200Hz正弦波
space_tab[SPACE_TAB_LEN]={....     2200Hz正弦波
phase_tabは、spaceは6サンプリング周期ごと、markは11サンプリングごとに区切られ、6区画で1波形となる。
つまり、1波形を6等分したもの=60度分。
1200baudでは66サンプリングで1サイクル=360度となる。
static const uint32_t *phase_tab[2][PHASE_CYCLE] = {
{ &space_tab[0], &space_tab[6], &space_tab[12], &space_tab[18], &space_tab[24], &space_tab[30], },
{ &mark_tab[0], &mark_tab[11], &mark_tab[22], &mark_tab[33], &mark_tab[44], &mark_tab[55], },
};
phase_tabには1波形分のサンプリングを6等分(60度分)した数の波形データが入れられており、最終エントリーの先頭から66サンプリング分(360度=1DMA分)が設定されている。つまり、どのエントリーからDMAを始めても1DMA分のデータは揃っていることになる。
send.c
dma_handler()
....
データの1bit単位で波形を1サイクル分設定する
phase_tabから取り出すポインターから66サンプリング分(360度分)がdma_blockに格納される。
   tp->dma_blocks[next][idx++] = phase_tab[level][phase]; // 0: space, 1:mark
仮にlevel=1, phase=5の場合、phase_tab[1][5], つまり mark_tab[55](300度)から66サンプリング(6phase分=360度分)が取り出されることになる。
したがってサンプリングテーブル(mark_tab)はその長さ分(66x5/6=300度)が1波形分(66=360度)よりも余分に必要となる(合計 66x5/6 + 66=121)。
level=0, つまりspaceだったら次回の生成波形は60度(6サンプリング分)波形を前にずらす。
   if (!level) { // need adjust phase if space (2200Hz)
      if (--phase < 0) phase = PHASE_CYCLE - 1;
   }
減算する前のphaseが0、つまり先頭だったら最後尾にする。
なぜspace (2200Hz)だと1波形ごとに6サンプリング分前にずらすのか。
1200Hzで1サイクルが完結する時間スロットでは2200Hzでは2200/1200=11/6サイクル発生することになる。
つまり、サイクル完結に1/6(60度)足りない。仮にこの時間スロットの開始点で2200Nz波形を連続して生成する場合、その波形は前回の開始点波形にたいして1/6サイクル分(60度)後戻りした時点の波形から始まらないと連続性が保てない。
1200Hz波形は2200Hz波形を11/6倍だけ時間方向に引き伸ばしたものと考えることができる。よって2200Hzの後に1200Hz波形を同じタイムスロットで連続性を保って生成するには11サンプル分(60度)後戻りした時点の波形から始めると波形の連続性が保てることになる。
この様子を図示すると以下となる。Markが1サイクルする時間間隔ではSpaceは出力ゼロになるまで60度足りない。次の波形を連続させるには次の波形を―60度ポイントから波形生成を始める必要があるわけだ。
Photo_20221231131001
そもそも、1200Hzと2200Hzでのビット表現となるとどうしても波形は不連続になるが、その事は問題なのだろうか。
この波形不連続は、1200Hzと2200Hz以外の非常に多くの周波数成分を発生してしまうようだ。このノイズ成分により、信号復調率が落ちるのだろう(多分)。

2022年12月29日 (木)

PICOのPIOを使った指定周期DMAによるシリアラル信号出力プログラムについて

PICOのPIOを使ったPWM信号出力をDMAで動作させる方法について備忘録

注:PIOを使ったPICOのPWM機能をDMAで動作させる方法はいまいちわからなかった。(というか、PIOのTX FIFOにデータを送った場合、PIOはGPIOピンに0/1信号しか出せず、PWMレジスターに値を書き込めない。そもそもDMAには自己完結型のDREQインターバル設定ができるので、DMAでPWMレジスターに値を書き込むだけなら、PIOは特に必要ないかも。。。)

ここではPIOに32ビットデータを1ビットずつ設定した周期でGPIO出力させることでPWM信号を作る方法について備忘録する。

  • DMAはDREQによってDMA転送を1データ単位(32ビット)ごとに実行する。
  • DMAは自らのDMAインターバルによってDREQを自動発生することもできる。
    • channel_config_set_dreq(&pwm_dma_chan_config, DREQ_PWM_WRAP0 + led_pwm_slice_num)
  • DMA ch構成では、転送ソースアドレス、転送ターゲットアドレス、転送カウントを設定する。
    • dma_channel_configure(
        dma_chan,     // DMAチャネル
        &dma_chan_config,    // DMAチャネル構成情報
        &pio0_hw->txf[state_machine_id],  // PIOのTX FIFO
        // &pwm_hw->slice[led_pwm_slice_num].cc, // PWMレジスタの場合
        data_table,      // データ元
          256,                              // 転送カウント
        false                             // スタートタイミング(即時スタートの場合はtrue)
      );
  • DMAは設定した転送カウント分の転送が完了するとIRQを発生する。
  • このIRQを使って次回DMA転送するデータを設定する。

 

  • PIOはTX FIFOの1データ単位(32ビット)を取り出す。
  • 32ビットを1ビット取り出すにはshiftを使う
    • channel_config_set_transfer_data_size(&dma_chan_config, DMA_SIZE_32);
      新しくTX FIFOからデータを取り出すまで1ビットずつシフトし1ビットずつとりだす動作を32ビット行う
  • PIOのTX FIFOが空になるとDREQを発生させる。
    • channel_config_set_dreq(&pwm_dma_chan_config, DREQ_PIO0_TX0);
    • 上記と以下は等価
      channel_config_set_dreq(&pwm_dma_chan_config, pio_get_dreq(pio, state_machine_id, true));
  • 対象とするDMA chにこのDREQを紐付けることでTX FIFOが空になるごとにDMAを行うようにする

======= Program 1 DMAだけ、IRQなし =======

Main C Program

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include <hello.pio.h>

int main()
{
   PIO pio = pio0;
   uint state_machine_id = 0;
   uint offset = pio_add_program(pio, &pio_dac_program);
   pio_dac_program_init(pio, state_machine_id, offset, PICO_DEFAULT_LED_PIN, 2000.f);

   int pwm_dma_chan = dma_claim_unused_channel(true);

   dma_channel_config dma_chan_config = dma_channel_get_default_config(pwm_dma_chan);

   channel_config_set_transfer_data_size(&dma_chan_config, DMA_SIZE_32);
   channel_config_set_read_increment(&dma_chan_config, true);
   channel_config_set_write_increment(&dma_chan_config, false);
   channel_config_set_dreq(&dma_chan_config, pio_get_dreq(pio, state_machine_id, true)); // pio sm, TX

   dma_channel_configure(
      dma_chan,
      &dma_chan_config,
      &pio0_hw->txf[state_machine_id],
      data_table, 
      256, 
      false 
   );

   dma_channel_set_read_addr(dma_chan, mark_tab, true);   // DMAをスタートする

   while(true) {
      tight_loop_contents(); 
   }
}

PIO Program

.program pio_dac

.wrap_target
   out pins, 1
.wrap

% c-sdk {

static inline void pio_dac_program_init(PIO pio, uint sm, uint offset, uint pin, float fs) {
   pio_sm_config c = pio_dac_program_get_default_config(offset);

   sm_config_set_out_shift(&c, true, true, 32);
   sm_config_set_out_pins(&c, pin, 1);
   // sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); これはなくても動作は同じ
   sm_config_set_clkdiv(&c, (float)clock_get_hz(clk_sys) / fs);

   pio_gpio_init(pio, pin);

   pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
   pio_sm_init(pio, sm, offset, &c);
   pio_sm_set_enabled(pio, sm, true);
}
%}

======= Program 2  DMAとIRQの両方 =======

Main C Program

int dma_chan;
static bool mark_signal=true;
// DMA転送完了(指定データ数の転送完了)のIREQの度に送出するシグナルを交互に変える。
void dma_handler() {
//  指定されたデータテーブルのDMA転送を即時実行する。なのでDREQを待つことなしにDMAが開始される。
   if(mark_signal){
      dma_channel_set_read_addr(dma_chan, mark_tab, true);  // mark_tabの転送を即時実行
      mark_signal = false;
   }else{
      dma_channel_set_read_addr(dma_chan, space_tab, true);  // space_tabの転送を即時実行 
      mark_signal = true;
   }
   // IRQをクリア
   dma_hw->ints0 = 1u << dma_chan;
}

int main()
{
   PIO pio = pio0;
   uint state_machine_id = 0;
   uint offset = pio_add_program(pio, &pio_dac_program);
   pio_dac_program_init(pio, state_machine_id, offset, PICO_DEFAULT_LED_PIN, 2000.f);
   dma_chan = dma_claim_unused_channel(true);

   dma_channel_config dma_chan_config = dma_channel_get_default_config(dma_chan);

   channel_config_set_transfer_data_size(&dma_chan_config, DMA_SIZE_32);
   channel_config_set_read_increment(&dma_chan_config, true);
   channel_config_set_write_increment(&dma_chan_config, false);

   channel_config_set_dreq(&dma_chan_config, pio_get_dreq(pio, state_machine_id, true)); // pio sm, TX

   // Setup the channel and set it going
   dma_channel_configure(
      dma_chan,
      &dma_chan_config,
      &pio0_hw->txf[state_machine_id],
      mark_tab,    // 初期データテーブル
      66,                       // 転送データ(ワード)数
      false                     // トリガーされるまで待機
   );

   dma_channel_set_irq0_enabled(dma_chan, true);
   irq_set_exclusive_handler(DMA_IRQ_0, dma_handler);  // IRQハンドラーとしてdma_handlerを設定
   irq_set_enabled(DMA_IRQ_0, true);

   dma_handler(); // DMAを即時実行。DMA開始後はPIOが発生するDREQのペースでDMA転送される
   // 上記実行以降はDREQとIREQによってPIOの実行ペースでデータ転送が実行される。
   //  そのおかげでメインプログラムはデータ転送を気にすることなく他のタスクが実行可能となる
   while(true) {
      tight_loop_contents(); 
   }
}
SDKドキュメント。dma_channel_set_read_addr()の第三パラメータは即時実行指定。

◆ dma_channel_set_read_addr()




static void dma_channel_set_read_addr ( uint  channel,
    const volatile void *  read_addr,
    bool  trigger 
  )    
inlinestatic

Set the DMA initial read address.

Parameters
channel DMA channel
read_addr Initial read address of transfer.
trigger True to start the transfer immediately
ビルド環境
親DIR
   + led_fade (ソースコードDIR)
   + build (ビルドDIR)
親DIRのCMakeLists.txt

cmake_minimum_required(VERSION 3.16)    //使っている環境が3.16だからとりあえず3.16に設定

include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
project(led_fade C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
add_subdirectory(led_fade)

ソースコードDIRのCMakeLists.txt

add_executable(led_fade
   led_fade.c
)

pico_generate_pio_header(led_fade ${CMAKE_CURRENT_LIST_DIR}/hello.pio)

target_link_libraries(led_fade
   pico_stdlib
   hardware_pio
   hardware_dma
)

pico_add_extra_outputs(led_fade)

 

======= Program 3 文字列を波形出力する =======

送出する文字列に沿って波形を出してみたくなったので、その実験コードを書いてみた。
uint8_t xfer_data[] = "HIJKLMNO"; // 波形送出する文字列
int xfer_length;
const uint32_t *dma_signal[80];  // up to 10 data
int dma_signal_count, dma_count;

void dma_handler()
{
   if (dma_count >= dma_signal_count) // no more data to DMA
      dma_count = 0;              // repeater forever
   // DMAする順にWaveform Tableのポインターを取り出しDMAをキックする。
   dma_channel_set_read_addr(dma_chan, dma_signal[dma_count], true);

   // Clear the interrupt request.
   dma_hw->ints0 = 1u << dma_chan;

   dma_count++; // prepare for next DMA transfer
}
送り出すビットが1か0によって、DMA単位ごとのwaveform(1200Hz/2200Hz)を設定する
void data_set()
{  
   int i, j, k = 0;
   uint8_t dma_data;

   xfer_length = strlen(xfer_data);
   for (i = 0; i < xfer_length; i++){
      dma_data = xfer_data[i];   // 送出するCharactorを取り出す
      for (j = 0; j < 8; j++)    {      // LSBから順にWaveformを割り当てる
         if ((dma_data >> j) & 1)  //  BitごとにWaveform Tableポインターを設定する
            dma_signal[k] = mark_tab;  
         else
            dma_signal[k] = space_tab;
         k++;
      }
   }
   dma_signal_count = k;
   dma_count = 0;   
}
main()の中で、dma_handler()を呼ぶ前(DMAをキックする前)にdata_set()をコールする。
   data_set(); // set wave table to DMA
   dma_handler(); // trigger DMA

 

2022年12月28日 (水)

PICOのPWMをDMAで動作させる方法

Raspberry Pi PICOのPWMをDMAで動作させる方法について備忘録。
以下、動作検証を行ったサンプルプログラム。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/pwm.h"
#include "hardware/clocks.h"
int main()
{
// オンボードLEDがつながっているPINをPWMに設定する
   gpio_set_function(PICO_DEFAULT_LED_PIN, GPIO_FUNC_PWM);

//  このPINのSlice番号を得る
   int led_pwm_slice_num = pwm_gpio_to_slice_num(PICO_DEFAULT_LED_PIN);
//  PWM構成情報を作成する
   pwm_config config = pwm_get_default_config();
//  PWM構成情報にデータ要求周期のクロック分割を設定する。
//  ここではLED点灯変化が目視できるようにクロック数を1/800に設定している。
//  なお、800.fのfはfloatを表している。
   pwm_config_set_clkdiv(&config, 800.f);
//  このPWM構成情報でPWMを初期化する
   pwm_init(led_pwm_slice_num, &config, true);

//   PWM用に、空いているDMAチャンネル番号を取得する
   int pwm_dma_chan = dma_claim_unused_channel(true);
//  取得したDMAチャンネルの構成情報を取得する
   dma_channel_config pwm_dma_chan_config = dma_channel_get_default_config(pwm_dma_chan);
//  DMA転送単位を32ビットに設定する
   channel_config_set_transfer_data_size(&pwm_dma_chan_config, DMA_SIZE_32);
//  DMA読み出し元(データテーブル)アドレスを自動インクリメント設定する
   channel_config_set_read_increment(&pwm_dma_chan_config, true);

//  DMA書き込み先(PWMレジスタ)アドレスは固定にする
   channel_config_set_write_increment(&pwm_dma_chan_config, false);
//  PWM(取得したスライス番号)のデータ要求によってDMA REQが発生するよう指定する
   channel_config_set_dreq(&pwm_dma_chan_config, DREQ_PWM_WRAP0 + led_pwm_slice_num);

//  DMAチャンネル構成情報を設定する
   dma_channel_configure(
      pwm_dma_chan,
      &pwm_dma_chan_config,
      &pwm_hw->slice[led_pwm_slice_num].cc, // 書き込み先PWM counter compare
      data_table,               // 読み出し元
      256,                                                              // データテーブルからの読み出しデータ数、これを読みだしたらDMA停止
      true                   // DMA即時開始
   );

   while(true) {
      tight_loop_contents();
   }
}
上記のコードはPWMの実行周期を設定し、その周期でDMA REQを発生させることで、data_tableからPWMレジスタにデータをDMA転送させている。
なお、data_tableはuint32_t data_table[256]で、0x00000000から0xFFFFFFFFまでの値でLED輝度変化が目視できるテーブルにする。

検証にあたってはこのブログを参考にした。

2022年12月27日 (火)

PICOのPIOについて

Raspberry Pi PicoのPIOについて調べてみたので備忘録。

Raspberry Pi Picoはメインプログラムとは別にアセンブラレベルコードを実行することができる。これはメインプログラムとは別に(並行して)動作するプログラムのようだ。基本アセンブラレベルのコードを書くことになる。これはかなり強力な機能で、メインプログラムでタイミングを意識することなく、決まったサイクルでオペレーションを実行できることになる。

その1からその2まで記録。。。

その1ーーー

開発手順

ここではオンボードLEDを点滅するPIOを書いてみる。

  1. hello.pioを書く。この中にアセンブラレベルのコードを書く。このhello.pioはCヘッダーファイルに変換され、Cプログラムから参照される。
  2. Cプログラムを書く。
  3. buildする。

hello.pioディレクトリに以下を配置
   + CMakeLists.txt
   + hello-pio 
          + hello.pio
          + hello-pio.c
          + CMakeLists.txt
   + build

buildディレクトリにて以下を実行してbuildする。
$ cmake ..
$ make

以下、実際に書いたコード。

参考になった情報1情報2。ただし、情報1の通りコードを書いてもLEDはブリンクしない(サイクルが短すぎて点灯しっぱなしに見える)。

hello.pio

.program hello
loop:
;.wrap_target 
   set pins, 1 [19] ; Turn LED on and wait another 19 cycles
   nop [19] ; Wait 20 cycles
   nop [19] ; Wait 20 cycles
   nop [19] ; Wait 20 cycles
   nop [19] ; Wait 20 cycles
   set pins, 0 [19] ; Turn LED off and wait another 19 cycles
   nop [19] ; Wait 20 cycles
   nop [19] ; Wait 20 cycles
   nop [19] ; Wait 20 cycles
   nop [19] ; Wait 20 cycles
;.wrap
jmp loop

//  上記loopのwrapも同じ結果になる

% c-sdk {
static inline void hello_program_init(PIO pio, uint sm, uint offset, uint pin) {
   //  Configオブジェクトを定義する 
   pio_sm_config config = hello_program_get_default_config(offset);
   //    Configオブジェクトにgpioピンpinを割り当てる
   pio_gpio_init(pio, pin);
   sm_config_set_set_pins(&config, pin, 1);  //  pinから1ピンを連続割当、ここでは1ピンのみの割当。
   //  pinから1ピンを出力に設定。ここでは1ピンのみを設定。
   pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);

   // クロック分割数を決める。divの分だけsysclkがマスクされる。これをやらないとLED ON/OFFが早すぎてLED点灯しっぱなしに見える。
   static const float pio_freq = 2000;
   float div = (float)clock_get_hz(clk_sys) / pio_freq;
   sm_config_set_clkdiv(&config, div);

   // 初期化とイネーブ
   pio_sm_init(pio, sm, offset, &config);
   pio_sm_set_enabled(pio, sm, true);
}
%}

hello-pio.c

#include <stdio.h>
#include <stdbool.h>
#include <pico/stdlib.h>
#include <hardware/pio.h>
#include "hardware/clocks.h"  //  hello.pioでclk_sysを参照しているので必要
#include <hello.pio.h>     // hello.pioから生成されるヘッダーファイルをincludeする

#define LED_BUILTIN 25     // オンボードLEDのGPIO

int main() {

   stdio_init_all();

   PIO pio = pio0;
   uint state_machine_id = 0;
   // hello.pioで定義したhelloはhello_programと自動的に命名される。
   uint offset = pio_add_program(pio, &hello_program);
   hello_program_init(pio, state_machine_id, offset, LED_BUILTIN);
   while(1) {
      // Cプログラムで何かをやっていたとしても、これとは並行してpioが動作している
   }
}

CMakeLists.txt(hello.pioディレクトリ)

cmake_minimum_required(VERSION 3.12)

include(pico_sdk_import.cmake)

#project(pico_examples C CXX ASM)
project(hello-pio C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

pico_sdk_init()

add_subdirectory(hello-pio)

CMakeLists.txt(hello-pioディレクトリ) 

add_executable(hello-pio
   hello-pio.c
)
#  CMAKE_CURRENT_LIST_DIRはCMAKEによって自動設定される

pico_generate_pio_header(hello-pio ${CMAKE_CURRENT_LIST_DIR}/hello.pio)

target_link_libraries(hello-pio
   pico_stdlib
   hardware_pio
)
# hexファイルやuf2ファイルを生成する
pico_add_extra_outputs(hello-pio)

hello.pio.h

CMakeLists.txtのpico_generate_pio_header()によって以下のヘッダーファイルが生成される。

// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //

#pragma once

#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif

// ----- //
// hello //
// ----- //

#define hello_wrap_target 0
#define hello_wrap 10

static const uint16_t hello_program_instructions[] = {
// .wrap_target
   0xf301, // 0: set pins, 1 [19]
   0xb342, // 1: nop [19]
   0xb342, // 2: nop [19]
   0xb342, // 3: nop [19]
   0xb342, // 4: nop [19]
   0xf300, // 5: set pins, 0 [19]
   0xb342, // 6: nop [19]
   0xb342, // 7: nop [19]
   0xb342, // 8: nop [19]
   0xb342, // 9: nop [19]
   0x0000, // 10: jmp 0
// .wrap
};

#if !PICO_NO_HARDWARE
static const struct pio_program hello_program = {
   .instructions = hello_program_instructions,
   .length = 11,
   .origin = -1,
};

static inline pio_sm_config hello_program_get_default_config(uint offset) {
   pio_sm_config c = pio_get_default_sm_config();
   sm_config_set_wrap(&c, offset + hello_wrap_target, offset + hello_wrap);
   return c;
}

static inline void hello_program_init(PIO pio, uint sm, uint offset, uint pin) {

   pio_sm_config config = hello_program_get_default_config(offset);

   pio_gpio_init(pio, pin);
   sm_config_set_set_pins(&config, pin, 1);
   pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);

   static const float pio_freq = 2000;
   float div = (float)clock_get_hz(clk_sys) / pio_freq;
   sm_config_set_clkdiv(&config, div);

   pio_sm_init(pio, sm, offset, &config);
   pio_sm_set_enabled(pio, sm, true);
}

#endif

その2ーーー

sm_config_set_out_shiftについて

.program hello

.wrap_target
   out pins, 1   ;  結果的に1ワード分のデータを右シフトで1ビットずつ出力される。
.wrap

% c-sdk {

static inline void hello1_program_init(PIO pio, uint sm, uint offset, uint pin, float fs) {
   pio_sm_config c = pio_dac_program_get_default_config(offset);

//  TX FIFOからデータをpullするまでのビットシフト実行指定、右向きに32ビットまでシフトする。
//  つまり32ビット全部をpinに順次送り出し、全て送り出したらTX FIFOから自動で新しいデータをpullする。
   sm_config_set_out_shift(&c, true, true, 32);

//  pinから1ピンをoutに設定する
   sm_config_set_out_pins(&c, pin, 1);

//  クロックマスク周期を設定する(動作速度を調整する)
   sm_config_set_clkdiv(&c, (float)clock_get_hz(clk_sys) / fs);

SDKドキュメントは以下のとおり。

◆ sm_config_set_out_shift()

static void sm_config_set_out_shift ( pio_sm_config  c,
    bool  shift_right,
    bool  autopull,
    uint  pull_threshold 
  )    
inlinestatic

Setup 'out' shifting parameters in a state machine configuration.

Parameters
c Pointer to the configuration structure to modify
shift_right true to shift OSR to right, false to shift OSR to left
autopull whether autopull is enabled
pull_threshold threshold in bits to shift out before auto/conditional re-pulling of the OSR

2022年12月17日 (土)

PICO TNCno製作 その3

KENWOODハンディトランシーバー対応。トランシーバーはTH-K20、ちょっと(かなり)古い。

KENWOOD対応のヘッドセットのケーブル・ジャックを活用しようとして驚いた。ケーブルを切ったのはこのヘッドセット。
Img03220_small

まぁ、幾分細いワイヤーがはいっているんだろうと思ったら、、、なんと超極細エナメル線を編んだワイヤーが4本入っていた。これはどうしたものやら。とりあえずカッターナイフで極細エナメル線の被覆を削り落としてクリップを付けて配線を確認した。
Img03218_small

3芯ケーブルに半田付けし、熱収縮チューブでカバーした。
Img03222_small

KENWOOD ハンディのスピーカーマイク配線は以下のとおり。
Kenwoodpinlayout

つまりMICラインとPTTラインは分けないといけない。ここがYaesuと違う所。そこで、ビーコン発生機のジャック端子の真ん中(Ring)にPTT信号を割り当てる改造を実施。

35mmjack

この結果、ハンディのスピーカーマイク端子とビーコン発生機に差し込むジャックとの配線は以下のようになった。マイク信号線はマイク端子(Tip)へ、MIC/PTT共通信号線はPTT端子(Ring)へ、PTT/GND信号線はGND端子(Sleeve)へ接続した。
Kenwood

これでとりあえず動くようになった。

Img03228_small
しかし、トランシーバーから電波を出すとPicoが誤動作し、PTTが切れない。つまり電波が出っぱなしになる。この状態で今回作ったケーブルを手で覆い隠すと誤動作(PTTオン)が止まる。つまり、トランシーバーからの電波に対してビーコン発生機をシールドする必要がある。

Picoが誤動作している可能性があると思い、ケーブルのビーコン発生機ジャック根元部分にコアを入れてみたが効果なし。
Img03229_small

今度はトランシーバー側にコアを入れてみた。こちらは効果があり、安定的に動作するようになった。ちなみにコアはFT-114-43、バラン用に購入してあったもの。
Img03230_small

以上でKENWOOD Handy対応も目処がたった。

追加のノイズ対策

今回の誤動作はPTTがオンになりっ放しになるというもの。スピーカーマイクケーブルのトランシーバー側にコアを入れて改善したことから、このケーブルを介してノイズがビーコン発生機に入ってきていることが想像できる。そして、ビーコン発生機のPTT信号出力トランジスターを誤動作させているということだろうか。であれば、トランジスター自体にノイズ対策を施すことは効果がありそうだ。

それで以下の対策を施した。
Emitterreg
Q1のベース・エミッター間にR3 10KΩ抵抗を入れた。この抵抗がない状態ではベースに入ってきたノイズ電流によってQ1が誤動作する場合が有り得るが、そのノイズ電流をGNDに流す役割をするのがR3になる。仮に0.01mAのノイズ電流がベースに入ってきたしても、10KΩに流れたとして電圧は0.1Vとなりベース・エミッター間飽和電圧(大体0.8V)よりも十分小さいのでQ1を動作させることはないがノイズ電流はGNDに流すことができる。

試しにコアを取り外してみたが、やっぱりコアが無いとPTTがオンになったままになる。ということで、上記R3の追加は決定的な対策にはならないことが判明。

2022年12月12日 (月)

PICO TNCの製作 その2

PICO TNC製作の続編その2。

プロトタイプが完成した。
Img03140_small

事前設定したインターバルで、ハンディ―トランシーバーのマイク端子経由にてMic-EフォーマットでGPSデータを送信することができるようになった。

PICO TNC コマンドの使い方

  • mycall JA0WBT または JA0WBT-7  :  SSID有無どちらでもよい。mycallを設定しないとBeaconは出ない。
  • unproto JA0WBT V WIDE1-1   :  Vの前はDestination Addressだが、ソフトウエア内部でMic-E形式のDestinationに上書きするようにした。なのでどんな文字列でも構わない。Vの後はデジパス1でNewパラダイムに従ってWIDE1-1を指定する。
  • digi ON  : デジパスを指定しているのでONを指定する。
  • gps $GNGGA   : プロトタイプに接続しているGPSモジュールのGGAフォーマットは$GNGGAと頭に付けてくるのでそれを指定する。これ以外のフォーマットタイプは今はサポートしていない。
  • btext HelloWorld  : BeaconのInformationに付加する自由テキスト。
  • beacon every n  : Beacon発生インターバル。n=1で1/6秒。最大360=60分。
  • PERM  : パラメータを設定したら実行。これにより設定したパラメータ値がFlashメモリーに保存され、パワーサイクルでも消えない。

以下がdispコマンド実行結果。MYALIASは設定しなくてもよい。

disp

ECHO ON
TXDELAY 100
GPS $GNGGA
TRace OFF
MONitor ALL
DIGIpeater ON
BEACON On EVERY 6
UNPROTO SUTPW8 V WIDE1-1
MYCALL JA0WBT-7
MYALIAS JA0WBT
BTEXT HelloWorld

OK
cmd:

送信されるパケットデータ

PICOの出力(トランシーバーのマイク入力)を入力(トランシーバーのスピーカ出力)にループさせてPICO TNCにて自分の出力を自分でデコードさせた。さらに実際にトランシーバー(FT-70D)のマイク入力に接続し、電波送信してI-Gateにて受信した。その受信した結果が以下:

17:43:53R JA0WBT-7>SUTPW8,WIDE1-1 Port=1 <UI C Len=24>:
`AB'l l[/`"9L}HelloWorld
17:43:53R JA0WBT-7>SUTPW8,WIDE1-1 Port=2 <UI C Len=24>:
`AB'l l[/`"9L}HelloWorld
17:43:55R JA0WBT-7>SUTPW8,WIDE1-1* Port=1 <UI C Len=24>:
`AB'l l[/`"9L}HelloWorld
17:43:55R JA0WBT-7>SUTPW8,WIDE1-1* Port=2 <UI C Len=24>:
`AB'l l[/`"9L}HelloWorld

I-Gateは2ポート設定しているので(1ポートにする方法がわからん)、同じ電波信号をPort1とPort2で2回取り込んでいる。最初のペアがPICO TNCが生成したフレームで、次のペアがPICO TNC内でループバックして再送信した信号になる。ループバックした信号はデジピートしたことになるのでWIDE1-1の後ろに*が付加されている。

Digipeaterとしての機能

Digipeaterとしの機能を確認する。FT3Dからビーコンを送信し、それをFT-70Dで受信。FT-70DのSpeakerをPICO TNCの入力に接続し、MicをPICO TNCの出力に接続した。

Img03160_hdr_small

Beaconrepeat

結果は、FT3Dからのビーコン(上図①)はFT-70Dでビーコン再送(上図④)された。そのビーコンはFT3Dで受信することができた(以下写真)。一方、UI-VIEW32はFT3Dからのビーコン(上図①)は受信表示したが、FT-70Dからのビーコン再送(上図④)は受信表示されなかった。
Img03161_small

考察として、UI-VIEW32はオリジナル信号を受信した後に、そのデジピート信号を受信した場合、それは受信表示しないのではないかと思われる。もしそれを許したら、同じ信号をデジピートの分だけ何度もI-Gateしてしまうから。一方、FT3Dは自分が発信した信号のデジピートであっても自分以外の送信機からの信号なのでそのまま表示したのだと思う。

UI-VIEW32のTerminalで確認したところ、上記の通りと判断できる。以下がTerminalの表示内容。3つ目のWIDE1-1に*が付いているのでデジピートされた信号と判断される。デジピートしたのはPico TNCになるので、Pico TNCはちゃんとデジピートしているのだけれど、それをUI-VIEW32がInternetには送り出さなかったということだ。

19:34:21R JA0WBT-7>SUTPW7,WIDE1-1 Port=1 <UI R Len=17>:
`AB(l!m[/`"9l}_0
19:34:21R JA0WBT-7>SUTPW7,WIDE1-1 Port=2 <UI R Len=17>:
`AB(l!m[/`"9l}_0
19:34:22R JA0WBT-7>SUTPW7,WIDE1-1* Port=2 <UI R Len=17>:
`AB(l!m[/`"9l}_0

 

乾電池駆動について

この実験に際して、PICO TNCを単四乾電池2本で駆動した。5分インターバルで一晩ビーコンを出し、朝停波。その状態で実験を続けていたところ午後3時ころには電池がなくなった(Watchdog Timer Failure発生)。結構電力を消費している。

基板への実装とケースへの取り付け

ブレッドボードに作った回路を

Img03170_small

この回路をユニバーサル基板に配置する。

Img03163_hdr_small

実装が終わった状態。RS232CでPCでモニターする。
Img03175_small

乾電池(単三2本)での動作確認。
Img03171_small

ダイソーの3個100円のケースにいれる。RS232Cのボードもユニバーサル基板に実装した。
Img03228_small

完成した状態。
Img03187_small

この状態で持ち運んで軌跡確認を行った。
Img03210_small

実験結果はまずまず良好で、基本機能が動作することを確認できた。

より以前の記事一覧