Raspberry Pi Pico C/C++ PIOでポート出力してみる

#PIOとは

公式ドキュメント[RP2040 Datasheet](https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf)にはこんな説明があります。
>PIO is programmable in the same sense as a processor. There are two PIO blocks with four state machines each, that
>can independently execute sequential programs to manipulate GPIOs and transfer data. Unlike a general purpose
>processor, PIO state machines are highly specialised for IO, with a focus on determinism, precise timing, and close
>integration with fixed-function hardware.


要は、PIOとはプログラム可能なIOに特化したペリフェラルです。
（自分なりの理解なので間違えているかも。）
マイコンのペリフェラルというとUARTとかPWMなんかがありますよね。
これらはある程度レジスタ変更(定数変更)で動作を変えることはできますが、
あくまでもUARTはUART通信専用,PWMはPWM専用でそれ以外のことはできません。
対してPIOはアルゴ変更（ユーザーが自由にプログラム）でき自由度の高いペリフェラルになっています。
ペリフェラルはCPUとは独立して動くので速くて正確なタイミングで動かすことができるはずです。

こんなおもしろい機能試してみないわけにはいきませんよねってことで試してみました。
(今回はとりあえず出力だけです。)


#フォルダ構造
試した時のフォルダ構造について書いておきます。

```
pio_test
  | pio_test.c
  | pio_test.pio 
  | CMakeLists.txt
  | pico_sdk_import.cmake
  │
  └─build
      | pio_test.h
```
①pio_test.c
　CPU側のプログラムを書くファイル
　　main()が書いてあるファイル

