Switching the CH32V003 System Clock to 48 MHz

The previous article, Bare Assembly Bring-Up on the CH32V003, ended with the smallest useful program: reset vector, stack, .bss, GPIO, and an LED blink loop.

That program can run from the reset clock. For a first blink, that is enough. The delay loop is not precise, and it does not need to be.

The next step is to put the chip on a known system clock. On a CH32V003 board with a 24 MHz HSE crystal, the usual target is 48 MHz through the PLL.

That is also the clock path used by OS3 on this target. But the code below stays at the same level as the original bring-up: one assembly file, direct register writes, and the CH32V003 reference manual.

What The Manual Says

The relevant blocks are RCC and Flash.

RCC owns the clock tree:

  • R32_RCC_CTLR at 0x40021000;
  • R32_RCC_CFGR0 at 0x40021004.

Flash owns the instruction fetch wait-state setting:

  • R32_FLASH_ACTLR at 0x40022000.

After reset, the system clock source is HSI, the internal 24 MHz RC oscillator. To run from the external crystal and PLL, the sequence is:

  1. set Flash latency for 48 MHz;
  2. enable HSE and wait for HSERDY;
  3. select HSE as the PLL source while PLL is off;
  4. enable PLL and wait for PLLRDY;
  5. select PLL as SYSCLK;
  6. wait until SWS[1:0] confirms that PLL is actually active.

The order matters. The CPU fetches instructions from Flash. If the core is switched to 48 MHz before Flash gets one wait state, the code can fail right at the clock transition.

Flash Wait State First

The Flash control register has a LATENCY[1:0] field. The CH32V003 reference manual gives these values:

00: 0 wait states, recommended for 0 MHz <= SYSCLK <= 24 MHz
01: 1 wait state, recommended for 24 MHz < SYSCLK <= 48 MHz

So before touching HSE or PLL, write 01b to FLASH_ACTLR.LATENCY.

.equ FLASH_ACTLR,      0x40022000
.equ FLASH_LATENCY_1,  0x1

flash_latency_48mhz:
    li   t0, FLASH_ACTLR
    lw   t1, 0(t0)
    li   t2, -4              # ~0x3: clear LATENCY[1:0]
    and  t1, t1, t2
    li   t2, FLASH_LATENCY_1
    or   t1, t1, t2
    sw   t1, 0(t0)
    fence iorw, iorw
    ret

There is no equivalent SRAM wait-state setting in this setup. The manual’s clock tree shows HCLK feeding SRAM/DMA, with HCLK rated up to 48 MHz. The memory timing adjustment required before this switch is the Flash latency field.

RCC Bits For 48 MHz

The clock control register gives us HSE and PLL enable/ready bits:

.equ RCC_CTLR,     0x40021000
.equ RCC_CFGR0,    0x40021004

.equ RCC_HSEON,    (1 << 16)
.equ RCC_HSERDY,   (1 << 17)
.equ RCC_HSEBYP,   (1 << 18)
.equ RCC_PLLON,    (1 << 24)
.equ RCC_PLLRDY,   (1 << 25)

The clock configuration register gives us system clock selection, status, bus prescaler, ADC prescaler, and PLL source:

.equ CFGR_SW_MASK,      (3 << 0)
.equ CFGR_SW_PLL,       (2 << 0)     # SW[1:0] = 10: PLL selected
.equ CFGR_SWS_MASK,     (3 << 2)
.equ CFGR_SWS_PLL,      (2 << 2)     # SWS[1:0] = 10: PLL active
.equ CFGR_HPRE_MASK,    (0xF << 4)
.equ CFGR_ADCPRE_MASK,  (0x1F << 11)
.equ CFGR_PLLSRC_MASK,  (1 << 16)
.equ CFGR_PLLSRC_HSE,   (1 << 16)

For a passive crystal, keep HSEBYP clear. HSEBYP is for an active external clock fed into OSC_IN, not for a crystal or ceramic resonator.

The manual also notes that HSEBYP must be written while HSEON is zero. The code therefore clears both bits, then enables HSE normally.

Switching The Clock

Here is the clock routine without helper macros:

.equ CLOCK_STARTUP_TIMEOUT, 0x40000

clock_init_48mhz:
    # HSEBYP must be configured while HSEON is 0.
    li   t0, RCC_CTLR
    lw   t1, 0(t0)
    li   t2, (RCC_HSEBYP | RCC_HSEON)
    not  t2, t2
    and  t1, t1, t2
    sw   t1, 0(t0)

    # Enable HSE.
    lw   t1, 0(t0)
    li   t2, RCC_HSEON
    or   t1, t1, t2
    sw   t1, 0(t0)

    # Wait for HSERDY.
    li   t2, CLOCK_STARTUP_TIMEOUT
