CH32V103のGPIO割り込みを調べる

概要

上記で受信バッファが無いことがわかりましたので、受信割り込みを使って受信してみようとしました。しかし情報が少ないのでGPIO割り込みから実験してみました。

GPIO割り込み

void key_int(){
  Serial.println("key_int");
}

void setup() {
  Serial.begin(115200);
  pinMode(PA0, INPUT);
  attachInterrupt(PA0, GPIO_Mode_IN_FLOATING, key_int, EXTI_Mode_Interrupt, EXTI_Trigger_Falling);
}

void loop() {
  Serial.println(digitalRead(PA0));
  delay(100);
}

手元の環境だと上記で動きました。Arduinoなのである程度共通処理なのですがattachInterrupt()関数の引数がちょっと違います。割り込みコールバック関数のkey_int()関数になにかアトリビュートを設定しないといけないのかはわかりませんでした。おそらく必要なさそうです。

attachInterrupt()関数の定義

void attachInterrupt(uint32_t pin, GPIOMode_TypeDef io_mode,  void (*callback)(void), EXTIMode_TypeDef it_mode, EXTITrigger_TypeDef trigger_mode)
{
  PinName p = digitalPinToPinName(pin);
  GPIO_TypeDef* port = set_GPIO_Port_Clock(CH_PORT(p));
  if (!port) return ;
  pinV32_DisconnectDebug(p);

  ch32_interrupt_enable(port, io_mode, CH_GPIO_PIN(p), callback, it_mode, trigger_mode);
}

ドキュメントや利用方法の情報がないので引数の定義を調べつつ、処理を追っていく必要があります。

GPIOMode_TypeDef

/* Configuration Mode enumeration */
typedef enum
{
    GPIO_Mode_AIN = 0x0,
    GPIO_Mode_IN_FLOATING = 0x04,
    GPIO_Mode_IPD = 0x28,
    GPIO_Mode_IPU = 0x48,
    GPIO_Mode_Out_OD = 0x14,
    GPIO_Mode_Out_PP = 0x10,
    GPIO_Mode_AF_OD = 0x1C,
    GPIO_Mode_AF_PP = 0x18
} GPIOMode_TypeDef;

pinModeとの違いがわかりませんが、GPIOの初期化から再設定しているような感じの設定値です。今回はプルアップされているボタンを接続したのでGPIO_Mode_IN_FLOATINGを指定してみました。

EXTIMode_TypeDef

/* EXTI mode enumeration */
typedef enum
{
    EXTI_Mode_Interrupt = 0x00,
    EXTI_Mode_Event = 0x04
} EXTIMode_TypeDef;

イベントか割り込みかの指定です。割り込みを選んでみましたがEXTI_Mode_Eventにしたほうが細かい割り込み制御をしなくてもよいのかもしれません。

EXTITrigger_TypeDef

/* EXTI Trigger enumeration */
typedef enum
{
    EXTI_Trigger_Rising = 0x08,
    EXTI_Trigger_Falling = 0x0C,
    EXTI_Trigger_Rising_Falling = 0x10
} EXTITrigger_TypeDef;

トリガーの設定です。プルアップのボタンだったので、押したときのLOWに落ちるFallingを指定しました。

ch32_interrupt_enable

void ch32_interrupt_enable(GPIO_TypeDef *port, GPIOMode_TypeDef io_mode,uint16_t pin, void (*callback)(void), EXTIMode_TypeDef it_mode, EXTITrigger_TypeDef trigger_mode)
{
    GPIO_InitTypeDef GPIO_InitStruct={0};
    EXTI_InitTypeDef EXTI_InitStruct={0};
    // RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
    uint8_t id = get_pin_id(pin);
    uint8_t gpio_port_souce=0;
    GPIO_InitStruct.GPIO_Pin  = pin;
    GPIO_InitStruct.GPIO_Mode = io_mode;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(port, &GPIO_InitStruct);

    #if defined(GPIOA_BASE)
    if(port == GPIOA)  gpio_port_souce = GPIO_PortSourceGPIOA;
    #endif
    #if defined(GPIOB_BASE) 
    if(port == GPIOB)  gpio_port_souce = GPIO_PortSourceGPIOB;
    #endif
    #if defined(GPIOC_BASE)
    if(port == GPIOC)  gpio_port_souce = GPIO_PortSourceGPIOC;
    #endif
    #if defined(GPIOD_BASE)
    if(port == GPIOD)  gpio_port_souce = GPIO_PortSourceGPIOD;
    #endif
    #if defined(GPIOE_BASE)
    if(port == GPIOE)  gpio_port_souce = GPIO_PortSourceGPIOE;
    #endif
    #if defined(GPIOF_BASE)
    if(port == GPIOF)  gpio_port_souce = GPIO_PortSourceGPIOF;
    #endif
    #if defined(GPIOG_BASE)
    if(port == GPIOG)  gpio_port_souce = GPIO_PortSourceGPIOG;
    #endif
    #if defined(GPIOH_BASE)
    if(port == GPIOH)  gpio_port_souce = GPIO_PortSourceGPIOH;
    #endif

    GPIO_EXTILineConfig(gpio_port_souce, id);
    EXTI_InitStruct.EXTI_Line = exti_lines[id];
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
    EXTI_InitStruct.EXTI_Mode = it_mode;
    EXTI_InitStruct.EXTI_Trigger = trigger_mode;
    EXTI_Init(&EXTI_InitStruct);

    gpio_irq_conf[id].callback = callback;

    NVIC_SetPriority(gpio_irq_conf[id].irqnb, EXTI_IRQ_PRIO);
    NVIC_EnableIRQ(gpio_irq_conf[id].irqnb);
}

