皆さん こんにちは。
今回は 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 のコードをアップしましたのでご自由にお使いください。
(ただし当方では動作の保証は行っていません)
お疲れさまでした。