JosueGutierrez

Electronics Engineer

Dynamixel Interface – Castellated PCB

The Dynamixel Interface is a board that allows easy communication with a MCU or Processor with a simple UART module, the board integrates a voltage level converter and a Tri-state buffer to accomplish good communication between a Dynamixel servomotor and almost any microcontroller in the market. The board is breadboard-friendly and development ready as the connection pins are 0.1″ standard pitch and the castellated holes for SMD mounting and manufactured by PCBWay.

The Dynamixel interface is now easier to connect with a bunch of microcontrollers thanks to its voltage converter IC which makes possible the communication with low-power MCUs or even FPGAs, the interface offers communication indications LED for RX and TX, as well for the flow communication pin.

The connection to the MCU is as simple as providing power to the interface and connecting the 3 pins for the UART ( CTRL, TX, and RX ). I would use the following configuration for all the newest X-Series motors as well as for the old AX and MX Series motors with any 5V to 3.3V MCU like Arduino, PIC, STM, and ESP32.

I have tested the interface with an M5Stack Stamp S3 MCU along with an ST7735 LCD showing the motor position and moving the motor with the 3 push-buttons.

The castellated holes are one of the features that I most like about the PCBWay manufacturing skills, they always come just great even when choosing thinner PCB panels, and this design was not the exception, so I ordered some panels.

If you have intentions to fabricate more than one PCB of your design I really recommend panelizing your PCB in order to obtain the best deal and to have some spares in case the magic smoke decides to take some of those boards, I usually do my panels to fit almost the same PCB space as all my panels but you can also do your panel bigger or just let PCBWay to designed for you.

In this case, I decided to design my own Panel but I am sure that you can specify how would you like your panel to be made as some special silkscreen like identifications numbers, holes, fiducials, PCB batch number, PCB side, and feed direction for your PnP and so much more.

ESP32-S3 Stick Development Board

Sometimes there are projects that need more than one piece of the same PCB or it’s a design that is probably going to be used with other projects, that is the case for my ESP32-S3 Stick controller, as the ESP32-S3 is a very powerful and versatile microcontroller with Wifi and BLE and a lot of memory I am going to be using this PCB with many projects.

If you have been following my projects you will notice that it is pretty much the same development board as the ESP32-S2 Stick but now integrating the ESP32 S2 MCU and changing all the resistors, capacitors, and LEDs from 0603 to 0402 that makes a huge change for the PCB layout process.

The ESP32-S3 is a dual-core XTensa LX7 MCU, capable of running at 240 MHz. Apart from its 512 KB of internal SRAM, it also comes with integrated 2.4 GHz, 802.11 b/g/n Wi-Fi, and Bluetooth 5 (LE) connectivity that provides long-range support. It has 45 programmable GPIOs and supports a rich set of peripherals.

The ESP32-S3 Sticks incorporate some special features that I consider every ESP32 development board must have, such as a USB-Serial converter with RX and TX LEDs, a simple GPIO-LED, and an addressable RGB LED, at a second hand is always useful to have a reset and a general purpose push button.

This board like many ESP32 boards is breadboard friendly for prototyping but it also has castellated pins that allow it to be soldered directly to other PCB like any other SMD component, so that makes it a very versatile board for rapid prototyping or development.

In order to test this new development board I have also designed a 7 Color Paper display driver that interconnects the Stick with a bed of Pogo-Pins that I bought from Aliexpress that are very easy to solder and fit the through-holes quite nicely.

I have really enjoyed working with the ESP32-S3 Stick and the Paper display, but I still have to think about a good final project for it, if you have any ideas you are welcome to leave them in the comments below.

All the files are available on my GitHub if you are interested in making one on your own or also if you want to make some improvements or specific application modifications.

ESP32-S2 Stick

After designing the nRF PRO I decided to continue using the form factor and add a more capable MCU as the ESP32-S2 that has Wifi capability with a lot of Memory for the app and data.

It is a breadboard-compatible development board featuring the ESP32-S2 series of SoC, Xtensa® single-core 32-bit LX7 microprocessor 4 MB flash and optional 2 MB PSRAM in chip package 26 GPIOs, rich set of peripherals such as UART, SPI, I2C, TOUCH, DAC, ADC, and USB, a 2x2mm SK6812 RGB serial LED and a 0603 SMD blue LED. USB-C connector, castellated holes for low-profile integrations, and an onboard PCB antenna.

ESP32-S2 Stick

I have been using this board for all my projects as it’s easier to add to all my current and old projects such as the display-Array, the Cistercian Display, and the Dynamixel Configurator.

3-Digit Cistercian Clock
Single Digit Cistercian Clock
Dynamixel Configurator

