最近在使用CH32V307,查找资料的时候有幸搜到了《杰哥的{运维,编程,调板子}小笔记》,博客里面有很多干货,其中《试用沁恒CH32V307评估板》这篇刚好解答了我的疑惑,通过这篇文章也了解了沁恒的RISC-V单片机的编译、仿真、烧写等原理,受益匪浅,特分享给大家。
image.png
以下为文章原文。


背景

之前有一天看到朋友在捣鼓 CH32V307,因此自己也萌生了试用 CH32V307 评估板的兴趣,于是在沁恒官网申请样品,很快就接到电话了解情况,几天后就顺丰送到了,不过因为疫情原因直到现在才拿到手上,只能说疫情期间说不定货比人还快。

开箱

收到的盒子里有一个 CH32V307 评估板,和一个 WCH-Link,相关资料可以在 官网 或者 openwch/ch32v307 下载。在说明书中有如下的图示:
image.png
板子自带的跳线帽不是很多,建议自备一些,或者用杜邦线替代。比较重要的是 WCH-Link 子板上 CH549 和 CH2V307 连接的几个信号,和下面 BOOT0/1 的选择。

WCH-Link

可以看到评估板自带了一个 WCH-Link,所以不需要附赠的那一个,直接把 11 号 Type-C 连接到电脑上即可。这里还遇到一个小插曲,用 Type-C to Type-C 的线连电脑上不工作,连 PWR LED 都点不亮,换一根 Type-A to Type-C 的就可以,没有继续研究是什么原因。电脑上可以看到 WCH-Link 的设备:VID=1a86, PID=8010。比较有意思的是,在 RISC-V 模式(CON 灯不亮)的时候 PID 是 8010,ARM 模式(CON 灯亮)的时候 PID 是 8011,从 RISC-V 模式切换到 ARM 模式的方法是连接 TX 和 GND 后上电,反过来要用 MounRiver,详见 WCH-Link 使用说明 V1.0 V1.3 和原理图 V1.1
给沁恒开源 WCH-Link 原理图并开放固件点个赞,在淘宝上也可以看到不少 WCH-Link 的仿真器,挺有意思的。
在 ARM 模式下,它实现了类似 CMSIS-DAP 的协议,可以用 OpenOCD 调试:

  1. source [find interface/cmsis-dap.cfg]
  2. adapter speed 1000
  3. cmsis_dap_vid_pid 0x1a86 0x8011
  4. transport select swd
  5. init
  6. $ openocd -f openocd.cfg
  7. Open On-Chip Debugger 0.11.0
  8. Licensed under GNU GPL v2
  9. For bug reports, read
  10. http://openocd.org/doc/doxygen/bugs.html
  11. Info : CMSIS-DAP: SWD Supported
  12. Info : CMSIS-DAP: FW Version = 2.0.0
  13. Info : CMSIS-DAP: Interface Initialised (SWD)
  14. Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 0 nTRST = 0 nRESET = 1
  15. Info : CMSIS-DAP: Interface ready
  16. Info : clock speed 1000 kHz
  17. Warn : gdb services need one or more targets defined
  18. Info : Listening on port 6666 for tcl connections
  19. Info : Listening on port 4444 for telnet connections

不过这里我们要用的是 RISC-V 处理器 CH32V307,上面的就当是 WCH-LINK 使用的小贴士。
给评估板插上 USB Type-C 以后,首先上面的 WCH-Link 部分中红色的 PWR 和绿色的 RUN 亮,CON 不亮,说明 WCH-LINK 的 CH549 已经启动,并且处在 RISC-V 模式(CON 不亮)。CH549 是一个 8051 指令集的处理器,上面的跑的 WCH-LINK 固件在网上可以找到,在下面提到的 MounRiver Studio 目录中也有一份。

OpenOCD

目前开源工具上游还不支持 CH32V307 的开发,需要用 MounRiver,支持 Windows 和 Linux,有两部分:

