Peripheral handles - IntergatedCircuits/STM32_XPD GitHub Wiki
In the XPD drivers, handles are the central reference that facilitate the global control of a peripheral instance. They achieve this by holding references to:
- The address of the peripheral register space (including a bit-band alias address, where applicable)
- The peripheral's clocking control function
- Lower-level dependencies (e.g. NVIC interrupt lines, pins, DMA channels) setup and teardown callouts
- User-subscribed callback functions
- DMA handles for each transfer request
Additionally the handles may contain certain (e.g. data transfer) state information in order to provide a more integrated API.
Using the handle-based API calls to control the peripherals allows the programmer to have two distinct layers of firmware code, one being the device pinout specific board support layer, the other being the peripheral-user application.
The following chapters will introduce the reader to the proper use of XPD peripheral handles, demonstrating all common use cases step-by-step through a simple timer example.
A handle itself is a global variable which should always be initialized using the peripheral specific macro:
/** @file bsp.c */
#include <xpd_tim.h>
TIM_HandleType demoTmr;
void DemoTmr_Bind(void)
{
TIM_INST2HANDLE(&demoTmr, TIM1);
}The TIM_INST2HANDLE() macro sets the necessary handle variables to the values that correspond to the TIM1 peripheral instance, making the handle variable ready for use by the application. The parameter of this macro is always the peripheral instance itself, and it should always be written without macro aliasing (i.e. not allowed to define TIMx as TIM1, then passing TIMx as parameter here). It is also necessary to set the handle's Callbacks.DepInit and Callbacks.DepDeinit function pointers before the handle is used. The former shall be set to the dependency setup callback function reference (invoked during TIM_vCounterInit()), while the latter is the teardown one (invoked during TIM_vDeinit()). In case the peripheral doesn't have any dependencies (such as controlled I/O pins or interrupt lines), these fields can be set to NULL, and no callbacks will occur.
Now that the handle has been defined in a (board-specific) source file, the peripheral can be managed by the application code. The below code demonstrates how a timer can be used in polling mode to periodically execute a specified function each millisecond.
/** @file app.c */
#include <xpd_tim.h>
extern TIM_HandleType demoTmr;
extern void DemoTmr_Bind(void);
int main(void)
{
TIM_InitType timerSetup = {
.Mode = TIM_COUNTER_UP,
/* Multiply Prescaler and divide Period with x > 0
* in case Period value exceeds timer resolution */
.Prescaler = 1,
};
DemoTmr_Bind();
/* Period = timer frequency / desired update frequency */
timerSetup.Period = TIM_ulClockFreq_Hz(&demoTmr) / 1000;
/* Return value explicitly ignored - init is always successful */
(void) TIM_vCounterInit(&demoTmr, &timerSetup);
TIM_vCounterStart(&demoTmr); /* Timer is started */
while (1)
{
while (TIM_FLAG_STATUS(&demoTmr, U) == 0) { /* Wait for the timer period to elapse */ }
TIM_FLAG_CLEAR(&demoTmr, U); /* Clear update flag */
msAction(); /* Perform action each millisecond */
}
}The generic handle control model requires the first API call of any peripheral to be the _Init() function - the only exception being the peripheral clock configuration functions, as seen here as well. This function enables the clocking for the peripheral, sets up the peripheral according to the provided generic configuration data, and calls the dependency initialization (if exists). The _Start() function actually commences a certain peripheral action (in this case it starts the timer's counter). Any process that has been started can be stopped using the corresponding _Stop() API call.
Getting and clearing peripheral flags is implemented with simple macros, therefore the flag names are always peripheral specific, nevertheless they can be found in the code's doxygen documentation as well as in the reference manuals.
As it has been mentioned before, the handles provide callback services which the application can subscribe to. These callbacks are implemented with the use of interrupt service routines. Care must be taken as even though the XPD API performs the peripheral interrupt request setup, the interrupt line has to be enabled in the NVIC module as well to be functional. As these interrupt lines are allocated differently throughout the STM32 devices, they are bound to the board-specific firmware level. The updated implementation would look like this:
/** @file bsp.c */
#include <xpd_tim.h>
static void demoTmr_depInit(void * callerHandle)
{
NVIC_EnableIRQ(TIM1_BRK_UP_TRG_COM_IRQn);
}
static void demoTmr_depDeinit(void * callerHandle)
{
NVIC_DisableIRQ(TIM1_BRK_UP_TRG_COM_IRQn);
}
TIM_HandleType demoTmr;
void DemoTmr_Bind(void)
{
TIM_INST2HANDLE(&demoTmr, TIM1);
demoTmr.Callbacks.DepInit = demoTmr_depInit;
demoTmr.Callbacks.DepDeinit = demoTmr_depDeinit;
}
/* Global interrupt handler related to the timer's interrupt line */
void TIM1_BRK_UP_TRG_COM_IRQHandler(void)
{
TIM_vIRQHandler(&demoTmr);
}The above code demonstrates how to implement simple dependency callback functions, which change the interrupt line control status depending on the peripheral initialization state. It also shows how to map an interrupt handler to a peripheral handle-based interrupt-servicing XPD call. Each peripheral with interrupt-triggering capabilities has at least one {PERIPH}_IRQHandler() function, which should be called with the interrupt line related handle reference. In case of multiple such functions, the naming similarities give away the solution.
For the application side, things get much easier. The callback function has to be implemented, and its reference has to be set in the handle's appropriate field (always within its Callbacks structure).
/** @file app.c */
#include <xpd_tim.h>
extern TIM_HandleType demoTmr;
extern void DemoTmr_Bind(void);
static void OnPeriodElapsed(void * callerHandle)
{
msAction(); /* Perform action each millisecond - in interrupt context */
}
int main(void)
{
/* This form of initialization sets unspecified fields to zero */
TIM_InitType timerSetup = {
.Mode = TIM_COUNTER_UP,
/* Multiply Prescaler and divide Period with x > 0
* in case Period value exceeds timer resolution */
.Prescaler = 1,
};
DemoTmr_Bind();
/* Period = timer frequency / desired update frequency */
timerSetup.Period = TIM_ulClockFreq_Hz(&demoTmr) / 1000;
/* Return value explicitly ignored - init is always successful */
(void) TIM_vCounterInit(&demoTmr, &timerSetup);
demoTmr.Callbacks.Update = OnPeriodElapsed; /* Set callback function pointer */
TIM_vCounterStart_IT(&demoTmr); /* Timer is started with Update interrupt request */
while (1) {}
}The callback function can be used as any handle's any callback, as long as the function prototypes are matching. A callback can be discontinued by stopping the interrupt-triggering operation, by disabling the peripheral interrupt request or the NVIC line, or by setting the handle's field to NULL.