Tanuki_Bayashin’s diary

電子工作を趣味としています。最近はラズベリーパイPicoというマイコンを使って楽しんでいます

ラズパイPicoで使うPico‐SDK C/C++ に関する覚え書き

【目次】

1.はじめに

 ラズベリーパイPicoというマイコンがあります(以下Picoと表記します)。倒立振子を作り上げたくてPico-SDK(Pico用のC/C++にて開発するための環境)に手を出してきました。いくつか機能別にコードの書き方をまとめておきたいと思います。(自分用の備忘録)

参考:
製品としてのPicoに関する情報がいっぱい載ってます。
Raspberry Pi Documentation - Raspberry Pi Pico and Pico W

Pico‐SDKの構築やプログラミングに関するサイトです。
https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf

※rev1. AD3~4について、A/Dコンバーターの節にて追記しました。以下のサイトを参照しました。(2022/10/12)
macOSからPi Picoを使う - その3:腰も砕けよ 膝も折れよ:So-net blog


2.各処理の記述

2.1 GPIO

 GPIOの処理に関して説明します。GPIO(General-purpose input/output)とは汎用入出力を意味していて、Picoの各ピンについて入力とするか、出力とするかを指定すると、それに応じて入出力それぞれの役割をピンに与えることができます。
 入力であれば’0’なのか、’1’なのかを識別し、プログラムにてピンの状態を知ることができます。出力側であれば、’0’または’1’に応じてピンの状態をLOWレベルかHIGHレベルにするかを指定することができます。
 具体的なコードの書き方をリスト1に示します。

リスト1

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

#define BUTTON_BLACK 14     // 黒のタクトsw Push -> ON
#define LED_RED 15          // RED LED

void main()
{
    int sw_black;

    gpio_init(BUTTON_BLACK);
    gpio_set_dir(BUTTON_BLACK, GPIO_IN);
    gpio_pull_up(BUTTON_BLACK);

    gpio_init(LED_RED);
    gpio_set_dir(LED_RED, GPIO_OUT);

    gpio_put(LED_RED, 1); // RED LED is ON
    sw_black = gpio_get(BUTTON_BLACK);
}
図1 Raspberry Pi Pico のピン配置(GPIOとA/D変換に関して)

図1の出典:Raspberry Pi Documentation - Raspberry Pi Pico and Pico W

解説:
1ー3行目:
 必要なヘッダファイルをinclude しています。少なくとも3行目のヘッダファイルは必要です。
(それ以外は、printf()などで使う可能性があります)

5ー6行目:
 マクロを定義しています。

8ー21行目:
 main()関数の中身です。
10行目:
 sw_blackという変数を定義しています。
12ー14行目:
 12行目ではBUTTON_BLACKの中身は14なので、PicoのGPIO14を(Picoの物理的な14番ピンとは異なります。GPIOの番号を指定する必要があります)使える形に指定しています。これを「インスタンス(またはオブジェクト)を生成する」と言います。

 13行目ではGPIO14を入力側として指定しています。
 14行目ではプルアップの指定をしています。これにより、外部にプルアップ抵抗を接続しなくても、それがついているかのように動作するようになります。

16ー17行:
 16行目でこちらでもLED_REDの中身であるGPIO15を使える形にしています。
 17行目にて、GPIO15を出力側として指定しています。

19-20行目:
 GPIO15の値を1にしています。(接続されている赤のLEDを点灯しています)
 GPIO14から値を読み取り、sw_blackに格納しています。(BUTTON_BLACKが押されれば、プルアップされているので0となります)

2.2 パルス幅変調(PWM)

 パルス幅変調(PWM: pulse width modulation)に関する記述の仕方を見ていきます。(パルス幅変調そのものの説明は略します)
 具体的なコードの書き方をリスト2に示します。

スト2

#include "pico/stdlib.h"
#include "hardware/pwm.h"

#define PIN_PWM 12 //(GPIO12 #16Pin PWM_A[6])

