Pico Green Clock (Part I)
This post is about this clock:
https://thepihut.com/products/electronic-clock-for-raspberry-pi-pico
Part I is a running commentary about the hardware.
Schematic
https://www.waveshare.com/w/upload/2/2d/Pico-Clock-Green-Schdoc.pdf
DS3231 Real-Time Clock
Datasheet: https://www.waveshare.com/w/upload/9/9b/DS3231.pdf
“The RTC is a low-power clock/calendar with two programmable time-of-day alarms and a programmable square-wave output.”
- Pin 9 is GP6, SPI0 SCK and I2C1 SDA. Here we use it as SDA for the DS3231.
- Pin 10 is GP7, SPI0 TX and I2C1 SCL. Here we use it as SCL for the DS3231.
Definitions:
// I2C defines
#define I2C_PORT i2c1
#define I2C_SDA 6
#define I2C_SCL 7
And initialisation code:
// I2C Initialisation. Using it at 100Khz.
i2c_init(I2C_PORT, 100*1000);
gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
gpio_pull_up(I2C_SDA);
gpio_pull_up(I2C_SCL);
INT#/SQW is “Active-Low Interrupt or Square-Wave Output”. It’s an output pin, whose function can be set using the INTCN bit in the Control Register. Hopefully I’ll get to this later. For now it doesn’t really matter (I’m not going to read this value).
The I2C address for the DS3231 is 0x68. It’s mentioned here.
Let’s try to read 7 bytes from the device. They should contain the current date and time.
According to the datasheet:
Data transfer from a slave transmitter to a master receiver. The first byte (the slave address) is transmitted by the master. The slave then returns an acknowledge bit. Next follows a number of data bytes transmitted by the slave to the master. The master returns an acknowledge bit after all received bytes other than the last byte. At the end of the last received byte, a not acknowledge is returned.
So our code sends 0x00 (the address), and receives 7 bytes:
#define I2C_ADDRESS 0x68
uint8_t val = 0x00;
i2c_write_blocking(I2C_PORT, I2C_ADDRESS, &val, 1, true);
uint8_t raw_date_time[7];
i2c_read_blocking(I2C_PORT, I2C_ADDRESS, raw_date_time, sizeof raw_date_time, false);
printf("%02x %02x %02x %02x %02x %02x %02x\n", raw_date_time[0], raw_date_time[1],
raw_date_time[2], raw_date_time[3], raw_date_time[4], raw_date_time[5],
raw_date_time[6]);
The main point is that we use true
for the nostop
parameter in the first call, and then we read the data. If we want to write data, we send the address + data, but then nostop
is false
.
The result after the first run:
05 33 04 04 04 08 16
The result after the second run:
33 33 04 04 04 08 16
- Byte #0: seconds in BCD (5 in the first run, 33 in the second)
- Byte #1: minutes in the same way (33)
- Byte #2: hour + 12/24# bit + AM#/PM bit
- Byte #3: day of the week (start is user defined, 4 is Thursday in this case)
- Byte #4: day of the month (4)
- Byte #5: month + century (8)
- Byte #6: year (2016)
So the second time/date is 2016-08-04 04:33:33
Notes:
- The century toggles when the year goes from 99 to 00, so it’s essentially a carry flag
With this information, we can write some code to retrieve and set the current time/date.
Display: SM5166P and SM16106SC
It looks like the display is a combination of eight COM lines (COM0 to COM7) and 24 D lines (D0 to D23). I can only see a grid of 9x22 “pixels”, but there are plenty more LEDs for days and other functions.
Let’s start with SM5166P which looks a bit simpler.
SM5166P
Datasheet: https://www.waveshare.com/w/upload/8/8b/Sm5166p.pdf
Sadly, it’s in Chinese. But that’s not a good reason not to use it. The main information appears to be in a table that describes the relationship between the inputs (A0, A1 and A2) and the outputs (D0 to D7 in the table, OUT1 to OUT8 in a previous diagram).
Of course, there is no one-to-one mapping from 3 bits to 8 bits. It looks like this is a controller that converts 3 bits to one of 8 outputs (e.g. 001 is output 1, 110 is output 6 etc.)
- GP16 is A0
- GP18 is A1
- GP22 is A2
Those inputs are controlled by GPIO pins. But they don’t work on their own, we also need the other controller(s).
SM16106SC
Datasheet: https://www.waveshare.com/w/upload/3/30/SM16106SC.pdf
Surprisingly, this one is also in Chinese. But it has nice diagrams.
My understanding so far:
There are two instances of the SM16106SC where one has an input called SDI (“I” for input, I guess), and has an output called SDO. This SDO is the input to the second instance (so it’s the SDI for the second chip).
The relationship between SDI and SDO is simple: SDO at time t is SDI at time t-15. The serial input goes “through” the outputs, and ends up in SDO. In other words, we send one bit at a time and it “flows” through the outputs and then goes out of the system, possibly as input to the next system.
If that’s all true, then it’s time to experiment a bit (to confirm that that’s how it works). But there’s a little bit more - the clock.
The host also sends a clock signal (high/low), and on the rising signal the output is updated (I think!). So the code is responsible for the data signal as well as the clock signal.
Two more signals are:
- OE# which is “output enable”. According to some translation of the datasheet: “The output enables the control port. When OE is low, the OUT0
OUT15 output is started; when OE is high, The OUT0OUT15 output is turned off”. - LE, for which the translation says: “Data latch control port. Serial data is passed in to the output latch when LE is high, and low when LE is low will be latched”
Not very clear, but the diagram shows the combination of CLK and SDI, then LE goes high, then low, and then OE# goes low (at which point all the outputs are updated), then high. So it looks like:
- Shift the data using CLK and SDI (this includes shifting it out of the first chip and into the second chip)
- Once the data has been shifted, raise and lower LE to get the data “stored” for output
- Then lower the OE# to enable the output, and raise before the next update
I may be wrong, of course.
The datasheet says: “up to 25MHz”. Not sure if that’s a full cycle. Pausing 1us between CLK updates should be plenty.
Summary
This works as expected (= guessed). But because LE and OE# have to be updated per row, the screen has to be refreshed constantly in order to keep a “stable” picture.
Refreshing each row with a 2ms timeout seems to work. The light isn’t as strong, but the display is pretty stable (not flickering).
Buzzer
The buzzer is a single pin: GPIO 14.
Fairly straight-forward to buzz - set the pin direction as output, then set it to 1 to beep and 0 to stop.
#define GPIO_BUZZ 14
gpio_init(GPIO_BUZZ);
gpio_set_dir(GPIO_BUZZ, GPIO_OUT);
gpio_put(GPIO_BUZZ, 0);
To buzz (synchronously):
gpio_put(GPIO_BUZZ, 1);
sleep_ms(milliseconds);
gpio_put(GPIO_BUZZ, 0);
50ms is a short beep. For a real alarm something like 500ms or more may be required (I will have to test it when no one is around - it’s pretty loud and annoying).
The main challenge here is to do it in an event-driven way, we can’t just sleep_ms
for a second while nothing else gets updated (including button presses). So we need a slightly more sophisticated state machine for the buzzer (and for everything else!)
Also, I’m not trying to use PWM to control the volume. This can come later.
Input
Buttons
Button input should be straight-forward in terms of pins (GPIO) but a bit more involved when trying to understand what just happened, adding support for long/short presses and for auto-repeat (very important when trying to set minutes).
All of the “logic” is at a higher level than the hardware, so let’s start with the hardware. We have 3 buttons, K0, K1 and K2:
- K0 is GP15
- K1 is GP17
- K2 is GP2
Light Input
There seems to be something called AIN which is a voltage divider using a light dependent resistor. I’m not quite sure what to do with it, as there doesn’t seem to be a way to control brightness (which would probably make senes: less ambient light = less light).
The sample code seems to only show the screen every X frames when there is less light. I’ll have to think about that one.