解压缩后,可以看到它的 OpenOCD 配置:

  1. ## wch-arm.cfg
  2. adapter driver cmsis-dap
  3. transport select swd
  4. source [find ../share/openocd/scripts/target/ch32f1x.cfg]
  5. ## wch-riscv.cfg
  6. #interface wlink
  7. adapter driver wlink
  8. wlink_set
  9. set _CHIPNAME riscv
  10. jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x00001
  11. set _TARGETNAME $_CHIPNAME.cpu
  12. target create $_TARGETNAME.0 riscv -chain-position $_TARGETNAME
  13. $_TARGETNAME.0 configure -work-area-phys 0x80000000 -work-area-size 10000 -work-area-backup 1
  14. set _FLASHNAME $_CHIPNAME.flash
  15. flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 $_TARGETNAME.0
  16. echo "Ready for Remote Connections"

其中 ch32f1x.cfg 就是 stm32f1x.cfg 改了一下名字,可以看到 WCH OpenOCD 把它的 RISC-V 调试协议称为 wlink,估计是取 wch-link 的简称吧。除了 wlink 部分,其他就是正常的 RISC-V CPU 调试的 OpenOCD 配置,比较有意思的就是 IDCODE 设为了 0x00001,比较有个性。
在网上一番搜索,找到了 WCH OpenOCD 的源码 Embedded_Projects/riscv-openocd-wch,是网友向沁恒获取的源代码,毕竟 OpenOCD 是 GPL 软件。简单看了一下代码,是直接把 RISC-V Debug 中的 DMI 操作封装了一下,然后通过 USB Bulk 和 WCH-Link 通信。我从 riscv-openocd 找到了一个比较接近的 commit,然后把 WCH 的代码提交上去,得到了 diff,有兴趣的可以看看具体实现,甚至把这个支持提交到上游。
有源码以后,就可以在 macOS 上编译了(需要修复三处 clang 报告的编译错误,最终代码):

  1. $ ./bootstrap
  2. $ ./configure --prefix=/path/to/prefix/openocd --enable-wlink --disable-werror CAPSTONE_CFLAGS=-I/opt/homebrew/opt/capstone/include/
  3. $ make -j4 install

如果遇到 makeinfo 报错,把 homebrew 的 texinfo 加到 PATH 即可。
编译完成后,就可以用前面提到的 wch-riscv.cfg 进行调试了:

  1. $ /path/to/prefix/openocd -f wch-riscv.cfg
  2. Open On-Chip Debugger 0.11.0+dev-01623-gbfa3bc7f9 (2022-04-20-09:55)
  3. Licensed under GNU GPL v2
  4. For bug reports, read
  5. http://openocd.org/doc/doxygen/bugs.html
  6. Info : only one transport option; autoselect 'jtag'
  7. Ready for Remote Connections
  8. Info : Listening on port 6666 for tcl connections
  9. Info : Listening on port 4444 for telnet connections
  10. Info : WCH-Link version 2.3
  11. Info : wlink_init ok
  12. Info : This adapter doesn't support configurable speed
  13. Info : JTAG tap: riscv.cpu tap/device found: 0x00000001 (mfg: 0x000 (<invalid>), part: 0x0000, ver: 0x0)
  14. Warn : Bypassing JTAG setup events due to errors
  15. Info : [riscv.cpu.0] datacount=2 progbufsize=8
  16. Info : Examined RISC-V core; found 1 harts
  17. Info : hart 0: XLEN=32, misa=0x40901125
  18. [riscv.cpu.0] Target successfully examined.
  19. Info : starting gdb server for riscv.cpu.0 on 3333
  20. Info : Listening on port 3333 for gdb connections

这也验证了上面的发现:因为绕过了 jtag,直接发送 dmi,所以 idcode 是假的:

  1. if(wchwlink){
  2. buf_set_u32(idcode_buffer, 0, 32, 0x00001); //Default value,for reuse risc-v jtag debug
  3. }

接下来就可以用 GDB 调试了。里面跑了一个样例的程序,就是向串口打印:

  1. $ screen /dev/tty.usbmodem* 115200
  2. SystemClk:72000000
  3. 111
  4. 111
  5. 111
  6. 111
  7. 111

