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.

Leave a Reply