5.2.1. Guideline for driver development

5.2.1.1. Introduction

This document describes an example procedure to develop a driver for EGOS. This document assumes that the reader already has knowledge of what an Operating System (OS) is, and understands the basics of developing with EGOS.

The following document takes the driver for the AX-SFEU Sigfox communication device as an example.

An important note is that a driver is a process. It receives messages sent by the application of the end user. It may also have one or more task depending on the complexity of the driver and the needs in responsiveness.

5.2.1.2. File structure

The naming of each file of the driver follows an important naming convention :

Text in {} is optional, text in <> is required and depends on the file and the driver:

{egos_}drv{_'driver type'{_'subtype'{_'technology'{__impl_'module name'}}}}

Examples:

  • driver type: "com" for communication
  • subtype: "at", "lpwan", "lwan", etc.
  • technology: "sigfox", "lora", etc.
  • module name: AX SFEU (sigfox module name)

Which gives us the following: egos_drv_com_lpwan_sigfox__impl_ax_sfeu.c

If the file is prefixed with egos_, it is a public files accessible to the end user.

For example, the common header file for all the lpwan devices is drv_com_lpwan.h whereas the c file for the specific implementation for the AX-SFEU is drv_com_lpwan_sigfox__impl_ax_sfeu_.c.

Here is the file architecture :

  • The egos_err__code.h file contains the error codes sent by the driver.

  • The egos_drv_com_lpwan_sigfox.h file defines every structures and enumerations available for the user. It contains the messages the drivers can send, the API signals, the API configuration, the API functions, the API's Send structure and the name of the Sigfox process common for all the Sigfox module.

  • The drv_com_lpwan.h file is a header inclusion file needed for the LPwan process and contains function prototypes, constants, structures, etc common to the different LPwan

  • The drv_com_lpwan_.c file is used to launch the process corresponding to the driver.

  • The drv_com_lpwan_sigfox.h file lists all the possible Sigfox chip implementation through their header files and contains function prototypes, constants, structures, etc common to the different Sigfox implementations

  • The drv_com_lpwan_sigfox_.c file is used to select the right Sigfox chip implementation and contain chip independent Sigfox code.

  • The drv_com_lpwan_sigfox__impl_ax_sfeu.h file defines the states of the driver, the internal error codes of the driver and the driver's API prototypes specific to this device and this implementation. It also contains specific structure definitions like the chest of the driver.

  • The drv_com_lpwan_sigfox__impl_ax_sfeu_.c file is the main file for the implementation, it contains the local functions of the driver (like opening and closing the communication channel to the chip), the definition of the driver's process, the definition of the API's functions.

Additionally, if the use of the mapper is needed, two more file have to be completed :

  • The drv_com_lpwan_sigfox_impl_ax_sfeu__map.cxx file abstracts the list of the AT commands to be sent to the Sigfox module

  • The drv_com_lpwan_sigfox_impl_ax_sfeu__reg.h file abstracts the list of registers of the Sigfox module

Another usefull file is egos_types.h. It contains the definition of all the base data types of the OS. Most of the types used in the AX-SFEU driver are basic types or structures containing basic types :

  • nUX : unsigned number, with X (multiples of 8) the number of bits (ex : nU8 a byte, nU32 4 bytes...)

  • bB1 : boolean, binary data

  • nSX : signed number, with X (multiples of 8) the number of bits (ex : nU8 a byte, nU32 4 bytes...)

Using the macros DECL_TYPEDEF('type', 'type name') and DECL_INSTANCE('type', 'type name'), with 'type' being struct, enum, union..., allows the definition of a structure, enum, union... and also defines all the necessary subtypes (like pointers, constants, volatile ....).

For example :

DECL_TYPEDEF(struct, sExampleStruct)
{ nU32 nField1
; nU32 nField2
; nU64 nField3
;} DECL_INSTANCE(struct, sExampleStruct);

defines the sExampleStruct type, a structure with 3 fields, but also the psExampleStruct type (for pointer to a sExampleStruct), the vsExampleStruct type (for volatile sExampleStruct), the csExampleStruct type (for const sExampleStruct). Some of the most used prefix combinaison are also defined : pcsExampleStruct, cpsExampleStruct, pvsExampleStruct, ppsExampleStruct... The order of the prefix is important, and the meaning is easily understood by just reading the prefixes from left to right (c for constant, p for pointer to, v for volatile, etc...).

Finally, if you need to connect a new HW component to your C, follow this guide :

How to use your shield with EGOS

5.2.1.3. Useful functions and APIs

