ラズピコ PIO UARTで受信してみる

  • 2023.03.24
  • PIO
ラズピコ PIO UARTで受信してみる

皆さん こんにちは。

今回は PIO を使って UART で受信してみます。
PIOについて詳しく知りたい方は PIO~解説編~ をご覧になってください。

受信した文字をループバックして送信するのに、前回説明した送信のコードを使っています。

(タイトルは「UARTで受信してみる」ですが、実際には送受信しています)

UARTのフォーマット

UARTで送受信する電文のフォーマットがわかっていないとPIOで送受信するプログラムを書くことができません。
まずはフォーマットを確認しておきましょう。

このフォーマットはマイコン端子で見た論理レベルになります。
当然、送受信どちらも同じです。

まずスタートビットが1ビットあります。このレベルは”low”です。
その後データビットが続きます。LSBから送り始めます。
データ長は7か8ビットが一般的ですが、今回はデータ長を8ビットに決めました。
その後パリティビットが1ビットあります。
パリティビットは”なし”にすることも可能です。
パリティビットをつける場合には偶数(even)か奇数(odd)のどちらかに決めます。
今回は偶数にしました。
最後にストップビットが1ビットです。

(今回はスタートビット+8ビット+偶数パリティ+ストップビット1の固定フォーマットで受信するコードを書きました)

今回のフォーマットを図に示すと、以下の通りになります。

これは1バイトあたりの図になり、11ビットで構成されます。
nバイトを送信(または受信)する場合にはこれをn回繰り返すことになります。
左から右に向かって時が流れていきます。

パリティが偶数の場合、各ビットの1の数が偶数になるようにパリティを 0 または 1 にします。

例えば小文字の ‘P’ は2進数で 01110000b (0x70) ですから偶数パリティは 1 になります。
奇数の場合、パリティは 0 です。

通信速度が115200bpsの場合、スタートビット(1ビット)分の幅が1秒間に115200個存在することになります。
bpsは bit per second の略で、1秒間あたりのビット数です。

受信する場合、PIOアセンブラは端子から受け取ったデータをこのフォーマットに従って取り込み、C言語システムへ送ります。

プロジェクトの作成

以下の設定でプロジェクトを作成します。

Project Name: picoPioUartRx

Library Options の PIO interface をチェックする
Console Options の Console over UART のチェックをはずす
IDE Options の Create VSCode project をチェックする
Debugger: は PicoProbe を選択する

起動後、何かファイルが開いてたら File – Close Folder で閉じた後、File – Open Folder から picoPioUartRx のフォルダーを辿って選択します。

こちらの環境では C:\Users\m3925\Documents\Pico\pico-project-generator\picoPioUartRx です。

m3925の部分は皆さんのユーザー名に置き換えてください。

vscode関連の json ファイルは前回作成済のプロジェクトのものをドラッグ&ドロップでコピー&ペーストすると楽です。

コードの編集

CMakeLists.txtを編集する

PIOでプログラムする場合、アセンブラを書くファイルが必要になり、その拡張子は pio になります。

今回のプロジェクト名は picoPioUartRx ですから後で picoPioUartRx.pio というファイルを作成します。

C/C++ SDK の環境では コンパイルする際にこのファイルをヘッダーファイルとして取り込むことになります。

picoPioUartRx.c では冒頭に

#incldue "picoPioUartRx.pio.h"

と記述することで、アセンブラのファイルをCのヘッダーファイルとして取り込むことができます。

CMakeLists.txtでもこのファイルの存在を知らせてあげる必要があり、その部分は編集して追加します。

(この部分はコードジェネレーターが対応してくれないので不足分を追加する必要があります)

CMakeLists.txtの add_executable()の後あたりに以下の1行を追加します。

pico_generate_pio_header(picoPioUartRx ${CMAKE_CURRENT_LIST_DIR}/picoPioUartRx.pio)

picoPioUartRx.pioを編集する

以下のコードを書いて、picoPioUartRx.pio で保存し、picoPioUartRx.c と同じフォルダにコピーしておきます。

; 以降はアセンブラにコメント解説を入れておきました。
// 以降はCのコメント解説です。

.program picoPioUartRx
    wait 0 pin 0        ; GPIO0のスタートビットを待ちます
    set y, 8 [10]       ; yに8をセットし10クロック遅延します
bitloop:                ; 
    in pins, 1          ; 指定されたピンから1ビット読みます
    jmp y-- bitloop [6] ; 次のビットを読む時間になるまで待ちます

.program picoPioUartTx
.side_set 1 opt

