yoshiyuki's blog

Arduino/Teensy/Raspberry pi pico を利用した I2C, SPI 通信アプリを紹介します

Raspberry Pi Pico / Serial 通信の関数を作る

Raspberry Pi Pico を自作アプリで操作できる I2Cツールにするファームウェアを作成したのですが、この記事ではその副産物の紹介です。

はじめに

自作アプリから Raspberry pi pico を操作するために PC - Raspberry pi pico 間の USB Serial 通信を利用したかったのに、SDK の資料 (Raspberry pi pico の公式ページの Datasheets の pico/raspberry-pi-pico-c-sdk.pdf) に Serial 通信の関数が無い、というところから始まって、pico-sdk の中からそれを見付けた、というのが以前の話でした。今回は、見付かった Serial 関数を使うに際して必要だった処理を関数にまとめたので、それを紹介します。

作成した Serial 通信の関数

作成した関数と、動作確認用のコードは以下になります。作成した関数はヘッダにまとめています。

プロジェクトフォルダ: pico-test

pico-test
 |-- pico_sdk.import.cmake
 |-- CMakeLists.txt
  -- try_08
      |-- try_08.c
       -- CMakeLists.txt
  -- module_serial
      |-- module_serial.h
      |-- module_serial.c
       -- CMakeLists.txt

pico-test\CMakeLists.txt

cmake_minimum_required(VERSION 3.12)

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

project(pico-test)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialize the SDK
pico_sdk_init()

# Add subdirectory
add_subdirectory(try_08)
add_subdirectory(module_serial)

pico-test\try_08\try_08.c

#include <stdio.h>
#include "pico/stdlib.h"

#include "../module_serial/module_serial.h"

int main() {
    uint8_t cntDAT;
    uint8_t getDAT[1];
    uint8_t inDAT[256];
    uint32_t uintTMP;
    int i;

    stdio_init_all();

    while (true) {
        cntDAT = 0;

        while(serial_available() > 0){
            serial_read(getDAT, 1);
            inDAT[cntDAT] = getDAT[0];
            cntDAT++;
            busy_wait_us_32(10000);
        }

        if(cntDAT > 0){
            // send back
            inDAT[cntDAT] = 0x0A;
            serial_write(inDAT, cntDAT+1);

            // serial_print
            serial_print("Hello world! ");
            serial_print_ln("Hello world!");

            uintTMP = 0xA5CC6633;

            // serial_print_32bit_d
            serial_print_32bit_d(uintTMP);
            serial_print(" ");
            serial_print_32bit_d_ln(uintTMP);

            // serial_print_32bit_b
            serial_print_32bit_b(uintTMP);
            serial_print(" ");
            serial_print_32bit_b_ln(uintTMP);

            // Load test
            for(i=0; i<1000; i++){
                uintTMP = i;
                serial_print_32bit_d(uintTMP);
                serial_print_ln(" Hello world!");
            }
        }
    }
}

pico-test\try_08\CMakeLists.txt

add_executable(try_08
    try_08.c
)

# Add pico_stdlib library which aggregates commonly used features
target_link_libraries(try_08 pico_stdlib module_serial)

# enable usb output, disable uart output
pico_enable_stdio_usb(try_08 1)
pico_enable_stdio_uart(try_08 0)

# create map/bin/hex file etc.
pico_add_extra_outputs(try_08)

pico-test\module_serial\module_serial.h

#ifndef MODULE_SERIAL_H
#define MODULE_SERIAL_H

#include <stdio.h>
#include "pico/stdlib.h"
#include "class/cdc/cdc_device.h"

uint32_t serial_available();

uint32_t serial_read(uint8_t *read_data, int read_data_length);

uint32_t serial_write_1byte(uint8_t write_data);
uint32_t serial_write(uint8_t *write_data, int write_data_length);

uint32_t serial_print(char *write_data);
uint32_t serial_print_ln(char *write_data);

uint32_t serial_print_32bit_d(uint32_t write_data_32bit);
uint32_t serial_print_32bit_d_ln(uint32_t write_data_32bit);

uint32_t serial_print_32bit_b(uint32_t write_data_32bit);
uint32_t serial_print_32bit_b_ln(uint32_t write_data_32bit);

#endif

pico-test\module_serial\module_serial.c

#include "./module_serial.h"

uint32_t serial_available(){
    return tud_cdc_available();
}

uint32_t serial_read(uint8_t *read_data, int read_data_length){
    return tud_cdc_read(read_data, read_data_length);
}

uint32_t serial_write_1byte(uint8_t write_data){
    int cntTO = 0;
    uint8_t write_data_buffer[1] = {write_data};

    while(1 > tud_cdc_write_available()){
        busy_wait_us_32(1000);
        cntTO++;

        if(cntTO == 1000) return 0;
    }

    uint32_t uintTMP = tud_cdc_write(write_data_buffer, 1);
    tud_cdc_write_flush();

    return uintTMP;
}