void main()
{
    static pwm_config pwm_slice_config;
    uint pwm_slice_num;

    /* GPIOにPWMを割り当て */
    gpio_set_function(PIN_PWM, GPIO_FUNC_PWM);
    pwm_slice_num = pwm_gpio_to_slice_num(PIN_PWM);

    /* clkdiv と wrap を指定 */
    pwm_set_clkdiv(pwm_slice_num, 12.20703);
    pwm_set_wrap(pwm_slice_num, 1023);

    /* レベル値(デューティカウント値)を設定(ここでは0) */
    pwm_set_chan_level(pwm_slice_num, PWM_CHAN_A, 0);

    /* pwm start */
    pwm_set_enabled(pwm_slice_num, true);
    //pwm_set_enabled(pwm_slice_num, false);
    /* END of PWM Module Setting */

    while(1){
        int pwm_value = calc_pwm();    // calc_pwm() は架空の関数です

        //pwm width 0 -> 1023
        pwm_set_chan_level(pwm_slice_num, PWM_CHAN_A, pwm_value);

    }

}
図2 Raspberry Pi Pico のピン配置(PWM用)

図2の出典:Raspberry Pi PicoのGPIO機能まとめ/MicroPythonでのプログラム方法 | メタエレ実験室

解説:大事そうな箇所だけ説明します。

8,9行目:
static pwm_config という変数の型はヘッダファイル"hardware/pwm.h" の中で定義されていると思われます。

11-13行目:
GPIO12(実はPWM_A[6]にあたる)をPWMとして使えるようにしています。
pwm_slice_num という変数には上のPWM_A[6]の6が代入されるようです。PWMの各種設定時に必要となります。

15-17行目:
次に clkdiv と wrap を指定します。Picoのメインクロック125MHzとPWMの動作周波数f[Hz]とclkdiv, wrap の間には以下の関係があります。

 125×10^6 = f× clkdiv × (wrap + 1)

今回はf=10KHz、wrap=1023としたかったので、
 clkdiv = 125×10^6 ÷ (10000 ×1024) = 12.20703
となりました。計算結果の小数部は1/16の倍数で丸められるようです。
(clkdivは分周比、wrapはPWMのデューティー比を指定するときの分解能に当たります)

19-20行目:
PWMの端子(GPIO12)からいよいよ値を出力します。ただし、ここでは0としています。

22-25行目:
PWMの機能を有効にしています。23行内の関数で’true’ とすることで、有効になるようです。
ここを’false’ とすると(24行目、コメント行内)無効にすることもできます。

27‐33行目:
 無限ループです

28行:
ある関数calc_pwm() からの戻り値を変数’pwm_value’ にて受け取っています。

31行:
この関数により、PWMの端子に’pwm_value’ の値を出力しています。
ここで’pwm_value’ は上のwrap と対応していて、0-1023の間で指定できます。

解説は以上です。

以下の3つの記事を参考にしました。

lipoyang.hatenablog.com

rikei-tawamure.com

qiita.com

2.3 A/Dコンバーター

 A/Dコンバーターに関する記述の仕方を解説したいと思います。

 PicoではADC0~ADC2とA/Dコンバーターは3個入っています。
(調べたところ、AD3とAD4もありました。詳細は1章のrev1. の資料を参照してください)(2022/10/12 追記)

また、A/Dコンバーターの出力は0‐4095の間です(12bit)。そして入力端子の最大電圧はADC_VREF(Picoの35番ピン)とAGND(33ピン)を設定することにより変更可能です。(指定しなければ3.3V。最大値は仕様書をお調べください)

 リスト3にPico-SDKのサンプルプログラムを載せます。
リスト3

