PSoC 6 のデュアルコアでLチカ (6)


これは、PSoC Advent Calendar 2017の9日目に突っ込まれた記事です。

前回までのあらすじ

前回の記事では、PWM出力の立ち上がりと立ち下がりのタイミングを使ってLEDの点灯と消灯を制御していました。これらのタイミングは、単一のPWMを使用しているので衝突する心配はありません。
今回は、ふたつめのハードウェアタイマを使用して、リソースの競合について考えます。

本プロジェクトのコンセプト

今回のプロジェクトではハードウェアタイマをふたつ使います。

二つのPWMは、分周比をわずかに変えてあります。PWM1000が1000分周、PWM1005が1005分周です。クロック周波数を100kHzに設定しているので、PWMの周波数はそれぞれ100Hzと99.5Hzとなります。LEDは、この差分の0.5Hz(2秒)の周期で緩やかに明るさを変化させます。

Cortex-M0+側のプログラム

このプロジェクトでは、Cortex-M0+がセマフォを初期化し、PWMを制御しています。

main_cm0p.c
#include "project.h"

#define SEMAPHORE_LED  (8)  /* セマフォの識別番号 */

/* Clear 割り込みの処理ルーチン */
void Int_Clear_isr(void) {
    /* セマフォを獲得する */
    while (
        Cy_IPC_Sema_Set(SEMAPHORE_LED, false) != CY_IPC_SEMA_SUCCESS
    ) ;
    /* GPIO 出力をクリア (Low) する。 */
    Cy_GPIO_Clr(Pin_LEDR_PORT, Pin_LEDR_NUM);
    /* セマフォを解放する */
    while (
        Cy_IPC_Sema_Clear(SEMAPHORE_LED, false) != CY_IPC_SEMA_SUCCESS
    ) ;
}

int main(void) {
    __enable_irq(); /* 全体の割り込みを許可する */

    /* PWM の起動 */
    PWM1000_Start();
    PWM1005_Start();

    /* Cortex-M4 を叩き起こして CY_CORTEX_M4_APPL_ADDR から実行させる。 */
    Cy_SysEnableCM4(CY_CORTEX_M4_APPL_ADDR); 

    /* Clear 割り込みの初期化 */
    Cy_SysInt_Init(&Int_Clear_cfg, Int_Clear_isr);
    NVIC_ClearPendingIRQ(Int_Clear_cfg.intrSrc);
    NVIC_EnableIRQ(Int_Clear_cfg.intrSrc);

    for (;;) {
    }
}

セマフォの使い方については、PSoC 6 のデュアルコアでLチカ (2)を参照してください。一番の違いは、セマフォの獲得と解放を割り込みサービスルーチン(ISR)の中で行っているところです。きっと、こんな所でロックさせない方がいいんだよね。

Cortex-M4側のプログラム

一方、Cortex-M4側のプログラムは、以下のようになっています。

main_cm4.c
#include "project.h"

#define SEMAPHORE_LED  (8)  /* セマフォの識別番号 */

/* Set 割り込みの処理ルーチン */
void Int_Set_isr(void) {
    /* セマフォを獲得する */
    while (
        Cy_IPC_Sema_Set(SEMAPHORE_LED, false) != CY_IPC_SEMA_SUCCESS
    ) ;
    /* GPIO 出力をセット (High) する。 */
    Cy_GPIO_Set(Pin_LEDR_PORT, Pin_LEDR_NUM);
    /* セマフォを解放する */
    while (
        Cy_IPC_Sema_Clear(SEMAPHORE_LED, false) != CY_IPC_SEMA_SUCCESS
    ) ;
}

int main(void) {
    __enable_irq(); /* 全体の割り込みを許可する */

    /* Set 割り込みの初期化 */
    Cy_SysInt_Init(&Int_Set_cfg, Int_Set_isr);
    NVIC_ClearPendingIRQ(Int_Set_cfg.intrSrc);
    NVIC_EnableIRQ(Int_Set_cfg.intrSrc);

    for (;;) {
    }
}

こちらも、セマフォの処理を入れただけの変更です。

ところで、排他制御は必要だったの?

今回のプロジェクトでは、二つのCPUがGPIOという一つのリソースを取り合うという構成なので、GPIOのアクセスで排他制御を入れました。ここで排他制御の対象となっているのはCy_GPIO_Clr()関数とCy_GPIO_Set()関数です。

cy_gpio.h
__STATIC_INLINE void Cy_GPIO_Clr(GPIO_PRT_Type* base, uint32_t pinNum)
{
    base->OUT_CLR = CY_GPIO_OUT_MASK << pinNum;
}

__STATIC_INLINE void Cy_GPIO_Set(GPIO_PRT_Type* base, uint32_t pinNum)
{
    base->OUT_SET = CY_GPIO_OUT_MASK << pinNum;
}

これらの関数は、インライン関数であり、しかも、GPIOをClear/Setする専用レジスタに対する書込みとして定義されているので、「アトミック」です。したがって、このプロジェクトに関してはここまでの排他制御を必要としないでしょう。

ところが、他の関数、例えば下に示した割り込みマスクを制御する関数などは注意が必要です。

cy_gpio.h
__STATIC_INLINE void Cy_GPIO_SetInterruptMask(GPIO_PRT_Type* base, uint32_t pinNum, uint32_t value)
{
    uint32_t tempReg;

    tempReg= base->INTR_MASK & ~(CY_GPIO_INTR_EN_MASK << pinNum);
    base->INTR_MASK = tempReg | ((value & CY_GPIO_INTR_EN_MASK) << pinNum);
}

tempRegにINTR_MASKの値を取り出してからINTR_MASKレジスタに書き込むまでの間に別のCPUがINTR_MASKを書き換えたとしたら、INTR_MASKレジスタの値は正しく更新されないという事になります。同一ポートの異なる端子が別々のCPUから操作されるような場合には可能性があります。

この例のようにプログラムを書くユーザがライブラリで生成されるAPIの中身にまで責任を持てない場合には、安全策として排他制御を行った方が賢明でしょう。

関連文献

AN215656 – PSoC 6 MCU Dual-Core CPU System Design
AN217666 - PSoC 6 MCU Interrupts
CE216795 - PSoC(R) 6 MCU Dual-Core Basics
PSoC 6 MCU: PSoC 63 with BLE Architecture Technical Reference Manual

関連記事

PSoC 6 のデュアルコアでLチカ (1)
PSoC 6 のデュアルコアでLチカ (2)
PSoC 6 のデュアルコアでLチカ (3)
PSoC 6 のデュアルコアでLチカ (4)
PSoC 6 のデュアルコアでLチカ (5)