2 Key Concepts of MPLAB Harmony v3 Drivers
Atomicity
Once started, an operation (sequence of instructions) is considered atomic, if it is indivisible and non-interruptible. A data item is considered atomic if it cannot be subdivided as it is read or written.
Atomicity is necessary for shared data or instructions that need to have exclusive access of the CPU. The sequence of instructions that needs to be protected from shared access is called a critical code section, and the data that needs to be protected from shared access is called critical data.
Atomicity is achieved by guarding the critical code or critical data. One of the approaches to guard the critical regions is to use a lock, which is set before accessing the shared resource and then released when done.
Interrupt Safety
A sequence of code is said to be “interrupt-safe” when the occurrence of an interrupts does not alter the output or functional behavior of the code. To be “interrupt-safe”, the sequence of code in consideration must be atomic (indivisible and uninterruptible), and the relevant interrupts must be disabled before entering the sequence.
In MPLAB Harmony v3, interrupts can be disabled globally by using the
Interrupt System Service API, that is, SYS_INT_Disable()
can be
enabled by using the API, SYS_INT_Enable()
and can restore the saved
state by using the API, SYS_INT_Restore()
. However, MPLAB Harmony
v3 libraries are modular and by convention they respect the abstractions of other
libraries, never attempting to directly access their internal resources. Due to this
convention, the only code in the system that should ever attempt to access the
internal resources owned by a driver, is the driver code itself.
This means it is not necessary to globally disable interrupts in most cases to guarantee correct and reliable operation of a MPLAB Harmony v3 driver. While performing a non-atomic access to data structures or peripheral hardware, a driver temporarily masks the interrupts of the peripheral it owns. This prevents the driver’s own interrupt-driven tasks functions from potentially corrupting data that is also accessed by the driver’s interface functions.
This can be done using the interrupt system service and it is efficient than globally disabling all the interrupts because it allows higher priority interrupts (which do not affect the driver) to occur, protecting their response time latency. This can be done using the Interrupt System Service, as shown in the following example.
// Disables the interrupt for SPI0.
Bool spiIntStatus; = false;
spiIntStatus = SYS_INT_SourceDisable(SPI0_IRQn);
// Restore the interrupt for SPI0.
SYS_INT_SourceRestore(spiIntStatus, SPI0_IRQn);
These functions can also be used to guard non-atomic accesses by the driver’s interface functions to resources that are shared with the driver’s ISR. This method works to ensure safe access to a shared resource without disabling interrupts globally when using an Asynchronous driver with bare-metal configuration.
If a section of code must be truly atomic (uninterruptible), interrupts can be globally disabled for a short period of time using the Interrupt System Service functions, as shown in the following example.
Bool interruptState;
// Save global interrupt state and disable interrupt
interruptState = SYS_INT_Disable();
// Critical Section
// Restore interrupt state
SYS_INT_Restore(interruptState);
Thread Safety
In an RTOS-based environment, it is possible that a driver and its clients may each run in its own RTOS thread. If the RTOS is preemptive, it is possible that the scheduler may interrupt any of these threads at any time and switch to another. If the other thread happens to access the same shared resource or execute the same critical section of the code, that section of the code must be guarded and made atomic. The sequence of code with such guards is known as “thread-safe” code. In the MPLAB Harmony v3 framework, thread safety is achieved by using the methods provided by the MPLAB Harmony Operating System Abstraction Layer (OSAL).
MPLAB Harmony v3 drivers run efficiently in an interrupt driven system. For an interrupt driven system running in an RTOS-based environment, it is common for the driver’s task functions to be called from an interrupt context. A mutex can be used to guard against simultaneous access to shared resources by different threads. However, this will not prevent an ISR from accessing the shared resources or critical code. If a mutex is used to accomplish thread safety, it must be augmented by temporarily disabling (masking) the associated interrupt source as described for a bare-metal environment in the Interrupt Safety section above.
Callback Functions
In the Non-Blocking method, instead of status polling, the callback mechanism can also be used to check the transfer status. For example, the application registers a callback function with a driver and makes the transfer request. The driver calls back the registered function on the completion of the request submitted by the driver client.
MPLAB Harmony v3 provides driver specific APIs to register the callback
functions. For example, the Asynchronous SPI driver provides API,
DRV_SPI_TransferEventHandlerSet()
, this allows a client to set
a transfer event handling function for the driver to call back when a queued
transfer has finished.
- Must be treated like an ISR
- Must be short
- Do not call application functions that are not interrupt safe
- Do not call other driver’s interface functions
Blocking API
A blocking API hangs up execution flow until it has performed its function and returns a result.
For example, the following code shows the I2C driver
Synchronous mode API, DRV_I2C_WriteTransfer
.
DRV_I2C_WriteTransfer(app_eepromData.i2cHandle, APP_EEPROM_I2C_SLAVE_ADDR, (void *)app_eepromData.i2cTxBuffer, 2);
The execution flow blocks when the API,
DRV_I2C_WriteTransfer()
is called until the requested bytes are
transferred to the EEPROM.
Non-Blocking API
A non-blocking API receives the application request and returns immediately without providing the result. The result of the non-blocking API call is provided separately through an Asynchronous event. The application verifies the event to take further action.
The following code example shows the usage of the I2C driver
Asynchronous mode API, DRV_I2C_WriteReadTransferAdd()
,
which is a non-blocking API.
void APP_EEPROM_I2CEventHandler ( DRV_I2C_TRANSFER_EVENT event, DRV_I2C_TRANSFER_HANDLE transferHandle, uintptr_t context)
{
switch(event)
{
case DRV_I2C_TRANSFER_EVENT_COMPLETE:
/* I2C Transfer Complete. */
app_eepromData.reqStatus = APP_EEPROM_REQ_STATUS_DONE;
break;
case DRV_I2C_TRANSFER_EVENT_ERROR:
app_eepromData.reqStatus = APP_EEPROM_REQ_STATUS_ERROR;
break;
default:
break;
}
}
int main ( void )
{
/* Initialize all modules */
……
/* Open the I2C Driver */
app_eepromData.i2cHandle = DRV_I2C_Open( DRV_I2C_INDEX_0, DRV_IO_INTENT_READWRITE );
if(app_eepromData.i2cHandle == DRV_HANDLE_INVALID)
{
app_eepromData.state = APP_STATE_ERROR;
}
else
{
/* Register I2C transfer complete Event Handler for EEPROM. */
DRV_I2C_TransferEventHandlerSet(app_eepromData.i2cHandle, APP_EEPROM_I2CEventHandler, 0);
/* Submit I2C transfer to read stored temperature values from EEPROM. */
DRV_I2C_WriteReadTransferAdd(app_eepromData.i2cHandle,
APP_EEPROM_I2C_SLAVE_ADDR, app_eepromData.i2cTxBuffer, 1,
app_eepromData.i2cRxBuffer, 5, &app_eepromData.transferHandle);
/* Display the read temperature on console */
}
……
}
In the main function, the application registers a callback function (event handler
“APP_EEPROM_I2CEventHandler”
) with the I2C driver.
The control immediately returns after the submission of the I2C request,
calling the API DRV_I2C_WriteReadTransferAdd().
The application is
notified by the driver on the completion of the request through an event to the
callback function (event handler).
Synchronization
MPLAB Harmony v3 provides driver interface functions having both a blocked and non-blocking usage model. There are situations when developers expect a blocking model. For example, the file system style read and write functions block and not return until the entire transfer has completed.