Raspberry

Display Array Clock Firmware

For this clock project, I’m using a Raspberry Pi Pico, but the code should be easily ported to any other microcontroller, as the functionality of the clock itself is basic. The Raspberry Pi Pico has a built-in RTC so results easier to implement the clock. Other Microcontrollers with Wifi Capabilities could also obtain the clock data from a web service instead of an intrnal RTC making the board capabilities more atractive as weather or many other kind of IOT notificaions.

The firmware uses already converted images to have the nicest look, this could also be done with a Numerical Font and interchangeable backgrounds but I think it would not look this great.

For the image conversion, I use the software LCD Image Converter which is a very useful piece of software, fo the displays in the Display Array Board I use RGB565 and copy the data to *.h file in my project.

Image conversion

By clicking the button Show Preview we have access to the data Image to add to our project and send it to the display.

Image Data

After converting all the images for the clock we just need to call the corresponding image and send it to the corresponding display, for this, I have just used some simple if statements.

if(t.hour >= 1 & t.hour <= 9){
          lcdDrawNumber(pio,sm,display1,0);
          lcdDrawNumber(pio,sm,display2,t.hour);
       }else if(t.hour >= 10 & t.hour <= 12){
          lcdDrawNumber(pio,sm,display1,1);
          lcdDrawNumber(pio,sm,display2,t.hour-10);
       }else if(t.hour >= 13 & t.hour <= 21){
          lcdDrawNumber(pio,sm,display1,0);
          lcdDrawNumber(pio,sm,display2,t.hour-12);
       }else if(t.hour >= 22 ){
          lcdDrawNumber(pio,sm,display1,1);
          lcdDrawNumber(pio,sm,display2,t.hour-22);
       }else if (t.hour == 0){
          lcdDrawNumber(pio,sm,display1,1);
          lcdDrawNumber(pio,sm,display2,2);
       }

After doing this for minutes, AM, and PM screens we can add some animations by sending the Space image and the colon symbol image at different rates, and also this can be applied to the configuration state of the clock.

if(ConfigureTime_MODE == OFF){
         lcdDrawNumber(pio,sm,display3,COLON);
         sleep_ms(500);
         lcdDrawNumber(pio,sm,display3,SPACE);
         sleep_ms(500);
      }else{
         if(Selection  == HRS){
            sleep_ms(200);
            lcdDrawNumber(pio,sm,display1,SPACE);
            lcdDrawNumber(pio,sm,display2,SPACE);
            lcdDrawNumber(pio,sm,display3,COLON);
            sleep_ms(200);
         }else if(Selection == MINS){
            sleep_ms(200);
            lcdDrawNumber(pio,sm,display3,COLON);
            lcdDrawNumber(pio,sm,display4,SPACE);
            lcdDrawNumber(pio,sm,display5,SPACE);
            sleep_ms(200);
         }
      }

Resulting in an animation like the following:

Matrix Clock Animation

Raspberry Pi Pico connection with Display Array Board

Raspberry Pi Pico and Display Array Board

Posible error in the Pico SDK

There is a chance that your pico-SDK has an error and the program would not start. In your SDK directory: pico-sdk/src/common/pico_time/time.c file comment line 17, //CU_SELECT_DEBUG_PINS(core) and now the program should run with any problem.

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include "pico.h"
#include "pico/time.h"
#include "pico/util/pheap.h"
#include "hardware/sync.h"
#include "hardware/gpio.h"

CU_REGISTER_DEBUG_PINS(core)
//CU_SELECT_DEBUG_PINS(core) //Comment this line for RTC troubles

const absolute_time_t ABSOLUTE_TIME_INITIALIZED_VAR(nil_time, 0);
// use LONG_MAX not ULONG_MAX so we don't have sign overflow in time diffs
const absolute_time_t ABSOLUTE_TIME_INITIALIZED_VAR(at_the_end_of_time, ULONG_MAX);

typedef struct alarm_pool_entry {
    absolute_time_t target;
    alarm_callback_t callback;
    void *user_data;
} alarm_pool_entry_t;

Below you can download the source code of this clock and also the Precompiled UF2 File. The media files can be downloaded on the previous post.


