Posted on Leave a comment

CH32V203C8 & TM1637 LED Clock – Part 5

26 March 2025

What should have been a simple exercise in setting up a couple of push button turned into a deep dive (again). There was no problem actually reading the input pins and printing out messages like “Button 1 pressed” in the foreground task. But I really wanted it to be an asynchronous process, so I set up the EXTI external interrupt controller to generate an interrupt for each of the input pins. First I forgot to specify which GPIO port was associated with each pin. The CH32V family has the same architecture for EXTI support as the STM32, if you’re familiar with those. There are sixteen (sometimes more) possible inputs, but they can be on any available GPIO pin, so a little mapping is required. But that mapping is handled by the AFIO (alternate function IO) controller, and that interface has to have its peripheral clock enabled before it does anything. I finally figured it out, but it was terrifically late and I was more than a little frustrated at that point.

In other news, I thought of a simple way to make the colon flash, so I’m going to try to do that now. At first I thought I would have to go back to updating the LED module every second, but it turns out I only need to update that one digit (digit 2, which has its decimal point wired up as the two colon LEDs) every second. Then I can set the STK compare value to half a second in the future (i.e., 72,000,000 HCLK cycles from the current STK counter value), then have the STK interrupt on compare match and that interrupt handler can clear the colon bits – again, just a single digit update on the LED module.

Surprisingly, that worked the first time. It’s not as distracting as I thought it would be. It makes me wonder if a “on for one second and off for one second” colon would be less distracting or just weird. Then I wouldn’t need a separate timer interrupt to clear the colon, instead just using an additional prescaler variable in the RTC interrupt handler.

Back to the user interface. I have a separate interrupt handler for each of the buttons, but only because I arbitrarily choose PB3 and PB4 as the input pins. The first five GPIO pins on each GPIO port can be routed to the first five individual EXTI interrupt vectors, while EXTI5 through EXTI9 share a single vector, as do EXTI10 through EXT15:

GPIO    EXTI        IRQn    Handler
----    ---------   ----    --------------------
0       EXTI0       6       EXTI0_IRQHandler
1       EXTI1       7       EXTI1_IRQHandler
2       EXTI2       8       EXTI2_IRQHandler
3       EXTI3       9       EXTI3_IRQHandler
4       EXTI4       10      EXTI4_IRQHandler
5       EXTI9_5     23      EXTI9_5_IRQHandler
6       EXTI9_5     23      EXTI9_5_IRQHandler
7       EXTI9_5     23      EXTI9_5_IRQHandler
8       EXTI9_5     23      EXTI9_5_IRQHandler
9       EXTI9_5     23      EXTI9_5_IRQHandler
10      EXT15_10    40      EXTI15_10_IRQHandler
11      EXT15_10    40      EXTI15_10_IRQHandler
12      EXT15_10    40      EXTI15_10_IRQHandler
13      EXT15_10    40      EXTI15_10_IRQHandler
14      EXT15_10    40      EXTI15_10_IRQHandler
15      EXT15_10    40      EXTI15_10_IRQHandler

The “Handler” names are the ones specified in the SDK-supplied startup file, startup_ch32v20x_D6.S. Write your own startup code and you can name them as you like.

The first thing I have to figure out is how to debounce these switch inputs. Like most mechanical push buttons, these little buttons I’m using in this prototype circuit have a certain amount of clickety-clack action when making contact. The internal contacts are literally bouncing several times before making firm and constant contact with each other. This shows up at the GPIO input pin as a series of transitions from high to low and back and forth several times before settling into a stable signal. I set up the EXTI trigger to look for falling edges, where the signal goes from a high level (no button pressed) to a low level (button pressed). It doesn’t measure how long it stays low or any other ‘quality’ measurement of the signal.

The simplest thing (almost always my favorite thing) is to just start measuring how long the button has been held down, and just ignoring any suspiciously short “presses”. We also don’t want to “accidentally” start setting the clock by inadvertently brushing against the buttons. We again turn to the STK to help us time this event.

The challenge is that this is something that shouldn’t be “handled” within the interrupt handler. The best interrupt handler gets in, gets the job done, and gets out – fast. Waiting around for an external event to happen is not on the list of Things That Are Done.

So what we can do, instead, is to set a flag that can be checked in the foreground task, i.e., the endless while() loop within the main() function. Why bother, then, with all the interrupt stuff at all, you ask? That’s an excellent question. It’s because eventually (from a development standpoint) the system will be 99.999% in a low-power mode and not actually executing anything… until something interesting happens. Having a full-time, wait-and-see polling loop in the foreground doesn’t work when the CPU is shut down.

In the foreground task, I check to see if the ‘set hours’ button, the one connected to PB3, has been pressed, by checking the flag set in the interrupt handler. If it has, we can capture the system time from the STK timer, then wait while the button is pressed to see if half a second has elapsed. If it has, we increment the hours counter, which has now been taken out of the RTC interrupt handler and placed in the global scope, then update the display. Once this has happened, we reset the elapsed time counter and keep going as long as the button is pressed. Once the button has been released, we clear the button pressed flag. Repeat for the ‘minute set’ button.

And for the purposes of this simple project, the UI is complete. That’s the third of three goals, so now is a good place to stop.

Posted on Leave a comment

CH32V203C8 & TM1637 LED Clock – Part 4

25 March 2025

Running overnight, the clock module seems to be keeping time well. I managed to minimize the hum it generates by wiggling the USB connection of the WCH-LinkE programming adapter, which is where the module and my development board are getting their power.

Another thought I had to minimize the excessive noise is to display the time one segment at a time, completely obviating the function of the TM1637 chip. I think I will save that trick for when I am implementing a direct-drive LED interface using just the -203 chip.

I’m also considering whether or not to implement the flashing colon function at all. To have it flash on and off at 1 Hz, I would need to let the once-per-second interrupt handler update the time display with the colon on, then trigger a separate timer function to interrupt after a half a second has passed, then just re-write only the second digit, this time with the colon bit cleared. But in reality, the flashing colon might prove too distracting in actual operation.

Alternately, I could halve the RTC prescaler and get a 2 Hz interrupt, eliminating the need to involve a second timer in the process. So many options! And yet, maybe not even worth doing in any case.

But today I should really focus on designing and implementing the user interface for this little clock so that I can easily set the time, when needed. I would also like to implement a simpler way to toggle daylight saving time on and off, rather than force the user to go through the whole time-setting procedure twice a year.

OK, I’ve decided to leave the colon on all the time, at least for now. I also moved a little code around so that the LED module only gets updated once a minute, instead of every second.

Now, about that user interface… The simplest thing I can get away with on this project is two push buttons, one to set the hours and the other to set the minutes. Each press and release will increment the count by one, while holding the button down will cause it to count up at around 2 Hz, rolling over when it reaches its maximum count. A bonus feature will be to toggle the daylight savings time mode by pushing both buttons at once.

I connected two momentary contact push button switches to GPIO pins PB3 and PB4, mostly because those were the next pins in the completely arbitrary sequence with which I have been assigning GPIO pins. Now to alter the GPIO initialization code to set them up as inputs with pull-up resistors enabled.

Posted on Leave a comment

CH32V203C8 & TM1637 LED Clock – Part 3

24 March 2025

Many hours later, I am still not seeing any LED segments that are compliant to my will. They just sit there, not illuminated, mocking me.

It did occur to me to fire up the old J4-led-key project, just to have a look at some working waveforms. They would not be exactly the same, as the TM1637 and TM1638 are slightly different, but similar enough to perhaps, just maybe, give me a clue as to what I am doing wrong.

However, in reviewing that older code, I see that I am currently sending all the data and commands as a single block, without intervening “start” signals. That could certainly be it. I will create a separate TM1637_start() function to drop the data line while the clock is inactive, which is how the TM1637 senses new commands incoming, whereas the TM1638 has a STB strobe/chip-select input pin to do that.

And that was it. It was the tiniest of little glitches on the waveform diagram in the data sheet, but they were labeled “start”, so that one was all mine.

The truth is that this implementation is still not correct. I can see on the oscilloscope where the -203 and the TM1637 are both trying to control the data line during the “ACK” acknowledge phase of the transfer. I don’t know what the long term effect of this will be to either chip. The correct thing to do would be to reconfigure the data line as an input for the duration of the ACK pulse, then set it back to being an output.

Now I have a row of very dimly lit segments glowing at me. It’s the top row of segments, by the way, what are usually referred to as the “A” segment (often as lower case, “a”) in the traditional seven-segment layout. I had sent a total of six (6) data bytes of 0x01 to be written to the display memory. This chip can actually address six digits, even though this module only has four connected.

Now that I can see the difference between “on” and “off”, I can now map out with some certainty the relationship between the bits I’m sending and the segments and digits that the chip is driving. All the schematic diagrams of the module that I have encountered in my searches show that the GRID1-GRID4 chip outputs are connected to digits 1-4 on the actual multiplexed LED assembly, with digit 1 being the leftmost.

Some code permutations later, I can confirm these mappings on this module. Additionally, the center colon is mapped to the decimal point of the second digit.