②pio_test.pio
　PIO側のプログラムを書くファイル
　　PIO用のアセンブラコードとPIOの初期化関数(C言語）を書く
　　　(こいつからpio_test.hが生成される)

③CMakeLists.txtはこんな感じ

```CMakeLists.txt
cmake_minimum_required(VERSION 3.12)

# Pull in SDK (must be before project)
include(pico_sdk_import.cmake)

project(pio_test C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialize the SDK
pico_sdk_init()

add_executable(pio_test)

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

target_sources(pio_test PRIVATE pio_test.c)

target_link_libraries(pio_test PRIVATE
        pico_stdlib
        hardware_pio
        )

pico_add_extra_outputs(pio_test)

```

④pico_sdk_import.cmake
　SDKインポート用のファイル。pico-sdk\external\に入っているものをコピーして置いています。

⑤pio_test.h
　pio_test.pioから生成されるファイル（ビルドすると作成されます）
　　アセンブラを機械語に翻訳したデータ？や(プログラム名)_program_get_default_config()なんかが入っています。
　　（サンプルコードhallo_pioを読んでてhelo_program_get_default_configがどこにもない!?って
　　　悩んでいたんですが、ビルドするとアセンブラを解析して勝手に生成されるようです。）

#PIOで出力してみる
公式ドキュメントを見た感じではPIOの出力には
OUT
SET
side-set
この3種類の方法があるようです。
簡単にまとめるとこんな感じです。

| 命令 | 値の指定 | 同時に操作できるピン数 | 消費する命令数 |
|:-:|:-:|:-:|:-:|
|OUT| Output Shift Register |  最大32 | 1 |
|SET| 即値 | 最大32  | 1 |
|side-set| 即値 |  最大5(※1) | 0 |

※1side-setはDelayと共有する形で5bit分用意されています。
　なのでside-setのピン数を増やすとその分Delayが使えなくなります。

実はアセンブラは32命令までしか記述できません。
32命令以内に収めるにはside-setの活用が鍵になりそうですね。



それぞれ試してみます。

##OUT
```
out pins, 2
;Output Shift Register (OSR)の2bit分をpins(出力ピン)に移動する
```
OUT命令はOutput Shift Register (OSR)にあるデータを出力ピンのレジスタに移動する命令です。
移動先にはピン以外のレジスタも指定できます。
最大32ピンまで操作できます。


GPIO2,GPIO3を互い違いにトグルさせるコードを書いてみました。

```c:pio_test.c
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "pio_test.pio.h"

#define PIO_OUT00    ( 2 )
#define PIO_OUT01    ( 3 )

void main( void )
{
    PIO pio = pio0;
        /* PIO0を使用する。 */

    uint offset = pio_add_program( pio, &pio_test_program );
        /* 指定したPIOのインストラクションメモリにプログラムをロードする */
        /* プログラムの先頭アドレス(オフセット)が戻り値として返ってくる */

    uint sm = pio_claim_unused_sm( pio, true );
        /* 指定したPIOインスタンスの使用していないステートマシンを取得 */

    pio_test_program_init( pio, sm, offset, PIO_OUT00, 2 );
        /* PIOの初期化を実施する */

    while ( true )
    {
        pio_sm_put_blocking( pio, sm, 0x0001 ); /* ( GPIO3, GPIO2 ) = ( L, H ) */
            /* TX FIFOにデータを書き込み */

        sleep_us( 1 );
            /* 1us待機 */

        pio_sm_put_blocking( pio, sm, 0x0002 ); /* ( GPIO3, GPIO2 ) = ( H, L ) */
            /* TX FIFOにデータを書き込み */

        sleep_us( 1 );
            /* 1us待機 */

    }
}
```

```c:pio_test.pio
.program pio_test

loop:
    pull            ; TX_FIFOからOSRへデータをPULL
    out pins, 2     ; OSRから2bit分出力ポートへ
    jmp loop        ; 先頭に戻る


% c-sdk {
static inline void pio_test_program_init( PIO pio, uint sm, uint offset, uint out_base, uint out_pin_num )
{
    pio_sm_config c = pio_test_program_get_default_config( offset );
        /* PIOステートマシンコンフィグのデフォルト値を取得 */

    sm_config_set_out_pins( &c, out_base, out_pin_num );
        /* PIOステートマシンコンフィグの出力ピン設定を編集する */
        /* ベースピン番号とベースから何ピンまで使うかを指定 */

    {
        uint pin_offset;
        for ( pin_offset = 0; pin_offset < out_pin_num; pin_offset++ )
        {
            pio_gpio_init( pio, out_base + pin_offset );
            /* GPIOをPIOに割り当てる */
        }
    }

    pio_sm_set_consecutive_pindirs( pio, sm, out_base, out_pin_num, true );
        /* ピンの向きを設定 */

    sm_config_set_clkdiv( &c, 6 );
        /* クロック周波数6分周 */

    pio_sm_init( pio, sm, offset, &c );
        /* PIOステートマシンコンフィグを反映しプログラムカウンタを設定 */

    pio_sm_set_enabled( pio, sm, true );
        /* PIOステートマシンを有効にする */
}
%}

```

ポート出力をロジアナで見てみるとこんな感じです。ちゃんと動いてますね。(ch2,ch3)
![キャプチャ0.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398022/ad6f1341-98f8-19a0-1f22-793f0af31f5a.jpeg)





##SET
```
set pins, 0x01
;pins(出力ピン)に0x01をセットする
```
SET命令は出力ピンのレジスタに即値をセットする命令です。
セット先にはピン以外のレジスタも指定できます。
最大32ピンまで操作できます。

上記のプログラムに加えてGPIO4,GPIO5をSETで操作してみます。

```c:pio_test.pio
.program pio_test

loop:
    pull            ; TX_FIFOからOSRへデータをPULL
    out pins, 2     ; OSRから2bit分出力ポートへ
    set pins, 0x01  ; SETポートへ0x01をセット( GPIO5, GPIO4 ) = ( L, H )
    set pins, 0x02  ; SETポートへ0x02をセット( GPIO5, GPIO4 ) = ( H, L )
    set pins, 0x01  ; SETポートへ0x01をセット( GPIO5, GPIO4 ) = ( L, H )
    set pins, 0x02  ; SETポートへ0x02をセット( GPIO5, GPIO4 ) = ( H, L )
    set pins, 0x01  ; SETポートへ0x01をセット( GPIO5, GPIO4 ) = ( L, H )
    set pins, 0x02  ; SETポートへ0x02をセット( GPIO5, GPIO4 ) = ( H, L )
    jmp loop        ; 先頭に戻る


% c-sdk {
static inline void pio_test_program_init( PIO pio, uint sm, uint offset, uint out_base ,uint out_pin_num, uint set_base ,uint set_pin_num )
{
    pio_sm_config c = pio_test_program_get_default_config( offset );
        /* PIOステートマシンコンフィグのデフォルト値を取得 */

    sm_config_set_out_pins( &c, out_base, out_pin_num );
        /* PIOステートマシンコンフィグの出力ピン設定を編集する */
        /* ベースピン番号とベースから何ピンまで使うかを指定 */
    sm_config_set_set_pins( &c, set_base, set_pin_num );
        /* PIOステートマシンコンフィグのSETピン設定を編集する */
        /* ベースピン番号とベースから何ピンまで使うかを指定 */

    {
        uint pin_offset;
        for ( pin_offset = 0; pin_offset < out_pin_num; pin_offset++ )
        {
            pio_gpio_init( pio, out_base + pin_offset );
            /* GPIOをPIOに割り当てる */
        }
        for ( pin_offset = 0; pin_offset < set_pin_num; pin_offset++ )
        {
            pio_gpio_init( pio, set_base + pin_offset );
            /* GPIOをPIOに割り当てる */
        }
    }

    pio_sm_set_consecutive_pindirs( pio, sm, out_base, out_pin_num, true );
        /* ピンの向きを設定 */
    pio_sm_set_consecutive_pindirs( pio, sm, set_base, set_pin_num, true );
        /* ピンの向きを設定 */

    sm_config_set_clkdiv( &c, 6 );
        /* クロック周波数6分周 */

    pio_sm_init( pio, sm, offset, &c );
        /* PIOステートマシンコンフィグを反映しプログラムカウンタを設定 */

    pio_sm_set_enabled( pio, sm, true );
        /* PIOステートマシンを有効にする */
}
%}

```

指示通り波形が出ていますね(ch4,ch5)。
分周無しだと速すぎるので6分周して1クロック48nsに指定していますが、確かに1命令48nsくらいで実行されていそう。
数ns単位で正確にポート叩けるってかなり驚異的です。
![キャプチャ1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398022/5c571133-1de1-109a-8383-5a6e9a0a6c31.jpeg)





##side-set
```
out pins, 1 side 1
;出力ピンに1をセットと同時にside-setピンに1をセット
```
side-setは命令のついでにside-setピンに即値をセットできる命令のオプションです。
最大5ピンまで操作できます。

上記のプログラムに加えてGPIO6,GPIO7をside-setで操作してみます。

```c:pio_test.pio
.program pio_test

.side_set 2                   ;side-setとして2ピン使用する

loop:
    pull           side 0x00  ; TX_FIFOからOSRへデータをPULL
    out pins, 2    side 0x01  ; OSRから2bit分出力ポートへ
    set pins, 0x01 side 0x02  ; SETポートへ0x01をセット( GPIO5, GPIO4 ) = ( L, H )
    set pins, 0x02 side 0x03  ; SETポートへ0x02をセット( GPIO5, GPIO4 ) = ( H, L )
    set pins, 0x01 side 0x02  ; SETポートへ0x01をセット( GPIO5, GPIO4 ) = ( L, H )
    set pins, 0x02 side 0x01  ; SETポートへ0x02をセット( GPIO5, GPIO4 ) = ( H, L )
    set pins, 0x01 side 0x00  ; SETポートへ0x01をセット( GPIO5, GPIO4 ) = ( L, H )
    set pins, 0x02 side 0x01  ; SETポートへ0x02をセット( GPIO5, GPIO4 ) = ( H, L )
    jmp loop       side 0x02  ; 先頭に戻る


% c-sdk {
static inline void pio_test_program_init( PIO pio, uint sm, uint offset, uint out_base ,uint out_pin_num, uint set_base ,uint set_pin_num, uint side_base, uint side_pin_num )
{
    pio_sm_config c = pio_test_program_get_default_config( offset );
        /* PIOステートマシンコンフィグのデフォルト値を取得 */

    sm_config_set_out_pins( &c, out_base, out_pin_num );
        /* PIOステートマシンコンフィグの出力ピン設定を編集する */
        /* ベースピン番号とベースから何ピンまで使うかを指定 */
    sm_config_set_set_pins( &c, set_base, set_pin_num );
        /* PIOステートマシンコンフィグのSETピン設定を編集する */
        /* ベースピン番号とベースから何ピンまで使うかを指定 */
   sm_config_set_sideset_pins( &c, side_base );
        /* PIOステートマシンコンフィグのsideピン設定を編集する */
        /* ベースピン番号を指定(何ピン使うかはアセンブラ側で指定する) */

    {
        uint pin_offset;
        for ( pin_offset = 0; pin_offset < out_pin_num; pin_offset++ )
        {
            pio_gpio_init( pio, out_base + pin_offset );
            /* GPIOをPIOに割り当てる */
        }
        for ( pin_offset = 0; pin_offset < set_pin_num; pin_offset++ )
        {
            pio_gpio_init( pio, set_base + pin_offset );
            /* GPIOをPIOに割り当てる */
        }
        for ( pin_offset = 0; pin_offset < side_pin_num; pin_offset++ )
        {
            pio_gpio_init( pio, side_base + pin_offset );
            /* GPIOをPIOに割り当てる */
        }
    }

    pio_sm_set_consecutive_pindirs( pio, sm, out_base, out_pin_num, true );
        /* ピンの向きを設定 */
    pio_sm_set_consecutive_pindirs( pio, sm, set_base, set_pin_num, true );
        /* ピンの向きを設定 */
    pio_sm_set_consecutive_pindirs( pio, sm, side_base, side_pin_num, true );
        /* ピンの向きを設定 */

    sm_config_set_clkdiv( &c, 6 );
        /* クロック周波数6分周 */

    pio_sm_init( pio, sm, offset, &c );
        /* PIOステートマシンコンフィグを反映しプログラムカウンタを設定 */

    pio_sm_set_enabled( pio, sm, true );
        /* PIOステートマシンを有効にする */
}
%}
```
指示通り波形が出ていますね(ch6,ch7)。
![キャプチャ2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398022/ae3e7c33-eccd-6a18-118e-25e344c5b4d4.jpeg)