The Milos driver architecture is just a way to keep track and organization of running device drivers. The main goal is to provide a clear interface between driver software from Devices and the platform hardware.
The Devices module exposes a number of functions to manage drivers through a common data structure. This structure also acts as a handle to call these functions and access the device. This way can be separated the functionality of the driver from the OS and the applications while keeping the code open for other platforms. This also gives the foundation concept that a driver can be installed/opened/closed at runtime.
Basically, all the drivers must be declared as a __DEVICE structure. This structure contains pointers to functions to be accessed by the application, and a function pointer to provide platform-dependent code functionality. This ensures application<---->driver<---->platform relationship.
This relationship takes place at four development levels:
Let's take for example the development of a driver for an UART:
1- From the application-developer point of view, the developer wants to access the devices through standard functions like __deviceOpen(), __deviceSize(), __deviceRead(), etc. This guarantees (at minimum) compatibility among applications running on different platforms.
2- From the driver-developer point of view, a direct link to the platform hardware is provided through the __serialPlatIoCtl() function (the __DEV_PLAT_IOCTL dv_plat_ioctl member of the __DEVICE structure).
3- So when an application calls __deviceFlush() the Devices module routes the call to __serialFlush(), that generates a call to __serialPlatIoCtl() with the __SERIAL_PLAT_INIT_TX code as parameter. __serialPlatIoCtl() will execute the IO control code, for example, enabling the TX interrupt (platform-dependent). Here, at platform level, is where the interrupt-level code, pin configuration, etc, should be done.
4- So we have a driver that works well with a specific platform. But each platform may have it's variants, for example an established and already working UART driver for the stm32 that has to be mapped to different TX and RX pins for a specific board. Touching the code on the platform implementation or application may break compatibility. So there is a board directory inside the platform directory dedicated to enumerate and configure the available devices the platform---->driver--->application can use, for each board variant.
In the plat_config.h configuration file there is a constant that declares the board file to be included in the compilation.
For every specific driver functionality that the device module functions do not apply, there is the __deviceIOCtl() function. This functions may implement any custom functionality that cannot be acceded with standard Device Functions. IOCTL codes can be found in the ioctl.h file. Most of the IOCTL codes definitions can be shared between drivers. For instance, the __IOCTL_SETRXTIMEOUT code can be used in the SPI and Serial device drivers. If the driver implementation requires a new IOCTL code, it should be appended at the end of the list, in the ioctl.h file.
Some drivers may require the compilation of an underlying driver. That is the case of the SD Card driver, that uses the SPI driver. Drivers can be "logically stacked" one on top of the other. So if the SPI MOSI and MISO pins changes on the board because of a design change, it is just enough to change the (or make a new) file for the board. This way the SD Card driver nor the SPI driver will not suffer any changes.
To create a device in order to be used with the Devices functions, fill the required fields of the __DEVICE structure. For example, the serial port driver is defined in the following way:
__STATIC __DEVICE serialDevices = { "serial1", /* Name */ __DEV_USART, /* Type */ 0, /* Flags */ BOARD_UART1_IRQ, /* RX interrupt number */ BOARD_UART1_IRQ, /* TX interrupt number */ 0, /* Initialized flag */ 0, /* Opened flag */ &serialTxEvt, /* TX event */ &serialRxEvt, /* RX event */ __NULL, /* Owner thread */ &serialPdb, /* Private data block */ &uartParams, /* Hardware/custom parameters */ __NULL, /* Pointer to next device driver */ /* Device interface functions */ __serialInit, /* Initialize */ __serialDeinit, /* De-initialize */ __serialIOCtl, /* IO control */ __serialOpen, /* Open */ __serialClose, /* Close */ __serialRead, /* Read */ __serialWrite, /* Write */ __serialFlush, /* Flush */ __serialSize, /* Query RX-TX buffer size */ __serialPlatIoCtl, /* Platform-related IO control */ }
To add a driver to the available list of drivers call the __deviceAdd() function and pass the above structure to the function. This could be done (the recommended way) inside the __boardInitHW() function, called from __cpuInitHardware() at the moment of the RTOS initialization, or at any time. That will add the driver to the list, and to obtain a pointer to the driver call the __deviceFind() function passing the name of the device (i.e. "serial1").
This way an application can obtain a pointer to a "serial1" device no matter where it is on the board. It will always be "serial1". And it opens to the possibility of adding device drivers at runtime through a sort of "plug-and-play" mechanism.
This schema can also be useful to replace a driver by another. Take a look at the Debug Terminal (the default terminal if enabled) configuration in the global_config.h file. There are macros to define the input device and the output device. One can compile the Debug Terminal to use the serial port as the input AND output device (the default). Try to change the output device to "charlcd1" (and the __CONFIG_DBGTERM_OUT_DEVICE_INIT_MODE and __CONFIG_DBGTERM_OUT_DEVICE_OPEN_MODE constants to fit the Character LCD initialization). The Debug Terminal will use __deviceRead() with the Serial driver and __deviceWrite() with the Character LCD driver. The result is that the output of the Debug Terminal will be displayed in the Character LCD, even the key pressings made through the serial port.
The above could be applied if for example there is a GPS model that uses, let's say, SPI instead of a serial port. One can change the platform default values to initialize the SPI Driver instead of the Serial driver and the GPS driver would not notice the difference.
To use the device just call __deviceInit() and pass the pointer the __deviceAdd() function returned. If __deviceInit() returns __DEV_OK, call __deviceOpen() to open the device. From now on the device can be acceded with the rest of the device functions.
This is the list of the Devices module functions that can be called passing a __DEVICE structure pointer:
Some drivers will need custom parameters on initialization to work. For example the Serial buffers size or if the Character LCD driver will use a blinking cursor or not. All of these parameters can be given some when calling __deviceInit() and others when calling __deviceOpen(). If no parameters are given at all, the driver will ask the platform for default values.
For example, the SD driver will need an underlying device to work with (SPI or future SDIO). Usually the SD slot will be attached to a fixed SPI device, so it is useless for the app developer to "guess" where it could be. So, when calling __deviceInit() and __deviceOpen() without parameters, the SD driver will ask the platform to initialize a proper driver with the __SD_PLAT_INIT_DEFAULTS IO control code. Or, if there is an exception to this case, the app developer can initialize a SPI driver (or any driver) and pass it to the SD __deviceInit() function.
Other case could be that of the Serial driver. For example when used with the GPS driver, it sure will need more buffer space than when used with the Debug Terminal (because the NMEA string could be longer than the length of a command through the terminal command line). In that case, the GPS driver will ask the platform to initialize a proper Serial driver, and knowing the GPS platform part which serial driver will it use, it will initialize the proper Serial driver with the required buffer space.
All the drivers, from the version v0.3.3b work this way. They will ask the platform for parameters when no parameters are passed at __deviceInit() or __deviceOpen() (by calling the platform IO control function or directly by using macros). This way we free the app developer from thinking about peripherals location and/or initialization; the platform is the first responsible in providing the drivers the required information, and can be overridden by the app developer parameters.