There are several functions, macros and APIs that may be needed for the development of a driver:

  • mpOSS_EXE__CHEST : This macro returns the pointer to the current process' chest

  • mEGOS_EXE__THIS_DECL : This macro is used to transmit This pointer inside functions

  • mOSS_EXE__CURRENT : This macro returns the current task/process' handle

  • mOSS_EXE__PROC : This macro defines a new process

  • mOSS_EXE__STARTED : This macro returns the current process' reason of start

  • mOSS_MSG__RECEIVE : This macro starts the message loop (hides a switch/case)

  • mOSS_MSG__ID : This macro returns the unique value associated to a message and a message type

  • mOSS_MSG__EXIT : This macro exits from message loop (acts like a break from the switch/case)

  • mOSS_MSG__LOOP : This macro ends the message loop (closes the switch/case)

  • mOSS_MSG__SEND : This macro sends a message to a process with a specific ID

  • mOSS_MSG__SEND_DATA : This macro sends a message to a process with a specific ID and with data

  • uErr_Code (hal_com_Transmit)(sHalCom_ApiTransmit api) : This function sends data through the selected channel

  • uErr_Code (hal_com_Transfer)(sHalCom_ApiTransfer api) : This function sends data through the selected channel and read back data

  • uErr_Code (hal_com_Open)(sHalCom_ApiId i_sId) : opens a HAL communication channel

  • uErr_Code (hal_com_Close)(sHalCom_ApiId i_sId) : closes a HAL communication channel

  • uErr_Code (hal_com_Enable)(sHalCom_ApiEnable i_sEnable) : enables the HAL communication channel

  • uErr_Code (hal_com_Disable)(sHalCom_ApiId i_sId) : Disables the HAL communication channel

5.2.1.4. General steps

Here are the following steps to write a driver for the OS.

5.2.1.4.1. Step 1 : Know your hardware

This step is pretty obvious but is also necessary. Learn what are the possibilities and functionalities offered by the component you are writing a driver for. Useful information are the commands and their format (text, value...), timings, if the commands are synchronous or asynchronous. Knowing if the driver is for an external or internal element is also important, as it will dictate how you communicate with it.

5.2.1.4.2. Step 2 : The API

With Step 1 in mind, determine the API you want to give to the end user. An end user does not need all the available functionalities a chip has to offer. For example, he or she does not need to use the GPIO possibilities for a communication device. Generally, an end user just needs to access the core functionalities of a device.

5.2.1.4.3. Step 3 : The data

Determine the data you need to store and how (structure, types...). Choose a type that makes sense, for example if the device has a serial ID in hexadecimal format (ie "0011223344556677", a string of 16 bytes), it may make more sense to store that serial ID as a number (0x0011223344556677 an int, or nU64, on 8 bytes) even if you read it as an array or as a string. Do not worry about conversion yet.

For example, the Sigfox protocol uses a 4 bytes long ID, and the AX-SFEU transfers this ID over UART as a string of 8 bytes corresponding to the hexadecimal characters of the ID. The ID is received as a 8 bytes arrays, but is stored as a 4 bytes number, because it makes more sense since it is a number. It also allows easier comparisons and uses a little less RAM to store.

5.2.1.4.4. Step 4 : The mapper

Learn how to use the mapper. If your module responds to simple commands, or commands with a predetermined pattern, the mapper can abstract everything for you. It also takes care of all the conversion you might need in Step 3. Using the mapper, you don't really need to care about how the commands are sent and received, just specify the types, conversions, etc, and focus on the logic of the driver.

For more in depth explanation of the mapper, see Mapper configuration and use

5.2.1.4.5. Step 5 : Blocking sequence

Once the mapper is understood, look for blocking sequence when using the mapper (i.e. sending commands). If those sequences are too long and may interfere with the responsiveness of the driver, consider using a task for the communication with the device. The role of the main driver process will then be to process commands from the user, and if a communication with the device is needed, the process sends a message with the command to the task. Doing so removes too long blocking sequence from the process, letting it react a lot faster. Keep in mind that using a task uses a lot of stack and should be avoided if possible. If timing is never an issue, using a task is not advised.

Due to ongoing developments, the use of a task is not advised in any case. If blocking sequences exists and can hinder the driver to function optimally, prefer the use of a child process instead of a child task

5.2.1.4.6. Step 6 : Specification document

With these pieces of information, start a specification document. It should contain at least the sequence diagram for each of the API in nominal use, the sequence diagram of the initialization phase, the state machine for the process (and the task is one is necessary), error codes, data types, data structure. Once this effort is done, it will greatly reduce the development time. A more precise document means a better development, but unless you know perfectly how your device work and how the OS behave, do not spend too much time on writing the perfect specification document.

5.2.1.4.7. Step 7 : The structure

Start a first version of the driver, go for the general structure and the reception and the sending of messages first between the user and the driver. Here follows some recommendations :