image/svg+xmlOpen Source Licenses HardwareSoftwareDocumentationCERN-OHL-P-2.0GPL-3.0-or-laterCC-BY-4.0


Download

SPI Display Array Board – Clock

The main idea of this project was to make a customizable clock, a clock that would have the option to change its appearance very easily, The first option was to make it with RGB LEDs and change the color at will, maybe to have the ability to change the color according to the time of the day or with the ambient temperature, but what is more customizable than a display by itself, nowadays we can find very nice IPS Displays that are very cheap.

Maybe the hardest step of this project was the clock creation, making the graphics for each clock was really challenging as I wanted to look very similar to the real ones, like the Flip Clock, the seven-segment Display clock, the LED matrix clock.

Flip Clock
LED Matrix Clock
7-Segments Display Clock
Blue VFD Clock
Ink Clock
Wood Clock

These are only a few clocks that I have created, but it will be awesome to see other designs and that is why I’m sharing the design files of this project, I think it would be awesome to see how a project can evolve in the maker community.

I have also designed a 3D printed enclosure or in this case a frame. It consists of only 3 parts that glued together to make a single clock piece.

3D Printed clock bracket

The PCB is intended to be compatible with any Microcontroller or SBC like the Raspberry as it only needs some GPIOs and an SPI Port. This makes the PCB compatible with a simple microcontroller as an Arduino to other more advanced microcontrollers. We could say this board is a breakout board for the arrangement of six displays and 4 buttons.

SPI Display Array Board CAD

Testing the clock designs

Before writing the clock firmware I decided to test all the designs that I have already created on the computer, so using the Raspberry Pi Pico I have written a simple program that allows me to set an image for each display in order to simulate how the clock would look like.

The code for this simple task consists of the initialization of the displays and just sensing the raw data of each image to the corresponding display.

#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include "ST7735.h"

#include "clockDigital.h"
#include "clockFlip.h"
#include "clockMatrix.h"
#include "clockVFD.h"
#include "clockInk.h"
#include "clockWood.h"

#define ONBOARD_LED 25
 
int main(){
    stdio_init_all();

    PIO pio = pio0;
    uint sm = 0;
    uint offset = pio_add_program(pio, &SPILCD_program);
    lcdPIOInit(pio, sm, offset, PIN_SDI, PIN_SCK, SERIAL_CLK_DIV);


    gpio_init(ONBOARD_LED);
    gpio_set_dir(ONBOARD_LED, GPIO_OUT);

    gpio_init(PIN_CS1);
    gpio_init(PIN_CS2);
    gpio_init(PIN_CS3);
    gpio_init(PIN_CS4);
    gpio_init(PIN_CS5);
    gpio_init(PIN_CS6);
    gpio_init(PIN_DC);
    gpio_init(PIN_RST);
    gpio_init(PIN_BLK);
    
    gpio_set_dir(PIN_CS1, GPIO_OUT);
    gpio_set_dir(PIN_CS2, GPIO_OUT);
    gpio_set_dir(PIN_CS3, GPIO_OUT);
    gpio_set_dir(PIN_CS4, GPIO_OUT);
    gpio_set_dir(PIN_CS5, GPIO_OUT);
    gpio_set_dir(PIN_CS6, GPIO_OUT);
    gpio_set_dir(PIN_DC, GPIO_OUT);
    gpio_set_dir(PIN_RST, GPIO_OUT);
    gpio_set_dir(PIN_BLK, GPIO_OUT);

    gpio_put(ONBOARD_LED, 1);
    gpio_put(PIN_CS1, 0);
    gpio_put(PIN_CS2, 0);
    gpio_put(PIN_CS3, 0);
    gpio_put(PIN_CS4, 0);
    gpio_put(PIN_CS5, 0);
    gpio_put(PIN_CS6, 0);
    gpio_put(PIN_RST, 1);
    lcdInit(pio, sm, st7735_initSeq);
    gpio_put(PIN_BLK, 1);

    lcdStartPx(pio,sm);

    for (int i = 0; i < 160*80*2; i++){
       lcdPut(pio, sm, one_Flip[i]);
    }

    gpio_put(PIN_CS1, 1);
    for (int i = 0; i < 160*80*2; i++){
       lcdPut(pio, sm, two_Matrix[i]);
    }

    gpio_put(PIN_CS2, 1);
    for (int i = 0; i < 160*80*2; i++){
       lcdPut(pio, sm, three_Digital[i]);
    }

    gpio_put(PIN_CS3, 1);
    for (int i = 0; i < 160*80*2; i++){
       lcdPut(pio, sm, four_VFD[i]);
    }

    gpio_put(PIN_CS4, 1);
    for (int i = 0; i < 160*80*2; i++){
       lcdPut(pio, sm, five_Ink[i]);
    }

    gpio_put(PIN_CS5, 1);
    for (int i = 0; i < 160*80*2; i++){
       lcdPut(pio, sm, six_Wood[i]);
    }
}