wait_hse:
    lw   t1, 0(t0)
    li   a3, RCC_HSERDY
    and  t1, t1, a3
    bnez t1, wait_hse_done
    addi t2, t2, -1
    bnez t2, wait_hse
    j    clock_fail
wait_hse_done:

    # Configure CFGR0 before enabling PLL:
    # - keep SW away from PLL while configuring
    # - no HCLK prescaler
    # - reset ADC prescaler field
    # - select HSE as PLL source
    li   t0, RCC_CFGR0
    lw   t1, 0(t0)
    li   t2, (CFGR_SW_MASK | CFGR_HPRE_MASK | CFGR_ADCPRE_MASK | CFGR_PLLSRC_MASK)
    not  t2, t2
    and  t1, t1, t2
    li   t2, CFGR_PLLSRC_HSE
    or   t1, t1, t2
    sw   t1, 0(t0)

    # Enable PLL.
    li   t0, RCC_CTLR
    lw   t1, 0(t0)
    li   t2, RCC_PLLON
    or   t1, t1, t2
    sw   t1, 0(t0)

    # Wait for PLLRDY.
    li   t2, CLOCK_STARTUP_TIMEOUT
wait_pll:
    lw   t1, 0(t0)
    li   a3, RCC_PLLRDY
    and  t1, t1, a3
    bnez t1, wait_pll_done
    addi t2, t2, -1
    bnez t2, wait_pll
    j    clock_fail
wait_pll_done:

    # Switch SYSCLK to PLL.
    li   t0, RCC_CFGR0
    lw   t1, 0(t0)
    li   t2, ~CFGR_SW_MASK
    and  t1, t1, t2
    li   t2, CFGR_SW_PLL
    or   t1, t1, t2
    sw   t1, 0(t0)

    # Wait until SWS reports PLL as the active system clock.
    li   t2, CLOCK_STARTUP_TIMEOUT
wait_sws:
    lw   t1, 0(t0)
    li   a3, CFGR_SWS_MASK
    and  t1, t1, a3
    li   a4, CFGR_SWS_PLL
    beq  t1, a4, wait_sws_done
    addi t2, t2, -1
    bnez t2, wait_sws
    j    clock_fail
wait_sws_done:
    ret

clock_fail:
    j    clock_fail

The timeout does not try to recover. For a bring-up program, that is fine. If HSE or PLL never becomes ready, staying in a visible failure loop is better than continuing with an unknown clock.

Complete Program

This is the earlier blink program extended with Flash latency and 48 MHz clock setup. It is still a single assembly file.

.equ RCC_APB2PCENR, 0x40021018
.equ RCC_IOPDEN,    (1 << 5)
.equ GPIOD_CFGLR,   0x40011400
.equ GPIOD_OUTDR,   0x4001140C
.equ LED_PIN,       4

.equ FLASH_ACTLR,      0x40022000
.equ FLASH_LATENCY_1,  0x1

.equ RCC_CTLR,     0x40021000
.equ RCC_CFGR0,    0x40021004

.equ RCC_HSEON,    (1 << 16)
.equ RCC_HSERDY,   (1 << 17)
.equ RCC_HSEBYP,   (1 << 18)
.equ RCC_PLLON,    (1 << 24)
.equ RCC_PLLRDY,   (1 << 25)

.equ CFGR_SW_MASK,      (3 << 0)
.equ CFGR_SW_PLL,       (2 << 0)
.equ CFGR_SWS_MASK,     (3 << 2)
.equ CFGR_SWS_PLL,      (2 << 2)
.equ CFGR_HPRE_MASK,    (0xF << 4)
.equ CFGR_ADCPRE_MASK,  (0x1F << 11)
.equ CFGR_PLLSRC_MASK,  (1 << 16)
.equ CFGR_PLLSRC_HSE,   (1 << 16)

.equ CLOCK_STARTUP_TIMEOUT, 0x40000

.section .vectors, "ax"
.option norvc
.align 2

.globl vector_base
vector_base:
    j _start

    .rept 255
        j default_handler
    .endr

default_handler:
    j default_handler

.option rvc

.section .text
.align 2

.globl _start
_start:
    csrci mstatus, 0x8
    la   sp, _stack_top

    la   t0, _bss_start
    la   t1, _bss_end

1:
    bgeu t0, t1, 2f
    sw   zero, 0(t0)
    addi t0, t0, 4
    j    1b

2:
    call flash_latency_48mhz
    call clock_init_48mhz
    call led_init

main_loop:
    call led_toggle
    call delay
    j    main_loop