This board is very versatile as I have been able to use it easily to test any idea that I have in a protoboard before adding it to a custom PCB, also is easy to add to any new project and not having to place all of those components and having to RF match the antenna every single time. The dimensions are efficient and I love how it looks as it’s a very thin PCB.

ESP32-S2 Stick Dimensions

The board has enough pins with the correct peripheral in hand to accommodate to any project you may have.

ESP32-S2 Stick Pinout

Lately, I have been using the services of PCBWay for my projects as they have awesome customer support and great quality PCBs, I really love how they do the castellated holes for my boards in a couple of days.

ESP32-S2 Stick Panel
ESP32-S2 Stick Development board

Let me know in the comments below how would you use this board or any project idea that you would like me to explore using this board. If you need more detailed info go to my GitHub.

31 Segments Cistercian Display

I have always loved displays, I really like anything that emits light and that is controllable, that is why I have created this new display that consists of the representation of numbers “Cistercian”, this single digit is capable of representing a number from 0 to 9999.

Cistercian Numbers

The easiest way to make a display is with individual LEDs, so the design is based on 7 segments displays that we all know and several reference pictures and representations that I have seen, that is how I import those shapes to a PCB and added some 0805 LEDs. I have made these PCBs with PCBWay as they have always delivered me some awesome look castellated holes and the price is pretty low, even for this panel that has 3 ENIG-finish PCBs with castellated holes.

Cistercian Displays 0805
Lighting a segment

After playing with this new hardware I realized that is inconvenient to use that many GPIOs in order to control the desired number to set in the display and there are a lot of LEDs controllers available in the market, also RGB LED controllers over SPI or I2C, so I have make some changes to the PCB and instead of having a common cathode pin is better and there are more controllers with common anode.

The controller that I have decided to use is a simple serial to parallel converter from LUMISIL and embedded in the same PCB routing fewer pins to the castellated holes and making it easier to use overall, and also has the possibility to set a custom constant current source for the LEDs.

Another alternative that I might implement is to use 3D printing to do the front face of the display as it will result in more customizable in terms of colors and non the standard PCB colors, I have also tried the Flexy-Pins for connection with the castellated holes.

3D printed display and Flexy-Pins connecting with the castellated holes

If you are looking to create your own PCBs I do really recommend using PCBWay and their Online Gerber Viewer as it has saved me from making a lot of mistakes in the past.

PCBWAY Gerber Viewer

Driving the Display Array with an ESP32

The Raspberry Pi Pico is a very capable microcontroller, good enough to drive the Display Array, easy to program, a lot of memory and speed but it does not have WiFi, That is why I have checked the ESP32 and it is also fast and with a lot of memory.

I have found that the ESP32-S2 is a nice option for the display array as it has a USB interface for the programming and not a real need for a USB-Serial converter, it has enough GPIOs even for an 8 display board.

I am very new with the ESP32 so I have tried with MicroPython, CircuitPython, Arduino, and Espressif repository running in Visual Studio Code, and the easiest for me was this last one, so I took the SNTP example located at the protocols example folder. In Visual Studio Code is the Espressif plug-in which makes it very easy and fast to get started with the ESP32.

After installing and opening the example we just need to configure the example for the Wifi credentials.

And now is as easy as adding the SPI configurations for the displays.

