Posted on Leave a comment

CH32V003 driving WS2812B LEDs with SPI – Part 9

6 March 2025

Hmmm. Good news? Well, news. The absolutely simplest test I could envisage ran all night and did not hang up. You saw the code. It was only checking the SPI status register to see if the transmit register was empty (and waiting forever for it to be so) and only then shipping out a 0x55 test pattern to the SDO pin on PC6, and repeat, ad infinitum.

This test was conducted on the most symptom-prone variation of the -003 chips I have on hand, the CH32V003F4U6 QFN20. Now, to be fair, this was done using the upgraded and augmented “robust” prototype, and not the original test platform, a very small solderless breadboard. Should I go back and test the original circuit? Of course! My scientific rigor knows no bounds.

Now I should create a similarly minimalistic diagnostic using the WCH SDK. I copied the same SDK initialization function that I was using in the previous code and added this within the while(1) loop in the main() function:

while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) {
    // wait for SPI transmit register to be empty
}

SPI_I2S_SendData(SPI1, 0x55); // test pattern 01010101

And it was running along quite nicely, until it wasn’t. Just hung up again. Let’s add a little instrumentation to the code and have it spit out some statistics from time to time. I added some variables to track the counting:

uint32_t loops = 0, millions = 0;

And added this code to occasionally print out a report:

loops++; // count the loops

if(loops == 1000000) { // report only every 1,000,000 loops
    millions++; // count those millions
    loops = 0; // reset loop counter
    printf("Loops (millions): %u\r\n", millions);
}

And off it goes! And stops after “only” 51 million loops. Try again, little machine! OK, 382 million loops this time, but hung up solid again. And this is only taking a few minutes each time.

So the immediate conclusion is that there is something in the SDK that is gunking up the SPI state. I’ll have to look at the SPI_I2S_GetFlagStatus() function in more detail and see how that could be acting up. The SPI_I2S_SendData() function literally only writes the passed value to the SPI data register.

Well, the SPI_I2S_GetFlagStatus() function also is only doing the minimum necessary things to check the status of an individual flag, i.e., read the SPI status register and mask out the status bit of interest, returning either ‘SET’ or ‘RESET’ as appropriate.

Not surprisingly, the WCH development board with the CH32V003F4P6 TSSOP20 package runs flawlessly.

At this point, I see two ways forward with this investigation. I can implement the WS2812B-SPI driver in assembly language and see if that works as expected. The other option is to update my C language framework using the new -003 SVD file and fitting some optimizations into the project wizard, which will take more work than the assembly language framework.

But why choose? Can’t I do both?

I’ll attempt the more full-featured LED demo in assembly first, as the base is already in place for that. But before I forget, there’s something very interesting that I noticed and almost failed to note here. Once I cranked up the system clock from 8 MHz to 48 MHz, the chip still worked. Even though I didn’t configure the flash memory controller to use an additional wait state. Even though the RM says the “prefetch buffer” must be enabled, although it never says how. Even though the CH32X035 yurked and horked all over the place when I did the same exact thing. To be fair, the CH32X is a QingKe V4 and the -003 is a V2. I had noticed this behavior before and had decided to err on the side of caution in the future. But now I want to see if it’s an issue or not. If weird and random things happen over and above the current weird and random things that are happening, we’ll know where to look.

I recall hearing that with great power comes great responsibility. I’m so totally feeling that right now as I struggle to come up with a register usage policy that 1) makes sense and 2) I like. I have 15 registers at my disposal and I can use them as I see fit. The GNU assembler adheres to the ABI (application binary interface) that assigns a few of the registers to specific tasks, such as the stack pointer and return address register. There are some other assumptions made in the implementations of the pseudo-instructions that prove quite useful. But I need a system that is simple to remember.

I may have mentioned it before, but here is a RISC-V reference page that I come back to all the time:

https://projectf.io/posts/riscv-jump-function/#functions

Here are the general-purpose registers that I have access to in this RV32EC architecture:

Register Alias Notes
-------- ----- -----
x0       zero  All the values you want, as long as you want a zero
x1       ra    Return address
x2       sp    Stack pointer
x3       gp    Global variable pointer
x4       tp    Thread pointer
x5       t0    Temporary register 0
x6       t1    Temporary register 1
x7       t2    Temporary register 2
x8       s0    Saved register 0
x9       s1    Saved register 1
x10      a0    Function argument 0
x11      a1    Function argument 1
x12      a2    Function argument 2
x13      a3    Function argument 3
x14      a4    Function argument 4
x15      a5    Function argument 5

There is simultaneously so much and so little you can do with the zero register, x0. It’s really handy when you want to write a zero somewhere, or compare something to zero, or subtract something from zero… you get the idea. But writing anything to it doesn’t do anything.

I’ve become accustomed to using the function argument registers, a0-a5, in their intended manner, so I think I will continue doing so on this project, at least.

The return address register, ra, under the current ABI, is generally x1 but can be x5 in some circumstances. The GNU assembler will assume you want to use x1 as the return address register when it decomposes the pseudo instruction ‘call’ into ‘jal’ or ‘jump and link’. In truth, you can ‘jump and link’ using any register you want. But I’m willing to go along with this idea for the time being. The same thing applies to the ‘ret’ (return from function/subroutine) pseudo instruction.

The stack pointer register, x2/sp, is really more up for grabs. Unlike many other microcontroller architectures that I have used in the past, the RISC-V instruction set does not have a predetermined idea of which of the registers ‘should be’ the stack pointer. Use any one you want. Really.

I already ran into the issue of trying to use s2/x18 on this project. It’s not there. Only x0-x15 are available on the RV32EC platform.

So instead of trying to figure out which of the other ‘suggested usage’ aliases for the remaining registers to use, I think I’ll just use x3-x9 for my random register needs. If I need more, I think it would be OK to use some of the functions argument registers as well. Also, if I can’t keep up with x3-x9, I can always rename them to something else more memorable using a macro.

Since I will be needing a reasonably accurate timer function in order to send the ‘reset’ signal to the string of WS2812B LEDs, I’ll need to configure the STK system timer to help with that.

Leave a Reply