之后则是针对各个外设,基于沁恒提供的示例代码进行相应的开发了。

Baremetal 代码

接下来看看沁恒提供的代码是如何配置的。在 EVT/EXAM/SRC/Startup/startup_ch32v30x_D8C.S 可以看到初始化的汇编代码。比较有意思的是,这个核心扩展了 mtvec,支持 ARM 的 vector table 模式,即放一个指针数组,而不是指令:

  1. .section .vector,"ax",@progbits
  2. .align 1
  3. _vector_base:
  4. .option norvc;
  5. .word _start
  6. .word 0
  7. .word NMI_Handler /* NMI */
  8. .word HardFault_Handler /* Hard Fault */

这些名字如此熟悉,只能说这是 ARVM 了(ARM + RV)。后面的部分比较常规,把 data 段复制到 sram,然后清空 bss:

  1. handle_reset:
  2. .option push
  3. .option norelax
  4. la gp, __global_pointer$
  5. .option pop
  6. 1:
  7. la sp, _eusrstack
  8. 2:
  9. /* Load data section from flash to RAM */
  10. la a0, _data_lma
  11. la a1, _data_vma
  12. la a2, _edata
  13. bgeu a1, a2, 2f
  14. 1:
  15. lw t0, (a0)
  16. sw t0, (a1)
  17. addi a0, a0, 4
  18. addi a1, a1, 4
  19. bltu a1, a2, 1b
  20. 2:
  21. /* Clear bss section */
  22. la a0, _sbss
  23. la a1, _ebss
  24. bgeu a0, a1, 2f
  25. 1:
  26. sw zero, (a0)
  27. addi a0, a0, 4
  28. bltu a0, a1, 1b
  29. 2:

最后是进行一些 csr 的配置,然后进入 C 代码:

  1. li t0, 0x1f
  2. csrw 0xbc0, t0
  3. /* Enable nested and hardware stack */
  4. li t0, 0x1f
  5. csrw 0x804, t0
  6. /* Enable floating point and interrupt */
  7. li t0, 0x6088
  8. csrs mstatus, t0
  9. la t0, _vector_base
  10. ori t0, t0, 3
  11. csrw mtvec, t0
  12. lui a0, 0x1ffff
  13. li a1, 0x300
  14. sh a1, 0x1b0(a0)
  15. 1: lui s2, 0x40022
  16. lw a0, 0xc(s2)
  17. andi a0, a0, 1
  18. bnez a0, 1b
  19. jal SystemInit
  20. la t0, main
  21. csrw mepc, t0
  22. mret

这里有一些自定义的 csr,比如 corecfgr(0xbc0),intsyscr(0x804,设置了 HWSTKEN=1, INESTEN=1, PMTCFG=0b11, HWSTKOVEN=1),具体参考 QingKeV4_Processor_Manual。接着代码往 0x1ffff1b0 写入 0x300,然后不断读取 FLASH Interface (0x40022000) 的 STATR 字段,没有找到代码中相关的定义,简单猜测与 Flash 的零等待/非零等待区有关,因为后续代码要提高频率,因此 Flash 控制器需要增加 wait state。

编译

可以用 MounRiver 编译,也可以用 SiFive 的 riscv64-unknown-elf 工具链进行编译,参考 Embedded_Projects/CH32V307_Template 项目中的编译方式,修改 riscv64-elf.cmake 为:

  1. set(CMAKE_SYSTEM_NAME Generic)
  2. set(CMAKE_C_COMPILER riscv64-unknown-elf-gcc)
  3. set(CMAKE_CXX_COMPILER riscv64-unknown-elf-g++)
  4. # Make CMake happy about those compilers
  5. set(CMAKE_TRY_COMPILE_TARGET_TYPE "STATIC_LIBRARY")

