Baremetal Atmega Lesson 1: AVR toolchain setup and flash demo program

Introduction

Hey, you must be fimilier with the classic Arduino Uno. This was my first ever microcontroller board and frankly the most used one too, I loved it. But first what is Arduino software stack, Arduino is a abstraction layer on top of the baremetal AVR utilities. This abstraction layer makes things so easy that even a highscool kid can learn microcontroller programming and make stuff out of it. This comes at a cost, you are restricted with the arduino framework, design pattern, the arduino IDE. To make things easy, arduino framework hides lot of features and configuration is provided in the MCU. If you want to be a good embedded engineer you cant stay with Arduino framework forever.

So now its time to level up. Lets understand how can we program our arduino board without the arduino software framework:

  1. Writing raw C code with standered int main() function and compile ourself with the avr-gcc compiler.
  2. Operating the peripherals by manupulating registers with bitwise operations.
  3. Flasing the code using the avrdude utility.
  4. (Optional) Get rid of the arduino bootloader and use external programmers.

Toolchain installation

Linux

# Arch Linux
sudo pacman -S avr-gcc avr-libc avrdude avr-gdb avr-binutils

# Ubuntu
sudo apt-get install gcc-avr binutils-avr avr-libc gdb-avr avrdude

# Fedora
sudo dnf install avr-binutils avr-gcc avr-gcc-c++ avr-libc avrdude

Windows

For windows you can install WinAVR which is collection of tools for AVR.

MacOS

For mac checkout. hohmebrew-avr



Hello world

Now that you have the avr toolchain installed, its time to write a simple program, compile and flash on to the microcontroller. Create a new directory with a file main.c and write following code in it.

int main(){
    while();
}

This is a very minimal code to be compiled and flashed on to the microcontroller. Lets compile it using avr-gcc. The arguments for avr-gcc is pretty much like standered gcc compiler, we just have to give extra flag -mmcu which tells the compiler what microcontroller we are using, in our case Arduino Uno has the ATmega328P mcu.

avr-gcc -mmcu=atmega328p main.c -o main.elf

The elf file is to be converted to a hex format which will be flashed on to the microcontroller. We will use the avr-objcopy utility which will converted the elf to ihex(Intex Hex) format. The -O flag set the output format which is ihex in this case. -R .eeprom which remove the eeprom section from the final hex file.

avr-objcopy -O ihex -R .eeprom main.elf main.hex

Uploading code

Now that we have the firmware to be flashed onto the microcontroller, we have two methods(There are more, but these two are most common).

Arduino bootloader

The Arduino bootloader is a small program pre-installed on Arduino boards, allowing easy uploading of code via USB without requiring an external programmer, by handling serial communication and program flashing. To flash the code we will use the avrdude utility. To flash through serial we need to know what serial port the Arduino uno is connected, for that I have nice utility called lsserial which is a python script to print info about all the serial devices connected to the computer.

avrdude -F -c arduino -p atmega328p -P /dev/ttyACM0 -b 115200 -U flash:w:main.hex

Lets examine the avrdude flags:

  • -F This flag is to disable mcu signature matching, every avr board has some signature eg Device signature = 1E 95 16 (ATmega328PB) and if your Arduino board may be using some different varient of Atmega32 mcu, it will abort the upload. We will see how to correctly find which mcu we are using later.
  • -c This flas is used to specify what programmer we are using, in this case the arduino bootloader.
  • -p We specify what mcu we intend to flash, for now we are using the standered atmega328p for Arduino uno.
  • -P Here we specify the serial port Arduino is connected to, you can find that using the lsserial utility
  • -b The baud rate for the serial data line
  • -U This is used to carry out any kind of memory operation, here we specify the hex file to be flashed. We will discuss this in detail later.

In-circuit serial programming(ICSP)

AVR ICSP (In-Circuit Serial Programming) is a method used to program AVR microcontrollers directly on a circuit without removing them. It uses the SPI interface (MISO, MOSI, SCK) to upload firmware, enabling faster and more efficient development and debugging.

I highly recommend using external ICSP programmer. Here are list of reasons which makes ICSP programmer better than arduino bootloader:

  1. Allows programming of fuse bits.
  2. Works without pre-loaded bootloader.
  3. Faster than bootloader-based uploads.

There are many ICSP programmer, I am going to use the USBasp and you can get one too for under $10. Finally you can make use of the ICSP Pin header in arduino board.

If you dont want to manual wiring, you can get a AVR-ISP 10Pin to 6pin Adapter

Now lets finally upload the code, this is the command for that. The only change is the -c flag as we are using usbasp

avrdude -F -c usbasp -p atmega328p -U flash:w:main.hex

If you see output like this, congrats!!!! You just progrmmed your Arduino uno without the Arduino IDE.

Writing 166 bytes to flash
Writing | ################################################## | 100% 0.23 s 
Reading | ################################################## | 100% 0.13 s 
166 bytes of flash verified

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

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)