2025/12/13(土)CH32V003J4M6でハードウェアSerialとSWIOを同時使用 +Tips

CH32V003関連Tips集。特にSOIC 8pinのCH32V003J4M6について。

ハードウェアシリアルを有効にするとSWIOが死ぬ問題

CH32V003J4M6.png

CH32はSWIOと呼ばれるピン1本でファームウェアの書き込みができます。CH32V003J4M6では、SWIO(8pin)に他に多くの機能が割り当てられており、いずれかのペリフェラルをonにするとSWIO機能がオフになってしまいます。

SWIOがオフになると、CH32V003への書き込みに失敗するようになります。これを解決するにはWCH-LinkUtilityを使用してROMを消去する必要があります(参考サイト)。

回避策としては、

  • deley()や他の入力ピンなどを使用して、条件付きでSWIOをonにする。
  • ハードウェアシリアルを使用しない(ソフトウェアシリアルで代用)。

などが知られていますが、どれも不便です。

解決法

void setup() {
    Serial.begin(9600);
    GPIO_PinRemapConfig(GPIO_PartialRemap2_USART1, ENABLE);     // TX=PD6, RX=PD5
    USART1->CTLR1 &= (-1 ^ 0x04);                               // Disable RX (=PD5/SWIO)

    GPIO_InitTypeDef  GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;           // or 2MHz
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;            // assin PD6 for peripheral
    GPIO_Init(GPIOD, &GPIO_InitStructure);
}

void loop() {
    Serial.println("Hello");
    delay(1000);
}

ハードウェアシリアルの出力機能だけ有効にしています。

解説

  • Remapレジスタを変更して、RXとTXを逆にします(TX=PD6, RX=PD5=SWIO)。
    • GPIO_PartialRemap2_USART1 と GPIO_PartialRemap1_USART1 の2ビット指定ですが、10b にしたいので GPIO_PartialRemap2_USART1=1 だけ設定します。*1
  • その上で、USART1のRX機能(シリアル入力機能)をオフにします。これにより、SWIOピン(RX=PD5=SWIO)機能が再び有効になります。
  • このままでは、PD6からうまくシリアル出力ができないので、TX=PD6をシリアル用に初期化します。

別解(非Arduino環境でも使用可)

直接レジスタを書き換えるほうが短くなります。

void setup() {
    Serial.begin(9600);
    AFIO->PCFR1   |= (GPIO_PartialRemap2_USART1 & 0x07ffffff);      // TX=PD6, RX=PD5
    USART1->CTLR1 &= (-1 ^ 0x04);                                   // Disable RX (=PD5/SWIO)
    GPIOD->CFGLR  &= (-1 ^ (15<<(6*4)));                            // clear PD6 setting
    GPIOD->CFGLR  |= (GPIO_Speed_50MHz | GPIO_Mode_AF_PP)<<(6*4);   // assin PD6 for peripheral
}

*1 : 他の指定は詳細はCH32V003リファレンスマニュアルをUSART1_RMで検索してください。

StandbyモードとAWU(Auto Wake Up)タイマーの使用

void setup_AWU_timer() {
    RCC->RSTSCKR |= RCC_LSION;                  // Enable LSI clock
    EXTI->EVENR  |= EXTI_Line9;                 // AWU event on
    EXTI->RTENR  |= EXTI_Line9;                 // AWU Rising edge trigger
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    PWR_AWU_SetPrescaler(PWR_AWU_Prescaler_4096);       // 128kHz/4096 = 31.25Hz, tick 32msec
}

void sleep_by_AWU(int delay) {
    PWR->AWUWR = delay;   // 0 to 0x3f     
    PWR_AutoWakeUpCmd(ENABLE);

    PWR_EnterSTANDBYMode(PWR_STANDBYEntry_WFE);

    PWR_AutoWakeUpCmd(DISABLE);
}

AWU割り込みの使用

volatile bool AWU_flag = false;
extern "C" {
    void AWU_IRQHandler() __attribute__((interrupt("WCH-Interrupt-fast")));
    void AWU_IRQHandler() {
        EXTI->INTFR |= EXTI_Line9;              // clear interrupt flag
    }
}

