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.

Leave a Reply