/**
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"

int main() {
    stdio_init_all();
    printf("ADC Example, measuring GPIO26\n");

    adc_init();

    // Make sure GPIO is high-impedance, no pullups etc
    adc_gpio_init(26);
    // Select ADC input 0 (GPIO26)
    adc_select_input(0);

    while (1) {
        // 12-bit conversion, assume max value == ADC_VREF == 3.3 V
        const float conversion_factor = 3.3f / (1 << 12);
        uint16_t result = adc_read();
        printf("Raw value: 0x%03x, voltage: %f V\n", result, result * conversion_factor);
        sleep_ms(500);
    }
}

解説:
A/Dコンバーターに関する部分だけ説明します。
(図1の右上の辺りにADCのピンが集まっています)

16行目:
 A/Dコンバーターの初期化をしています。
19行目:
 GPIO26をA/Dコンバーターとして使えるように初期化しています。GPIOとADCの関係は以下の通りです。
 GPIO26(31番ピン) <-> ADC0
 GPIO27(32番ピン) <-> ADC1
 GPIO28(34番ピン) <-> ADC2

26行目:
 A/Dコンバーターから値を読み取り、’result’ という変数に格納しています。

説明は以上です。

2.4 タイマ割り込み

 マイコンを使って信号を処理したり、何かを制御したいときなどには一定の間隔で処理を行いたいときがあります。
 そんなときに使われるのがタイマー割り込みです。以下にその記述の仕方を示します。
リスト4にPico-SDKのサンプルプログラムにあったものを載せます。
リスト4

/**
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

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

/// \tag::timer_example[]
volatile bool timer_fired = false;

int64_t alarm_callback(alarm_id_t id, void *user_data) {
    printf("Timer %d fired!\n", (int) id);
    timer_fired = true;
    // Can return a value here in us to fire in the future
    return 0;
}

bool repeating_timer_callback(struct repeating_timer *t) {
    printf("Repeat at %lld\n", time_us_64());
    return true;
}

int main() {
    stdio_init_all();
    printf("Hello Timer!\n");

    // Call alarm_callback in 2 seconds
    add_alarm_in_ms(2000, alarm_callback, NULL, false);

    // Wait for alarm callback to set timer_fired
    while (!timer_fired) {
        tight_loop_contents();
    }

    // Create a repeating timer that calls repeating_timer_callback.
    // If the delay is > 0 then this is the delay between the previous callback ending and the next starting.
    // If the delay is negative (see below) then the next call to the callback will be exactly 500ms after the
    // start of the call to the last callback
    struct repeating_timer timer;
    add_repeating_timer_ms(500, repeating_timer_callback, NULL, &timer);
    sleep_ms(3000);
    bool cancelled = cancel_repeating_timer(&timer);
    printf("cancelled... %d\n", cancelled);
    sleep_ms(2000);

    // Negative delay so means we will call repeating_timer_callback, and call it again
    // 500ms later regardless of how long the callback took to execute
    add_repeating_timer_ms(-500, repeating_timer_callback, NULL, &timer);
    sleep_ms(3000);
    cancelled = cancel_repeating_timer(&timer);
    printf("cancelled... %d\n", cancelled);
    sleep_ms(2000);
    printf("Done\n");
    return 0;
}
/// \end::timer_example[]

解説:
 タイマー割り込みの部分のみ解説します(タイマー割り込みと呼んでいいのか実は自分でも疑問です)。
 
30行目の

add_alarm_in_ms(2000, alarm_callback, NULL, false);

という処理については説明は省きます。(よく分かっていないです)

37‐46行の部分と48―55行目の部分がメインとなってきます。コメントで(英語で)書かれていますが、繰り返す周期をプラスで指定すると、処理が終わってから指定した値にて時間を空けて再び同じ処理を行うこととなり、マイナスで指定すると、処理を始める周期が指定した値にて繰り返されることになります。

add_repeating_timer_ms();
の関数を
add_repeating_timer_us();
に変えると、μsec にて繰り返す周期を指定できます。(周期(第1引数)はint型が好ましいようです)
説明が簡単すぎますが、後は繰り返したい処理(関数の名前は自由なようです)の戻り値の型や、引数の型などに気を付ければうまく動作すると思います。

うまくいかないときは”1.はじめに”にて示したサイトにて調べて頂くか、いろいろ試してみて体得して頂ければと思います。
(細かく説明すると、自分でもよく分かっていない部分があるので、うまく説明できないかと思いこのような記述とさせていただきます)

2.5 処理の時間計測

関数 time_us_32() というものを使いました。CPUが動作を開始してからの時間が戻ってくるようです。('time_us_64()' というものもあります。こちらは64ビットにて値が返されます)
リスト5にて処理にかかる時間を計測する手順を示します。(自己流です)

リスト5

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

void main()
{
static uint32_t t_start, t_term;
    t_start = time_us_32();

    //ある処理

    t_term = time_us_32() - t_start;

}

t_termには’//ある処理’ にかかった時間が入ります。
(’//ある処理’ にはなんかしらかの記述があるものと想定してください(笑))

以下のサイトに時間に関する関数について詳しく説明がなされています。
(無くなっていました。 2023/10/04訂正)
(おわり)