Intro to nvim-platformio.lua

If you work with microcontrollers you must have heard of PlatformIO. It is a tool which we can use to program and debug multiple families of microcontrollers in various frameworks with little to no manual setup work. PlatformIO takes care of installing the tool, setting up the project, build, upload and debug. PlatformIO comes with an extension for VS Code which wraps the underlaying PlatformIO cli tool and exposes a very nice interface to setup project and use other tools. But if you are a n/vim user like me, you only have the cli. I was fine with cli but I wanted that ease of use like VS Code extension so I made this neovim plugin called nvim-platformio.lua

Installation process

  1. Install the PlatformIO core
curl -fsSL -o get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py
python3 get-platformio.py
  1. Now check if you have PlatformIO cli ready to use
pio --version
  1. Added anurag3301/nvim-platformio.lua plugin and its dependencies for installation in your neovim package manager, here I have given example for lazy.nvim
return {
    "anurag3301/nvim-platformio.lua",
    dependencies = {
        { "akinsho/nvim-toggleterm.lua" },
        { "nvim-telescope/telescope.nvim" },
        { "nvim-lua/plenary.nvim" },
    },
}

And that's you have nvim-platformio.lua and PlatformIO ready to be used 🥳.

Usage

Setup a project

  1. Create a new directory for the project and cd into it, in my case ill be using a ESP32.
mkdir esp_project
cd esp_project
  1. Open neovim and run :Pioinit, this will open a telescope picker where you can fuzzy find and select the board you want to use. Then press enter.
  1. After selecting the board, you'll have option to select the desired framework such as Arduino, ESP-IDF, STM32 Cube etc. And press enter.
  2. Now a toggleterm window will pop up which will run the pio project setup command and install any missing tools or dependencies.
  3. Now you should have a PlatformIO project setup with .ccls file which will let your LSP make aware of the include path for the libraries.
  4. Open the src/main.c file and write the code. In my case I wrote a simple ESP-IDF code which blink led and print to Serial line.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"

#define BLINK_GPIO 12

void app_main() {
    gpio_pad_select_gpio(BLINK_GPIO);
    gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);
    while(1) {
        gpio_set_level(BLINK_GPIO, 1);
        ESP_LOGI("LED", "LED ON");
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        gpio_set_level(BLINK_GPIO, 0);
        ESP_LOGI("LED", "LED OFF");
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}
  1. Compile and flash the code by running :Piorun.
  2. Open the serial monitor to see the output by running :Piomon

This was a brief introduction to PlatformIO and nvim-platformio.lua.

  • Run :help PlatformIO to see the detailed usage and option for each command.
  • Checkout platformio.ini docs for project configuration.
  • PlatformIO cli to extend usage.

DS1302 RTC Drivers for STM32 Part 2

Checkout Part 1 to understand inner workings and interface of DS1302

Setup delay_us

You need to first write a delay_us funtions which can do blocking microsecond delays. We need to write our own because STM32CubeHal doesnt not have a microsecond delay.

void delay_us(uint32_t microseconds){
    // Delay code
}

Driver Programming

Init

typedef struct{
    GPIO_TypeDef* port;
    uint16_t pin;
}GpioPin;

typedef struct{
    GpioPin CE_Pin;
    GpioPin IO_Pin;
    GpioPin SCLK_Pin;
}DS1302_HandelTypeDef;

The struct GpioPin represents a single GPIO pin with its Port and Pin number. We take three of these GpioPin to store CE, IO and SCLK pins for DS1302 and call it a struct DS1302_HandelTypeDef. We will be passing this struct to all the funtions of ds1302 so they can access the corresponding pin.

Following is a example initialisation configuration for DS1302_HandelTypeDef.

DS1302_HandelTypeDef rtc = {
    .CE_Pin = {GPIOA, GPIO_PIN_9},          // CE -> PA9
    .SCLK_Pin = {GPIOB, GPIO_PIN_10},       // SCLK -> PB10
    .IO_Pin = {GPIOA, GPIO_PIN_8}           // IO -> PA8
};

Now Lets write a init funtion which will make all the pins as output, It has to be called before using any other funtion.

