How to write a driver for the next I2C

So, you were given a board, and it contains 4 heaped smart peripheral chips with their own internal SPI / I2C configuration registers. It can be such chips as si4703, tic12400 or drv8711. Doesn’t matter. Let’s say that there are no drivers for your I2C / SPI chip on GitHub or the quality of these open source drivers leaves much to be desired. How to assemble a high-quality driver for an I2C/SPI chip?

It is clear that it is necessary that the driver be modular, supported, test fit, diagnosable. First of all, you need to understand how to organize the structure of files with the driver. It can be done like this.

*.c/*.h files with the functionality itself.*

There must be an initialization function, a handler in a loop, checking Link (a). Functions for reading and writing register by address. Plus a set of high-level functions for setting and reading specific parameters. This is the minimum-minimum at which most developers lay down their hands. Next comes the advanced level material.

*.c/*.h Separate files with driver code that should be processed in the interrupt handler

This is to emphasize the fact that this ISR code should be treated with special caution. That this code should be optimized for speed, that this code itself should not cause other interrupts.

Separate *.h file listing types*

In this file, you should define the main data types for this software component. Also define unions and bitfields for everyone register.

Separate *.h file listing constants*

Here it is necessary to define the addresses of registers, enumerations. It is very important to quickly find a file with constants and edit them, so we make a separate *.h file for constants.

*.h file with driver parameters*

Each driver needs non-volatile parameters with settings. It is these settings that will be applied during initialization at power start. You must specify at least the data type and parameter name somewhere

#ifndef SX1262_PARAMS_H
#define SX1262_PARAMS_H

#include "param_drv.h"
#include "param_types.h"

#ifdef HAS_GFSK
#include "sx1262_gfsk_params.h"
#else
#define PARAMS_SX1262_GFSK
#endif

#ifdef HAS_LORA
#include "sx1262_lora_params.h"
#else
#define PARAMS_SX1262_LORA
#endif

#define PARAMS_SX1262       \
    PARAMS_SX1262_LORA      \
    PARAMS_SX1262_GFSK      \
    {SX1262, PAR_ID_FREQ, 4, TYPE_UINT32, "Freq"},   /*Hz*/                              \
    {SX1262, PAR_ID_WIRELESS_INTERFACE, 1, TYPE_UINT8, "Interface"},    /*LoRa or GFSK*/          \
    {SX1262, PAR_ID_TX_MUTE, 1, TYPE_UINT8, "TxMute"},                                        \
    {SX1262, PAR_ID_RX_GAIN, 1, TYPE_UINT8, "RxGain"},         \
    {SX1262, PAR_ID_RETX, 1, TYPE_UINT8, "ReTx"},              \
    {SX1262, PAR_ID_IQ_SETUP, 1, TYPE_UINT8, "IQSetUp"},                                         \
    {SX1262, PAR_ID_OUT_POWER, 1, TYPE_INT8, "OutPower"}, /*loRa output power*/          \
    {SX1262, PAR_ID_MAX_LINK_DIST, 8, TYPE_DOUBLE, "MaxLinkDist"}, /*Max Link Distance*/ \
    {SX1262, PAR_ID_MAX_BIT_RATE, 8, TYPE_DOUBLE, "MaxBitRate"}, /*Max bit/rate*/   \
    {SX1262, PAR_ID_RETX_CNT, 1, TYPE_UINT8, "ReTxCnt"},


#endif /* SX1262_PARAMS_H  */