The OS is based on the ability to send messages, so the message loop might be the core of the driver. This kind of driver might also have a state machine. Meaning that there could be a situation where for each type of message received, there is a different behavior for each state of the driver. If this is the case, the driver might have the following structure :

    mOSS_MSG__RECEIVE()
    {
        case mOSS_MSG__ID(msgid1)
        {
            state machine
            {
                case state1
                case state2
                ...
            }
        }

        case mOSS_MSG__ID(msgid2)
        {
            state machine
            {
                case state1
                case state2
                ...
            }
        }
    }
    mOSS_MSG__LOOP()

or

    state machine
    {
        case state1
        {
            mOSS_MSG__RECEIVE()
            {
                case mOSS_MSG__ID(msgid1)
                case mOSS_MSG__ID(msgid2)
                ...
            }
            mOSS_MSG__LOOP()
        }

        case state2
        {
            mOSS_MSG__RECEIVE()
            {
                case mOSS_MSG__ID(msgid1)
                case mOSS_MSG__ID(msgid2)
                ...
            }
            mOSS_MSG__LOOP()
        }
    }

Those code structure cover all the possible situations and allows a different treatment for each situation, and should be equivalent. It is just a matter of preference to work on the message or on the state. Both of these structure generate a lot of raw code, a lot of duplicate code and might be hard to maintain.

Most of the times, some messages do not care about the state of the driver or some states do not use messages at all, so a structure like the following might be enough :

    mOSS_MSG__RECEIVE()
    {
        case mOSS_MSG__ID(msgid1)
        {
            //No state machine related processing
        }

        case mOSS_MSG__ID(msgid2)
        {
            state machine
            {
                case state1
                case state 2
                ...
            }
        }
    }
    mOSS_MSG__LOOP()

    state machine
    {
        case state1
        {
            //No message processing
        }

        case state2
        {
            switch (msgid)
            {
                ...
            }
        }
    }

This structure is less intensive likewise, covers less possibilities but better separates the state machine and the message reception.

Once the global structure is created, the instructions of your module will be sent through a communication port.

Opening a communication channel is done with the hal_com_Open and hal_com_Enable function, and closing it is done with the hal_com_Disable and hal_com_Close functions.

    l_uResult
    = hal_com_Open
      (.i_eId = cDRV_YOUR_DRIVER__CFG__COM
      );

    if
    ( cERR__CODE__OK._ == l_uResult._
    )
    // Communication created
    {
        l_uResult
        = hal_com_Enable
          (.i_eId   = cDRV_YOUR_DRIVER__CFG__COM
          );
        //check if com is enabled
        if
        ( cERR__CODE__OK._ ==       l_uResult._
        )
        {   //everything is ok


            //sending your instructions
            mpDRV_YOUR_DRIVER__MAP(INSTRUCTION1);

            mpDRV_YOUR_DRIVER__MAP(INSTRUCTION2);

            ...;

            mpDRV_YOUR_DRIVER__MAP(INSTRUCTIONX);

            do
            // check if the communication is done
            {
                l_uResult
                = hal_com_IsDone
                  ( .i_eId = cDRV_YOUR_DRIVER__CFG__COM
                  );

            } while (cERR__CODE__OK._ != l_uResult._);

            }
            else
            { //com opening is ok but not the enabling

            }

            //Disable communication
            (void)  hal_com_Disable
                    ( .i_eId = cDRV_YOUR_DRIVER__CFG__COM
                    );
    }
    else
    { //com opening is not okay

    }

    //close communication
    (void)  hal_com_Close
            ( .i_eId = cDRV_YOUR_DRIVER__CFG__COM
            );

For correct resources sharing and power management, each communication (ie a set of instruction) SHALL be surrounded by the opening and the closing of this port

5.2.1.4.8. Step 8 : Specification Documentation - Second Pass

Iterate on the specification document. Some of the mechanisms of the OS might work differently than expected, and modify the specification accordingly.

5.2.1.4.9. Step 9 : The functionalities

Now that the base structure is written, there are two possibilities : either you really know how the OS works and all the dependencies your driver is going to need, or you are not sure.

In the first case, start by writing the test campaign for each API, each function of the driver. Then implement the first API or function and test it. Corrects bugs if needed. Continue with the rest of the functions/API the same way, and always tests the whole driver for each tests to make sure you didn't break previous work.

In the second case, start by implementing the first function/API. When it is ready, write the test campaign for this API/function only then test it. Correct bugs if needed. Now that you have a first grasp of the development of a driver, you can write the rest of the test campaign and implement and test the rest of the driver.

If the test campaign was too difficult to implement, it mean the specification document was not good enough, iterate again, change sequence diagrams if needed, until the tests can be performed easily and pass.

5.2.1.4.10. Step 10 : Finishing

Ask someone to review your code, take action on the remarks.

At this point the driver should be finished and fully operational.