; An 8n1 UART transmit program.
; OUT pin 0 and side-set pin 0 are both mapped to UART TX pin.

; TX FIFO からOSRに取り込み、サイドセットピンを"high"にして、7クロック遅延します
    pull       side 1 [7]
; Xに8をセットし、サイドセットピンを"low"にして、7クロック遅延します(スタートビット)
    set x, 8   side 0 [7]
bitloop:
; OSRから1ビットpinsに出力します
    out pins, 1
; Xがゼロになるまでループします
    jmp x-- bitloop   [6]
; ゼロになったら pull side 1 [7] の命令に移動します

% c-sdk {
#include "hardware/clocks.h"
#include "hardware/gpio.h"

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

    // 連続する複数のピンに方向を指定します
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, false);

    // GPIOを初期化します
    pio_gpio_init(pio, pin);

    // GPIOをプルアップします
    gpio_pull_up(pin);

    // デフォルト設定を得ます
    pio_sm_config c = picoPioUartRx_program_get_default_config(offset);

    // ピンを入力に設定します(pin = GPIO0)
    sm_config_set_in_pins(&c, pin); // for WAIT, IN

    // 右シフト、自動pushする、スレッショルドを9に設定します
    // LSBファーストなので右シフトする
    // データ8ビット+パリティ付きなのでスレッショルドは9
    sm_config_set_in_shift(&c, true, true, 9);

    // 1クロックを通信速度の8倍に設定します
    // (クロック8個で1ビット分の幅になる仕様)
    float div = (float)clock_get_hz(clk_sys) / (8 * baud);
    sm_config_set_clkdiv(&c, div);
    
    // SMを初期化します
    pio_sm_init(pio, sm, offset, &c);

    // SMを有効にします
    pio_sm_set_enabled(pio, sm, true);
}

static inline void picoPioUartTx_program_init(PIO pio, uint sm, uint offset, uint pin_tx, uint baud) {
    // マスクしてピンを設定します
    pio_sm_set_pins_with_mask(pio, sm, 1u << pin_tx, 1u << pin_tx);

    // マスクしてピン方向を設定します
    pio_sm_set_pindirs_with_mask(pio, sm, 1u << pin_tx, 1u << pin_tx);

    // GPIO初期化します
    pio_gpio_init(pio, pin_tx);

    // デフォルト設定を得ます
    pio_sm_config c = picoPioUartTx_program_get_default_config(offset);

    // 右シフト、自動pullしない、スレッショルドを9に設定します
    // LSBファーストなので右シフトする
    // アセンブラで pull命令を書いているので自動pullは不要
    // データ8ビット+パリティ付きなのでスレッショルドは9
    sm_config_set_out_shift(&c, true, false, 9);

    // 出力に設定します(pin_tx = GPIO0)
    sm_config_set_out_pins(&c, pin_tx, 1);
    // サイドセットピンを設定します(pin_tx = GPIO0)
    // (出力ピンとサイドセットピンに同じものを指定することも可能)
    sm_config_set_sideset_pins(&c, pin_tx);

    // 1クロックを通信速度の8倍に設定します
    // (クロック8個で1ビット分の幅になる仕様)
    float div = (float)clock_get_hz(clk_sys) / (8 * baud);
    sm_config_set_clkdiv(&c, div);

    // SMを初期化します
    pio_sm_init(pio, sm, offset, &c);
    // SMを有効にします
    pio_sm_set_enabled(pio, sm, true);
}

%}

前半にアセンブラのコードを書きます。
今回は受信と送信用のコードが書かれています。

% c-sdk { (1) %} の(1)の部分はC言語のヘッダーファイルとして出力されます。
初期化処理等をここに書いておきます。
ヘッダファイルになるので、関数の実態を記述する場合には static inline を指定します。

picoPioUartRx.cを編集する

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

#define SERIAL_BAUD PICO_DEFAULT_UART_BAUD_RATE  // 115200
#define PIO_RX_PIN 1
#define PIO_TX_PIN 0

// パリティ付きで送信します
void picoPioUartTx_program_putc(PIO pio, uint sm, char c, bool even_parity) {
    uint32_t byte = (uint32_t)c;
    uint8_t parity = 0;
    for (int i = 0; i < 8; i++) {
        parity ^= byte & 0x1;
        byte >>= 1;
    }
    byte = (uint32_t)c;
    if (parity) {
        if (even_parity) {
            byte |= 0x100;  // 偶数になるようにパリティを付加します
        }
    } else {
        if (!even_parity) {
            byte |= 0x100;  // 奇数になるようにパリティを付加します
        }
    }
    pio_sm_put_blocking(pio, sm, (uint32_t)byte);  // TX FIFOへputします
}

