pioasmでDCCパルス幅をカウントする

SmileSoundの開発では、サウンドデコーダという複雑な機能を実現するためには、マイコン(RP2040)の資源をフルに活用する事が、様々な機能を実現するうえで重要になってきます。現状使用している、NMRA DCC Libraryには大きなハンデがあります。このDCC受信ライブラリは、汎用性を重視しているので、DCCパルスの認識・受信処理を外部割込によるソフトウェアで実装しています。このため、CPUを占有する時間が長く、他の処理の動作を妨げることが多々あります。外部割込は、おおよそ58us~100us程度で発生するので、高性能なRP2040であっても、それなりにヘビーであります。

通常、このような処理はソフトウェアではなく、タイマー周辺機能にある「インプットキャプチャ」を使う事が一般的です。しかし、インプットキャプチャは、マイコンごとに設定が大きく異なり、さらにいくつかの制約により、DCC用には向かない機能となっている場合もあります。NMRA DCC Libraryがインプットキャプチャを用いないのは、そのような理由からです。

さて、SmileSoundでは、RP2040専用で実装するのですから、RP2040固有の機能を使っても特に支障はありません。そこで、インプットキャプチャを実装しようと思ったのですが、なんとRP2040にはインプットキャプチャがない!FPUもなければインプットキャプチャもないということで、非常にシンプル側に攻めたマイコンとなっています。しかし、ご安心を、RP2040には、PIOと呼ばれるユーザーカスタム可能な周辺機能を使用することができます。そこで、pioを使って、DCCのパルス幅をカウントし、NMRA DCC Libraryのパケット処理部に流し込む機能を実装していきます。

まず、pioとは何ぞや?という話をするのは、他のサイトでたくさん解説されているので割愛します。

pioを使って実装するにあたり、どうDCCパルスを解釈させるかという事を考えます。DCCパルスは、半周期58us幅の”1″パルスと、半周期95us~9900us幅の”0″パルスの2つがあります。半周期のパルス幅(duty)に着目して、読み出す実装とします。

次に、pioの制約です。pioのアセンブラプログラムで使えるX・Yのレジスタは、なんと0-31の値しか使えません。また、データを蓄えるFIFOは32bit×8段、シフトレジスタは32bitなので、4bitでパルス幅をカウントしたいと思います。おおよそ200us程度までカウントできれば問題ないので、14.5usで1dというようにして実装します。

pioのアセンブラプログラムを実装するのに、PCにいろいろインストールする必要があるのですが、面倒なので、Webに置いてあるアセンブラpioasmを使わせてもらいます。左側にpioasmを書くと、右側にC言語のヘッダに自動変換してくれる優れものです。

実際のpioasmのソースは以下です。4bit幅でカウントして、シフトレジスタに格納(IN X 4)しているので、32bitあたり8個のパルス幅を格納できます。8個溜まると、自動的にRX FIFOにpushされるautopush機能を初期化時に有効にしてます(sm_config_set_in_shift )。RX FIFOは8段となるように設定変更してますので、合計で64個のパルス幅がカウント可能になってます。

;
; Copyright (c) 2022 DesktopStation Co., Ltd.
;
; SPDX-License-Identifier: BSD-3-Clause
;

.program dcc_pulse_dec

; Repeatedly get 4bit word of data from the DCC pulse
; "0" pulse may be 98us or more, "1" pulse is 58us

.define DCC_LOOP_COUNTER 15                   ; the detection threshold for a 'frame sync' burst

.wrap_target

next_burst:
    set X, DCC_LOOP_COUNTER              ; 4bitにしたいので、maxの15をセット。ちなみにデクリメントしかできない。
    wait 0 pin 0                         ; ピンが0になるのを待つ。

burst_loop:
    jmp pin data_set                     ; ピンがHigh(1)でdata_setにジャンプ
    jmp X-- burst_loop                   ; Xレジスタをデクリメント。X>0ならburst_loopにジャンプ

                                         ; X==0ならここに来る。カウントオーバーなので、長い0 pulseとみなす。
    in X 4                               ; set 0000 to the Input Shift Register
    jmp next_burst                       ; next_burstへジャンプ。