An important note is that whatever is written to the memory, stays in the memory, even if you overwrite other locations. For example, I omitted the command to write to the fourth digit, but instead of going dark, it remained illuminated with its previous value, 0x01. I think it best to completely update the entire display at once. This takes a total of ~175 us with my present code, giving me a maximum theoretical frame rate of 5714 Hz. That’s updating all six possible digits, and this module will only ever have four, so maybe we can push that framerate up a bit more? No?

This little LED module will display any combinations of segments that you wish. You can even adjust the overall brightness, but that is for the entire display at once, not for individual segments. Here are the available brightness levels and the associated command to set each one:

Brightness
(duty cycle)    Command
------------    -------
[off]           0x80
1/16            0x88
2/16            0x89
4/16            0x8A
10/16           0x8B
11/16           0x8C
12/16           0x8D
13/16           0x8E
14/16           0x8F

Cranking it up to 14/16 duty cycle gives a very nice, bright display. It also produces a great deal of electromagnetic interference (EMI) in the audio range, so if you have any amplified speakers or other sensitive audio equipment operating in the area, you’re going to hear about it. There’s probably a way to properly shield this module to minimize this unwanted radiation.

So lighting segments and digits is all fun and good, but the module has no reasonable concept of numbers or letters built in to itself. We have to provide the correct combination of segments in the right places for the time (or whatever) to be displayed.

Seven segment displays, not just the LED variety, have been a favorite of mine since I was a much smaller and younger person. And every time I build a device that uses these little devices, I end up hand-coding the look-up table to convert from numbers (and some other symbols) to readable glyphs. You’d think I would just look up the most recent project and copy those codes over, but you’d be thinking wrongly. If I end up publishing this article, then I’ll have a reasonably accessible place to find it in the future, assuming I ever do this again.

So one more time, here are the digits 0-9 as I like to represent them using seven segment displays, assuming the following bit position-to-segment mapping:

Bit Segment
--- ------------
0   a
1   b
2   c
3   d
4   e
5   f
6   g
7   decimal point

Digit   Value
-----   -----
0       0x3F
1       0x06
2       0x5B
3       0x4F
4       0x66
5       0x6D
6       0x7D
7       0x07
8       0x7F
9       0x6F

To illuminate the center colon, add 0x80 to the value of digit 2.

There are some other symbols that are handy to have around when dealing with clocks. As there is no dedicated “AM” or “PM” indicator on this display, we might need to spell that out for the user. The letter “M” is, shall we say, challenging, but the “A” or “P” would be easy enough, and most likely legible. Actually, the first six letters of the Roman alphabet, “ABCDEF”, are plausible, as long as you really mean “AbCdEF”. These digits come in handy if you need to display hexadecimal values on a seven segment display. Stranger things have happened. Having both “F” and “C” available is nice if you also want to implement a temperature function, without having to pick a side. Then, of course, you’d need a degree symbol “°”, just to be clear. A hyphen or dash is sometimes useful, for example to indicate a negative temperature, and it’s just the “g” segment lit up all by itself. Even easier is the blank or space character, which, like the concept of zero, is just nothing, yet meaningful in context.

Here is the list of the these other characters. They only take up one byte each, so it’s better to have them and not need them than to need them and not have them.

Glyph   Value
-----   -----
A       0x77
b       0x7C
C       0x39
d       0x5E
E       0x79
F       0x71
P       0x71
°       0x63
hyphen  0x40
space.  0x00

I can collect all those magical, hand-crafted values into a table and then look them up as needed. But before I can do that, I need to decide how, exactly, I want this clock to keep track of time.

The simplest possible clock that would be personally useful to me is a 12 hour lock displaying hours and minutes. Both the hours and minutes are composed of units and tens components. So the format of the display will be this:

Digit   Function
-----   ------------------------
1       Hours, tens
2       Hours, units, plus colon
3       Minutes, tens
4       Minutes, units

The simplest case is digit 4, the units component of the minute count. It will always be a digit between 0 and 9 and it will always be displayed. The next simplest case is the minute’s tens component. It will always be a digit between 0 and 5, and likewise will always be displayed.

Complications set in when we get to the hours counter. A traditional 12 hour format clock goes from 1 to 12, with 12 really meaning zero, at least to the 24 hour clock folk. Also, the hour’s tens digit will either be a 1 or not displayed. Fun stuff!

I previously discussed an approach wherein a periodic interrupt does the absolute minimum per iteration to update the representation of the time that will be displayed. This is my preferred approach in this situation and contrasts with simply keeping an abstract value representing the time, such as seconds past a certain point in time, then having to rebuild a “displayable” time from scratch every time it needs displaying.

I’ve already got the built-in real-time clock (RTC) peripheral of the -203 chip initialized and generating an interrupt every second. Right now it’s simply printing the count of seconds since startup on the serial console. I’ll need to add just a bit of code to make it do what I want. It will actually be semi-sorta interesting to look at, code-wise, once completed, but it’s very modular in nature and easily extendible. The “never nesters” out there are gonna Hate It.

While we’re not displaying seconds, we’re keeping track of them. Also, since the hours are a bit of a special case, we’ll keep track of them as a simple count. Within the body of the RTC interrupt handler, I declare a couple of static variables to hold these values:

static uint8_t seconds = 0; // not displayed, but we count them
static uint8_t hours = 0; // displayed after conversion

Then, deeper within the section of the interrupt handler that is specifically there to handle the once-per-second interrupt (there are two other ones in there, as well), I place the clock updating code:

seconds++; // increment seconds counter
if(seconds >= 60) { // seconds overflow
    seconds = 0; // reset seconds
    MINUTE_UNITS++; // increment minutes units
    if(MINUTE_UNITS > DIGIT_9) { // minute unit overflow
        MINUTE_UNITS = DIGIT_0; // reset minute units
        MINUTE_TENS++; // increment minutes tens
        if(MINUTE_TENS > DIGIT_5) { // minutes ten overflow
            MINUTE_TENS = DIGIT_0; // reset minute tens
            hours++; // increment hours
            if(hours >= 12) { // hour overflow
                hours = 0; // reset hours
            }
            if(hours == 0) { // special case for 12 hour clocks
                // spell it out
                HOUR_TENS = DIGIT_1;
                HOUR_UNITS = DIGIT_2;
            } else {
                HOUR_UNITS = hours % 10; // hours units
                HOUR_TENS = hours >= 10 ? DIGIT_1 : GLYPH_SPACE; // leading zero suppression
            }
        }
    }
}

TM1637_update(); // update the LED module

This handles the simple and most likely case, a new second that does not overflow the minutes counter. In the one-in-sixty chance that it does overflow, the units portion of the minute is incremented. When that overflow, the tens get updated. When that overflows, we start counting the hours. It was just easier to handle the hours as a single quantity, because of its two special cases.

I changed the seconds prescaler to 1 to watch the clock module count all the way around, which took 12 minutes. It was only slightly better than waiting the full 12 hours.

Now it’s running and I have a decision to make: Do I plunge into the “user interface” part of the project so that we can set the clock to the correct time (or continue to plug it in a midnight or noon, which works totally fine right now), or do I figure out how to make the colon flash?

Let me know your preference in the comments.

Posted on Leave a comment

CH32V203C8 & TM1637 LED Clock – Part 2

23 March 2025

Keeping the WCH-supplied SDK RTC example program handy, I will add the necessary portions to my original test program. The first task, as always, is proper initialization.

Or is it? Here is the mystery of dealing with peripherals in the “backup power domain”. It might be ticking over just fine as the rest of the chip wakes up from its slumber. But how to tell?

It seems I need a better understanding of the backup domain in general. The specific chip I’m using today, the CH32V203C8T6, is referred to within the documentation as the “CH32V20x_D6”, which is its specific classification abbreviation. This is what I call the “small 203”. It has 64 KB of flash program memory and 20 KB of SRAM. The “big 203” is either the CH32V203RB (64 pin package) or the CH32V208, available in various packages. They have a nebulous amount of flash and SRAM. It’s quite hard to tell from the documents.

But our “little 203” has ten (10) 16-bit backup data registers that are in the backup power domain and should retain their contents as long as VBAT is maintained. The bigger parts have 42 such registers. These backup data registers can optionally be reset to zeros when a “tamper” event is detected. To the shredders! We’ve been breached! No perilous secrets being kept here, so I’m not going to arm the tamper detector… just yet.

What’s odd to me is that the RTC_Init() function in the RTC_Calendar example sets backup data register 1 to the specific value 0xA1A1, as if to say, “I was here”. Yet the software never subsequently checks this location.

I’m thinking that I might keep the derived calendar values, assuming I progress to that level, in these very backup data registers. But I’m getting ahead of myself. How to properly initialize the RTC, but only if it needs it?

Assuming that the RTC will need to be initialized at least once, there has to be code to do that, even if I can’t yet determine when, exactly, to do so. So I will write a straight-through process that is proceeding as if it knows, truly, the RTC must be set up from absolute zero. It will be like booting up the original IBM PC with DOS, who always thought it was Tuesday, 1 January 1980 upon waking.

The first thing the SDK-supplied example does in its initialization is to enable the PWR and BKP clocks on the PB1 bus:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);

I seem to recall some confusion over whether or not the PWR clock actually has to be enabled or not, but that may have been specific to the -003 chips, as documented by CNLohr’s ch32v003fun repository. A very simple test indicates that it is, indeed, reset to all zeros at boot. So let’s enable them now, using the above code snippet.