然后交叉编译就可以了。需要注意的是对 libnosys 的处理,如果没有正确链接,就会出现 syscall,然后在 ecall handler 里面死循环。
如果不想用 CMake,也可以用下面的精简版 Makefile:

  1. USER := User/main.c User/ch32v30x_it.c User/system_ch32v30x.c
  2. LIBRARY := ../../SRC/Peripheral/src/ch32v30x_misc.c \
  3. ../../SRC/Peripheral/src/ch32v30x_usart.c \
  4. ../../SRC/Peripheral/src/ch32v30x_gpio.c \
  5. ../../SRC/Peripheral/src/ch32v30x_rcc.c \
  6. ../../SRC/Debug/debug.c \
  7. ../../SRC/Startup/startup_ch32v30x_D8C.S
  8. LDSCRIPT = ../../SRC/Ld/Link.ld
  9. # disable libc first
  10. CFLAGS := -march=rv32imafc -mabi=ilp32f \
  11. -flto -ffunction-sections -fdata-sections \
  12. -nostartfiles -nostdlib \
  13. -T $(LDSCRIPT) \
  14. -I../../SRC/Debug \
  15. -I../../SRC/Core \
  16. -I../../SRC/Peripheral/inc \
  17. -I./User \
  18. -O2 \
  19. -Wl,--print-memory-usage
  20. # link libc & libnosys in the end
  21. CFLAGS_END := \
  22. -lc -lgcc -lnosys
  23. PREFIX := riscv64-unknown-elf-
  24. all: obj/build.bin
  25. obj/build.bin: obj/build.elf
  26. $(PREFIX)objcopy -O binary $^ $@
  27. obj/build.elf: $(USER) $(LIBRARY)
  28. $(PREFIX)gcc $(CFLAGS) $^ $(CFLAGS_END) -o $@
  29. clean:
  30. rm -rf obj/*

烧写 Flash

编译好以后,根据 WCH OpenOCD 的文档,可以用下面的配置来进行烧写:

  1. #interface wlink
  2. adapter driver wlink
  3. wlink_set
  4. set _CHIPNAME riscv
  5. jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x00001
  6. set _TARGETNAME $_CHIPNAME.cpu
  7. target create $_TARGETNAME.0 riscv -chain-position $_TARGETNAME
  8. $_TARGETNAME.0 configure -work-area-phys 0x80000000 -work-area-size 10000 -work-area-backup 1
  9. set _FLASHNAME $_CHIPNAME.flash
  10. flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 $_TARGETNAME.0
  11. init
  12. halt
  13. flash erase_sector wch_riscv 0 last
  14. program /path/to/firmware
  15. verify_image /path/to/firmware
  16. wlink_reset_resume
  17. exit

输出:

  1. $ openocd -f program.cfg
  2. Open On-Chip Debugger 0.11.0+dev-01623-gbfa3bc7f9 (2022-04-20-09:55)
  3. Licensed under GNU GPL v2
  4. For bug reports, read
  5. http://openocd.org/doc/doxygen/bugs.html
  6. Info : only one transport option; autoselect 'jtag'
  7. Ready for Remote Connections
  8. Info : WCH-Link version 2.3
  9. Info : wlink_init ok
  10. Info : This adapter doesn't support configurable speed
  11. Info : JTAG tap: riscv.cpu tap/device found: 0x00000001 (mfg: 0x000 (<invalid>), part: 0x0000, ver: 0x0)
  12. Warn : Bypassing JTAG setup events due to errors
  13. Info : [riscv.cpu.0] datacount=2 progbufsize=8
  14. Info : Examined RISC-V core; found 1 harts
  15. Info : hart 0: XLEN=32, misa=0x40901125
  16. [riscv.cpu.0] Target successfully examined.
  17. Info : starting gdb server for riscv.cpu.0 on 3333
  18. Info : Listening on port 3333 for gdb connections
  19. Info : device id = REDACTED
  20. Info : flash size = 256kbytes
  21. Info : JTAG tap: riscv.cpu tap/device found: 0x00000001 (mfg: 0x000 (<invalid>), part: 0x0000, ver: 0x0)
  22. Warn : Bypassing JTAG setup events due to errors
  23. ** Programming Started **
  24. ** Programming Finished **
  25. Info : Verify Success

访问串口 screen /dev/tty.usbmodem* 115200,可以看到正确地输出了内容。