void ds1302_init(DS1302_HandelTypeDef* handel){
    HAL_GPIO_WritePin(handel->IO_Pin.port, handel->IO_Pin.pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(handel->CE_Pin.port, handel->CE_Pin.pin, GPIO_PIN_RESET);

    GPIO_InitTypeDef gpioinit = {
        .Pin = handel->IO_Pin.pin,
        .Mode = GPIO_MODE_OUTPUT_PP,
        .Pull = GPIO_NOPULL,
        .Speed = GPIO_SPEED_FREQ_LOW
    };

    HAL_GPIO_Init(handel->IO_Pin.port, &gpioinit);
    
    gpioinit.Pin = handel->CE_Pin.pin;
    HAL_GPIO_Init(handel->CE_Pin.port, &gpioinit);

    gpioinit.Pin = handel->SCLK_Pin.pin;
    HAL_GPIO_Init(handel->SCLK_Pin.port, &gpioinit);
}

In this code, we first RESET all three pins and then configure them for GPIO Output one by one. You just have to pass address of DS1302_HandelTypeDef, eg. ds1302_init(&rtc).

Data input and output

Now lets discuss how can we talk to ds1302, in other words how to do I/O. If you recall from part 1, the I/O involves following steps.

  1. Set the CE pin high to initiate data transfer
  2. Start to produce clock signal on SCLK pin.
  3. First MCU writes the first byte which is the address it want to communicate.
    • If MCU wants to write, send the next byte which is the data.
    • If MCU wants to read, set its pin in input mode and read the coming data from DS1302.

First lets how to set the CE and produce clock on SCLK.

void ds1302_iotest(DS1302_HandelTypeDef* handel){
    // Start the communication by setting CE HIGH
    HAL_GPIO_WritePin(handel->CE_Pin.port, handel->CE_Pin.pin, GPIO_PIN_SET);
    
    // Send the address phase
    for(int i=0; i<8; i++){
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_SET);
        Delay_us(2);
    }

    // DATA io phase
    for(int i=0; i<8; i++){
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_SET);
        Delay_us(2);
    }

    // RESET SCLK and bring to rest state
    HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);

    // Stop the communicate by setting CE low
    HAL_GPIO_WritePin(handel->CE_Pin.port, handel->CE_Pin.pin, GPIO_PIN_RESET);
    Delay_us(2);
}

Now If you hookup to logic analyser and you should be able to see following.

We can extend above code to write or read from ds1302.

Write

Along with the DS1302_HandelTypeDef we will also pass two uint8_t ie. address and the data. At each clock pulse we will set the I/O pin high or low based on the data bit.

To get a specific bit at an index we can use following macro

#define GET_BIT(value, bit) (((value) >> (bit)) & 0x01)

This is the code to write a byte to given address