ちょっと長いですがGPIOとEXTIの設定をしたあとにNVICで割り込み設定をしているようでした。実はWCHのサンプルコード集があるのですがそこと処理が違っていました。

/*********************************************************************
 * @fn      EXTI0_INT_INIT
 *
 * @brief   Initializes EXTI0 collection.
 *
 * @return  none
 */
void EXTI0_INT_INIT(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    EXTI_InitTypeDef EXTI_InitStructure = {0};
    NVIC_InitTypeDef NVIC_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* GPIOA ----> EXTI_Line0 */
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

上記がCH32V103EVT.ZIPのEXTI0の初期化例なのですがNVIC_Initで設定しています。最初はEVTのコードを持ってきて動かしたのですがどうも割り込みが正常に動かなかったのでNVIC_SetPriority()関数とNVIC_EnableIRQ()関数で初期化する必要がありそうでした。

割り込み発生時に呼び出される関数はどこ?

ch32_interrupt_enable()関数でコールバック関数を渡していますが、直接その関数が呼び出されるわけではありません。

/**
  * @brief This function his called by the HAL if the IRQ is valid
  * @param  GPIO_Pin : one of the gpio pin
  * @retval None
  */
void _gpio_exti_callback(uint16_t GPIO_Pin)
{
  uint8_t irq_id = get_pin_id(GPIO_Pin);
  if (gpio_irq_conf[irq_id].callback != NULL) {
    gpio_irq_conf[irq_id].callback();
  }
}

設定したコールバック関数は上記で呼び出されていました。そしてこの関数を呼び出している関数がさらにありました。

データシートを確認

https://www.wch-ic.com/downloads/CH32xRM_PDF.html

ここまで来てデータシートをやっと確認します。CH32F103と、今回利用しているCH32V103は同じデータシートになっています。そしてCH32F103はCortex-M3ベースと書いてあります。

STM32 Nucleo Board STM32F103: 開発ツール・ボード 秋月電子通商-電子部品・ネット通販
電子部品,通販,販売,半導体,IC,LED,マイコン,電子工作STM32 Nucleo Board STM32F103秋月電子通商 電子部品通信販売

実際のところ、STM32F103をベースに設計されています。上記が開発ボードです。

そして、今回利用しているCH32V103の開発ボードです。命令セットはCortex-M3からRISC-Vに変わっていますが、基本的な作りはSTM32に非常に似ています。そしてArduino環境もSTM32のArduino環境をベースにして作られているのでSTM32を触ったことがある人はCH32も比較的触りやすいはずです。私はSTM32はほとんど触ったことがないので手探りで調べています。

Table 9-1 Vector table of CH32F103

割り込みテーブルを確認したところEXTI0から4までありました。

もっと探してみると、下の方にEXTI9_5とEXTI15_10がありました。

  {.irqnb = EXTI0_IRQn,     .callback = NULL}, //GPIO_PIN_0
  {.irqnb = EXTI1_IRQn,     .callback = NULL}, //GPIO_PIN_1
  {.irqnb = EXTI2_IRQn,     .callback = NULL}, //GPIO_PIN_2
  {.irqnb = EXTI3_IRQn,     .callback = NULL}, //GPIO_PIN_3
  {.irqnb = EXTI4_IRQn,     .callback = NULL}, //GPIO_PIN_4
  {.irqnb = EXTI9_5_IRQn,   .callback = NULL}, //GPIO_PIN_5
  {.irqnb = EXTI9_5_IRQn,   .callback = NULL}, //GPIO_PIN_6
  {.irqnb = EXTI9_5_IRQn,   .callback = NULL}, //GPIO_PIN_7
  {.irqnb = EXTI9_5_IRQn,   .callback = NULL}, //GPIO_PIN_8
  {.irqnb = EXTI9_5_IRQn,   .callback = NULL}

  #ifndef CH32V10x  
  , //GPIO_PIN_9
  {.irqnb = EXTI15_10_IRQn, .callback = NULL}, //GPIO_PIN_10
  {.irqnb = EXTI15_10_IRQn, .callback = NULL}, //GPIO_PIN_11
  {.irqnb = EXTI15_10_IRQn, .callback = NULL}, //GPIO_PIN_12
  {.irqnb = EXTI15_10_IRQn, .callback = NULL}, //GPIO_PIN_13
  {.irqnb = EXTI15_10_IRQn, .callback = NULL}, //GPIO_PIN_14
  {.irqnb = EXTI15_10_IRQn, .callback = NULL}  //GPIO_PIN_15
  #endif

定義をさがしてみたところ、上記の設定があります。CH32V103だと16個のGPIO割り込みが使えそうですね。

/* Private_Defines */
#ifdef CH32V00x
#define NB_EXTI   (8) 

#elif defined(CH32X035)
#define NB_EXTI   (26)

#else
#define NB_EXTI   (16)
#endif

数は少し上にこんな宣言がありました。X035はちょっと特殊な感じですね。

つづいてEXTIとGPIOの関連を確認してみます。数字の0から15はPx0-Px15に対応するようです。xはPA、PB、PC、PDの4つあって、すべてのIOに割り込みが設定できると書いてあります。

上記に説明がありましたが、EXTI1はPA1、PB1、PC1、PD1、PE1から選択する必要があるようです。つまり16個のGPIO割り込みが設定可能ですが、PA1とPB1の組み合わせはできないみたいですね。番号を変えればPA1とPB2の組み合わせはおそらく可能だと思われます。

設定自体はEXTI0から15まで個別にPAからPDのどれを利用するかを選択可能でした。

EXTI0のコールバック関数

void EXTI0_IRQHandler(void)     __attribute__((interrupt("WCH-Interrupt-fast")));
/**
  * @brief This function handles external line 0 interrupt request.
  * @param  None
  * @retval None
  */
void EXTI0_IRQHandler(void)
{
  EXTI_ClearITPendingBit(EXTI_Line0); 
  _gpio_exti_callback(EXTI_Line0);
}

上記の関数が割り込み時のコールバック関数になります。EXTI_ClearITPendingBit関数で割り込みフラグをクリアして、_gpio_exti_callback関数で設定したコールバック関数を呼び出しています。

EXTI0_IRQHandlerを呼び出しているコードはインラインアセンブラで書いてあって、関数テーブルから割り込み番号に応じた関数を呼び出しています。

    .weak   EXTI0_IRQHandler
    .weak   EXTI1_IRQHandler
    .weak   EXTI2_IRQHandler
    .weak   EXTI3_IRQHandler

上記のようにweakで定義してあるので、自分で定義していない場合でもエラーにならない設定になっています。ちなみに未定義の割り込みを実行するとハングアップします。何もしない関数が定義されているのではなく、ハングアップという動作でしたので注意してください。

バグ

/**
  * @brief This function handles external line 5 to 9 interrupt request.
  * @param  None
  * @retval None
  */
void EXTI9_5_IRQHandler(void)
{
  uint32_t pin;
  for (pin = GPIO_Pin_5; pin <= GPIO_Pin_9; pin = pin << 1) {
    EXTI_ClearITPendingBit(pin); 
    _gpio_exti_callback(pin);
  }
}

複数のピンで共通の割り込みの場合、すべてのPINに対してコールバックが実行されています。

fix: call digital interrupt callback only for actually triggered pins by Tasssadar · Pull Request #39 · openwch/arduino_core_ch32
Hello, currently, the interrupt callback for attachInterrupt is called for all pins in a given EXTI line, even though th...

修正のプルリクエスが投げられていますが取り込まれていません。。。WCHはArduinoで使わない方がいいのかな。。。

まとめ

STM32を触ったことがある人の場合には問題ないかもしれませんが、事前知識なしで触ると情報が少ないので大変でした。WCHの資料とともにキーワードで検索してSTM32の資料も参考にしつつ理解を深めていく必要がありそうです。

そしてSTM32の仕様をそのまま引きずっているので、CH32V00xとCH32X035、そしてそれ以外で細かい動きが違うのでArduinoライブラリ側のコードをみてもifdefがかなりはいっていて、対応が面倒な感じでした。

コメント