For the SPI Display Array clock Firmware go to the Part2 of this project.


image/svg+xmlOpen Source Licenses HardwareSoftwareDocumentationCERN-OHL-P-2.0GPL-3.0-or-laterCC-BY-4.0


Visit my Tindie Store for the assembled board.

Download


First Impressions – Raspberry Pi PICO

The Raspberry Pi Pico is a very inexpensive microcontroller compared to other brands with ARM Cortex M0+ ( This one is a dual-core ), also has one special feature only found in more expensive microcontrollers like Cortex M4 it is the Raspberry PIO which is a way to create special hardware peripherals. In my Feather MK26F board, it is called FLEXBUS, and the equivalent can be found in other microcontrollers. So the remarkable feature for me of this microcontroller are:

  • XIP( Execute in place ) The ability to save and execute programs running on an external QUAD-SPI flash.
  • 246Kb of RAM, that is a lot of RAM, mostly seen on mid-range microcontrollers.
  • 133 Mhz Cortex M0+ dual-core
  • 8 PIO, Programmable IO State machines.
  • USB-Bootloader with SWD debug capabilities.
  • Low price ( only $4 USD for the entire board ).

The Raspberry Pi PICO also has some bad things to take into account, and this is the lack of the reset button and a good IDE for the developing process. These features could come in the future as they are minor changes. 

There are not many peripherals in this microcontroller but the PIO makes this even. it has 2 hardware SPI, 2 hardware UART, 2 hardware I2C, 3 12-bit ADC pins, 16 PWM channels, and Accelerated floating-point libraries on-chip ( We will see this in the example program below ).

RaspPico_pinout.png

I am not a fan of MicroPython or CircuitPython but it is nice to have in hand for a fast proof of concept. I like programming in C/C++ so I installed Visual Studio with the provided SDK.

PythonComp.png

The example that really grabbed my attention is the PIO ST7789, this example uses a custom peripheral with the PIO, making an impressive SPI  with a baud rate of 60 Mbps and the use of the hardware interpolation libraries.