data_set:
    in X 4                               ; 4bit幅でシフトレジスタにXを突っ込む。溜まると勝手にRX FIFOにpushされる設定。

.wrap

% c-sdk {
static inline void dcc_pulse_dec_init(PIO pio, uint sm, uint offset, uint pin) {
    pio_sm_config c = dcc_pulse_dec_program_get_default_config(offset);


    // PIOのピン設定。
    pio_gpio_init(pio, pin);
    // ピンの設定。inputにする。複数ピンを束ねて設定できるが1ピンのみにする。
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, false);

    // 受信FIFOを8段重ねる設定。これで、4x8x8=256文字のデータを一気に受信できるはず。
    sm_config_set_fifo_join (&c, PIO_FIFO_JOIN_RX);

    // ISR(インプットシフトレジスタ)の設定。autopushがミソ。
    sm_config_set_in_shift (&c,
                            true,       // shift right
                            true,       // enable autopush
                            32);        // autopush after 32 bits


    // Map the IN pin group to one pin, namely the `pin`
    // parameter to this function.
    //
    sm_config_set_in_pins (&c, pin);

    // Map the JMP pin to the `pin` parameter of this function.
    //
    sm_config_set_jmp_pin (&c, pin);

    // Set the clock divider to 1 tick per 7.25us(1cnt 14.5us) burst period
    //
    float div = clock_get_hz (clk_sys) / (1.0 / 7.25e-6);
    sm_config_set_clkdiv (&c, div);


    // Load our configuration, and jump to the start of the program
    pio_sm_init(pio, sm, offset, &c);
    // Set the state machine running
    pio_sm_set_enabled(pio, sm, true);
}
%}

生成されたヘッダは以下の通りです。

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

#pragma once

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

// ------------- //
// dcc_pulse_dec //
// ------------- //

#define dcc_pulse_dec_wrap_target 0
#define dcc_pulse_dec_wrap 6

static const uint16_t dcc_pulse_dec_program_instructions[] = {
            //     .wrap_target
    0xe02f, //  0: set    x, 15                      
    0x2020, //  1: wait   0 pin, 0                   
    0x00c6, //  2: jmp    pin, 6                     
    0x0042, //  3: jmp    x--, 2                     
    0x4024, //  4: in     x, 4                       
    0x0000, //  5: jmp    0                          
    0x4024, //  6: in     x, 4                       
            //     .wrap
};

#if !PICO_NO_HARDWARE
static const struct pio_program dcc_pulse_dec_program = {
    .instructions = dcc_pulse_dec_program_instructions,
    .length = 7,
    .origin = -1,
};

static inline pio_sm_config dcc_pulse_dec_program_get_default_config(uint offset) {
    pio_sm_config c = pio_get_default_sm_config();
    sm_config_set_wrap(&c, offset + dcc_pulse_dec_wrap_target, offset + dcc_pulse_dec_wrap);
    return c;
}

static inline void dcc_pulse_dec_init(PIO pio, uint sm, uint offset, uint pin) {
    pio_sm_config c = dcc_pulse_dec_program_get_default_config(offset);
    // PIOのピン設定。
    pio_gpio_init(pio, pin);
    // ピンの設定。inputにする。複数ピンを束ねて設定できるが1ピンのみにする。
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, false);
    // 受信FIFOを8段重ねる設定。これで、4x8x8=256文字のデータを一気に受信できるはず。
    sm_config_set_fifo_join (&c, PIO_FIFO_JOIN_RX);
    // ISR(インプットシフトレジスタ)の設定。autopushがミソ。
    sm_config_set_in_shift (&c,
                            true,       // shift right
                            true,       // enable autopush
                            32);        // autopush after 32 bits
    // Map the IN pin group to one pin, namely the `pin`
    // parameter to this function.
    //
    sm_config_set_in_pins (&c, pin);
    // Map the JMP pin to the `pin` parameter of this function.
    //
    sm_config_set_jmp_pin (&c, pin);
    // Set the clock divider to 1 tick per 7.25us(1cnt 14.5us) burst period
    //
    float div = clock_get_hz (clk_sys) / (1.0 / 7.25e-6);
    sm_config_set_clkdiv (&c, div);
    // Load our configuration, and jump to the start of the program
    pio_sm_init(pio, sm, offset, &c);
    // Set the state machine running
    pio_sm_set_enabled(pio, sm, true);
}

