ch32v307 dev board, part 2

I'm continuing to build a development environment for my ch32v307 based projects. In this post, I'll explore a problem I ran into.

This is part of a series on the ch32v307 dev board

Interrupts

First problem: interrupts.

Below is an interrupt handler, which has some special needs. This processor has the ability to atomically swap register and stack state on interrupt, which saves a bunch of work. But it needs a signal to tell it when the interrupt handler is done, so the previous state and stack are restored.

The compiler also needs to know about which functions are interrupt handlers, so it can properly handle this special case. For WCH's special compiler, they've created a "WCH-Interrupt-fast" interrupt attribute. But I'm using xpack's riscv gcc 12.2.0, which does not have support for it.

Code (reformatted to be shorter):

__attribute__((interrupt("WCH-Interrupt-fast"))) void SysTick_Handler(void) {
    time=SysTick->CNT;
    SysTick->CTLR=0;
    SysTick->SR=0;
    printf("delta time:%d\r\n",time-0x20);
}

Compiler output:

src/main.c:36:1: warning: argument to 'interrupt' attribute is not '"user"', '"supervisor"', or '"machine"' [-Wattributes]
   36 | void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
      | ^~~~

Assembly:

00000210 <SysTick_Handler>:
     210:       e000f7b7                lui     a5,0xe000f
     214:       4790                    lw      a2,8(a5)
     216:       47d4                    lw      a3,12(a5)
     218:       80c1aa23                sw      a2,-2028(gp) # 20000094 <time>
     21c:       0007a023                sw      zero,0(a5) # e000f000 <_eusrstack+0xc0007000>
     220:       6509                    lui     a0,0x2
     222:       0007a223                sw      zero,4(a5)
     226:       fe060593                addi    a1,a2,-32
     22a:       c8850513                addi    a0,a0,-888 # 1c88 <_read_r+0x2e>
     22e:       afb1                    j       98a <iprintf>

Relevant bug report:

naked interrupt will break when s registers get used · Issue #90 · cnlohr/ch32v003fun
As introduced in exti example. Be it function call, register pressure or pure size optimization. see: https://www.eevblog.com/forum/microcontrollers/bizarre-problem-on-ch32v003-with-systick-isr-cor…

Since the compiler didn't understand the "WCH-Interrupt-fast" flag, it didn't emit a mret instruction. The last instruction relies on iprintf to return to the caller, which it does with an ret instruction. This breaks future interrupts, so your interrupt handler gets called once and that's it.

The benefit from this design is it takes 14 cycles between the interrupt happening and when the SysTick counter is read at offset 214 above (SysTick->CNT is at 0xE000F000 +8).

Interrupt workaround: option 1

Code:

__attribute__((interrupt)) void SysTick_Handler(void) {
    time=SysTick->CNT;
    SysTick->CTLR=0;
    SysTick->SR=0;
    printf("delta time:%d\r\n",time-0x20);
}

This just uses a plain interrupt attribute, which assumes it has to preserve all the registers and stack, so it has to do a whole lot more work.

Assembly:

00000210 <SysTick_Handler>:
     210:       7175                    addi    sp,sp,-144
     212:       c706                    sw      ra,140(sp)
[lots more sw statements]
     232:       e682                    fsw     ft0,76(sp)
[lots more fsw statements]
     252:       e000f7b7                lui     a5,0xe000f
     256:       e672                    fsw     ft8,12(sp)
     258:       4790                    lw      a2,8(a5)
     25a:       e476                    fsw     ft9,8(sp)
     25c:       e27a                    fsw     ft10,4(sp)
     25e:       e07e                    fsw     ft11,0(sp)
     260:       47d4                    lw      a3,12(a5)
     262:       80c1aa23                sw      a2,-2028(gp) # 20000094 <time>
     266:       0007a023                sw      zero,0(a5) # e000f000 <_eusrstack+0xc0007000>
     26a:       6509                    lui     a0,0x2
     26c:       fe060593                addi    a1,a2,-32
     270:       0007a223                sw      zero,4(a5)
     274:       d2050513                addi    a0,a0,-736 # 1d20 <_read_r+0x2c>
     278:       7ac000ef                jal     ra,a24 <iprintf>
     27c:       40ba                    lw      ra,140(sp)
[lots more lw statements]
     29c:       6036                    flw     ft0,76(sp)
[lots more flw statements]
     2c4:       6149                    addi    sp,sp,144
     2c6:       30200073                mret

All this extra work takes time, and it totals 48 cycles before it reads the counter at offset 258. But it properly returns with mret and my interrupts work again.

Interrupt workaround: option 2

From the github bug report above, another workaround is offered:

__attribute__((naked)) void SysTick_Handler() {
  __asm volatile ("call SysTick_Handler_real; mret");
}

void SysTick_Handler_real(void) {
    time=SysTick->CNT;
    SysTick->CTLR=0;
    SysTick->SR=0;
    printf("delta time:%d\r\n",time-0x20);
}

This splits the interrupt handler into two pieces, one that generates the needed assembly, and one that does the regular C work.

Disassembly:

00000210 <SysTick_Handler>:
     210:       20b5                    jal     27c <SysTick_Handler_real>
     212:       30200073                mret

0000027c <SysTick_Handler_real>:
     27c:       e000f7b7                lui     a5,0xe000f
     280:       4790                    lw      a2,8(a5)
     282:       47d4                    lw      a3,12(a5)
     284:       80c1aa23                sw      a2,-2028(gp) # 20000094 <time>
     288:       0007a023                sw      zero,0(a5) # e000f000 <_eusrstack+0xc0007000>
     28c:       6509                    lui     a0,0x2
     28e:       0007a223                sw      zero,4(a5)
     292:       fe060593                addi    a1,a2,-32
     296:       cb050513                addi    a0,a0,-848 # 1cb0 <_read_r+0x50>
     29a:       addd                    j       990 <iprintf>

Because SysTick_Handler is marked with the naked attribute, it doesn't generate a ret instruction after the mret (which wouldn't hurt anything, but it's not needed).

This is a little slower than the buggy interrupt handler, with the counter read at offset 280 taking 16 cycles after the interrupt fires. But it has the benefit of not breaking future interrupts.

Working Interrupts

Now that I have interrupts again, the example USB High Speed Serial (CDC) code works. Someday, I'd like to turn that example code into a better GPS PPS over USB device. My previous project used Full Speed USB, which is limited to 1 ms polling. High Speed USB should be able to poll at 0.125 ms, which I expect to improve the timing results.