flash_latency_48mhz:
    li   t0, FLASH_ACTLR
    lw   t1, 0(t0)
    li   t2, -4
    and  t1, t1, t2
    li   t2, FLASH_LATENCY_1
    or   t1, t1, t2
    sw   t1, 0(t0)
    fence iorw, iorw
    ret

clock_init_48mhz:
    li   t0, RCC_CTLR
    lw   t1, 0(t0)
    li   t2, (RCC_HSEBYP | RCC_HSEON)
    not  t2, t2
    and  t1, t1, t2
    sw   t1, 0(t0)

    lw   t1, 0(t0)
    li   t2, RCC_HSEON
    or   t1, t1, t2
    sw   t1, 0(t0)

    li   t2, CLOCK_STARTUP_TIMEOUT
wait_hse:
    lw   t1, 0(t0)
    li   a3, RCC_HSERDY
    and  t1, t1, a3
    bnez t1, wait_hse_done
    addi t2, t2, -1
    bnez t2, wait_hse
    j    clock_fail
wait_hse_done:

    li   t0, RCC_CFGR0
    lw   t1, 0(t0)
    li   t2, (CFGR_SW_MASK | CFGR_HPRE_MASK | CFGR_ADCPRE_MASK | CFGR_PLLSRC_MASK)
    not  t2, t2
    and  t1, t1, t2
    li   t2, CFGR_PLLSRC_HSE
    or   t1, t1, t2
    sw   t1, 0(t0)

    li   t0, RCC_CTLR
    lw   t1, 0(t0)
    li   t2, RCC_PLLON
    or   t1, t1, t2
    sw   t1, 0(t0)

    li   t2, CLOCK_STARTUP_TIMEOUT
wait_pll:
    lw   t1, 0(t0)
    li   a3, RCC_PLLRDY
    and  t1, t1, a3
    bnez t1, wait_pll_done
    addi t2, t2, -1
    bnez t2, wait_pll
    j    clock_fail
wait_pll_done:

    li   t0, RCC_CFGR0
    lw   t1, 0(t0)
    li   t2, ~CFGR_SW_MASK
    and  t1, t1, t2
    li   t2, CFGR_SW_PLL
    or   t1, t1, t2
    sw   t1, 0(t0)

    li   t2, CLOCK_STARTUP_TIMEOUT
wait_sws:
    lw   t1, 0(t0)
    li   a3, CFGR_SWS_MASK
    and  t1, t1, a3
    li   a4, CFGR_SWS_PLL
    beq  t1, a4, wait_sws_done
    addi t2, t2, -1
    bnez t2, wait_sws
    j    clock_fail
wait_sws_done:
    ret

clock_fail:
    j    clock_fail

led_init:
    li   t0, RCC_APB2PCENR
    lw   t1, 0(t0)
    li   t2, RCC_IOPDEN
    or   t1, t1, t2
    sw   t1, 0(t0)

    li   t0, GPIOD_CFGLR
    lw   t1, 0(t0)
    li   t2, ~(0xF << 16)
    and  t1, t1, t2
    li   t2, (0x1 << 16)
    or   t1, t1, t2
    sw   t1, 0(t0)
    ret

led_toggle:
    li   t0, GPIOD_OUTDR
    lw   t1, 0(t0)
    li   t2, (1 << LED_PIN)
    xor  t1, t1, t2
    sw   t1, 0(t0)
    ret

delay:
    li   t0, 480000
1:
    addi t0, t0, -1
    bnez t0, 1b
    ret

The build and flash commands are the same as in the bring-up article:

riscv32-unknown-elf-as -g -mabi=ilp32e -march=rv32ec_zicsr \
  -o startup.o startup.S

riscv32-unknown-elf-ld -g -T ch32v003-min.ld \
  startup.o -o blink-48mhz.elf

riscv32-unknown-elf-objcopy -O binary blink-48mhz.elf blink-48mhz.bin

minichlink -w blink-48mhz.bin flash -b -r

After the switch, the delay loop runs against a 48 MHz core clock. It is still not a timer, but it now has a known clock behind it. That is the useful change: UART baud divisors, SPI divisors, SysTick periods, and delay constants can now be calculated from a stable system frequency.

Checklist

For this 48 MHz bring-up step:

  1. Set FLASH_ACTLR.LATENCY to one wait state.
  2. Keep HSEBYP clear for a passive crystal.
  3. Enable HSEON.
  4. Wait for HSERDY.
  5. Select HSE as the PLL source while PLL is off.
  6. Keep HPRE undivided if HCLK should equal SYSCLK.
  7. Enable PLLON.
  8. Wait for PLLRDY.
  9. Select PLL as the system clock with SW[1:0] = 10b.
  10. Wait until SWS[1:0] = 10b.

That is the whole move from a reset-clock blink to a blink program running from a known 48 MHz system clock.