How I made this real time clock with Raspberry Pi Pico
Following my previous LED matrix project I wanted to make a larger LED matrix. I have some 7cm x 9cm single sided perfboards in my inventory which look like this:
So my first plan was to make a LED matrix as large as the board, and put the control circuit on another perfboard of the same size. The perfboard has 48 columns and 30 rows, so theoratically I could make a 15 x 24 LED matrix.
But after soldering the LEDs, I realized that I could not connect the LED board and the controller board together! If I were using a double-sided perfboard, I could solder headers on the back of the LED board and plug them into the controller board. Unfortunately I only have single-sided board, so I could not solder the headers on the back of the LED board.
In order not to waste this project, I had to put the control circuit on the same board with the LEDs. This would make the LED matrix half size of planned. After consideration I decided to make a digital clock with this LED matrix.
To display the 4 digits and the second indicator of the clock, the LED matrix needs to be at least 21 columns wide, which can fit into the 48-column perfboard, and leaving some columns for the control chips. So I decided to go with a 21x7 LED matrix. Since there were still plenty of space at the bottom, I could install the Raspberry Pi Pico on it too.
The soldering process was pretty long and cumbersome so I’ll omit it in this article. See my previous post for the schematic and the soldering tips.
The first version looked like this:
Some facts about this board:
- Common anode each row, and common cathodes each column.
- The four 16-pin sockets are for 74HC595 chips.
- The 2x20 headers at the bottom right are for Raspberry Pi Pico.
- The board is powered by USB 5V, directly draw from the
VBUS
pin of the Raspberry Pico. - The resistors are 330Ω so the current through each LED is
(5V-1.8V)/330Ω = 9.7mA
. So each row could potentially draw9.7mA * 21 = 203.7mA
, which is much larger than the output rating of 74HC595. So BJTs are used to provide/sink currents. The row BJTs areS9012 PNP
, and column BJTs areS9013 NPN
. See the schematic in my previous article.
After powered on I noticed a dangerous signal: the 74HC595 chips became overheat in less than 10 seconds! Soon I realized the reason: the outputs of 74HC595 were connected to the base terminals of the BJTs directly. This could be the reason that the BJTs drew too much current.
So I added a 1kΩ resistor to each BJT, and the problem was solved. Now the board look like this:
I also added two push buttons to the bottom left corner for adjusting the clock.
Coding the clock
The source code is hosted on my GitHub. In this article I will only explain some key thoughts.
How to control 74HC595
My previous post used a PIO state machine to control the 74HC595 chips. When creating this LED matrix, I was inspired by this article and created a more compact PIO program:
set(x, 31) # set X to loop counter
label('loop') # loop for DATA pin
out(pins, 1) .side(0b00) # send 1 bit to DATA pin
# set SHIFT pin low
jmp(x_dec, 'loop').side(0b01) # if not all bits sent, loop
# set SHIFT pin high
pull() .side(0b10) # row completed, manual pull
# to discard unused dat
# set LATCH pin high
wrap()
This program uses two sideset bits to represent the LATCH and the SHIFT. This is really smart since creating LATCH pulse doesn’t need to use two set
instructions which take 2 cycles. It only requires the LATCH pin and the SHIFT pin to be placed next to each other (e.g. in my hardware design, the LATCH pin is GP3, then the SHIFT pin has to be GP4), which is a trivial trade-off.
Register X is used as the loop counter. In the above program, the loop counter is set to 31, which supports for four daisy-chained 74HC595s. In order to support different numbers of chips, I change the above program so that the initial value of the loop counter can be passed before the main loop starts:
@asm_pio(
out_shiftdir=PIO.SHIFT_RIGHT,
out_init=(PIO.OUT_LOW),
sideset_init=(PIO.OUT_LOW, PIO.OUT_LOW),
autopull=True,
)
def pio_74hc595():
pull() # pul the number of bits
mov(y, osr) # load the nubmer of bits to Y wrap_target()
mov(x, y) # set X to loop counter
label('loop') # loop for DATA pin
out(pins, 1) .side(0b00) # send 1 bit to DATA pin
# set SHIFT pin low
jmp(x_dec, 'loop').side(0b01) # if not all bits sent, loop
# set SHIFT pin high
pull() .side(0b10) # row completed, manual pull
# to discard unused dat
# set LATCH pin high
wrap()class PIO_74HC595:
def __init__(self, out_base, sideset_base, freq, daisy_chain=4):
self.sm = StateMachine(
0,
pio_74hc595,
out_base=out_base,
sideset_base=sideset_base,
freq=freq
)
# put the number of bits into OSR so that
# it can be loaded to Y when SM starts
self.sm.put(daisy_chain * 8 - 1)
Note the last line self.sm.put(daisy_chain * 8 - 1)
put the initial counter to the OSR register, which is moved to Y at the beginning of the state machine ( mov(y, osr)
). Then before sending each row, mov(x, y)
is called to set X to the initial value stored in Y.
Controll the LED matrix with daisy-chained 74HC595s
I would like to create a generic library for controlling LED matrix. The problem is that different LED matrices could have different layouts:
- Is the data sent to row ICs first, or column ICsfirst?
- Are the chips connected from QA to QH, or from QH to QA?
- Are rows or columns active high, or active low?
For example, in my LED matrix,
- data is sent from the Pico to the row IC, then chained to column ICs
- row IC is from QA to QH, while column ICs are from QH to QA. In other words, QA of the row IC is the top-most row, and QH of the column IC is the left-most row
- row LEDs are active low (since PNP BJTs are used), and column LEDs are active high (since NPN BJTs are used)
In order to handle different cases, I added a layout
parameter which supports combinations of some flags. This could support all possible layouts.
# handle row/column corresponding to the layout
for e in self.layout: # generate row data (row selector)
if e & 1 == LedMatrix.ROW:
if e & 4 == LedMatrix.A2H:
row_selector = 1 << (self.rows - i - 1)
else:
row_selector = 1 << i
if e & 2 == LedMatrix.ACTIVE_LOW:
row_selector = ~row_selector
data[idx:idx + self.row_bytes] =
row_selector.to_bytes(self.row_bytes, 'big')
idx += self.row_bytes
# generate column data
if e & 1 == LedMatrix.COLUMN:
row_data = self.buf[
self.col_bytes * i:self.col_bytes * (i+1)]
# reverse bits
if e & 4 == LedMatrix.H2A:
row_data = bytearray(reverse_lookup[c]
for c in reversed(row_data))
if e & 2 == LedMatrix.ACTIVE_LOW:
row_data = bytearray(~c for c in row_data)
data[idx:idx + self.col_bytes] = row_data
idx += self.col_bytes
Another thing worth to mention is how we reverse the bits. If the 74HC595s are soldered reversely, i.e. QH is the left-most column, then we must reverse the bits in order to display the content correctly. The most effecient way of reversing bits of a byte is using a lookup table. So I created a lookup table reverse_lookup
to store the reversed bits of all 256 possible values of a byte.
The LedMatrix
class also creates a frame buffer managed by the framebuf
library. This can be used to draw text efficiently.
Draw the texts
I didn’t find a good raster font to fit into my 4x7 matrix for each character, so I decided to create my own font. I used a C program to create the bit pattern, and saved the result to font4x7.bin
file, which is then read by a Python class BitmapFont4x7
(src). To draw a character, the BitmapFont4x7
class uses the glyph data to create a FrameBuffer
object, which can be blit
ed to the LED matrix.
RTC clock
I am very happy to know that rp2040 supports RTC (real time clock). Unfortunately, without an onboard battery, the initial time could not be possible accurate. That means I still need to add two buttons for adjusting the clock.
Another issue with the rp2040 RTC is that it does not support subseconds. Calling RTC.datetime()
will not return any data for milliseconds. This probem is relatively easy to solve though. I created a calibrate()
function which basically wait until the next second starts, and record the current utime.ticks_ms()
and use it as a start point. Then future call to datetime()
can be enhanced with the milliseconds retrieved from ticks_ms()
.
def calibrate(self):
second = self.rtc.datetime()[6]
while True:
new_second = self.rtc.datetime()[6]
if new_second == second:
utime.sleep_ms(1)
else:
self.initial_ticks = utime.ticks_ms()
breakdef datetime(self):
(years, months, days, weekdays, hours, minutes, seconds, _) =
self.rtc.datetime()
subseconds = (utime.ticks_ms() - self.initial_ticks) % 1000
return (years, months, days, weekdays, hours, minutes,
seconds, subseconds)
Button IRQ with debouncing
We know that rp2040 IRQ can be triggered on rising edges or falling edges. The problem is that when the button is pressed, the IRQ might be triggered multiple times. To solve this, I created a class for buttons that will remember the ticks when the button is pressed, and makes sure that the actual callback is not called in a certain amount of time.
Combine all together
The final clock.py uses all above features to create a digital clock. See how it works:
Lessons learned
- Always use resistors to limit the current when connecting to the output of 74HC595. The maximum ratings of 74HC595 does not mean it could only supply that much current; instead, it means that overdraw/oversink would cause permanent damage.
- ULN2803a is a good choice to replace 8 NPN transistors. It also has 2.7kΩ resistors on the base terminals of the transistors, so no need to use external resistors when connecting to 74HC595.
- Multithread: I tried to use
_thread
library to separate the redraw operations into a separate thread. Unfortunately, the thread stops exactly at the 1470th calls ofsm.put()
function, which I’m not sure why. There are little documents about multithreading in MicroPython, so I chose not to use multithread.
Thanks for reading!