/**
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <stdio.h>
#include <math.h>

#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/gpio.h"
#include "hardware/interp.h"
#include "st7789_lcd.pio.h"
#include "ST7735.h"
#include "logoSavage.h"


#define SCREEN_WIDTH    480 
#define SCREEN_HEIGHT   160 
#define IMAGE_SIZE      256
#define LOG_IMAGE_SIZE  8

#define PIN_BL      21
#define PIN_RESET   20
#define PIN_DC      19
#define PIN_DIN     18
#define PIN_CLK     17
#define PIN_CS1     16
#define PIN_CS2     11
#define PIN_CS3     10
#define PIN_CS4     9
#define PIN_CS5     8
#define PIN_CS6     7

#define SERIAL_CLK_DIV 1.f

static inline void lcdSetDCCS(bool dc, bool cs1, bool cs2, bool cs3, bool cs4, bool cs5, bool cs6) {
    sleep_us(1);
    gpio_put_masked((1u << PIN_DC) | (1u << PIN_CS1) | (1u << PIN_CS2) | (1u << PIN_CS3) | (1u << PIN_CS4) | (1u << PIN_CS5) | (1u << PIN_CS6),
     !!dc << PIN_DC | !!cs1 << PIN_CS1 | !!cs2 << PIN_CS2 | !!cs3 << PIN_CS3 | !!cs4 << PIN_CS4 | !!cs5 << PIN_CS5 | !!cs6 << PIN_CS6);
    sleep_us(1);
}

static inline void lcdWriteCMD(PIO pio, uint sm, const uint8_t *cmd, size_t count) {
    st7789_lcd_wait_idle(pio, sm);
    lcdSetDCCS(0, 0,0,0,0,0,0);
    st7789_lcd_put(pio, sm, *cmd++);
    if (count >= 2) {
        st7789_lcd_wait_idle(pio, sm);
        lcdSetDCCS(1, 0,0,0,0,0,0);
        for (size_t i = 0; i < count - 1; ++i)
            st7789_lcd_put(pio, sm, *cmd++);
    }
    st7789_lcd_wait_idle(pio, sm);
    lcdSetDCCS(1, 1,1,1,1,1,1);
}

static inline void lcdInit(PIO pio, uint sm, const uint8_t *init_seq) {
    const uint8_t *cmd = init_seq;
    while (*cmd) {
        lcdWriteCMD(pio, sm, cmd + 2, *cmd);
        sleep_ms(*(cmd + 1) * 5);
        cmd += *cmd + 2;
    }
}

static inline void lcdStartPx(PIO pio, uint sm) {
    uint8_t cmd = ST7735_RAMWR;
    lcdWriteCMD(pio, sm, &cmd, 1);
    lcdSetDCCS(1, 0,0,0,0,0,0);
}


int main() {
    stdio_init_all();

    PIO pio = pio0;
    uint sm = 0;
    uint offset = pio_add_program(pio, &st7789_lcd_program);
    st7789_lcd_program_init(pio, sm, offset, PIN_DIN, PIN_CLK, SERIAL_CLK_DIV);

    gpio_init(PIN_CS1);
    gpio_init(PIN_CS2);
    gpio_init(PIN_CS3);
    gpio_init(PIN_CS4);
    gpio_init(PIN_CS5);
    gpio_init(PIN_CS6);

    gpio_init(PIN_DC);
    gpio_init(PIN_RESET);
    gpio_init(PIN_BL);
    
    gpio_set_dir(PIN_CS1, GPIO_OUT);
    gpio_set_dir(PIN_CS2, GPIO_OUT);
    gpio_set_dir(PIN_CS3, GPIO_OUT);
    gpio_set_dir(PIN_CS4, GPIO_OUT);
    gpio_set_dir(PIN_CS5, GPIO_OUT);
    gpio_set_dir(PIN_CS6, GPIO_OUT);

    gpio_set_dir(PIN_DC, GPIO_OUT);
    gpio_set_dir(PIN_RESET, GPIO_OUT);
    gpio_set_dir(PIN_BL, GPIO_OUT);

    gpio_put(PIN_CS1, 1);
    gpio_put(PIN_CS2, 1);
    gpio_put(PIN_CS3, 1);
    gpio_put(PIN_CS4, 1);
    gpio_put(PIN_CS5, 1);
    gpio_put(PIN_CS6, 1);
    gpio_put(PIN_RESET, 1);
    lcdInit(pio, sm, st7735_initSeq);
    gpio_put(PIN_BL, 1);

 
    // Other SDKs: static image on screen, lame, boring
    // Pico SDK: spinning image on screen, bold, exciting

    // Lane 0 will be u coords (bits 8:1 of addr offset), lane 1 will be v
    // coords (bits 16:9 of addr offset), and we'll represent coords with
    // 16.16 fixed point. ACCUM0,1 will contain current coord, BASE0/1 will
    // contain increment vector, and BASE2 will contain image base pointer
#define UNIT_LSB 16
    interp_config lane0_cfg = interp_default_config();
    interp_config_set_shift(&lane0_cfg, UNIT_LSB - 1); // -1 because 2 bytes per pixel
    interp_config_set_mask(&lane0_cfg, 1, 1 + (LOG_IMAGE_SIZE - 1));
    interp_config_set_add_raw(&lane0_cfg, true); // Add full accumulator to base with each POP
    interp_config lane1_cfg = interp_default_config();
    interp_config_set_shift(&lane1_cfg, UNIT_LSB - (1 + LOG_IMAGE_SIZE));
    interp_config_set_mask(&lane1_cfg, 1 + LOG_IMAGE_SIZE, 1 + (2 * LOG_IMAGE_SIZE - 1));
    interp_config_set_add_raw(&lane1_cfg, true);

    interp_set_config(interp0, 0, &lane0_cfg);
    interp_set_config(interp0, 1, &lane1_cfg);
    interp0->base[2] = (uint32_t) savageElectronics_256x256;

    float theta = 0.f;
    float theta_max = 2.f * (float) M_PI;
    
    while (1) {
        theta += 0.02f;
        
        if (theta > theta_max)
            theta -= theta_max;
        int32_t rotate[4] = {
                cosf(theta) * (1 << UNIT_LSB), -sinf(theta) * (1 << UNIT_LSB),
                sinf(theta) * (1 << UNIT_LSB), cosf(theta) * (1 << UNIT_LSB)
        };
        
        interp0->base[0] = rotate[0];
        interp0->base[1] = rotate[2];
        
        lcdStartPx(pio, sm);
        
        for (int y = 0; y < SCREEN_HEIGHT; ++y) {
            interp0->accum[0] = rotate[1] * y;
            interp0->accum[1] = rotate[3] * y;
            for (int x = 0; x < SCREEN_WIDTH; ++x) {
                uint16_t colour = *(uint16_t *) (interp0->pop[2]);
     
                 switch(x){
                    case 0:
                        lcdSetDCCS(1, 0,1,1,1,1,1);
                        break;
                    case 80:
                        lcdSetDCCS(1, 1,0,1,1,1,1);
                        break;
                    case 160:
                        lcdSetDCCS(1, 1,1,0,1,1,1);
                        break;
                    case 240:
                        lcdSetDCCS(1, 1,1,1,0,1,1);
                        break;
                    case 320:
                        lcdSetDCCS(1, 1,1,1,1,0,1);
                        break;
                    case 400:
                        lcdSetDCCS(1, 1,1,1,1,1,0);
                        break;
                }

                st7789_lcd_put(pio, sm, colour & 0xff);
                st7789_lcd_put(pio, sm, colour >> 8);     

            }
        }
    }
}

This code has been slightly modified in other to work with the display array and I will live the source code below with the UF2 file. 

Download


Raspberry Pi Zero Dynamixel Hat

The Dynamixel Hat is a board capable of communicating the raspberry Pi ( Serial ) with the Dynamixel servos by using the 74LS241 Tri-state Buffer connected to 3-pin Molex connectors.

The board contains a TI TPS62143 regulator that steps down the voltage to power up the Raspberry Pi Zero that can be power on or off with the onboard slide switch. The TPS62143 is a synchronous step-down DC-DC converter optimized for applications with high power density.

This board accepts 2S and 3S Lipo batteries, the DC-DC converter accepts up to 17 Volts but the motors recommended voltage is 11.1V ( 3S Lipo ) as the MAX Input voltage is around 16 Volts.

The boards incorporate two I2C ports for sensors for your robotic project. These are JST 1 mm pitch and 4-pin connectors compatible with many boards on the QWIIC family.

There are also 3 LEDs and 3 Buttons for notification and interaction with the electronics, the buttons are programable through the Raspberry GPIOs and 1 LED also GPIO Configurable, the other 2 LEDs are the Activity and PowerOn notification.

Pinout

Test code:

import RPi.GPIO as GPIO
import serial
import time

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(18,GPIO.OUT)     # Control Data Direction Pin
GPIO.setup(6,GPIO.OUT)      # Blue LED Pin

GPIO.setup(26,GPIO.IN)      # S2 Push Button Pin
GPIO.setup(19,GPIO.IN)      # S3 Push Button Pin
GPIO.setup(13,GPIO.IN)      # S4 Push Button Pin


Dynamixel=serial.Serial("/dev/ttyS0",baudrate=1000000,timeout=0.1, bytesize=8)   # UART in ttyS0 @ 1Mbps


while True:

    if GPIO.input(26):
        GPIO.output(6,GPIO.LOW)
    else:
        GPIO.output(6,GPIO.HIGH)
        GPIO.output(18,GPIO.HIGH)
        Dynamixel.write(bytearray.fromhex("FF FF 01 05 03 1E CD 00 0B"))  # Move Servo with ID = 1 to position 205
        GPIO.output(18,GPIO.LOW)
        startDynamixel = Dynamixel.read()
        startDynamixel = Dynamixel.read()
        idDynamixel = Dynamixel.read()
        lenghtDynamixel = Dynamixel.read()
        errorDynamixel = Dynamixel.read()
        chkDynamixel = Dynamixel.read()
        print("Servo ID = " , int.from_bytes(idDynamixel,byteorder='big') , " Errors = ", int.from_bytes(errorDynamixel,byteorder='big'))
        time.sleep(1)

    if GPIO.input(19):
        GPIO.output(6,GPIO.LOW)
    else:
        GPIO.output(6,GPIO.HIGH)
        GPIO.output(18,GPIO.HIGH)
        Dynamixel.write(bytearray.fromhex("FF FF 01 04 02 2A 01 CD"))   # Read Voltage of Servo with ID = 1 
        GPIO.output(18,GPIO.LOW)
        startDynamixel = Dynamixel.read()
        startDynamixel = Dynamixel.read()
        idDynamixel = Dynamixel.read()
        lenghtDynamixel = Dynamixel.read()
        errorDynamixel = Dynamixel.read()
        voltDynamixel = Dynamixel.read()
        chkDynamixel = Dynamixel.read()
        print("Servo Voltage = " , int.from_bytes(voltDynamixel,byteorder='big'))
        GPIO.output(18,GPIO.HIGH)
        Dynamixel.write(bytearray.fromhex("FF FF 01 04 02 2B 01 CC"))   # Read Temperature of Servo with ID = 1
        GPIO.output(18,GPIO.LOW)
        startDynamixel = Dynamixel.read()
        startDynamixel = Dynamixel.read()
        idDynamixel = Dynamixel.read()
        lenghtDynamixel = Dynamixel.read()
        errorDynamixel = Dynamixel.read()
        tempDynamixel = Dynamixel.read()
        chkDynamixel = Dynamixel.read()
        print("Servo Temperature = " , int.from_bytes(tempDynamixel,byteorder='big'))
        GPIO.output(18,GPIO.HIGH)
        Dynamixel.write(bytearray.fromhex("FF FF 01 04 02 24 02 D2"))   # Read Position of Servo with ID = 1
        GPIO.output(18,GPIO.LOW)
        startDynamixel = Dynamixel.read()
        startDynamixel = Dynamixel.read()
        idDynamixel = Dynamixel.read()
        lenghtDynamixel = Dynamixel.read()
        errorDynamixel = Dynamixel.read()
        posDynamixel = Dynamixel.read(2)
        chkDynamixel = Dynamixel.read()
        print("Servo Position = " , int.from_bytes(posDynamixel,byteorder='little'))
        time.sleep(1)

    if GPIO.input(13):
        GPIO.output(6,GPIO.LOW)
    else:
        GPIO.output(6,GPIO.HIGH)
        GPIO.output(18,GPIO.HIGH)
        Dynamixel.write(bytearray.fromhex("FF FF 01 05 03 1E 32 03 A3"))    # Move Servo with ID = 1 to position 816
        GPIO.output(18,GPIO.LOW)
        startDynamixel = Dynamixel.read()
        startDynamixel = Dynamixel.read()
        idDynamixel = Dynamixel.read()
        lenghtDynamixel = Dynamixel.read()
        errorDynamixel = Dynamixel.read()
        chkDynamixel = Dynamixel.read()
        print("Servo ID = " , int.from_bytes(idDynamixel,byteorder='big') , " Errors = ", int.from_bytes(errorDynamixel,byteorder='big'))
        time.sleep(1)

Enable UART at 1Mbps in your Raspberry Pi Zero

Make sure to add the following to your Raspberry config file at /boot/config.txt:

enable_uart=1
init_uart_clock=16000000

Assembly Video

This board will be available in my Tindie store very soon: Savage Electronics Tindie

Download