/* LwIP SNTP example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_attr.h"
#include "esp_sleep.h"
#include "nvs_flash.h"
#include "protocol_examples_common.h"
#include "esp_sntp.h"

//#include "clock14SEG.h"
#include "clockBINA.h"
//#include "clockDigital.h"
//#include "clockFlip.h"
//#include "clockInk.h"
#include "clockLixieCyan.h"
#include "clockLixiePurple.h"
//#include "clockMatrix.h"
#include "clockNIMO.h"
//#include "clockNixie.h"
//#include "clockVFD.h"
//#include "clockWood.h"

#include "clockFlags.h"

#define zero_Theme      zero_BINA
#define one_Theme       one_BINA
#define two_Theme       two_BINA
#define three_Theme     three_BINA
#define four_Theme      four_BINA
#define five_Theme      five_BINA
#define six_Theme       six_BINA
#define seven_Theme     seven_BINA
#define eight_Theme     eight_BINA
#define nine_Theme      nine_BINA
#define colon_Theme     colon_BINA
#define space_Theme     space_BINA

#define zero_Theme3      zero_LixiePurple
#define one_Theme3       one_LixiePurple
#define two_Theme3       two_LixiePurple
#define three_Theme3     three_LixiePurple
#define four_Theme3      four_LixiePurple
#define five_Theme3      five_LixiePurple
#define six_Theme3       six_LixiePurple
#define seven_Theme3     seven_LixiePurple
#define eight_Theme3     eight_LixiePurple
#define nine_Theme3      nine_LixiePurple
#define colon_Theme3     colon_LixiePurple
#define space_Theme3     space_LixiePurple

#define zero_Theme2      zero_LixieCyan
#define one_Theme2       one_LixieCyan
#define two_Theme2       two_LixieCyan
#define three_Theme2     three_LixieCyan
#define four_Theme2      four_LixieCyan
#define five_Theme2      five_LixieCyan
#define six_Theme2       six_LixieCyan
#define seven_Theme2     seven_LixieCyan
#define eight_Theme2     eight_LixieCyan
#define nine_Theme2      nine_LixieCyan
#define colon_Theme2     colon_LixieCyan
#define space_Theme2     space_LixieCyan

#define zero_Theme1      zero_NIMO
#define one_Theme1       one_NIMO
#define two_Theme1       two_NIMO
#define three_Theme1     three_NIMO
#define four_Theme1      four_NIMO
#define five_Theme1      five_NIMO
#define six_Theme1       six_NIMO
#define seven_Theme1     seven_NIMO
#define eight_Theme1     eight_NIMO
#define nine_Theme1      nine_NIMO
#define colon_Theme1     colon_NIMO
#define space_Theme1     space_NIMO

#define PIN_NUM_BLK     37
#define PIN_NUM_RST     33
#define PIN_NUM_DC      34
#define PIN_NUM_MOSI    35
#define PIN_NUM_CLK     36
#define PIN_NUM_MISO    -1

#define PIN_NUM_CS1     1
#define PIN_NUM_CS2     2
#define PIN_NUM_CS3     3
#define PIN_NUM_CS4     4
#define PIN_NUM_CS5     5
#define PIN_NUM_CS6     6

#define PIN_NUM_BTA     10
#define PIN_NUM_BTB     11
#define PIN_NUM_BTC     12 
#define PIN_NUM_BTD     13
#define PIN_NUM_BTE     0
/*
 The LCD needs a bunch of command/argument values to be initialized. They are stored in this struct.
*/
typedef struct {
    uint8_t cmd;
    uint8_t data[16];
    uint8_t databytes; //No of data in data; bit 7 = delay after set; 0xFF = end of cmds.
} lcd_init_cmd_t;

//Place data into DRAM. Constant data gets placed into DROM by default, which is not accessible by DMA.
DRAM_ATTR static const lcd_init_cmd_t st_init_cmds[]={
     /* Sleep Out */
    {0x11, {0}, 0x80},
    /* Memory Data Access Control, MX=MV=1, MY=ML=MH=0, RGB=0 */
    {0x36, {0xC8}, 1},
    /* Interface Pixel Format, 16bits/pixel for RGB/MCU interface */
    {0x3A, {0x05}, 1},
    /* Porch Setting */
    {0xB1, {0x01, 0x2C, 0x2D}, 3},
    {0xB2, {0x01, 0x2C, 0x2D}, 3},
    {0xB3, {0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D}, 6},
    {0xB4, {0x07}, 1},
    /* LCM Control, XOR: BGR, MX, MH */
    {0xC0, {0xA2,0x02,0x84}, 3},
    {0xC1, {0xC5}, 1},
    /* VDV and VRH Command Enable, enable=1 */
    {0xC2, {0x0A, 0x00}, 2},
    /* VRH Set, Vap=4.4+... */
    {0xC3, {0x8A,0xEE}, 2},
    /* VDV Set, VDV=0 */
    {0xC5, {0x0E}, 1},
    /* Column Address */
    {0x2A, {0x00, 0x1A, 0x00, 0x69}, 4},
    /* Row Address */
    {0x2B, {0x00, 0x01, 0x00, 0xA0}, 4},
    /* Positive Voltage Gamma Control */
    {0xE0, {0x02, 0x1C, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2D, 0x29, 0x25, 0x2B, 0x39, 0x00, 0x01, 0x03, 0x10}, 16},
    /* Negative Voltage Gamma Control */
    {0xE1, {0x03, 0x1D, 0x07, 0x06, 0x2E, 0x2C, 0x29, 0x2D, 0x2E, 0x2E, 0x37, 0x3F, 0x00, 0x00, 0x02, 0x10}, 16},
    /* Inversion ON */
    {0x21, {0}, 0x80},
    /* Normal Display */
    {0x13, {0}, 0x80},
    /* Display On */
    {0x29, {0}, 0x80},
    /* Write Data */
    {0x2C, {0}, 0x80},
    {0, {0}, 0xff}
};