uint32_t serial_write(uint8_t *write_data, int write_data_length){
    int i;
    uint32_t uintTMP = 0;

    for(i=0;i<write_data_length;i++){
        uintTMP += serial_write_1byte(write_data[i]);
    }

    return uintTMP;
}

uint32_t serial_print(char *write_data){
    return serial_write(write_data, strlen(write_data));
}

uint32_t serial_print_ln(char *write_data){
    int len = strlen(write_data);
    char write_data_buffer[len + 1];
    int i;

    for(i=0; i<len; i++){
        write_data_buffer[i] = write_data[i];
    }

    write_data_buffer[len] = 0x0A;

    return serial_write(write_data_buffer, len + 1);
}

void convert_32bit_d(uint32_t data_32bit, uint8_t *data_out, int *data_digit){
    uint32_t denominator = 10;
    uint32_t data_buffer = data_32bit;
    int i;

    for(i=0; i<9; i++){
        if(data_buffer < denominator){
            *data_digit = i + 1;
            denominator /= 10;
            break;
        }

        if(i == 8){
            *data_digit = i + 2;
            break;
        }
        else{
            denominator *= 10;
        }
    }

    for(i=0; i<*data_digit; i++){
        data_out[i] = '0' + data_buffer / denominator;
        data_buffer %= denominator;
        denominator /= 10;
    }

}

uint32_t serial_print_32bit_d(uint32_t write_data_32bit){
    uint8_t out[11];
    int digit;

    convert_32bit_d(write_data_32bit, out, &digit);

    return serial_write(out, digit);
}

uint32_t serial_print_32bit_d_ln(uint32_t write_data_32bit){
    uint8_t out[11];
    int digit;

    convert_32bit_d(write_data_32bit, out, &digit);
    out[digit] = 0x0A;

    return serial_write(out, digit + 1);
}

void convert_32bit_b(uint32_t data_32bit, uint8_t *data_out){
    int i;

    for(i=0; i<32; i++){
        if((data_32bit & (0x00000001 << (31-i))) > 0){
            data_out[i] = '1';
        }
        else{
            data_out[i] = '0';
        }
    }
}

uint32_t serial_print_32bit_b(uint32_t write_data_32bit){
    uint8_t out[32];

    convert_32bit_b(write_data_32bit, out);

    return serial_write(out, 32);
}

uint32_t serial_print_32bit_b_ln(uint32_t write_data_32bit){
    uint8_t out[33];

    convert_32bit_b(write_data_32bit, out);

    out[32] = 0x0A;

    return serial_write(out, 33);
}

pico-test\module_serial\CMakeLists.txt

