鳥なき里のマイコン屋(146) ラズパイPico、C/C++SDK、マルチコアで割り込み1

Joseph Halfmoon

折角2コア積んでいるラズパイPicoなのに、大分前にちょっと2コアで動かしただけでした。これでは宝の持ち腐れです。無理にでも2コア使う方針といたしました。ま、動かしてみないと身に染みないこともあるっと。今回は2コアで外部端子からの割り込みを受けてみました。早速のお間抜けあり。

(末尾にテストに使ったCのソース全文と、CMakeLists.txtを掲げました。2021-11-22追記:こちらのソースには問題あるので、次回の方をご覧いただいた方が良いです。)

起動時はArm Cortex-M0+のシングルコアで動きだすラズパイPicoですが、2コア目に起動をかけるのは簡単でした。C/C++SDKを使って2コア目を動かして制御してみた回はこちら

一方、外部端子からの割り込みをハンドリングしてみた回はこちら。GPIOで割り込んだ後、LEDを一定期間点灯させるのに、ワンショットタイマでタイミングをとっていました。

ラズパイPicoのRP2040の割り込みは、両方のコアにArm規定のNVICが存在していて受け付けています。また、両方のコアに割り込み要因は配られているようです。割り込みの設定をしたコアがそのまま割り込みを受け付けるのが基本みたい。両方のコアで割り込みを受けるのも(レーシングとかなければ)特に問題ないみたいです。今回は、既に実験済の上記2つを「がっちゃんこ」して、以下のような動作をさせてみることにいたしました。

  • 2コアそれぞれでGPIO割り込みを受けてみる
  • それぞれの割り込みハンドラの中でそれぞれ異なるGPIO端子に接続したLEDを一定期間点灯させてみる

なお、GPIOに関するドキュメントは以下にあります。

Raspberry Pi Pico SDK Documentation  hardware_gpio

テスト用の回路

GPIOを使ったテストなので、何か外付けしないとできませぬ。必要最低線ということで取り付けましたのが以下です(先頭のアイキャッチ画像に母艦ラズパイ4とも合わせた写真ありです。)

MULTI_GPIO_INTR_DUT_SCH目論んでいる使い方としては以下のとおりです。

  1. プッシュスイッチ SW1 はGP18に接続。フォーリングエッジでコア0への割り込みをトリガ
  2. プッシュスイッチ SW2 はGP19に接続。フォーリングエッジでコア1への割り込みをトリガ
  3. 赤色LED D1はGP20に接続。コア0の外部割込みハンドラ内で点灯。
  4. 青色LED D2はGP21に接続。コア1の外部割込みハンドラ内で点灯。

基本的には「動作した」のですが、LEDの消灯処理のところでまずはやらかしてました。

問題の出た最初のコード

それぞれ実行中のコアで、それぞれ自分への割り込みハンドラの登録をしています。しかし、GPIOからの外部割込みについては、

  • 割り込み要因は13番の一つしか割り当てられていない。
  • API上、GPIOからの割り込みハンドラの設定時にGPIOの端子番号を指定できるが、今のところ、これには意味がないみたい

ということでした。このため、

  • コア0のGPIO割り込みハンドラは、GP18端子のときだけ実処理、それ以外は即終了。
  • コア1のGPIO割り込みハンドラは、GP19端子のときだけ実処理、それ意外は即終了。

ということにいたしました。ちょっと不安があったのですが、ハンドラ自体は動いているようです。

しかし、問題が出たのは、割り込みを受けた後、LEDを点灯し、「1秒後に消灯」の消灯の方でした。なお、点灯中は次のGPIO割り込みは不許可にしてます。最初の点灯消灯制御方法は以下です。

  1. 割り込みハンドラ内で、LEDを点灯
  2. 1秒後に、インターバルタイマのアラームをしかける
  3. アラームのコールバック内で、LEDを消灯する(あわせて再度のGPIO割り込みを許可)

コア0、コア1とも同じ制御でした。割り込みハンドラとインターバルタイマのコールバック部分はこんな感じ。以下はコア0用のもの。

int64_t alarm_callback0(alarm_id_t id, void *user_data) {
    gpio_put(EXT_LED0, false);
    gpio_set_irq_enabled(EXT_SW0, GPIO_IRQ_EDGE_FALL, true);
}

gpio_irq_callback_t gpio_callback0(uint gpio, uint32_t event) {
    if (gpio == EXT_SW0) {
        gpio_put(EXT_LED0, true);
        add_alarm_in_ms(1000, alarm_callback0, NULL, false);
    }
}

それぞれのプッシュスイッチを交互に押しているときはこの方法でも問題が出なかったのです。SW1を押すと、赤のLEDが点灯、約1秒後に消灯します。次にSW2を押すと、青のLEDが点灯、やはり約1秒後に消灯します。

しかし問題は、SW1とSW2の同時押しでした。手で押しているので、回路的にはまったくの同時ということはないでしょうが、消灯の管理をするための

インターバルタイマ