/* Send a command to the LCD. Uses spi_device_polling_transmit, which waits
 * until the transfer is complete.
 *
 * Since command transactions are usually small, they are handled in polling
 * mode for higher speed. The overhead of interrupt transactions is more than
 * just waiting for the transaction to complete.
 */
void lcd_cmd(spi_device_handle_t spi, const uint8_t cmd)
{
    esp_err_t ret;
    spi_transaction_t t;
    memset(&t, 0, sizeof(t));       //Zero out the transaction
    t.length=8;                     //Command is 8 bits
    t.tx_buffer=&cmd;               //The data is the cmd itself
    t.user=(void*)0;                //D/C needs to be set to 0
    ret=spi_device_polling_transmit(spi, &t);  //Transmit!
    assert(ret==ESP_OK);            //Should have had no issues.
}

/* Send data to the LCD. Uses spi_device_polling_transmit, which waits until the
 * transfer is complete.
 *
 * Since data transactions are usually small, they are handled in polling
 * mode for higher speed. The overhead of interrupt transactions is more than
 * just waiting for the transaction to complete.
 */
void lcd_data(spi_device_handle_t spi, const uint8_t *data, int len)
{
    esp_err_t ret;
    spi_transaction_t t;
    if (len==0) return;             //no need to send anything
    memset(&t, 0, sizeof(t));       //Zero out the transaction
    t.length=len*8;                 //Len is in bytes, transaction length is in bits.
    t.tx_buffer=data;               //Data
    t.user=(void*)1;                //D/C needs to be set to 1
    ret=spi_device_polling_transmit(spi, &t);  //Transmit!
    assert(ret==ESP_OK);            //Should have had no issues.
}

//This function is called (in irq context!) just before a transmission starts. It will
//set the D/C line to the value indicated in the user field.
void lcd_spi_pre_transfer_callback(spi_transaction_t *t){
    int dc=(int)t->user;
    gpio_set_level(PIN_NUM_DC, dc);
}

//Initialize the display
void lcd_init(spi_device_handle_t spi){
    int cmd=0;
    const lcd_init_cmd_t* lcd_init_cmds;
    lcd_init_cmds = st_init_cmds;

    //Initialize non-SPI GPIOs
    gpio_set_direction(PIN_NUM_DC, GPIO_MODE_OUTPUT);
    gpio_set_direction(PIN_NUM_RST, GPIO_MODE_OUTPUT);
    gpio_set_direction(PIN_NUM_BLK, GPIO_MODE_OUTPUT);

    gpio_set_direction(PIN_NUM_CS1, GPIO_MODE_OUTPUT);
    gpio_set_direction(PIN_NUM_CS2, GPIO_MODE_OUTPUT);
    gpio_set_direction(PIN_NUM_CS3, GPIO_MODE_OUTPUT);
    gpio_set_direction(PIN_NUM_CS4, GPIO_MODE_OUTPUT);
    gpio_set_direction(PIN_NUM_CS5, GPIO_MODE_OUTPUT);
    gpio_set_direction(PIN_NUM_CS6, GPIO_MODE_OUTPUT);

    gpio_set_direction(PIN_NUM_BTA, GPIO_MODE_INPUT);
    gpio_set_direction(PIN_NUM_BTB, GPIO_MODE_INPUT);
    gpio_set_direction(PIN_NUM_BTC, GPIO_MODE_INPUT);
    gpio_set_direction(PIN_NUM_BTD, GPIO_MODE_INPUT);
    gpio_set_direction(PIN_NUM_BTE, GPIO_MODE_INPUT);

    gpio_set_level(PIN_NUM_CS1, 0);
    gpio_set_level(PIN_NUM_CS2, 0);
    gpio_set_level(PIN_NUM_CS3, 0);
    gpio_set_level(PIN_NUM_CS4, 0);
    gpio_set_level(PIN_NUM_CS5, 0);
    gpio_set_level(PIN_NUM_CS6, 0);
    gpio_set_level(PIN_NUM_BLK, 0);

    //Reset the display
    gpio_set_level(PIN_NUM_RST, 0);
    vTaskDelay(100 / portTICK_RATE_MS);
    gpio_set_level(PIN_NUM_RST, 1);
    vTaskDelay(100 / portTICK_RATE_MS);

    //Send all the commands
    while (lcd_init_cmds[cmd].databytes!=0xff) {
        lcd_cmd(spi, lcd_init_cmds[cmd].cmd);
        lcd_data(spi, lcd_init_cmds[cmd].data, lcd_init_cmds[cmd].databytes&0x1F);
        if (lcd_init_cmds[cmd].databytes&0x80) {
            vTaskDelay(100 / portTICK_RATE_MS);
        }
        cmd++;
    }
}