void ds1302_writeByte(DS1302_HandelTypeDef* handel, uint8_t data, uint8_t address){
    HAL_GPIO_WritePin(handel->CE_Pin.port, handel->CE_Pin.pin, GPIO_PIN_SET);
    
    // Send the address
    for(int i=0; i<8; i++){
        HAL_GPIO_WritePin(handel->IO_Pin.port, handel->IO_Pin.pin, 
                            GET_BIT(address, i) ? GPIO_PIN_SET : GPIO_PIN_RESET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_SET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
        Delay_us(2);
    }

    // Send the data
    for(int i=0; i<8; i++){
        HAL_GPIO_WritePin(handel->IO_Pin.port, handel->IO_Pin.pin, 
                            GET_BIT(data, i) ? GPIO_PIN_SET : GPIO_PIN_RESET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_SET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
        Delay_us(2);
    }

    HAL_GPIO_WritePin(handel->CE_Pin.port, handel->CE_Pin.pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(handel->IO_Pin.port, handel->IO_Pin.pin, GPIO_PIN_RESET);
    Delay_us(2);
}

According to datasheet.

For data inputs, data must be valid during the rising edge of the clock and data bits are output on the falling edge of clock

That means we need to write data at rising edge and read data at falling edge of SCLK. Lets take an example where we write 0x32 to address 0x82. So the code would be ds1302_writeByte(&rtc, 0x32, 0x82). Following is the output of data analyser

Read

To read, first we pass the address then read the data sent by ds1302. To do as such we need to toggle input and output mode of the IO pin midway. Here are two funtions to switch the pin modes.

void ds1302_enableWriteMode(DS1302_HandelTypeDef* handel){
    GPIO_InitTypeDef gpioinit = {
        .Pin = handel->IO_Pin.pin,
        .Mode = GPIO_MODE_OUTPUT_PP,
        .Pull = GPIO_NOPULL,
        .Speed = GPIO_SPEED_FREQ_LOW
    };
    HAL_GPIO_Init(handel->IO_Pin.port, &gpioinit);
}

void ds1302_enableReadMode(DS1302_HandelTypeDef* handel){
    GPIO_InitTypeDef gpioinit = {
        .Pin = handel->IO_Pin.pin,
        .Mode = GPIO_MODE_INPUT,
        .Pull = GPIO_NOPULL,
    };
    HAL_GPIO_Init(handel->IO_Pin.port, &gpioinit);
}

For read operation, we cant send the address directly, we need to make the first bit high which makes it read operation. After sending the address, just read the state of IO pin at falling edge of SCLK. Here is the code.

uint8_t ds1302_readByte(DS1302_HandelTypeDef* handel, uint8_t address){
    // Start the communication
    HAL_GPIO_WritePin(handel->CE_Pin.port, handel->CE_Pin.pin, GPIO_PIN_SET);
    
    address |= 0x01;    // Make it read enabled
    // Send the address
    for(int i=0; i<8; i++){
        HAL_GPIO_WritePin(handel->IO_Pin.port, handel->IO_Pin.pin, 
                            GET_BIT(address, i) ? GPIO_PIN_SET : GPIO_PIN_RESET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_SET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
        Delay_us(2);
    }

    ds1302_enableReadMode(handel);  // Transition IO pin to input mode

    // Read the data byte
    uint8_t data = 0;
    for(int i=0; i<8; i++){
        // flip the i'th bits high if pin is high
        if(HAL_GPIO_ReadPin(handel->IO_Pin.port, handel->IO_Pin.pin) == GPIO_PIN_SET){
            data |= (1 << i);
        }
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_SET);
        Delay_us(2);
        HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
        Delay_us(2);
    }

    HAL_GPIO_WritePin(handel->CE_Pin.port, handel->CE_Pin.pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(handel->SCLK_Pin.port, handel->SCLK_Pin.pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(handel->IO_Pin.port, handel->IO_Pin.pin, GPIO_PIN_RESET);
    Delay_us(2);
    ds1302_enableWriteMode(handel);     // Reset IO pin to output mode
    return data;
}

Lets take an example where we read from the address 0x82. call funtions ds1302_readByte(0x82)

DS1302 RTC Drivers for STM32 Part 1

Introduction

The DS1302 is a Real-Time Clock (RTC) IC used to keep track of time and date. It has the capability to store data such as seconds, minutes, hours, day, date, month, and year. The DS1302 can be interfaced with microcontrollers using a simple serial communication protocol (SPI-like but with slight differences). It also includes 31 bytes of static RAM for temporary data storage.



Interface with module

Pinout

Above is the pinout of DS1302. This is description of each pin.

  • Vcc2: This is the main power supply which you connect to any 3.3v or 5v power source.
  • Vcc1: Connect a flat lithium sound button cell which powers the low power operation i.e. keeping the clock ticking.
  • X1/X2: Connect any 32.768kHz Quartz Crystal.
  • GND: Just a ground
  • CE: When Chip Enable pin is set high, it initiates the data transfer. It is kept high till the data transfer is being done.
  • I/O: This pin is used to send and receive data.
  • SCLK: The microcontroller generates clock on this line, this clock is used to synchronise the MCU and DS1302.

Communication

Communication happens in following step.

  1. Set the CE pin high to initiate data transfer
  2. Start to produce clock signal on SCLK pin.
  3. First MCU writes the first byte which is the address it want to communicate.
    • If MCU wants to write, send the next byte which is the data.
    • If MCU wants to read, set its pin in input mode and read the coming data from DS1302.

Addressing

The address space in ds1302 is divided into two parts.

  • Clock/Calendar: If the bit 6 is set to low, it allows you to access Clock/Calendar space. This is where datetime information is stored.
  • RAM: If the bit 6 is set to high, it allows you to access the RAM space. DS1302 provides your 31 bytes of storage where you can write or ready any personal data. It is a non-volatile storage space useful to store some configuration data.

The bit 7 is always set to high, The bit 0 lets your specify read or write operation, write operation when set to low and read operation when set to high. Bit 1-5 used to specify address from 0-31.

Clock/Calendar address

There are only 7 bytes useful to read or write in Clock/Calendar address space.

  1. Byte -> Second
  2. Byte -> Minutes
  3. Byte -> Hour
  4. Byte -> Date
  5. Byte -> Month
  6. Byte -> Day
  7. Byte -> Year

Lets try to calculate some address ourself:

  • How to read Month data
Month address -> 4 -> 0b00100

               B7  C/R  A4  A3  A2  A1  A0  R/W
Address Byte:   1   0   0   0   1   0   0    1
  • How to write year data
Year address -> 6 -> 0b00110

               B7  C/R  A4  A3  A2  A1  A0  R/W
Address Byte:   1   0   0   0   1   1   0    0

Ram address

You can access to all 31 bytes to read and write data from the RAM address space

Lets try to calculate some address ourself:

  • How to read from address 0
Month address -> 0 -> 0b00000

               B7  C/R  A4  A3  A2  A1  A0  R/W
Address Byte:   1   1   0   0   0   0   0    1
  • How to write to address 31
Year address -> 30 -> 0b11110

               B7  C/R  A4  A3  A2  A1  A0  R/W
Address Byte:   1   1   1   1   1   1   0    0

Clock/Calendar data

Data representation of DS1302 is rather different, I though it just stored the numbers directly in binary but it rather uses a different approach called BCD(binary-coded decimal) which first splits a decimal number into its individual digits and convert each decimal digit to binary using 4 bits.

Here is an example for BCD

Decimal Number: 56
Binary Value:   00111000

Decimal Digit:     5       6
BCD Value:        0101    0110

This is the complete Clock/Calendar datamap I took from the DS1302 datasheet. Lets go though them one by one

Seconds

BIT 7 BIT 6 BIT 5 BIT 4 BIT 3 BIT 2 BIT 1 BIT 0 RANGE
CH 10 Second Seconds 00-59
  • Bit 7 of second is CH(Clock Halt), when this bit is set high the clock is set to halt and stops ticking and continue ticking when you set low.
  • Bit 6-4 is the BCD representation of the tens digit of the second value eg. 5 in 56, 3 in 32. Range:0-5
  • Bit 3-0 is the BCD representation of the ones digit of the second value eg. 6 in 56, 2 in 32. Range:0-9

Example: Data value for 56 second would be 0b01010110

Minutes

BIT 7 BIT 6 BIT 5 BIT 4 BIT 3 BIT 2 BIT 1 BIT 0 RANGE
10 Minutes Minutes 00-59
  • Bit 7 is left empty i.e. logic 0
  • Bit 6-4 is the BCD representation of the tens digit of the minutes value eg. 5 in 56, 3 in 32. Range:0-5
  • Bit 3-0 is the BCD representation of the ones digit of the minutes value eg. 6 in 56, 2 in 32. Range:0-9

Hour

Hour is bit tricky because it operates in two modes i.e. 12 and 24 hrs.

BIT 7 BIT 6 BIT 5 BIT 4 BIT 3 BIT 2 BIT 1 BIT 0 RANGE
12/24 0 10 Hour Hour 0-23
AM/PM1-12
  • If Bit 7 is high then 12 hour mode selected
    • Bit 5 represent the meridiem. PM if High and AM if Low.
    • Bit 4 is the BCD representation of the tens digit of the minutes value. Range:0-1
    • Bit 3-0 is the BCD representation of the ones digit of the minutes value. Range:0-9
  • If Bit 7 is low then 24 hour mode selected
    • Bit 5-4 is the BCD representation of the tens digit of the minutes value. Range:0-2
    • Bit 3-0 is the BCD representation of the ones digit of the minutes value. Range:0-9
  • Bit 6 unused bit, so should set to low Example:
Time: 7:45 PM
Hour: 7, Mode: 12hrs, Meridiem: PM

      Mode  B6   Mer  H10   -----H1------
Data:   1    0    1    0    0   1   1   1


Time: 17:46
Hour: 17, Mode: 24hrs

      Mode  B6   ---H10--   -----H1------
Data:   0    0    1    1    0   1   1   0

Date

BIT 7 BIT 6 BIT 5 BIT 4 BIT 3 BIT 2 BIT 1 BIT 0 RANGE
0 0 10 Date Date 1-31
  • Bit 7-6 unused leave empty i.e. logic 0
  • Bit 5-4 is the BCD representation of the tens digit of the Date value. Range:0-3
  • Bit 3-0 is the BCD representation of the ones digit of the Date value. Range:0-9

Month

BIT 7 BIT 6 BIT 5 BIT 4 BIT 3 BIT 2 BIT 1 BIT 0 RANGE
0 00 10 month month 1-12
  • Bit 7-5 unused leave empty i.e. logic 0
  • Bit 4 is the BCD representation of the tens digit of the month value. Range:0-1
  • Bit 3-0 is the BCD representation of the ones digit of the month value. Range:0-9

Day

BIT 7 BIT 6 BIT 5 BIT 4 BIT 3 BIT 2 BIT 1 BIT 0 RANGE
00000 Day
  • Bit 7-3 unused leave empty i.e. logic 0
  • Bit 2-0 represent day of the week, range: 1-7

Year

BIT 7 BIT 6 BIT 5 BIT 4 BIT 3 BIT 2 BIT 1 BIT 0 RANGE
10 year year 00-99
  • Bit 7-4 is the BCD representation of the tens digit of the year value. Range:0-9
  • Bit 3-0 is the BCD representation of the ones digit of the year value. Range:0-9

Checkout Part 2 to understand inner workings and interface of DS1302