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_CTLRat0x40021000;R32_RCC_CFGR0at0x40021004.
Flash owns the instruction fetch wait-state setting:
R32_FLASH_ACTLRat0x40022000.
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:
- set Flash latency for 48 MHz;
- enable HSE and wait for
HSERDY; - select HSE as the PLL source while PLL is off;
- enable PLL and wait for
PLLRDY; - select PLL as
SYSCLK; - 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:
- Set
FLASH_ACTLR.LATENCYto one wait state. - Keep
HSEBYPclear for a passive crystal. - Enable
HSEON. - Wait for
HSERDY. - Select HSE as the PLL source while PLL is off.
- Keep
HPREundivided ifHCLKshould equalSYSCLK. - Enable
PLLON. - Wait for
PLLRDY. - Select PLL as the system clock with
SW[1:0] = 10b. - 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.