uint8_t clockTheme = 0;
uint8_t prevHours,prevMinutes,prevSeconds;
uint8_t imgBuffer[100];

uint8_t backlightStatus = 1;
uint8_t secondsCounter = 0;
uint8_t secondsStatus = 0;

void selectDisplay(uint8_t);
void lcdDrawNumber(spi_device_handle_t,uint8_t,uint8_t);
void lcdDrawFlag(spi_device_handle_t,uint8_t,uint8_t);

static void obtain_time(void);
static void initialize_sntp(void);

void app_main(void)
{
    esp_err_t ret;
    spi_device_handle_t spi;
    spi_bus_config_t buscfg={
        .miso_io_num=PIN_NUM_MISO,
        .mosi_io_num=PIN_NUM_MOSI,
        .sclk_io_num=PIN_NUM_CLK,
        .quadwp_io_num=-1,
        .quadhd_io_num=-1,
        .max_transfer_sz=100
    };
    spi_device_interface_config_t devcfg={
        .clock_speed_hz=40*1000*1000,           //Clock out at 10 MHz
        .mode=0,                                //SPI mode 0
        //.spics_io_num=PIN_NUM_CS1,            //CS pin
        .queue_size=7,                          //We want to be able to queue 7 transactions at a time
        .pre_cb=lcd_spi_pre_transfer_callback,  //Specify pre-transfer callback to handle D/C line
    };
    //Initialize the SPI bus
    ret=spi_bus_initialize(SPI2_HOST, &buscfg, SPI2_HOST);
    ESP_ERROR_CHECK(ret);
    //Attach the LCD to the SPI bus
    ret=spi_bus_add_device(SPI2_HOST, &devcfg, &spi);
    ESP_ERROR_CHECK(ret);
    //Initialize the LCD
    lcd_init(spi);
    lcdDrawNumber(spi,0,11);
    ///Enable backlight
    gpio_set_level(PIN_NUM_BLK, 1);
   
    time_t now;
    struct tm timeinfo;
    time(&now);
    localtime_r(&now, &timeinfo);
    if (timeinfo.tm_year < (2016 - 1900)) { // Is time set? If not, tm_year will be (1970 - 1900).
        obtain_time();
        time(&now); // update 'now' variable with current time
    }

    // Set timezone to Mexico City Central Standard Time 
    setenv("TZ", "CST6CDT,M4.1.0/2,M10.5.0", 1); //EST5EDT,M3.2.0/2,M11.1.0
    tzset();
    localtime_r(&now, &timeinfo);

    prevHours = 0;
    prevMinutes = 0;
    prevSeconds = 0;


    lcdDrawFlag(spi,6,0);

    while(1){
        
        if(timeinfo.tm_hour != prevHours){
            prevHours = timeinfo.tm_hour;
    
            if(timeinfo.tm_hour < 10){
                lcdDrawNumber(spi,1,0);
                lcdDrawNumber(spi,2,timeinfo.tm_hour);
            }else if (timeinfo.tm_hour < 20){
                lcdDrawNumber(spi,1,1);
                lcdDrawNumber(spi,2,timeinfo.tm_hour-10);
            }else{
                lcdDrawNumber(spi,1,2);
                lcdDrawNumber(spi,2,timeinfo.tm_hour-20);
            }
        }     

        if(timeinfo.tm_min != prevMinutes){
            prevMinutes = timeinfo.tm_min;

            if(timeinfo.tm_min < 10){
                lcdDrawNumber(spi,4,0);
                lcdDrawNumber(spi,5,timeinfo.tm_min);
            }else if (timeinfo.tm_min < 20){
                lcdDrawNumber(spi,4,1);
                lcdDrawNumber(spi,5,timeinfo.tm_min-10);
            }else if (timeinfo.tm_min < 30){
                lcdDrawNumber(spi,4,2);
                lcdDrawNumber(spi,5,timeinfo.tm_min-20);
            }else if (timeinfo.tm_min < 40){
                lcdDrawNumber(spi,4,3);
                lcdDrawNumber(spi,5,timeinfo.tm_min-30);
            }else if (timeinfo.tm_min < 50){
                lcdDrawNumber(spi,4,4);
                lcdDrawNumber(spi,5,timeinfo.tm_min-40);
            }else{
                lcdDrawNumber(spi,4,5);
                lcdDrawNumber(spi,5,timeinfo.tm_min-50);
            }
        } 

        vTaskDelay(20 / portTICK_PERIOD_MS);
        secondsCounter++;
        time(&now);                     // update 'now' variable with current time
        localtime_r(&now, &timeinfo);

        if(gpio_get_level(PIN_NUM_BTA) == 0){
            clockTheme = 0;
            setenv("TZ", "CST6CDT,M4.1.0/2,M10.5.0", 1); // Mexico City
            tzset();
            localtime_r(&now, &timeinfo);
            prevHours = 0;
            prevMinutes = 0;
            prevSeconds = 0;
            lcdDrawNumber(spi,1,0);
            lcdDrawNumber(spi,2,0);
            lcdDrawNumber(spi,4,0);
            lcdDrawNumber(spi,5,0);
            lcdDrawFlag(spi,6,0);
            lcdDrawNumber(spi,3,10);
            vTaskDelay(120 / portTICK_PERIOD_MS);
        }

        if(gpio_get_level(PIN_NUM_BTB) == 0){
            clockTheme = 1;
            setenv("TZ", "CET1CEST,M3.5.0/2,M10.5.0", 1); // Germany Berlin
            tzset();
            localtime_r(&now, &timeinfo);
            prevHours = 0;
            prevMinutes = 0;
            prevSeconds = 0;
            lcdDrawNumber(spi,1,0);
            lcdDrawNumber(spi,2,0);
            lcdDrawNumber(spi,4,0);
            lcdDrawNumber(spi,5,0);
            lcdDrawFlag(spi,6,1);
            lcdDrawNumber(spi,3,10);
            vTaskDelay(120 / portTICK_PERIOD_MS);
        }

        if(gpio_get_level(PIN_NUM_BTC) == 0){
            clockTheme = 2;
            setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1); // USA New York
            tzset();
            localtime_r(&now, &timeinfo);
            prevHours = 0;
            prevMinutes = 0;
            prevSeconds = 0;
            lcdDrawNumber(spi,1,0);
            lcdDrawNumber(spi,2,0);
            lcdDrawNumber(spi,4,0);
            lcdDrawNumber(spi,5,0);
            lcdDrawFlag(spi,6,2);
            lcdDrawNumber(spi,3,10);
            vTaskDelay(120 / portTICK_PERIOD_MS);
        }

        if (gpio_get_level(PIN_NUM_BTD) == 0){
            clockTheme = 3;
            setenv("TZ", "GMT0BST,M3.5.0/2,M10.5.0", 1); // UK London 
            tzset();
            localtime_r(&now, &timeinfo);
            prevHours = 0;
            prevMinutes = 0;
            prevSeconds = 0;
            lcdDrawNumber(spi,1,0);
            lcdDrawNumber(spi,2,0);
            lcdDrawNumber(spi,4,0);
            lcdDrawNumber(spi,5,0);
            lcdDrawFlag(spi,6,3);
            lcdDrawNumber(spi,3,10);
            vTaskDelay(120 / portTICK_PERIOD_MS);
        }

        if((secondsCounter > 25) & (secondsStatus == 0)){
            lcdDrawNumber(spi,3,10); 
            secondsStatus = 1;
            secondsCounter = 0;
        } else if((secondsCounter > 25) & (secondsStatus == 1)){
            lcdDrawNumber(spi,3,11); 
            secondsStatus = 0;
            secondsCounter = 0;
        }

        if((gpio_get_level(PIN_NUM_BTE) == 0) & (backlightStatus == 1)){
            backlightStatus = 0;
            gpio_set_level(PIN_NUM_BLK, 0);
            vTaskDelay(120 / portTICK_PERIOD_MS);
        } else if((gpio_get_level(PIN_NUM_BTE) == 0) & (backlightStatus == 0)){
            backlightStatus = 1;
            gpio_set_level(PIN_NUM_BLK, 1);
            vTaskDelay(120 / portTICK_PERIOD_MS);
        }
    }
}