*.c/*.h file with default configuration*

After starting the power supply, you need to somehow initialize the driver. To do this, we create separate files for the default configs. It promotes the “code separately configs separately” methodology

*.c/*.h file with CLI commands*

Each adult component must have a pen for management. In the world of computers, historically this handle has been the Command Line Interface (CLI) on top of the UART. Therefore, we create separate files for the command interpreter for each specific driver. So it will be possible to change the logic of the driver in Run-Time. Subtract raw register values, prescribe a specific register. Show diagnostics, serial number, revision, bullet packets in I2C, SPI, UART, MDIO, etc.

*.c/*.h files with diagnostics*

Each driver has a bunch of constants. These constants must be interpreted into human strings. Therefore, a file is created with Hash functions. The point is simple. You give binary value of a constant, you receive its value in the form of a line. These Hash functions are just called by CLI (shka) and logging.

const char* DacLevel2Str(uint8_t code){
    const char *name="?";
    switch(code){
    case DAC_LEV_CTRL_INTERNALY: name="internally"; break;
    case DAC_LEV_CTRL_LOW:       name="low"; break;
    case DAC_LEV_CTRL_MEDIUM:    name="medium"; break;
    case DAC_LEV_CTRL_HIGH:      name="high"; break;
    }
    return name;
}

*.c/*.h Files with unit tests and diagnostics*

The driver should be covered by unit tests. This will allow you to safely rebuild the code in order to simplify it. Tests are needed to debug a large piece of code that is difficult to step through with a step-by-step debugger. Tests will allow faster integration. They will help to understand what is broken in case of errors. Those. Tests save debugging time. Tests will encourage you to write better and more structured code.

Make *.mk file for driver assembly rules from Make*

Build from Make it most powerful way to manage modularity and scalability any code. With make, you can selectively build the driver based on the available resources on the PCB. The code will become universal and portable. When building from Makefile(s), you must manually define a makefile for each logical component or driver. Make is a whole programming language with its own operators and functions.

ifneq ($(SI4703_MK_INC),Y)
    SI4703_MK_INC=Y

    mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
    $(info Build  $(mkfile_path) )

    SI4703_DIR = $(WORKSPACE_LOC)Drivers/si4703
    #@echo $(error SI4703_DIR=$(SI4703_DIR))

    INCDIR += -I$(SI4703_DIR)

    OPT += -DHAS_SI4703
    OPT += -DHAS_MULTIMEDIA
    RDS=Y

    FM_TUNER=Y
    ifeq ($(FM_TUNER),Y)
        OPT += -DHAS_FM_TUNER
    endif
    
    SOURCES_C += $(SI4703_DIR)/si4703_drv.c
    SOURCES_C += $(SI4703_DIR)/si4703_config.c

    ifeq ($(RDS),Y)
        OPT += -DHAS_RDS
        SOURCES_C += $(SI4703_DIR)/si4703_rds_drv.c
    endif

    ifeq ($(DIAG),Y)
        ifeq ($(SI4703_DIAG),Y)
            SOURCES_C += $(SI4703_DIR)/si4703_diag.c
        endif
    endif

    ifeq ($(CLI),Y)
        ifeq ($(SI4703_COMMANDS),Y)
            OPT += -DHAS_SI4703_COMMANDS
            SOURCES_C += $(SI4703_DIR)/si4703_commands.c
        endif
    endif

endif

This is how the driver code should look like in the project folder

With structure of the driver were defined. Now just a few words about the functionality of the generic driver.

1–Must be initialization of the chip.

2–Each driver should have counters of various events: the number of sends, receptions, error counters, interrupts. This is needed for the health monitor procedure. So that the driver periodically checks itself for the accumulation of errors and, if detected, can display red text in the log (UART or SD card).

3–Each driver must have a subtraction function all raw registers at once memory blob. This will allow you to visually compare the configuration with the datasheet(s).

4–There must be a mechanism to continuously check the link(s). This will immediately determine the problem with the wires, if this occurs.

5–There must be a mechanism for writing and reading individual registers from the command line over the UART.

6–There must be a xxxxx_proc() function to interrogate the chip’s registers and variables. This function will synchronize the chip’s registers and their reflections in the microcontroller’s RAM memory.

7–Must be diagnostics chip. Ideally, a built-in register interpreter. Each bit that means at least something in the register map of the microcircuit. Or, if there is no NorFlash(a), a separate PC utility for parsing memory blob(a). Like this: https://github.com/aabzel/tja1101-register-value-blob-parser Since it is very difficult to visually analyze variables looking at the stream of zeros and ones and you can easily make a mistake. All this will be needed for maintenance and debugging.

Conclusion

This is the basis of any driver. The rest of the code depends on the specific chip. As you can see, in order to write an adequate driver, you need to take into account quite a lot of nuances. Feel free to split the driver into multiple files. This will then help with custom (mization), porting and packaging the driver into different projects with different resources.

If you have comments on what should be the functionality of a generalized driver for a peripheral I2C / SPI / MDIO chip, then write in the comments.

Similar Posts

Leave a Reply