// パリティが正しければ parity_check が true になります
char picoPioUartRx_program_getc(PIO pio, uint sm, bool even_parity,
                                bool* parity_check) {
    while (pio_sm_is_rx_fifo_empty(pio, sm)) tight_loop_contents();

    uint32_t c32 = pio_sm_get(pio, sm);
    // MSB側の上位9ビットに値があるのでシフトして下位に持ってきます
    c32 >>= 23;
    bool real_parity = false;
    if (c32 & 0x100) {
        real_parity = true;
    } else {
        real_parity = false;
    }

    uint32_t byte = c32 & 0xff;
    uint8_t pcheck = 0;

    // パリティの計算
    for (int i = 0; i < 8; i++) {
        pcheck ^= byte & 0x1;
        byte >>= 1;
    }
    if (real_parity) {
        if (even_parity) {
            if (pcheck) {
                *parity_check = true;
            } else {
                *parity_check = false;
            }
        }
    } else {
        if (!even_parity) {
            if (pcheck) {
                *parity_check = true;
            } else {
                *parity_check = false;
            }
        }
    }
    return (char)c32 & 0xff;
}

int main() {
    // 使うPIOを指定します
    PIO pio = pio0;

    // 使うSMを指定します
    uint sm_rx = 0;

    // 命令用のメモリーにアセンブラのプログラムを登録します
    // 第2引数には picoPioUartTx.pioファイルで書いたアセンブラコードの ".program
    // の後の名前 + _program に & をつけます"
    uint offset = pio_add_program(pio, &picoPioUartRx_program);
    picoPioUartRx_program_init(pio, sm_rx, offset, PIO_RX_PIN, SERIAL_BAUD);

    // 使うSMを指定します(送信と受信では別のSMを使ってみました)
    uint sm_tx = 1;
    // 命令用のメモリーにアセンブラのプログラムを登録します
    // 第2引数には picoPioUartTx.pioファイルで書いたアセンブラコードの ".program
    // の後の名前 + _program に & をつけます"
    uint offset2 = pio_add_program(pio, &picoPioUartTx_program);

    // 初期化関数を呼びます。picoPioUartTx.pio に記述している関数です
    picoPioUartTx_program_init(pio, sm_tx, offset2, PIO_TX_PIN, SERIAL_BAUD);

    bool parity_check;
    while (true) {
        // 受信するまで待機します
        char c = picoPioUartRx_program_getc(pio, sm_rx, true, &parity_check);
        // 受信した文字を送信します
        picoPioUartTx_program_putc(pio, sm_tx, c, true);  // trueで偶数パリティ
    }
}

コードの解説

コードに適時コメントを入れましたので参考になさってください。

最初に説明したフォーマットの図を眺めておくとコードも理解できると思います。

アセンブラの注意事項:

送受信どちらも、1データの幅が8クロックで設計されています。
これで理解できると思います。

受信側の以下の関数にはパリティチェック用のコードを書きました。

picoPioUartRx_program_getc()関数内でパリティが正しければ parity_check が true になります。

ただし今回使ったPCアプリの Tera Term では正しいパリティが付加されないようです。
データ長が8ビットの場合、パリティをつけることはできないのかも知れません。
私が書いコードではいつもパリティ"1"が検出されているので、ストップビット(=="1")を見ているのかも知れません。

PC側の準備

動作確認用として Tera Term を使います。

Tera Termをお持ちでない方は こちら からダウンロードしてお使いください。

インストール後、 Tera Term を起動し、シリアルポートでPicoprobeのCOMポートを選択します。

もしCOMポートが複数ある場合には、Picoprobe以外の通信ケーブルをはずしてデバイスマネージャーでCOMポート番号を確認しておきます。

起動後、設定メニューのシリアルポートから以下の設定を行います。
パリティは even に設定します。

スピート : 115200
データ : 8 bit
パリティ : even
ストップビット : 1
フロー制御 : none

それから設定メニューの端末から以下の設定を行います。

初期状態からローカルエコーにチェックを入れます。

動作させてみる

それでは F5キーを押してプログラムを動作させてみます。

Tera Termから送信した文字が戻ってくれば成功です。

いかがでしたか?
皆さんはうまくPIOでUARTの受信(送受信)ができましたか?

GitHub に picoPioUartRx のコードをアップしましたのでご自由にお使いください。
(ただし当方では動作の保証は行っていません)

お疲れさまでした。

PIOカテゴリの最新記事