static void obtain_time(void){

    ESP_ERROR_CHECK( nvs_flash_init() );
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK( esp_event_loop_create_default() );

    /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
     * Read "Establishing Wi-Fi or Ethernet Connection" section in
     * examples/protocols/README.md for more information about this function.
     */
    ESP_ERROR_CHECK(example_connect());

    initialize_sntp();
    
    time_t now = 0;                     // wait for time to be set
    struct tm timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 10;
    while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
        vTaskDelay(2000 / portTICK_PERIOD_MS);}
    time(&now);
    localtime_r(&now, &timeinfo);
    ESP_ERROR_CHECK( example_disconnect() );
}

static void initialize_sntp(void){
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "pool.ntp.org");
    sntp_init();
}

void selectDisplay(uint8_t display){
    switch(display){
        case 0:
            gpio_set_level(PIN_NUM_CS1, 0);
            gpio_set_level(PIN_NUM_CS2, 0);
            gpio_set_level(PIN_NUM_CS3, 0);
            gpio_set_level(PIN_NUM_CS4, 0);
            gpio_set_level(PIN_NUM_CS5, 0);
            gpio_set_level(PIN_NUM_CS6, 0);
        break;
        case 1:
            gpio_set_level(PIN_NUM_CS1, 0);
            gpio_set_level(PIN_NUM_CS2, 1);
            gpio_set_level(PIN_NUM_CS3, 1);
            gpio_set_level(PIN_NUM_CS4, 1);
            gpio_set_level(PIN_NUM_CS5, 1);
            gpio_set_level(PIN_NUM_CS6, 1);
        break;
        case 2:
            gpio_set_level(PIN_NUM_CS1, 1);
            gpio_set_level(PIN_NUM_CS2, 0);
            gpio_set_level(PIN_NUM_CS3, 1);
            gpio_set_level(PIN_NUM_CS4, 1);
            gpio_set_level(PIN_NUM_CS5, 1);
            gpio_set_level(PIN_NUM_CS6, 1);
        break;
        case 3:
            gpio_set_level(PIN_NUM_CS1, 1);
            gpio_set_level(PIN_NUM_CS2, 1);
            gpio_set_level(PIN_NUM_CS3, 0);
            gpio_set_level(PIN_NUM_CS4, 1);
            gpio_set_level(PIN_NUM_CS5, 1);
            gpio_set_level(PIN_NUM_CS6, 1);
        break;
        case 4:
            gpio_set_level(PIN_NUM_CS1, 1);
            gpio_set_level(PIN_NUM_CS2, 1);
            gpio_set_level(PIN_NUM_CS3, 1);
            gpio_set_level(PIN_NUM_CS4, 0);
            gpio_set_level(PIN_NUM_CS5, 1);
            gpio_set_level(PIN_NUM_CS6, 1);
        break;
        case 5:
            gpio_set_level(PIN_NUM_CS1, 1);
            gpio_set_level(PIN_NUM_CS2, 1);
            gpio_set_level(PIN_NUM_CS3, 1);
            gpio_set_level(PIN_NUM_CS4, 1);
            gpio_set_level(PIN_NUM_CS5, 0);
            gpio_set_level(PIN_NUM_CS6, 1);
        break;
        case 6:
            gpio_set_level(PIN_NUM_CS1, 1);
            gpio_set_level(PIN_NUM_CS2, 1);
            gpio_set_level(PIN_NUM_CS3, 1);
            gpio_set_level(PIN_NUM_CS4, 1);
            gpio_set_level(PIN_NUM_CS5, 1);
            gpio_set_level(PIN_NUM_CS6, 0);
        break;
        case 9:
            gpio_set_level(PIN_NUM_CS1, 0);
            gpio_set_level(PIN_NUM_CS2, 0);
            gpio_set_level(PIN_NUM_CS3, 0);
            gpio_set_level(PIN_NUM_CS4, 0);
            gpio_set_level(PIN_NUM_CS5, 0);
            gpio_set_level(PIN_NUM_CS6, 0);
        break;
        default:
            gpio_set_level(PIN_NUM_CS1, 1);
            gpio_set_level(PIN_NUM_CS2, 1);
            gpio_set_level(PIN_NUM_CS3, 1);
            gpio_set_level(PIN_NUM_CS4, 1);
            gpio_set_level(PIN_NUM_CS5, 1);
            gpio_set_level(PIN_NUM_CS6, 1);
        break;
    }
}

