2025/12/13(土)CH32V003J4M6でハードウェアSerialとSWIOを同時使用 +Tips
CH32V003関連Tips集。特にSOIC 8pinのCH32V003J4M6について。
ハードウェアシリアルを有効にするとSWIOが死ぬ問題

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
}
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 になる
}
参考資料
- ch32fun CH32シリーズのサンプル集
- arduino_core_ch32 公式ライブラリのリポジトリ