The RTC, being special, does not have a peripheral clock enable bit in any of the usual places. It is controlled by the RCC’s Backup Domain Control Register (RCC_BDCTLR).

The RTC works exactly as one would suppose, generating a periodic interrupt, if so configured. Right now, I’m just resetting the RTC counter to zero, then using it as a seconds counter, and printing out the current value every time the interrupt fires. Here’s the preliminary version of the rtc_init() function:

void rtc_init(void) { // initialize on-chip real-time clock

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
    PWR_BackupAccessCmd(ENABLE);

    BKP_DeInit();
    RCC_LSEConfig(RCC_LSE_ON);
    while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // add time out
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    RCC_RTCCLKCmd(ENABLE);
    RTC_WaitForLastTask();
    RTC_ITConfig(RTC_IT_SEC, ENABLE);
    RTC_SetPrescaler(32767);
    RTC_WaitForLastTask();
    RTC_SetCounter(0); // set to midnight
    RTC_WaitForLastTask();

    NVIC_InitTypeDef NVIC_InitStructure = {
        .NVIC_IRQChannel = RTC_IRQn,
        .NVIC_IRQChannelPreemptionPriority = 0,
        .NVIC_IRQChannelSubPriority = 0,
        .NVIC_IRQChannelCmd = ENABLE
    };
    NVIC_Init(&NVIC_InitStructure);
}

And here is the interrupt handler, very much as it was when I lifted it directly from the SDK example code:

void RTC_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void RTC_IRQHandler(void) {

    volatile uint32_t rtc; // seconds from RTC

    if (RTC_GetITStatus(RTC_IT_SEC) != RESET) {  /* Seconds interrupt */
        //USART1->DATAR = '!'; // *** debug ***
        rtc = RTC_GetCounter();
        printf("RTC = %i\r\n", rtc);
    }

    if(RTC_GetITStatus(RTC_IT_ALR)!= RESET) {    /* Alarm clock interrupt */
        RTC_ClearITPendingBit(RTC_IT_ALR);
        rtc = RTC_GetCounter();
    }

    RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW);
    RTC_WaitForLastTask();
}

I’ve found that there are two ways to keep track of time on a microcontroller, assuming you have a reasonably accurate time base and a periodic interrupt. One is to simply increment a counter every timer tick, which in this case is every second, and then translate that scalar value into a collection of more useful units, such as hours, minutes and seconds when needed. The second way is to do the “translation” in an incremental manner, as each tick occurs, since the typical case is advancing the seconds count and nothing more. Then you check for overflow into the minutes unit, likewise for the hours, and so on. But usually there is only ever one thing that needs updating, and this executes quite quickly with the right code.

I’ve even taken it farther and broken down the unit seconds and ten seconds groups separately, saving the nuisance of converting a binary value to decimal over and over. The same would apply to the minutes, hours and however far you want to go with it.

But first, it’s now time to start lighting up some LED segments and pretending to tell time. Then we can join the two pieces together and more properly tell the time with this circuit.

I had previously worked on a project that used a similar chip, the TM1638, with the “LED&KEY” module that has eight (8) seven-segment LED displays with decimal points, eight (8) discrete red LEDs and eight (8) momentary contact push buttons. The microcontroller interface is similar, but includes a “STB” (strobe) input that is used as a chip select line. The TM1638 can drive up to ten (10) seven segment LED displays as well as scan an 8×3 array of push button switches. While reviewing the code, it looks like I started with handling the bit wiggling interface in software, and left a note to add support for the SPI peripheral. In retrospect, I don’t think that is possible. But what do I know? I’ve been surprised by SPI hardware in the recent past.

What the interface is not is I2C. Per the data sheet: “Note: The communication method is not equal to 12C bus protocol totally because there is no slave address.” Good to know.

I’ve already set up the two GPIO pins I will need to talk to the TM1637 chip as outputs. Since there are no push buttons connected to the clock display module (yet), I won’t be needing to read back any data from the chip, so the data line can stay an output.

I’ll need to make a small adjustment to the GPIO initialization code as the TM1637 data sheet indicates that the “idle” state of both lines is high. Right now they are both low and I have no idea what the poor little chip must think of me.

Here is the summary of what the “Program Flow Chart” describes for updating the display:

Send memory write command: 0x40
Set the initial address: 0xC0
Transfer multiple words continuously: <segment patterns>
Send display control command: 0x80-0x87 = brightness, 0x88 = DISPLAY ON
Send read key command (we're not doing this one)

Right now I don’t know which address corresponds to which digit on the display. I’m also not exactly sure which bit corresponds with which LED segment. But I aim to find out. Let’s start out by sending a single bit set to all six of the available addresses, 0xC0-0xC5.

It seems I have misunderstood the part about the maximum clock frequency we can use to talk to the chip. The data sheet specifies the “Maximum clock frequency” as 500 KHz, with a 50% duty cycle; not the 250 KHz figure I quoted yesterday.

As the data line is only supposed to change when the clock line is low during normal data transmission, I will try to center the transitions within the clock pulses. With a maximum clock frequency of 500 KHz, each clock transition is 1 us apart. So to aim for the middle of the low part of the clock signal, we should wait 500 ns after the clock line goes low to update the data line. The SDK-provided delay functions, Delay_Us() and Delay_Ms(), only provide microsecond or millisecond time spans. Right now I’m only using Delay_Ms() to time the blinking of the on-board LED. It’s time to deploy some higher-resolution delay functions.

Actually, all I need to do here is to start the STK system timer in free-running mode at the full system frequency of 144 MHz to get ~6.9444… ns resolution. Then I can just pass in the number of clock cycles I want to waste in the delay, add that number to the current STK counter value, then wait for the STK counter to exceed that number. Here’s the STK initialization code:

#define STK_STE (1 << 0) // STK enable bit in CTLR

void stk_init(void) { // initialize system timer

    SysTick->CNT = 0; // reset counter
    SysTick->CTLR = SysTick_CLKSource_HCLK | STK_STE; // enable STK with HCLK/1 input
}

I had to #define the counter enable bit for the CTLR because it’s not #define’d anywhere else. The SysTick_CLKSource_HCLK value happened to be available in the RCC header file.

And here’s the actual delay() function code:

#define NS /7 // STK tick factor for nanoseconds
#define US *144 // STK tick factor for microseconds
#define MS *144000 // STK tick factor for milliseconds

void delay(uint32_t delay_time) { // delay for 'delay_time' clock ticks

    if(delay_time == 0) return; // already late

    uint64_t end = delay_time + SysTick->CNT; // calculate time to end
    while(end > SysTick->CNT) {
        // just wait
    }
}

Using the #define’d units NS, US or MS for nanoseconds, microseconds and milliseconds, respectively, you can eloquently express your desired delay time:

delay(500 NS);
delay(250 MS);
et cetera
Posted on Leave a comment

CH32V203C8 & TM1637 LED Clock – Part 1

22 March 2025

I found a little LED clock display module, driven by the Titan Micro Electronics TM1637 driver chip. I’d like to build a simple LED clock using this display module and a WCH CH32V203C8 RISC-V-based microcontroller.

I already had some of the “Blue Pill” development boards for the -203 chip from WeAct Studio. They are the same footprint as the STM32-based “Blue Pill” development boards, and fit nicely in a solderless breadboard. Another nice thing about these boards is that they already have a 32,768 Hz quartz crystal attached to PC14 and PC15, enabling the on-board real-time clock (RTC) of the -203. This one already had a WCH-LinkE programming cable built for it, a remnant of a previous project. This provides power, programming and serial communication lines from my laptop to the circuit. I added a purple wire for the NRST signal.

To wire up the TM1637 module to the prototype circuit, I will need another short cable. The LED module already has a four pin right-angle header soldered to it. The module needs +5V and ground, as well as digital clock and data lines. You know how I just can’t wait to build yet another custom cable for these projects. I’m getting pretty good at it, too.

I can’t really tell the pin numbering of the little LED module, but the individual signals are clearly marked on the PCB. Here is a description of the interface cable:

Pin Signal  Color   Description
--- ------  ------  ---------------
1   GND     black   ground
2   VCC     red.   +5V
3   DIO     green   data in and out
4   CLK     yellow  clock

The little LED module is skittering about on the desk quite a bit. I might have to 3D print a little stand for it. I don’t have a mechanical drawing for this module, but as they are still being sold online, I should be able to find one.

Looking online for some more information about these little LED modules, I see that I have the “v1.0” revision of the board, with a “CATALEX” logo and the date “02/10/2014” on the back. The current crop of boards available online show a “V1.1” revision, as well as square pad on the ground terminal, indicating pin 1. That was my guess, anyway. Sometimes I get lucky.

Note that this is the version of the LED module that has four complete seven segment displays and a center colon, but no decimal points.

The driver ship also supports scanning a small keyboard of up to sixteen (16) individual buttons, but does not support “n-key rollover”, so you can’t press more than one key at once. Well, you can, but the results are not guaranteed. To avail ourselves of this feature, I would have to tack on some wires directly to the chip on the back of the module. As the -203 has many as-yet unused pins that could be used for this function, we’ll keep that trick in our back pocket for now.

Having created a new MounRiver Studio 2 (MRS2) project for the software, named “C8-TM1637-clock”, of course, I can see that the USART serial lines are correctly connected and that the system is running at 96 MHz, which is the MRS2 default for these chips. I bumped that up to 144 MHz, because why not? The Blue Pill board already has a 8 MHz quartz crystal and 10 pF bypass capacitors (0402 packages – almost invisibly small) installed. Once all the “clockwork” of the clock is clocking clockfully, I can probably run the CPU from the internal RC oscillator, as the precision needed for keeping time will be the job of the 32,768 Hz crystal.

The Blue Pill board also has a blue LED mounted in active high configuration to pin PB2, via a 1.5KΩ to limit the current. Let’s blink that LED, just to make sure we can.

First, I create a new function called gpio_init() to set up everything. There, we enable the peripheral clock for GPIOB with this SDK call:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // enable GPIOB peripheral clock

Next, the first three pins of GPIOB are configured as push-pull outputs. I have arbitrarily decided to use PB0 as the clock line and PB1 as the data line for the TM1637 module. The code looks like this:

GPIO_InitTypeDef gpio_init_structure = { 
    .GPIO_Mode = GPIO_Mode_Out_PP,
    .GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2,
    .GPIO_Speed = GPIO_Speed_2MHz
 };

GPIO_Init(GPIOB, &gpio_init_structure);

No high-speed shenanigans are required, so I specified the lowest frequency, 2 MHz. The maximum clock speed for the TM1637 is 250 KHz. 2 MHz is overkill, but it’s the lowest setting available.

Within the main() function’s infinite loop, I put this code to blink the LED:

GPIO_WriteBit(GPIOB, GPIO_Pin_2, Bit_SET); // LED on
Delay_Ms(250); // short delay
GPIO_WriteBit(GPIOB, GPIO_Pin_2, Bit_RESET); // LED off
Delay_Ms(250); // short delay

And sure enough, there’s that blinking LED we all love to see early on in any embedded project. All is well with the world.

Now to set up the real-time clock (RTC) peripheral on this chip. It gets a whole chapter in the Reference Manual, Chapter 6. The manufacturer also supplies a code example called “RTC_Calendar” that demonstrates the RTC being set up and printing the time and date to the serial console, using an interrupt. We can peek at this code to get an idea of what is involved to get it clocking ourselves.

The RTC circuit on this chip is simple in its execution. It’s a 32 bit counter that has a programmable clock prescaler and choice of clock inputs. For time-of-day applications, it’s almost always going to be driven by a dedicated 32,768 Hz quartz crystal attached as the LSE (low speed, external) oscillator, divided down into one second pulses. Oddly, all the access and manipulation registers are only 16 bits wide. With 2^32 seconds of run time before it overflows, which is over 136 years, you’d think we’d be safe. But this is exactly the predicament we find ourselves in with the Unix Epoch, wherein the ancestors started counting seconds on 1 January 1970, thinking that the year 2106 would never come. Well, it will, and it’s only 81 years from the date of this writing. Your Humble Narrator fully intends to be complaining about things such as this well past this milestone in our future.

Since the madness of daylight saving time has yet to be expunged from our civilization, we also have to deal with that nonsense, if we’re going to have a clock that sorta-kinda reflects the societally-agreed-upon time. Leap years, on the other hand, are a completely natural and reasonable thing to handle, as the orbital velocity and rotational velocity of this planet are not (yet) tidally locked. One day, we can hope, it will be. Then peace will guide the planets and love, love will steer the stars. Until then, there’s a surprisingly elegant mathematical solution that should keep us pretty close for many centuries.

Another interesting thing about the RTC is that it is within the “backup power domain” of this chip. There is a separate power input pin on this family of chips for battery power, so that things such as the real-time clock and other critical functions can be preserved even when the flash & bang parts of the chip are powered up and down. There is a recognized division within the chip as far as access from one domain to another goes, in that there is a specific sequence of steps to be taken to reset and configure the RTC, even if the rest of the chip has been power cycled.

The Blue Pill board does not provide a separate battery connection, but instead routes the regulated 3.3V supply, via a Schottky diode, to the VBAT pin. I’m not too awfully worried about it at the moment. The next stage of this project, should it ever transpire, would be to create a bespoke PCB for the components and implement a direct LED drive circuit, obviating the need for the TM1637 circuit entirely. Alternately, a dedicated RTC chip with its own backup battery could be added to even the humblest -003 variant as another approach, using abundantly available modules.

There are three key ingredients to any successful timepiece:

1.  Keeping time
2.  Telling time
3.  Setting the time

Now you can also get fancy and add other functions, such as calendars, alarms, timers, and other really nice features. But the basic requirements of a useful clock must be addressed first. I’ve hinted at my solutions for the first two requirements (1: on-chip RTC, 2: TM1637 LED module) but haven’t talked about how we are going to set the time. The SDK example cheats, and just insists that “the initial time is 13:58:55 on October 8, 2019”, which was not when the software was published, so I assume it has some other significance.

Next steps will be to transfer over the appropriate code to set up the RTC, probably using a fake time as well, then start to get some of the LED segments glowing. The time setting user interface I will leave for last.

Posted on Leave a comment

CH32V00x – More Thoughts and More PCBs

13 March 2025

I’ve been thinking about some of my other assumptions with regard to these little chips and what it takes to program them. One thing is that I had been laboring under the false assumption that the interrupt vector table, should one decide to use one, must reside at location zero in the program memory. In truth, I understood that it could be located on any 1 KB boundary, and you can (must) tell it where by writing to the mtvec CSR. Per the QingKe V2 Processor Manual, Section 2,2 Exception, p. 4:

"It should be noted that the vector table base address needs to be 1KB aligned in the QingKe V2 microprocessor."

This limitation is not present on the other QingKe processor families.

So for a chip with a mighty expanse of 16 KB, you actually have 16 different choices available to you. One of the good reasons to stick the vector table at address zero is that it avoids any gaps in the program memory when you’re writing really small applications, as I tend to do with these chips. It totally doesn’t matter if you spread your ones and zeros across the entire continuum of available sites or cram them all at one end or the other.

Additionally, you could even point the mtvec CSR at one of two SRAM addresses, 0x20000000 or 0x20000400, and have an instantly reconfigurable vector table in volatile memory. I have no idea if this would actually work or not. You could even use the VTF mechanism to cover any interrupt requirements in the device set-up stage, as long as you only need two of them. The bigger chips offer four VTF slots.

I really need to design some new PCBs for all the incoming chips. One thing I noticed about the new CH32V002 parts was what looks like a ground pad on the bottom of the SOP8 package, the J4M6 variant. This appears to be an anomaly as the data sheet makes no mention of it. Additionally, the “photo” of the TSSOP20 package just has the identifier “813524E47” on it, and no part number or WCH logo, so these may just be placeholder photos until they can book some studio time for a proper photo-shoot.

I also see that there is supposed to be a QFN12 package with 11 available IO lines, the -D4U6 variant. What sorcery is this? It’s 2mm square. So tiny! I can’t wait to make some eensy weensy doo-dads with these little chips.

Other differences of note when compared to the original CH32V003:

12 bit ADC, 3 MS/sec sampling rate
8 channel Touch-Key channel detection
RV23EmC - hardware multiplication
4 KB SRAM
2.0-5.5 VDC system power supply
2 ms power on reset

Still no SPI on the SOP8 or the QFN12 packages. It’s not like I’m invested in understanding the SPI peripheral on this chip or anything…

As the SOP8 package still has both dedicated VSS and VDD pins, I can design a PCB that omits the solder pad, if it even really has one. I don’t expect to see the chips in person for at least another week.

The CH32V006 also have some upgrades when compared to the CH32V003 or -002:

62 KB flash
8 KB SRAM
2 USARTs
31 GPIO lines
    GPIOA PA0-PA7
    GPIOB PB0-PB6
    GPIOC PC0-PC7
    GPIOD PD0-PD7
Operational amplifier
3 timers, 2 watch dog timers, STK timer

So I will need a little prototyping board for each of the incoming chips:

CH32V002J4P6 - SOP8
CH32V002F4P6 - TSSOP20
CH32V006F8P6 - TSSOP20
CH32V006K8U6 - QFN32

I’d also like to design a DIP8 adapter for the SOP8 package that would let me use these chips as a drop-in replacement for the Atmel AVR ATtiny13 that is in absolutely everything I sell. I have a bunch of 1:1 pin-mapping DIP8 adapters for the SOP8 packages. They’re handy for breadboarding.

Posted on Leave a comment

CH32V003 driving WS2812B LEDs with SPI – Part 14

13 March 2025

Continuing my investigation into why the CH32V003 SPI port sometimes just locks up, I have looked at the source code for the two functions that are involved: The SPI_I2S_GetFlagStatus() function and the SPI_I2S_SendData() function. They both do exactly what you would hope that they would do.

What comes up as suspicious is my initialization of the port. Here are the values of the two control registers as well as the status register immediately after being initialized:

SPI_CTLR1 = 0xC154
SPI_CTLR2 = 0x0000
SPI_STATR = 0x0002

This varies from the final value of SPI_CTLR1 that I used in my assembly language version of the diagnostic: 0xC354.

So what’s the exact difference here? Using the SDK function to initialize the SPI port with what was just my best guess at what would be correct, we get the following bits set in CTLR1, versus what I told it to do:

Bit Field       SDK mine    Description
--- --------    --- ----    -----------
0   CHPA        0   0   clock phase (don't care)
1   CPOL        0   0   clock polarity (don't care)
2   MSTR        1   1   coordinator mode
5-3 BR          2   2   bit rate FCLK/8
6   SPE         1   1   SPI enable
7   LSBFIRST    0   0   not set = MSB first
8   SSI         1   1   select pin level
9   SSM         0   1   select 0=hardware, 1=software control
10  RXONLY      0   0   receive only mode (not used)
11  DFF         0   0   0 = 8 bit data
12  CRCNEXT     0   0   send CRC (not used)
13  CRCEN       0   0   enable hardware CRC (not used)
14  BIDIOE      1   1   enable output, transmit only
15  BIDIMODE    1   1   one line bidirectional mode

The only difference I see is that the SSM bit is cleared in the SDK initialization and set in mine. Since we’re not using the select line to select anything, it shouldn’t matter. It does matter that the NSS line is already set high before enabling the peripheral in coordinator mode. Per the RM, Section 14.2.2. Master Mode, p. 162:

"Configure the NSS pin, for example by setting the SSOE bit and letting the hardware set the NSS.  [I]t is also possible to set the SSM bit and set the SSI bit high.  To set the MSTR bit and the SPE bit, you need to make sure that the NSS is already high at this time."

And I know that it indeed does not work if the NSS is not set high before enabling the peripheral. The peripheral simply locks up with a “mode fault” error.

I added some code to print out the status register when a timeout occurs. I immediately see that it is always 0x0020, which means a “mode fault” has occurred. Here’s a list of things that can cause a mode fault on this peripheral:

When the SPI is operating in NSS pin hardware management mode, an external pull-down of the NSS pin occurs
in NSS pin software management mode, the SSI bit is cleared
the SPE bit is cleared, causing the SPI to be shut down
the MSTR bit is cleared and the SPI enters slave mode

Perhaps noise on the otherwise un-initialized NSS line is triggering an intermittent mode fault? Looking back, I see I have, in my ignorance, not specified which NSS handling strategy (hardware vs software) to use when configuring the peripheral.

Setting the SPI_NSS field to ‘SPI_NSS_Soft’ (1) when performing the SPI initialization, we get the following setup profile when the application starts:

SPI_CTLR1 = 0xC354
SPI_CTLR2 = 0x0000
SPI_STATR = 0x0002

So now it matches my bit-wise initialization of the control register. Now it’s time to let it run on ‘The Gauntlet’, as I have named my alternate test setup, overnight, and see what we shall see.

Posted on Leave a comment

CH32V003 driving WS2812B LEDs with SPI – Part 13

12 March 2025

Big news! New chips have dropped! Today I was able to order some of the very new and hitherto unobtanium CH32V002 and CH32V006 chips. Very similar to our dear friend the CH32V003, but with more SRAM (4 KB) and in the case of the -006, more pins and 8 KB SRAM. I also spied a new development board for the top-o-the-line CH32V317 chip. It’s got the -WC package, a 68 pin QFN. I’ll grab the -VC package (100 pins of LQFP100 goodness) as soon as they will sell me one.

Testing of the CH32V003 with the SPI-driven WS2812B LED continues, with promising results. So far, no faults have been detected. Having more working examples will help me figure out what is going on in the few cases where it consistently fails.

Let’s go back to some of the unfinished business from the last entry. I had been testing the new SVD files as processed by my Python script and converted into C language header files. I had noticed that some of the defined single-bit field bit masks did not correspond to any of the fields in the defined structures, so I wanted to go back and address that.

The simpler of the obvious things I should do is to include some additional commentary adjacent to these values so I at least know where they’re supposed to go. That worked as well as I would expect it to, and did not seem to introduce any difficulties to the process. Here are the single bit mask values for the PWR peripheral, mentioned previously:

// peripheral register single-bit values

#define PWR_PDDS    (1 << 1) // CTLR PDDS
#define PWR_PVDE    (1 << 4) // CTLR PVDE
#define PWR_PVDO    (1 << 2) // CSR PVDO
#define PWR_AWUEN   (1 << 1) // AWUCSR AWUEN

At least those comments will let me know where those values are supposed to be used.

Now if I can get it to emit bit field structure components for the registers that have only a single field, we should be done here (for now).

Let’s get those missing fields defined. We’re going to need every single one of them!

It seems my very vague recollection about ‘duplicate member’ errors was indeed what had been happening. Luckily for us, there are only a total of twelve name-space collisions in the whole system. Here is an example from the very first peripheral definition in the SVD, our old friend PWR:

union {
    uint32_t        AWUWR;  // 0x0C Automatic wake window comparison value register (PWR_AWUWR)
    struct {
        uint32_t    AWUWR:6;    // 0 AWU window value
    };
};

So both the register and the bit field within the register have the exact same name, as far as the SVD is concerned. I can take advantage of a pattern I have noticed in The Naming of Things here, in that register names often end in ‘R’, which I suppose stands for ‘Register’. That means that I can look for this pattern (register name is identical to field name and ends in ‘R’), and drop the trailing ‘R’ from the field name. Let’s see if that improves things at all.

It looks some fiddlin’, as my Python skills are rudimentary at best, but for the one case where the name substitution would be the correct solution to the duplicate member error, it seems to work OK. That leaves seven more cases to deal with.

A single instance where the register name and the single field within it were the same, and yet not ending in ‘R’, was again found in our friend PWR, the Auto-wakeup Crossover Factor Register (PWR_ AWUPSC). Here I elected to append the entirely arbitrary tag “_field” to the end of the field name. When referring to this setting, just read or write directly to the register instead of the field. Simpler that way.

That leaves six duplicates remaining. These fall into three groups. The first are the injected data readout registers for the ADC, all named IDATA (the RM names them ‘JDATA’). The second group are the ‘unique ID’ bit fields all named ‘U_ID’, and the third is just, I feel, bad naming in the flash controller, with a name collision between the similar Extended Key Register (FLASH_MODEKEYR) and BOOT Key Register (FLASH_BOOT_MODEKEYP), who both define their single, 32-bit wide fields with the same name: MODEKEYR.

And remember, this is just for the -003 chips. I’m sure there will be more issues like this when I eventually get to the ‘bigger’ chips with more peripherals.

I can only think of a couple of ways to deal with the ADC registers. I think the easiest and hopefully most reliable method would be to look for the known duplicate field ‘IDATA’, then amend it based on its uniquely named register, like this:

Register name   New field name
-------------   --------------
IDATAR1         IDATA1
IDATAR2         IDATA2
IDATAR3         IDATA3
IDATAR4         IDATA4

Hey, waddaya know? It worked! Can I get away with the same trick on the unique ID fields, as well? It seems that I can. Now we’re down to only one duplicate, at least on the -003: the ‘MODEKEYR’ field that somehow appears in both the extended key register and boot key register for the flash control peripheral. I think the only thing for this case is a special test, just for this particular combination.

And it solves the last remaining problem. However … as I was browsing through the script, I saw plenty of notes to Future Me, screaming and begging for help. There are still many things that need to be added and improved in this process. But for the moment, we can proceed.

Now you understand why I wanted to do the “simpler” assembly language version first. I’m eventually going to burrow even deeper than that, because of How Things Are. But now I should be able to hold up two examples of software, both ostensibly written in C, and pore over their disassembled guts and figure out why one works and the other one doesn’t.

Posted on Leave a comment

CH32V003 driving WS2812B LEDs with SPI – Part 12

10 March 2025

Here’s an interesting data point that just came to my attention: the on-going experiment with the WCH-official development board for the CH32V003F4P6 device has hung up after 229,552,000,000+ loops. That’s 229 billion with a ‘b’. What caused the hang-up? Unclear.

I was about to shut down the experiment as I thought it was no longer providing any useful data. Well, I was wrong about that. However, it looks more like a testing apparatus failure than the ‘unit under test’ (UUT), as trying to reset the device provided no indication of resumption on the serial console. Unplugging the WCH-LinkE caused the serial terminal to disconnect, as it does, and re-starting the connection showed the recently-reset device counting its millions of loops again. A more self-contained diagnostic set up is certainly worth thinking about at this stage. I’ll just note that here and move on with the other experiments.

I’ll get back to dusting off my C-language framework for these devices now. We’ve got freshly-minted new header files describing all the peripheral registers and all the single-bit-wide settings therein. I’ll take a peek at the -003 support file first and see if everything looks correct.

The first peripheral defined in the SVD file is the PWR power control system. It mostly controls the low power modes, power monitoring facility and ‘automatic wake up’ function. There are only five registers implemented and those are sparsely populated.

There’s going to be a certain amount of “What was I thinking?” involved in this kind of archeology. I see the things that I know for sure should be present, such as the ‘structure of structures’ that I define for each different peripheral, as well as some of the single-bit fields and register addresses.

But a closer look at those single-bit fields has me scratching my head. There seems to be a disconnect between the laid-out format of the structures and the fields. There are values defined that do not appear in the structure at all. Here’s what I’m looking at:

//------------------------------------------------------------------------------
// PWR
//------------------------------------------------------------------------------

typedef volatile struct { // PWR Power control

    union {
        uint32_t        CTLR;   // 0x0 Power control register (PWR_CTRL)
        struct {
            const uint32_t  CTLR_reserved_0:1;  // 0 - reserved
            uint32_t    PDDS:1; // 1 Power Down Deep Sleep
            const uint32_t  CTLR_reserved_2:1;  // 2 - reserved
            const uint32_t  CTLR_reserved_3:1;  // 3 - reserved
            uint32_t    PVDE:1; // 4 Power Voltage Detector Enable
            uint32_t    PLS:3;  // 5 PVD Level Selection
        };
    };
    uint32_t            CSR;    // 0x04 Power control state register (PWR_CSR)
    uint32_t            AWUCSR; // 0x08 Automatic wake-up control state register (PWR_AWUCSR)
    uint32_t            AWUWR;  // 0x0C Automatic wake window comparison value register (PWR_AWUWR)
    uint32_t            AWUPSC; // 0x10 Automatic wake-up prescaler register (PWR_AWUPSC)

} PWR_t;

#define PWR ((PWR_t *) 0x40007000) // peripheral pointer

// peripheral register single-bit values

#define PWR_PDDS    (1 << 1)
#define PWR_PVDE    (1 << 4)
#define PWR_PVDO    (1 << 2)
#define PWR_AWUEN   (1 << 1)

// peripheral register addresses

#define PWR_CTLR (*((volatile uint32_t *) 0x40007000))
#define PWR_CSR (*((volatile uint32_t *) 0x40007004))
#define PWR_AWUCSR (*((volatile uint32_t *) 0x40007008))
#define PWR_AWUWR (*((volatile uint32_t *) 0x4000700c))
#define PWR_AWUPSC (*((volatile uint32_t *) 0x40007010))

I’m specifically talking about the PWR_PVDO and PWR_AWUEN fields. Why are they not broken out as bit fields within the structure for their enclosing registers?

Ah, now I remember. I made the executive decision to not break out fields if there were only one field within a register. This seemed to make sense for registers such as the USART_DATAR register, where the register and the bit field were effectively the same thing.

But in this case, for whatever reason, the two bit fields within the registers do not start at bit position 0. Additionally, there would be no way for me to refer the the field within the register without knowing which register it belonged to – which is something I was hoping to avoid, because it’s possible, if properly encoded.

Were there naming ambiguities between registers and bit fields? That sounds like the kind of problem that this ‘solution’ addresses.

Well, I can always go back into the script and omit the ‘single field omission’ conditional. But before I do that, there’s some more fundamental testing I can do on these new include files. For example, do you even compile, bro? But those single-bit values need comments describing where they belong, for sure.

The simplest test of this is to create a new project that has just one source file in it that includes the device header and has a main() function. As it won’t have any need (yet) for interrupt support or a proper C runtime package, it will only fail because there is no ‘start’ function declared, which is what the linker script says is the ‘entry point’ of the program. But it should compile, if not link properly. Here’s what I think it should look like:

// filename:  F4-test.c
// part of bare-metal C framework test project
// 10 March 2025 - Dale Wheat

#include "ch32v003.h"

void main(void) { // main program function

    while(true) { // an endless loop
    }
}

// F4-test.c [end-of-file]

Whereas, this is all that would actually be required:

#include "ch32v003.h"
void main(void) {}

But you know how I am about these things. Ask a writer to solve a problem and it’s likely that “more writing” will be included near the top of the list.

I borrowed a makefile from another project and made various modifications to it to fit the present needs. And the result is that the header file induces a couple of errors, which is both bad news as well as good news. It could have been so much worse.

It looks like there’s a couple of typos in the WCH-supplied SVD file. Within the definition of the Programmable Fast Interrupt Controller (PFIC), there is a register called PFIC Interrupt Enable Status Register 1 (PFIC_ISR1). It contains fields to indicate which interrupts are enabled. They are called:

INTENSTA2   IRQ2
INTENSTA3   IRQ3
INTENSTA12  IRQ12
INTENSTA14  IRQ14
INTENSTA16_31   IRQ16-IRQ31

Farther down the list is the PFIC Interrupt Pending Status Register 1 (PFIC_IPR1). It contains a similarly named set of fields to indicate which interrupts are currently pending, but where we should see ‘PENDSTA14’ and ‘PENDSTA31_16’, we see ‘INTENSTA14’ and ‘INTENSTA16_31’ repeated.

As a scribbler of codes myself, whose fingers already know how to both copy and paste all by themselves, I think I see how this might have happened. I will let WCH know about this. They were both very prompt and exceedingly polite when I addressed a potential typo in the reference manual. But I will wait until I have made sure there are no other similar issues to report.

So for the moment, I will correct my local copy of the SVD file in question and re-run the conversion script.

This overcomes the compilation error. I get a warning (not an error) about the missing start symbol:

ld: warning: cannot find entry symbol start; defaulting to 0000000000000000

The default of so many zeros will work quite nicely, I think. That’s exactly where I wanted it to go, anyway. And it produces an ELF file! Here’s the meaningful part of the output listing:

Disassembly of section .text:

00000000 <main>:

#include "ch32v003.h"

void main(void) { // main program function

    while(true) { // an endless loop
   0:   a001                    j   0 <main>
   2:   0000                    unimp

Which is perfect. An endless loop, as expressed in C as “while(true) {}” gets translated, as it should, to RISC-V assembly as “0: j 0” or “jump to address 0”. Since it was such as short jump, relatively speaking, the compiler even used the ‘compressed’ version of the ‘j’ instruction, taking up only 16 bits of program memory. So even though the file is reported to be four (4) bytes long, in truth only the first two are doing anything important. I can even flash it to the chip and check it in the debugger. So we’re certainly on track for Great Things at this point.

Eliminating the “ENTRY(start)” command in the linker script gets rid of the warning. Per the GNU documentation for the linker found at:

https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_24.html

ENTRY is only one of several ways of choosing the entry point. You may indicate it in any of the following ways (shown in descending order of priority: methods higher in the list override methods lower down).

the `-e' entry command-line option;
the ENTRY(symbol) command in a linker control script;
the value of the symbol start, if present;
the address of the first byte of the .text section, if present;
The address 0.

As far as I know at the moment, the entry point for these devices will always be address 0, so we should be good. I prefer to be specific about these things, when I can, and not trust assumptions that might change in the future, as they often do. I could specify the entry point as a command line parameter to the linker, but I would normally rather have it in a document of some kind, such as the linker script. When we’re done setting up this framework, I’ll have decided one way or the other about this issue.

So do we have enough machinery in place to blink an LED? Let’s find out.

First, we have to enable the peripheral clock for the GPIO port where the LED has been installed. I’m using PA1, which is bit position 1 of GPIO port A. The GPIO ports are all on the PB2 bus, so we just set the IOPAEN clock enable bit in the PB2 Peripheral Clock Enable Register (RCC_APB2PCENR). It should only take this much C code to do this:

RCC->IOPAEN = ENABLE;

Since the definition for the GPIO ports map out all the fields, and because all the fields have unique names, we, as lazy human programmers, need not keep up with which register is which, and just generally wave in the general vicinity of the peripheral in question. I remembered it was in the RCC, and that was enough. I also remembered that the field was called IOPAEN, a mnemonic for “input output port A enable”. Additionally, I have taken the liberty of #define’ing the binary values ENABLE (1) and DISABLE (0) in the generated header file. I find that this family of chips largely uses a 1 to turn things on and a 0 to turn things off. This is, sadly, not universally true with other manufacturers. Good job, WCH! There are a few other goodies packed in there as well, which I’ll describe by and by.

Step two on the journey to blinking an LED is to configure the now-clocked GPIOA, or at least the pin we want. Do you remember my ‘cheat sheet’ of GPIO initialization codes? It comes in very handy for this kind of thing. The code I want is for a push-pull output with a maximum output frequency of 2 MHz. Why that exact frequency? It happens to be the slowest one available, the other choices being 10 MHz and 30 MHz. It’s an LED that we are going to be looking at with our human eyes, not a microwave signal being sent to outer space. The code for that is ‘2’. Now we just place that code in the right bit position, which for PA1 would be bit position 1. PA2 would be position 2, etc. Or we could just initialize all eight positions at once, even though we have Scientifically Proven that there are only two bits implemented in this port. The code looks like this:

GPIOA->CFGLR = 0x88888828;

All those 8s represent the setting for all the other bits, which is ‘input with pull-up or pull-down resistors’. This is the setting that uses the least amount of power, which will become more important once we need to put the chip to sleep when it needs to wait for something interesting to happen.

Now another way to do this would be to access the individual MODE and CNF fields for this GPIO pin and assign them their proper values, like this:

GPIOA->MODE1 = GPIO_MODE_OUTPUT_2MHz;
GPIOA->CNF1 = GPIO_CNF_PUSH_PULL;

But this requires some enumerated values that I haven’t bothered to put into the collection just yet, as well as much more code. It looks like it’s just two writes instead of one, as in the previous example, but since the compiler is granting our wish to deal with embedded device registers intelligently by using predefined bit fields, it’s going to involve a read-modify-write cycle on each field, along with all the bit shifting and masking that is required to do that.

Now everything is set up properly and we can just blink that LED all we want to now. Add this code inside the inner-most while() loop:

while(true) { // an endless loop
    GPIOA->ODR1 = ENABLE; // LED on
    GPIOA->ODR1 = DISABLE; // LED off
}

The ‘ODR1’ is the bit field corresponding to the output data register, or OUTDR, or ‘output data register. Setting it to ENABLE is the same as writing a 1 to it, which turns on the LED, as I have wired it up in the ‘active high’ configuration. Similarly, writing the DISABLE value of 0 turns it off.

You might be surprised to find when you run this program that the LED just comes on and stays on. Well, that’s a bit of an optical illusion. It’s actually blinking so fast you can’t see it. Try running it within the debugger and step through each program statement one at a time and you’ll see the expected behavior.

We can add a little loop in between the LED commands to slow it down. How about counting to a million? How long should that take? Here’s what the code would look like:

while(true) { // an endless loop
    GPIOA->ODR1 = ENABLE; // LED on
    for(uint32_t i = 0; i < 1000000; i++); // short delay
    GPIOA->ODR1 = DISABLE; // LED off
    for(uint32_t i = 0; i < 1000000; i++); // short delay
}

I’m seeing almost one second on and almost one second off. Those for() loops each create a new 32 bit unsigned integer variable called ‘i’, set it to zero initially, then increment it until it is no longer less than one million. Pretty quick!

Again, the compiler is doing some heavy lifting for us in the background here. Using the bit fields for the individual pins within the GPIO port has it reading, masking, OR’ing or perhaps AND’ing, as required, then finally writing for each transition. The chip itself has a more elegant way to address this frequently-occurring need.

In addition to the output data register, OUTDR, each of the GPIO ports has both a ‘bit set and reset’ register as well as a ‘bit clear’ register. Writing a 1 to any of the lower 8 bits of the BSHR register will set those bits, and only those bits, to 1. Writing a zero there does nothing, and leaves alone whatever is already there. Handy! The ‘lower byte of the upper half’ (?) of the BSHR register does the opposite: Any 1s written there will ‘reset’ that individual bit, and again any zeros written are ignored.

The BCR or ‘bit clear’ register does the same thing. Writing a 1 to a bit position clears that bit in the OUTDR and leaves the others intact. Why have two registers that do the same thing? You’re asking the wrong person. It’s not a ‘wrong question’; I genuinely don’t know the answer. You’ll find the exact same thing on the STM32 devices, so go figure.

So now we have a blinky example program that takes up all of 100 bytes. This is with the compiler’s ‘optimization’ setting of ‘for debug’, so it could probably go lower.

If we can blink an LED, what keeps us from configuring the SPI port and controlling a WS2812B addressable LED? Not much. Let’s do it.

The system clock was running at ~8 MHz for our very simple LED blinky test program. That’s what you get right after a reset with these chips. The internal HSI is running at ~24 MHz and is being divided by three (3) by the HPRE prescaler.

Here is the code to get it to run at 48 MHz, using the built-in PLL to double the HSI frequency:

RCC->HPRE = RCC_HPRE_1; // disable HCLK prescaler
RCC->PLLSRC = RCC_PLLSRC_HSI; // select HSI as PLL input
RCC->PLLON = ENABLE; // enable PLL
RCC->SW = RCC_SW_PLL; // select PLL as system clock, once it locks

Waiting for the PLL to lock is quite a bit simpler, from a coding standpoint, using this framework:

while(RCC->SWS != RCC_SWS_PLL) {
    // wait for PLL to lock
}

This while() loop just checks the system clock status bits to see when they eventually change over to the PLL, which will happen after the PLL locks. It will wait forever, if necessary, but it’s almost always a microscopically short time. Measure it, if you like. Let me know what you find.

Let’s initialize the SPI peripheral, again. Here’s what we need to get the setup we require for our special application of its unique talents. First, remember to enable GPIOC, which will be hosting our SDO on PC6. We configure it to be ‘output push-pull multiplexed 10 MHz max’:

RCC->IOPCEN = ENABLE; // enable GPIOC peripheral clock
GPIOC->CFGLR = 0x89888888; // PC6/SDO

Next enable the SPI peripheral clock setting bit SPI1EN somewhere, we don’t care where, within the RCC:

RCC->SPI1EN = ENABLE; // enable SPI peripheral clock

Then there’s just a short list of bits to flip in the SPI control register, and away we go:

SPI1->BIDIMODE = ENABLE;
SPI1->BIDIOE = ENABLE;
SPI1->SSM = ENABLE;
SPI1->SSI = ENABLE;
SPI1->BR = SPI_BR_8;
// note:  only now can we set these bits
SPI1->MSTR = ENABLE;
SPI1->SPE = ENABLE;

Again, the framework knows which bits are in which register, so we don’t have to.

One critical thing, among a list of other critical things that a proper C framework should do for us, that is not being handled (yet) is setting up the stack pointer. My previous code just happened to work because previous programs had set the stack pointer to the end of SRAM, 0x20000800, and there it remained, until I deliberately unplugged it and plugged it back in again to see what the stack pointer would be. And it was some random number, not pointing anywhere near the SRAM area at all. The code actually worked up to the point of the little delay loops, mostly because we were not calling any functions. Once the compiler tried to set up the ‘automatic variable’ i within the scope of each for() loop, it was reading and writing to No Where. This is considered a Bad Thing.

So let’s fix that by setting up the stack pointer. This ‘ought’ to be done in the C-runtime (which doesn’t yet exist) along with things like initializing variables and possibly setting up the system clock.

The C language, by itself, has no way to know how to set up the stack pointer on this chip, has no way to directly access any of the registers or CSRs, or anything like that. It’s intended to be ‘platform agnostic’ as far as that is possible. The GNU C Compiler Collection, on the other hand, has some extensions that let these things happen. We’ll use one now to set up the stack pointer.

    __asm__("la sp, 0x20000800"); // initialize stack pointer to end of SRAM

The ‘la’ instruction is actually pseudo-instruction to ‘load address’. Now that’s a hard-wired ‘magic number’, if I ever saw one. And it will change the second we move to a different chip within the family that has a different amount of SRAM. Let’s fix that by referring to a variable that’s ‘calculated’ in the linker script, called, so imaginatively, ‘end_of_RAM’:

__asm__("la sp, end_of_RAM"); // initialize stack pointer to end of SRAM

And that works, too, plus it gives us a clue as to where that value came from and what it means.

Now we can call functions! Well, almost, but we’ll fix that in a second. Let’s just splice in the already-working code from way back there.

void spi_send(uint8_t data) { // send 8 bit data via SPI

    while(SPI1->TXE == DISABLE) {
        // wait for transmit register to be empty before transmitting
    }

    SPI1->DATAR = data; // send data
}

void ws2812b_rgb(uint8_t red, uint8_t green, uint8_t blue) { // send RGB data to the WS2812B LED

    uint8_t i; // iterator

    for(i = 0; i < 8; i++) { // send 8 bits, MSB first
        spi_send(green & 0x80 ? 0x7E : 0x60); // send a one or a zero, depending
        green <<= 1; // shift all the bits in the byte
    }

    for(i = 0; i < 8; i++) { // send 8 bits, MSB first
        spi_send(red & 0x80 ? 0x7E : 0x60); // send a one or a zero, depending
        red <<= 1; // shift all the bits in the byte
    }

    for(i = 0; i < 8; i++) { // send 8 bits, MSB first
        spi_send(blue & 0x80 ? 0x7E : 0x60); // send a one or a zero, depending
        blue <<= 1; // shift all the bits in the byte
    }
}

void ws2812b_reset(void) { // hold WS2812B LED data line low for ~ 50 us

    spi_send(0x00); // send a zero
    for(uint32_t i = 0; i < 500; i++); // hold at least 50 us
}

Now the problem with having more than one function in a program, i.e., main() and only main(), is that now the compiler has to guess which one comes first in the binary image. It had better be main()! Well, it wasn’t. So I put back the ‘ENTRY(start)’ in the linker script and created a new function called start(). I moved the stack pointer initialization code to start(), then added some mumbo-jumbo (they are actually called “function attributes”) to make the compiler understand what I was doing. Here’s what it ended up looking like:

void start(void) __attribute__((naked, noreturn, section(".start")));
void start(void) { // what passes for a C-runtime

    __asm__("la sp, end_of_RAM"); // initialize stack pointer to end of SRAM
    __asm__("j main"); // continue to main()
}

So now the compiler knows that the start() function is ever so special, truly it is. It is what is known as ‘naked’, in that it has no procedural prologue or epilogue automatically added to it and has no built-in ‘return’ function appended to the end. It is also marked as ‘noreturn’, which simply means that it doesn’t return in any normal way, which is most certainly does not. There’s also that bit about it belonging to section “.start”, which is a special section that I invented and described in the linker script. It comes before the “main” part of the program code, so that’s how the linker knows to put that first in the binary image.

I also added a ‘jump’ instruction to the end of start() to tell it to jump to the main() function.

So now the program boots into the start() function, sets up the stack pointer and then jumps to the main function. I added a call to ws2812b_rgb() that sets the blue LED on at its minimum level when it turns on the other LED (which happens to be blue) and then sets all the internal LEDs to black when it turns off the other LED. And it just works.

I didn’t even bother calling the ws2812b_reset() function as there was enough time between LED togglings for it to get the idea.

So ideally I will move all this extraneous “support” scaffolding into a separate file and add that to the project makefile. I have been thinking about calling it ‘system.c’ and giving it its very own header file, ‘system.h’. My previous scheme had a handful of different files and it ended up making it quite difficult to create a completely new project, so I ended up coding up a bit of automation for that as well. If a little software is good, then a lot more is better, right?

To give it a good test overnight, I took out the human-perceivable delay and replaced it with the ws2812b_reset() function, which we need now. It’s back to just under 12,000 transmissions per second. I also left the other LED “blinking”, but at ~6 KHz it’s just a dim blur. I did add a line to the spi_send() function to turn off the LED while it was waiting for the TXE bit to be set. If it hangs there, I’ll see that there’s no dim blue blur, and be able to check the waveform on the oscilloscope to be doubly sure. Let’s see how well it does after a few million (or billion) cycles.

Posted on Leave a comment

CH32V003 driving WS2812B LEDs with SPI – Part 11

8 March 2025

Testing proves testing works. And the testing of the new bare-metal WS2812B LED driver overnight proves that the new bare-metal WS2812B LED driver works, as well. Running just over 12,000 updates per second, it has been running for approximately 16 hours with no unexplainable hang-ups. That’s around 700 million error-free transmissions, which is a lot. So that’s not the problem.

I will take this opportunity to bring some closure to one of the original project design goals, which was to have a nice little demo program showing the LED changing colors, as well as being able to adjust the apparent brightness of the LED in real time.

I had already connected a small potentiometer on the little solderless breadboard, configured as a voltage divider, and attached it to one of the analog-to-digital converter (ADC) input pins. I would like to use that potentiometer as a dial to adjust the LED brightness.

There are several steps required to get the ADC initialized and ready to read analog values. First, we have to configure the ADC prescaler in the RCC’s Clock Configuration Register 0, RCC_CFGR0. Right now, it’s being set to “HBCLK divided by 2”, which is both the default value after reset and the value that I write to the register during initial setup (by omission). As the “HBCLK” is currently 48 MHz, or will be once the PLL locks, the ADC clock will be 24 MHz, which we are cautioned is the maximum rate. So I’ll leave it like that for now.

I should note here that I am assigning new enumerated values with their ‘absolute’ values within their respective register, and not the numeric value they would have if we were accessing them through a C-style bit-field within a structure. For example, the SW field starts at bit position 0 and is two bits wide. It has four possible values, three of which are assigned. Being at bit position 0 within the register, the absolute and relative values are the same. But the very next field, SWS, starts at bit position 2 and is also two bits wide. The relative values are the same, but the absolute values are shifted left two places:

# RCC_CFGR0/SW values
RCC_SW_HSI      = 0x00000000 # HSI
RCC_SW_HSE      = 0x00000001 # HSE
RCC_SW_PLL      = 0x00000002 # PLL

# RCC_CFGR0/SWS values
RCC_SWS_HSI     = 0x00000000 # HSI
RCC_SWS_HSE     = 0x00000004 # HSE
RCC_SWS_PLL     = 0x00000008 # PLL

So I can just use them as they are and not have to worry about shifting them around to be in the right place when I need them. I typically bit-wise ‘or’ all the values together to come up with a single value to write to the register in question. That way I can set several bits or bit fields at once with a single write.

The include file generator script also produces bit masks for each of the register bit fields, e.g.:

RCC_SW = 0b00000000000000000000000000000011 # SW - System clock Switch, pos=0, width=2
RCC_SWS = 0b00000000000000000000000000001100 # SWS - System Clock Switch Status, pos=2, width=2

These are useful for masking out everything but the bit field contents, when you need just that information out of a register.

Next we have to enable the ADC’s peripheral clock. As a PB2 peripheral, I can just add its enable bit to the setup that’s already being done for the other peripherals:

# enable required peripheral clocks

    li x4, RCC_USART1EN | RCC_SPI1EN | RCC_ADC1EN | RCC_IOPCEN
    sw x4, RCC_APB2PCENR(x3)

Then we should configure the input pin that is to be used as the ADC input. PC2 is analog input 2 (A2). I added a comment to the GPIOC initialization code and changed the configuration word being written to the CFGLR configuration register for GPIOC:

# GPIOC

#   PC2/A2 - analog input 2
#   PC6 - SPI data out

la x3, GPIOC_BASE
li x4, 0x89888088
sw x4, GPIO_CFGLR(x3)

That very magic-looking number is actually easy to understand once you see my ‘cheat sheet’ of all the possible configuration values for these pins:

0   analog input
1   output push-pull 10 MHz max
2   output push-pull 2 MHz max
3   output push-pull 30 MHz max
4   floating input mode (no pull up or down) - default
5   output open drain 10 MHz max
6   output open drain 2 MHz max
7   output open drain 30 MHz max
8   input with pull up or down
9   output push-pull multiplexed 10 MHz max
A   output push-pull multiplexed 2 MHz max
B   output push-pull multiplexed 30 MHz max
C   reserved
D   output open drain multiplexed 10 MHz max
E   output open drain multiplexed 2 MHz max
F   output open drain multiplexed 30 MHz max

Each hexadecimal digit represents one bit of the GPIO port. On other device families that offer 16 or more pins per port, there are additional configuration registers, but they use the same format. The ‘0’ digit that you see in the third-to-last position corresponds to the ‘analog input’ configuration from the table and sets up PC2 as an analog input.

Then we can start to configure the ADC itself. Now the ADC on this little chip is quite versatile and talented. It’s quite similar to the ADCs available on the STM32 devices, if you are familiar with those. It’s way more complex than, for example, the Atmel (now Microchip) AVR devices.

Most of the initialization process will be to tell the ADC peripheral what we don’t want. Like I said, it’s got a lot of features and I’m not even going to scratch the surface of what all it can do. I just want to take a single reading from a single channel every once in a while.

The first step is to power on the ADC module, using the ADON bit in the ADC Control Register 2 (ADC_CTLR2). Then begins a ‘module stabilization time, t(stab), of typically 7 us. We’ll give it 10 us because we’re generous.

As I mentioned previously, it’s more effort to tell the ADC not to do stuff. Next, we have to tell it that we only want one channel to be converted, and that channel is A2.

After that, there is a calibration routine that mostly runs itself. First, we reset the calibration register by writing a 1 into the RSTCAL bit of the ADC_CRLR2 register, then we wait for it to clear itself, signaling that it has completed the reset. Then we do the exact same thing but with the CAL bit, and then the ADC is done calibrating itself.

After that, the ADC is ready to do its thing. Here is the complete initialization code:

# initialize ADC

    la x3, ADC1_BASE
    li x4, ADC1_ADON
    sw x4, ADC1_CTLR2(x3) # module power on

    li a0, 10
    call delay_us # module stabilization time (>7 us)

    li x4, (1 << 20)
    sw x4, ADC1_RSQR1(x3) # total of 1 conversions requested
    li x4, 2
    sw x4, ADC1_RSQR3(x3) # 1st regular conversion channel is A2

    li x4, ADC1_RSTCAL | ADC1_ADON
    sw x4, ADC1_CTLR2(x3) # reset calibration register

1:  lw x4, ADC1_CTLR2(x3)
    andi x4, x4, ADC1_RSTCAL
    bnez x4, 1b # wait for RSTCAL to go back to zero when reset is complete

    li x4, ADC1_CAL | ADC1_ADON
    sw x4, ADC1_CTLR2(x3) # start calibration function

1:  lw x4, ADC1_CTLR2(x3)
    andi x4, x4, ADC1_CAL
    bnez x4, 1b # wait for CAL to go back to zero when calibration is complete

And here is a simple function to start a conversion, wait for it to complete, then return the converted value:

adc_convert: # perform single conversion

    # on entry: none
    # on exit: a0[9..0] conversion result

    # register usage:
    #   x3:  pointer to ADC1_BASE
    #   x4:  read status register

    addi sp, sp, -16 # allocate space on stack
    sw ra, 12(sp) # preserve return address
    sw x3, 8(sp) # preserve x3
    sw x4, 4(sp) # preserve x4

    la x3, ADC1_BASE
    li x4, ADC1_ADON
    sw x4, ADC1_CTLR2(x3) # start conversion

1:  lw x4, ADC1_STATR(x3)
    andi x4, x4, ADC1_EOC
    beqz x4, 1b # wait for conversion to complete

    lw a0, ADC1_RDATAR(x3) # read conversion result

    lw ra, 12(sp) # restore return address
    lw x3, 8(sp) # restore x3
    lw x4, 4(sp) # restore x4
    addi sp, sp, 16 # restore stack pointer

    ret # return from function

Bear in mind that this is a 10 bit wide ADC, and we only really want an 8 bit range for the LED brightness. Scaling the value to fit the range just takes a single “shift right logical” instruction. Once we have a scaled value available that represents the position of the “LED brightness” dial, we can use that as the color intensity value that we’re sending to the LED.

So I finally have a blinking, multi-color LED whose brightness I can adjust in real time. Who says dreams don’t come true?