のところで問題が発生したようです。インターバルタイマが正常に動作しないので、赤のLEDも青のLEDも点灯したまま、それっきりとなりました。コアそのものが生きて動いているのは、それぞれのコアのwhileループ内で、たまにUSBシリアルにメッセージを出力していることで分かります。2個のコアでインターバルタイマを奪い合うのがマズイのね。

やっつけ修正してみたコード

インターバルタイマの「とりあい」が問題の原因であるようだったので

  • コア0はそのままインターバルタイマを使ってよろしい
  • コア1は、sleep_ms()で時間をつぶしてください

ということに変更しました。ハンドラの中でsleep_ms()するのは後ろめたいので後で直すつもりですが、「とりあえず」やっつけでご乱心の「テスト」だからということで許可(自分が。)

2021-11-22追記:まあ、ハンドラの中でsleepするという暴挙に出ているので当然なのですが、「やっつけ修正コード」だと PANIC! に陥ることがあります。次回の方をご覧いただいた方が良いです。

この修正を加えてみたところ、スイッチ2個を同時押ししても、ちゃんと赤、青のLEDがほぼ1秒後に消灯するようになりました。末尾のコードはこの「やっつけ」コードです。

なお、以下は、コア0,コア1から垂れ流されてくる「生きてループを回っているよ」を示すループ回数の出力の様子です。

MSGなお、例によって Picoのproject generatorにCMakeLists.txtを作ってもらいましたが、素のままではマルチコアのビルドができませぬのでご注意を。マルチコア使うときは、

target_link_libraries に pico_multicore

を追加しておかないとなりません。添付のCMakeLists.txtは上記の手修正いれています。

まあ、次回はもっと2コアの深みにハマるのだな、ヨシ(何が?)

鳥なき里のマイコン屋(145) ラズパイPico、C/C++SDKでDMAを使ってみる へ戻る

鳥なき里のマイコン屋(147) ラズパイPico、C/C++SDK、マルチコアで割り込み2 へ進む

実験に使用したCMakeLists.txt
# Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# initalize pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/home/pi/pico/pico-sdk")

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

project(multINTR C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1

add_executable(multINTR multINTR.c )

pico_set_program_name(multINTR "multINTR")
pico_set_program_version(multINTR "0.1")

pico_enable_stdio_uart(multINTR 0)
pico_enable_stdio_usb(multINTR 1)

# Add the standard library to the build
target_link_libraries(multINTR
    pico_stdlib
    pico_multicore)

pico_add_extra_outputs(multINTR)
実験に使用したCソースコード全文
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "hardware/gpio.h"

#define EXT_SW0     (18)
#define EXT_SW1     (19)
#define EXT_LED0    (20)
#define EXT_LED1    (21)

int64_t alarm_callback0(alarm_id_t id, void *user_data) {
    gpio_put(EXT_LED0, false);
    gpio_set_irq_enabled(EXT_SW0, GPIO_IRQ_EDGE_FALL, true);
}

// DO NOT USE
//int64_t alarm_callback1(alarm_id_t id, void *user_data) {
//    gpio_put(EXT_LED1, false);
//    gpio_set_irq_enabled(EXT_SW1, GPIO_IRQ_EDGE_FALL, true);
//}

gpio_irq_callback_t gpio_callback0(uint gpio, uint32_t event) {
    if (gpio == EXT_SW0) {
        gpio_put(EXT_LED0, true);
        add_alarm_in_ms(1000, alarm_callback0, NULL, false);
    }
}

gpio_irq_callback_t gpio_callback1(uint gpio, uint32_t event) {
    if (gpio == EXT_SW1) {
        gpio_put(EXT_LED1, true);
//        add_alarm_in_ms(1000, alarm_callback1, NULL, false);
        sleep_ms(1000);
        gpio_put(EXT_LED1, false);
    }
}

void core1_task() {
    int core1Counter = 0;
    gpio_init(EXT_SW1);
    gpio_init(EXT_LED1);
    gpio_set_dir(EXT_LED1, GPIO_OUT);
    gpio_set_dir(EXT_SW1, GPIO_IN);
    gpio_put(EXT_LED1, false);
    gpio_set_irq_enabled_with_callback(EXT_SW1, GPIO_IRQ_EDGE_FALL, true, gpio_callback1);

    while (true) {
        printf("CORE1: %d\r\n", ++core1Counter);
        sleep_ms(7777);
    }
}

int main()
{
    int loopCounter = 0;
    stdio_init_all();
    gpio_init(EXT_SW0);
    gpio_init(EXT_LED0);
    gpio_set_dir(EXT_LED0, GPIO_OUT);
    gpio_set_dir(EXT_SW0, GPIO_IN);
    gpio_put(EXT_LED0, false);
    gpio_set_irq_enabled_with_callback(EXT_SW0, GPIO_IRQ_EDGE_FALL, true, gpio_callback0);

    multicore_launch_core1(core1_task);

    while (true) {
        printf("CORE0: %d\r\n", ++loopCounter);
        sleep_ms(5000);
    }

    puts("NEVER!");
    return 0;
}