void lcdDrawNumber(spi_device_handle_t spi, uint8_t Display, uint8_t Number){
    selectDisplay(Display);
    switch(Number){
        case 0:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = zero_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = zero_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = zero_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = zero_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 1:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = one_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = one_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = one_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = one_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 2:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = two_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = two_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = two_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = two_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 3:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = three_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = three_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = three_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = three_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 4:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = four_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = four_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = four_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = four_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 5:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = five_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = five_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = five_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = five_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 6:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = six_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = six_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = six_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = six_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 7:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = seven_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = seven_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = seven_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = seven_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 8:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = eight_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = eight_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = eight_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = eight_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 9:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = nine_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = nine_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = nine_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = nine_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 10:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = colon_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = colon_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = colon_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = colon_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 11:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                    if(clockTheme == 0)
                        imgBuffer[j] = space_Theme[j+(100*k)];
                    else if (clockTheme == 1)
                        imgBuffer[j] = space_Theme1[j+(100*k)];
                    else if (clockTheme == 2)
                        imgBuffer[j] = space_Theme2[j+(100*k)];
                    else if (clockTheme == 3)
                        imgBuffer[j] = space_Theme3[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
    }
}

void lcdDrawFlag(spi_device_handle_t spi, uint8_t Display, uint8_t Flag){
    selectDisplay(Display);
    switch (Flag){
        case 0:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                        imgBuffer[j] = mex_Flag[j+(100*k)];  
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 1:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                        imgBuffer[j] = deu_Flag[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 2:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                        imgBuffer[j] = usa_Flag[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
        case 3:
            for(int k = 0; k < 256; k++){
                for(int j = 0; j < 100; j++){
                        imgBuffer[j] = gbr_Flag[j+(100*k)];
                }
                lcd_data(spi, imgBuffer,100);
            }
        break;
    }
}

For this new firmware version, I have drawn some flags to set different time zones available with the A, B, C, and D buttons.

After having the code ready and tested It is time to design a case for it. I am also a newbie in 3D design but this is what I manage to design.

This case makes it possible to add your own controller board and USB power to one side or at the back of the case. I will also make available the 3D design file ( Fusion 360 ). These files can be printed without any supports if you want, but maybe the main case will need a few of them.


Downloads

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


Dynamixel, Smart motor configurator

There has been sometimes that it has been hard to communicate with this type of smart motors, I don’t always remember the set ID or even the last configured baud rate for serial communication, or even the simple task of changing a single parameter requires to write a custom program to change that value to that specific motor, for that reason I decided to start building a handheld device that will make this kind of tasks easier and faster.

A simple UI in a TFT LCD will let configure these motors in a few steps, the project is going to integrate electronics used in other of my projects to make this development easier and faster, hoping to have feedback from the robotics community who have work with this kind of motors by letting me know the best path to follow.

In this first step I will have a first approach for the graphical interface, here are some ideas that I have tried, and the most recent one.

I will be using an ST7789V controller 2.8 inch TFT LCD for this project as I think it is a good size for the data it will work with. I will also be using the MK26F microcontroller that I have in a Feather format and a Dynamixel interface board for communication with the motors. So it needs to be all wired, connecting the SPI, ADCs, and GPIOs from the feather board to the TFT LCD Breakout board.

I have added some interesting things to the graphics for a more fluid and friendly interface as movement on the main menu icons, as shown in the next picture.

I have noticed that there are a lot of Dynamixel motors, the classic AX-12A use the protocol V1.0 but the newer one uses the V2.0 and also they change some registers address making this whole idea a bit difficult, I am still thinking how many motor models this device will support, as a first step I will make the firmware ready for the AX-12A and the XL-320 as I think are most popular one and also the cheapest.

The first step is to port the Dynamixel library for Arduino that I wrote many years ago to my microcontroller and then also update the library to be compatible with the V2.0 Dynamixel protocol.

So there are some slight differences between registers address and all of this have to take in count when showing the info the configurator, so I have started to register this tables in a model file and maybe would be updated in a more compressible file in the future.

Motor scanning and selection menu

In order to test or modify the motor register, we need to scan and select the connected motors, for this task I have created a short function that Ping the bus and waits for each motor ID, and adds it to the motor list at a given baud rate and protocol.

for(motorID = 0; motorID < 254; motorID++){

             if( !(Dynamixelping(motorID,protocol) == -128) ){

                    motorList[motorIndex++] = motorID;

             }

             displaydrawRectangle(64,0,((motorID*240)/253),4,GREEN);

}

After the motor scanning, it’s useful to know certain values of the motor so I decided to read these and display them in the same window with the capability to scroll the motor list and at the same time select the motor of interest.

Configure Registers Window

In this window will be able to read and write directly to register of the selected motor in the previous windows, it will be accessible to both the EEPROM and RAM registers in order to properly test the written values, limits, or alarms.The new values will be written on a numerical touchscreen keyboard.

This window also shows if the EEPROM data is write-protected, in order to write the torque of the motor should be disabled, this can be done by writing a 0 to the Torque Enable register in address 24.

Factory Reset Window

The factory reset window is a simple but very efficient way to restore motor parameters to factory defaults with a press of a button.

This option will perform a reset function on the Dynamixel instructions set, so all it been handled by the motor itself, the motor list is also accessible from this window but it has to be in consideration that motors should not have the same ID as it can cause errors in the communication.

When a reset has finished successfully this window will also inform and will let you continue with the reset operations that you may need for other motors.

Configuration Window

The configuration windows allow to change the Baud rate speed, the Dynamixel protocol and perform a touchscreen calibration if needed.

As you have already noticed the touchscreen calibration options are accessible with the tact switches in case that the touchscreen values have changed over time, the only action that needs the touchscreen is the keyboard on the register configuration window.


Download