void setup_AWU_timer() {
    RCC->RSTSCKR |= RCC_LSION;                  // Enable LSI clock
    EXTI->INTENR |= EXTI_Line9;                 // AWU interrupt
    EXTI->RTENR  |= EXTI_Line9;                 // AWU Rising edge trigger
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    PWR_AWU_SetPrescaler(PWR_AWU_Prescaler_4096);       // 128kHz/4096 = 31.25Hz, tick 32msec

    NVIC_EnableIRQ(AWU_IRQn);                   // Enable AWU_IRQHandler
}

スタンバイモード時を使用しつつSWIOも有効にする

ch32funのstandby_btnのソースで触れられている手法ですが、実際に実装するとやや面倒なので、実装例として書いておきます。

void setup_AWU_timer() {
    RCC->RSTSCKR |= RCC_LSION;                  // Enable LSI clock
    EXTI->EVENR  |= EXTI_Line9;                 // AWU event on
    EXTI->RTENR  |= EXTI_Line9 | EXTI_Line1;    // AWU Rising edge trigger
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    PWR_AWU_SetPrescaler(PWR_AWU_Prescaler_4096);       // 128kHz/4096 = 31.25Hz, tick 32msec

    AFIO->EXTICR |= 0b11 << (2*1);              // SELECT PD1 for Line1
    EXTI->EVENR  |= EXTI_Line1;                 // GPIO 1pin Event
    EXTI->FTENR  |= EXTI_Line1;                 // falling trigger
    EXTI->INTENR |= EXTI_Line1;                 // check SWIO Event


    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOD, &GPIO_InitStructure);
}

void sleep_ms_by_AWU(int delay_ms) {
    int count = delay_ms>>5;                    // div by 32
    count = (count<0)      ?    1 : count;
    count = (0x40 < count) ? 0x40 : count;      // 0 is 0x40
 
    PWR->AWUWR = count;   // 0 to 0x3f     
    PWR_AutoWakeUpCmd(ENABLE);

    GPIO_PinRemapConfig(GPIO_Remap_SDI_Disable, ENABLE);
    PWR_EnterSTANDBYMode(PWR_STANDBYEntry_WFE);
    GPIO_PinRemapConfig(GPIO_Remap_SDI_Disable, DISABLE);

    PWR_AutoWakeUpCmd(DISABLE);

    if (EXTI->INTFR & EXTI_Line1) {         // Wakeup by PD1=SWIO
        EXTI->INTFR |= EXTI_Line1;          // clear INT flag
        delay(50);
    }
}
  • PD1をプルアップ入力とし、イベント及び割り込みを有効に設定しておきます。
  • 割り込みルーチン自体は使わないので不要です。
  • スタンバイモードに入る直前に、SWIOをoffにしてPD1に割り当てます。
  • スタンバイモード復帰後、SWIOによる割り込みが発生しているかを確認します。
  • SWIOによる割り込みならば、delay() してSWIO操作を受け付けるようにします。
    • 復帰後の処理が十分長いのならば、delay() は不要です。

スタンバイモードの消費電流を減らすために

普通にスタンバイモードに入っても、1mA程度の電流を消費してしまいます。仕様値の 10uA に近づけるためには、物理的に存在しないピンを含めすべてのピンをプルアップ(またはプルダウン)入力に設定する必要があります。

実用時の入出力設定は様々だと思いますので、プログラムの一番最初で全ピンを初期化しておくと良いです。

void init_gpio_for_low_power() {
    // GPIO_Init require peripheral clock
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_All;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPD;      // input pull-down
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;   // or 2MHz
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_Init(GPIOD, &GPIO_InitStructure);
}

CPU周波数を下げると Delay() がうまく動かない

本来、次の命令をを実行することで、どんな動作周波数でもうまく動作することになっています。

SystemCoreClockUpdate();
Delay_Init();

しかし Delay_Init() の実装にバグがあり、動作周波数が8MHz未満のときに実行すると、delay()が長時間ループして帰ってこなくなります。

void Delay_Init(void) {
    p_us = SystemCoreClock / 8000000;	// =0 になる
    p_ms = (uint16_t)p_us * 1000;	// =0 になる
}

参考資料