#endif

これを、Arduinoでコンパイルできるようなスケッチを書きました。RP2040の開発環境は、お馴染みのearlephilhower版Arduino-picoライブラリです。Arduino IDEで簡単にRP2040のソフトを書けるので便利です。

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "dcc_pulse_dec.h"

#define PIN_DCC				2

//  pioasmを使って、dccpulse.pioからdcc_pulse_dec.hを生成しています。
//  https://wokwi.com/tools/pioasm

PIO gPio;
uint gPio_SM;
unsigned long gRecvPulseWidth[8];

void setup()
{
	//PIOのch 0を確保
    gPio = pio0;

    //アセンブラのPIOプログラムへのアドレス
    uint offset = pio_add_program(gPio, &dcc_pulse_dec_program);

    // 空いているステートマシンの番号を取得
    gPio_SM = pio_claim_unused_sm(gPio, true);
	
	// PIOプログラムを初期化
	dcc_pulse_dec_init(gPio, gPio_SM, offset, PIN_DCC);
	
	//デバッグ用シリアル
	Serial.begin(115200);
	
	
}


void loop()
{
	
	if( pio_sm_is_rx_fifo_full(gPio, gPio_SM) == true)
	{
		
		for( int i = 0; i < 8; i++)
		{
			gRecvPulseWidth[i] = pio_sm_get(gPio, gPio_SM);
		}
		
		Serial.println("RECV:");
		
		for( int i = 0; i < 8; i++)
		{
			print32bits(gRecvPulseWidth[i]);
		}
		
	}
	else
	{
		//Serial.print(".");
	}
	
}

void print32bits(unsigned long inBuf)
{
	
	Serial.print( getUS(inBuf & 15) );
	Serial.print(" ");
	Serial.print( getUS((inBuf >> 4) & 15) );
	Serial.print(" ");
	Serial.print( getUS((inBuf >> 8) & 15) );
	Serial.print(" ");
	Serial.print( getUS((inBuf >> 12) & 15) );
	Serial.print(" ");
	Serial.print( getUS((inBuf >> 16) & 15) );
	Serial.print(" ");
	Serial.print( getUS((inBuf >> 20) & 15) );
	Serial.print(" ");
	Serial.print( getUS((inBuf >> 24) & 15) );
	Serial.print(" ");
	Serial.println( getUS((inBuf >> 28) & 15) );
	
}

unsigned long getUS(unsigned long inVal)
{
	return (15 - inVal) * 14;
	
}

RP2040ボードで、GPIO2にDCCパケット(3.3Vのロジックレベル信号に回路で調整してくださいね)を流し込んで動かすと、シリアルモニタにパルス幅をひたすら出力していきます。

だいたい、42-56あたりが”1″で、98以上は”0″という形で解釈すれば良いと思います。

11111111 ←プリアンブル
11111111 ←プリアンブル
0  ←StartBit
11000001
0  ←StartBit
01101110
0  ←StartBit
00111111
0  ←StartBit
10011010
0  ←StartBit
00001111
1
1  ←StopBit

パルス幅を置き換えて、上記のようなパケットデータが出てきたら、プリアンブルとスタートビット・ストップビットを除去して、「11000001 01101110 00111111 10011010 00001111」というのが出てくるので、これを弊社が昔作った、Webパケット解析器に突っ込むと以下のように分析してくれます。狙ったとおりの感じですね。

ということで、まだNMRA DCC Libraryに突っ込む処理は入れてませんが、上記のサンプルをzipで固めておきました。

タイトルとURLをコピーしました