add_library(module_serial
        module_serial.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(module_serial pico_stdlib)

# enable usb output, disable uart output
pico_enable_stdio_usb(module_serial 1)
pico_enable_stdio_uart(module_serial 0)

作成した関数の解説

uint32_t serial_available(){
    return tud_cdc_available();
}

Serial 通信の受信 Buffer に格納されているデータ数を返します。関数名を合わせることが目的で、中身は pico-sdk の関数そのままになっています。

uint32_t serial_read(uint8_t *read_data, int read_data_length){
    return tud_cdc_read(read_data, read_data_length);
}

Serial 通信の受信 Buffer からデータを読み出します。これも関数名を合わせることが目的で、中身は pico-sdk の関数そのままです。

uint32_t serial_write_1byte(uint8_t write_data){
    int cntTO = 0;
    uint8_t write_data_buffer[1] = {write_data};

    while(1 > tud_cdc_write_available()){
        busy_wait_us_32(1000);
        cntTO++;

        if(cntTO == 1000) return 0;
    }

    uint32_t uintTMP = tud_cdc_write(write_data_buffer, 1);
    tud_cdc_write_flush();

    return uintTMP;
}

uint32_t serial_write(uint8_t *write_data, int write_data_length){
    int i;
    uint32_t uintTMP = 0;

    for(i=0;i<write_data_length;i++){
        uintTMP += serial_write_1byte(write_data[i]);
    }

    return uintTMP;
}

Serial 通信でデータ送信を行います。serial_write の方がメイン関数で、ここでデータを 1-byte 単位に分けて、実際の処理は serial_write_1byte が行います。
pico-sdk の関数ではデータ送信のためはまず tud_cdc_write でデータを送信 Buffer に格納して tud_cdc_write_flush で出力するという2段階の手順が必要なので、この関数でそれを一つにまとめています。また、tud_cdc_write_avairable で送信 Buffer に格納されたデータ数を見張り、一つ前のデータが出力されるのを待ってから次のデータを送信 Buffer に送ることで送信 Buffer が溢れることがないようにしています。

uint32_t serial_print(char *write_data){
    return serial_write(write_data, strlen(write_data));
}

uint32_t serial_print_ln(char *write_data){
    int len = strlen(write_data);
    char write_data_buffer[len + 1];
    int i;

    for(i=0; i<len; i++){
        write_data_buffer[i] = write_data[i];
    }

    write_data_buffer[len] = 0x0A;

    return serial_write(write_data_buffer, len + 1);
}

シリアル通信で文字列を送信するための関数です。_ln は文字列の最後に改行を付加します。先に作った serial_write 関数では文字列の送信があまりに面倒だったので、耐えられなくなってこれを作成しました。

void convert_32bit_d(uint32_t data_32bit, uint8_t *data_out, int *data_digit){
    uint32_t denominator = 10;
    uint32_t data_buffer = data_32bit;
    int i;

    for(i=0; i<9; i++){
        if(data_buffer < denominator){
            *data_digit = i + 1;
            denominator /= 10;
            break;
        }

        if(i == 8){
            *data_digit = i + 2;
            break;
        }
        else{
            denominator *= 10;
        }
    }

    for(i=0; i<*data_digit; i++){
        data_out[i] = '0' + data_buffer / denominator;
        data_buffer %= denominator;
        denominator /= 10;
    }

}

uint32_t serial_print_32bit_d(uint32_t write_data_32bit){
    uint8_t out[11];
    int digit;

    convert_32bit_d(write_data_32bit, out, &digit);

    return serial_write(out, digit);
}

uint32_t serial_print_32bit_d_ln(uint32_t write_data_32bit){
    uint8_t out[11];
    int digit;

    convert_32bit_d(write_data_32bit, out, &digit);
    out[digit] = 0x0A;

    return serial_write(out, digit + 1);
}

Serial 通信で数値を 10進数の文字列に変換したものを送信するための関数です。serial_print_32bit_d がメイン関数で _ln は最後に改行を追加します。入力は uint32_t で、扱える数値は 32-bit = 4,294,967,295 までとなります。
これも、先に作った serial_write ではレジスタ値を読み出してシリアルモニタで確認するのに苦労したことから、シリアルモニタでそのまま読める形に変換するようにしました。

void convert_32bit_b(uint32_t data_32bit, uint8_t *data_out){
    int i;

    for(i=0; i<32; i++){
        if((data_32bit & (0x00000001 << (31-i))) > 0){
            data_out[i] = '1';
        }
        else{
            data_out[i] = '0';
        }
    }
}

uint32_t serial_print_32bit_b(uint32_t write_data_32bit){
    uint8_t out[32];

    convert_32bit_b(write_data_32bit, out);

    return serial_write(out, 32);
}

uint32_t serial_print_32bit_b_ln(uint32_t write_data_32bit){
    uint8_t out[33];

    convert_32bit_b(write_data_32bit, out);

    out[32] = 0x0A;

    return serial_write(out, 33);
}

シリアル通信で数値を2進数の文字列に変換したものを送信するための関数です。serial_print_32bit_b がメイン関数で、_ln は最後に改行を付加します。入力は uint32_t です。
pico-sdk の I2C 関数を調べていた時レジスタの各 Bit の動きを追うのがたいへんだったので、見やすくなるようにこれを作成しました。

動作確認

前出のコードをビルドして生成された try_08.uf2 を Raspberry pi pico に書き込んで関数の動作確認を行いました。動作確認では Arduino IDE のシリアルモニタを使用して Raspberry pi pico との Serial 通信を行いました。
以下が結果です。
f:id:ysin1128:20211210115846p:plain

動作確認用のコードは、シリアルモニタから何かを送ると作成した Serial 通信の関数が色々と送信してくるようになっています。動作確認では「Hello Raspberry Pi Pico!」と送ってみました。
結果の1行目は serial_read と serial_write の動作確認で、正常に動作すればシリアルモニタから送信した文字列の末尾に改行を付加したものが Raspberry pi pico から返ってきます。実際に送信した「Hello Raspberry Pi Pico!」の文字列が返ってきています。
2行目は serial_print と serial_print_ln の動作確認で、どちらも「Hello world!」の文字列を、一方は改行無しで、他方が改行付きでシリアルモニタに送ってきています。
3行目は serial_print_32bit_d と serial_print_32bit_d_ln の動作確認で、0xA5CC6633 を10進数に変換した文字列を改行無し、改行付きの順でシリアルモニタに送ってきています。ちなみに 0xA5CC6633 = 2,781,636,147 です。
4行目は serial_print_32bit_b と serial_print_32bit_b_ln の動作確認で、0xA5CC6633 を2進数 (Binary) に変換した文字列を改行無し、改行付きの順でシリアルモニタに送ってきています。ちなみに 0xA5CC6633 = 1010_0101_1100_1100_0110_0110_0011_0011 です。
5行目以降は高負荷テストとして「(通し番号) Hello world!」の文字列を 1,000 回、繰り返しシリアルモニタに送って、大量にデータを送っても動作に問題が起きないことを確認しています。以下のように 999 まで問題無く送信できています。
f:id:ysin1128:20211210131109p:plain

以上のように想定通りの動作が問題無く確認できました。