Low-Level APIs for Embedded Systems

While desktop APIs address desktop-oriented issues such as window manipulation, process management, and file/database access, APIs for embedded and real-time systems tackle debugger interfacing, task management, low-level device I/O, and the like. Tom and Chad examine a pair of APIs that are typical of low-level programming interfaces in embedded environments.


March 01, 1999
URL:http://www.drdobbs.com/embedded-systems/low-level-apis-for-embedded-systems/184410882

Mar99: Low-Level APIs for Embedded Systems

The authors are engineers for Motorola. They can be contacted at [email protected] and [email protected], respectively.


An API is generally thought of as a set of named entry points into a software abstraction designed for a particular purpose. Microsoft's MFC, for instance, comprises an API for accessing and manipulating Windows objects. The UNIX stdio library, on the other hand, is an API for buffering and formatting file contents.

In the world of real-time and embedded systems, however, APIs are much different. While desktop APIs address desktop-oriented issues such as window manipulation, process management, and file/ database access, APIs for embedded and real-time systems tackle issues such as debugger interfacing, task management, and low-level device I/O. In fact, low-level APIs exist for almost every real-time OS system service. For real-time application developers, APIs encapsulate and abstract the capabilities of the device, making peripheral devices more tractable and speeding development. For embedded tool builders, low-level APIs provide access to parts of the machine that would otherwise be difficult or impossible to use. In this article, we'll discuss a pair of APIs that are typical of low-level programming interfaces in embedded environments -- the peripherals library and emulator-server library, both for the M-CORE architecture from Motorola (the company we work for).

The M-CORE architecture is a 32-bit RISC design targeted for high-performance embedded applications requiring reduced system power consumption. M-CORE-based microcontrollers are particularly suited to applications in battery-operated, portable products or highly integrated components functioning in extreme temperature environments. A wide array of peripheral devices can surround M-CORE-equipped microcontrollers, including timers, serial interfaces, A/D converters, network controllers, and even coprocessors -- all on a single piece of silicon.

Device Drivers

The concept of a device driver can vary depending on context. A UNIX device driver is a set of routines that is linked into the kernel and is designed to be accessed in a rigorously controlled way by the operating system. In a real-time operating system (RTOS), device drivers may or may not be as rigidly defined or utilized. They may follow a particular protocol, calling sequence, or interrupt convention, or they may only provide selected entry points to device functionality.

In the M-CORE peripherals library, a device driver is an API that provides a certain level of accessibility or service to a given device. At the lowest level, the API maps directly to device register control, essentially giving you a symbolic interface to the peripheral hardware. At higher levels a driver may support more sophisticated operations such as queuing, buffering, sophisticated error handling, or application-specific processing.

The M-CORE peripherals library, therefore, takes a device-centric, bottom-up approach to driver design, rather than a host-centric model that relies on a specific protocol for device interaction. Still, the intent of the library is to offer a uniform interface for a wide range of peripherals built around the M-CORE processor. The library consists of discrete levels of service, representing increasing degrees of device abstraction, which contribute a measure of uniformity to the process of peripheral access and control.

Levels of Service

The peripherals library module for a given device can be viewed as a service in support of that device. Device services are categorized by the degree of device abstraction that the service presents and the amount of interrupt processing support it provides. Library functions access device information through a device handle. The device handle is always the first parameter in any library call, but what the handle refers to will vary depending on the service level of the call.

Level 1 services reside at the lowest level; they interact directly with the hardware and return immediately to the caller (possibly with an indication that the requested action could not be taken due to the device being busy). The device handle in a level 1 call is always the base address of the memory-mapped device register block.

Because it exists at the level of the raw hardware, a level 1 service has minimal interrupt support, although a higher level service employing interrupts may be built upon a level 1 service (see Figure 1). The main benefit of a level 1 service is that it is symbolic: There are no hardware register names or structures to remember, and parameter checking is also available. (All M-CORE peripherals library modules provide an interface to level 1 services.)

A level 2 service presents you with a more abstract model of a device than a level 1 service. It is generally built upon a level 1 service, although it may make use of optimizations unavailable to the application programmer (such as the inlining of level 1 functions). An application program that uses a level 2 service would call only level 2 functions (although some of these may be simple passthroughs to lower-level functions).

The device handle in a level 2 call is the address of a device descriptor. The device descriptor is a user-allocated block of storage that contains, at minimum, a pointer to the device hardware registers, and beyond that any other state information required to implement service functionality. Other possible components of a device descriptor might be:

One device descriptor is created for each instance of a device associated with the service. An example of a level 2 service is a serial communications interface driver implementing a buffered character queue (see Figure 2).

Peripherals Library and RTOS

The M-CORE peripherals library API does not necessarily conform to any standard set of device driver entry points for a particular real-time operating system. In a sense, the peripherals library functions amount to device primitives that are pressed into service on behalf of a particular OS driver model. The actual OS interfacing can vary from system to system, but at least two driver attributes are essential in any RTOS environment -- interrupt handling and status signaling.

Interrupt Service Entry. The M-CORE processor can support both vectored and autovectored interrupts. In the case of vectored interrupts, the address of a function is mapped directly to the vector space of the processor. For autovectoring, all interrupts are routed through the INT/FINT vectors in the processor vector space. The code executed as a result of interrupt processing is known as the interrupt service routine (ISR).

To preserve generality for interrupt processing among all peripherals library modules, the ISR can be implemented as an interrupt dispatch routine. The dispatch routine calls an implicit or explicit Interrupt Service Function (ISF) that performs the actual work associated with the interrupt. The dispatch routine can take care of a number of things in preparation for calling the ISF:

For example, assume that there are two SCI devices on a chip, and two hardware vectors. Each of these will point to a single dispatch routine. The dispatch routine determines whether the interrupt is for the receive or transmit channel. It also knows the descriptor address (device handle) unique to the hardware device being serviced, and passes this to the appropriate interrupt service function.

Interrupt Status Communication. The interrupt service function may be either a routine explicitly designed for interrupt processing, or an API function capable of performing the requisite interrupt handling. When the ISF is called by the dispatcher it is still within the interrupt context, so there must be a way of communicating the ISF return status to the application program. This can be done by having the dispatcher call a Service Signaling Function (SSF) upon return from the ISF. The SSF is passed to the return code of the interrupt service function, along with other pertinent parameters such as the device descriptor and any returned data.

Separating the roles of interrupt service function and service signaling function makes it possible to use arbitrary API functions as ISFs, and provides for customization of the signaling function. The dispatcher as an interrupt service routine can be adapted to whatever interrupt mechanism is supported by the system hardware. Figure 3 illustrates the interrupt structure relationships.

Implementation Issues

The M-CORE peripheral library drivers are written in Standard C. The number and size of API functions is small enough per module that they are generally grouped into a single file and compiled as a unit. Because these routines operate at a very low level, they must be fairly efficient with respect to code size and speed. Optimizing driver code, however, can be tricky.

Listing One, for instance, is part of the receive routine in a level 2 driver for a UART. In the first line, the receive register (URX) is assigned to a local variable that is checked for certain status bits. If either the local variable or the UART register structure is not declared as volatile, optimized code may never reload the local variable from the receive register and the receive will always timeout. If the UART pointer is declared volatile, though, the generated code may be suboptimal due to unnecessary load and store operations.

A requirement of the peripherals library API was to provide optional parameter checking. This could have been done by having two versions of the library, one with parameter checking code included and the other without. Supporting two separate libraries suggested maintenance headaches down the road, so instead all API entry points are defined as macros that eventually call the underlying library routine. The body of the macro contains preamble logic for performing parameter checks conditionally; see Listing Two.

The documented API function is called UART_A_Receive, but the addressable function that performs the work is called UART_A_Receive_f. The manifest constant UART_A_PARAM_CHECKING is defined in a global include file, but may be redefined to toggle parameter checking on an individual invocation of the macro/function. If UART_A_PARAM _CHECKING is zero, the compiler ensures that code for the true action of the ternary operator is never generated. Conversely, if UART_A_PARAM_CHECKING is a nonzero constant, the compiler generates code to perform parameter checking. All peripherals library API routines return a status. If parameter checking is enabled and there is an error, no function call is ever made; the result of the ternary operator substitutes for the function return value.

This mechanism can be extended to support optional in-line code generation. In Listing Three, an interim macro is defined to check for in-line code expansion. If UART_A_INLINE_CODE is zero, the compiler will omit the in-line code generation as an optimization and call the function directly. The parameter checking block examines UART_A_PARAM_CHECKING as before, but instead of calling the addressable function directly, it invokes the in-line code macro, which will either expand or eventually call the real function.

Emulator Server Library

All processors based on the M-CORE architecture have within the core an on-chip emulation (OnCE) circuit. This circuit provides a simple, inexpensive debugging interface, allowing external access to the processor's internal registers. The OnCE is controlled through a serial interface mapped onto a JTAG Test Access Port (TAP) protocol (IEEE-1149.1a-1993). The Emulator Server Library (ESL) facilitates OnCE debugging.

The ESL is a set of processes and libraries that provide a generic debugging interface that connects high-level applications over various communication channels to target devices. ESL attributes include:

Figure 4 illustrates how the ESL system is used. A client application communicates with the ESL, which in turn communicates with a development board. The client application and ESL reside on a host computer system. The connection path may be parallel, serial, or network. The connection device translates ESL back-end protocol commands into OnCE/JTAG sequences.

Three components make up the ESL -- the API library, the protocol launcher, and the protocol modules.

Connecting to a Protocol Module

Figure 5 illustrates the API library's initial connection to the protocol launcher daemon. This occurs when the client application loads the API library (in Windows 95/NT) or when the ServerConnect API is called (in Solaris).

The process boundary may be a machine boundary. There is a configuration file associated with the API library that tells you where the launcher is. If the launcher resides on another machine, it must already be running. If it is on the same machine, the API library will launch it.

The application then specifies a target type to the library. This causes the launcher daemon to spawn the protocol module on the target machine; see Figure 6.

Finally, after the protocol module is configured, the launcher returns a port number to the API library. Communication is established between the API and protocol; see Figure 7.

Once the protocol module is configured, all communication to the evaluation board is direct from the API layer through the protocol module. When the client application disconnects from the API library, the launcher daemon terminates the protocol module process, if no other clients are attached.

ESL Examples

Listing Four shows how you load and connect to a protocol module. The code loads the API library, loads pointers to the APIs it needs, then connects to the protocol module for the Enhanced Background Debug Interface (EBDI), a connection cable that talks RS-232 to a host and translates ESL protocol to OnCE sequences.

Errors in loading or connecting should be handled here as well. For example, return values from ServerConnect other than SERVER_READY are connection errors. Any return value from SetMCUInformation other than SERVER_COMPLETE will be an error.

Listing Five shows how you use the GetAsync API to process any target events. Whenever a call is made that will generate a run-state event in the target, Get-Async is called to process those events. For example, after TargetReset, several events will be generated: Run state may change from go to stop and a reset event will occur. Listing Five assumes that users have asked the debugger to do a "target reset" command. The ESLGetAsync pointer was loaded as in Listing Four. Processing event types usually consists of updating memory display windows, register display windows, or code windows.

Listing Six is part of a Win32 console application that tests download speeds to a development board. This is not the entire code sequence, but illustrates the use of the SetTargetMemory API. The ESLSetTargetMemory function pointer was retrieved as in the previous examples.

DDJ

Listing One

while (!((data = uart->URX) & URX_CHARRDY) &&   /* no data */       !(data & URX_ERR) &&
       timeout != 0)
{
    if (timeout > 0)
    {
        for (i = 0; i < delay; i++)
            ;       /* ~1 us. busy-wait */
        --timeout;
    }
}

Back to Article

Listing Two

#define UART_A_Receive(UARTPtr,Datap)                                   \(                                                                       \
    (UART_A_PARAM_CHECKING) ?                                           \
    (                                                                   \
        ((UARTPtr) == NULL) ? DD_ERR_INVALID_HANDLE :                   \
        ((Datap) == NULL) ? DD_ERR_INVALID_ADDRESS :                    \
        UART_A_Receive_f(UARTPtr,Datap)                                 \
    )                                                                   \
    :                                                                   \
        UART_A_Receive_f(UARTPtr,Datap)                                 \
)

Back to Article

Listing Three

#define UART_A_Transmit_m(UARTPtr,Data)                                 \(                                                                       \
    (UART_A_INLINE_CODE) ?                                              \
    (                                                                   \
        !(((pUART_A_t)(UARTPtr))->USR & USR_TRDY) ?                     \
            UART_A_ERR_DATA_PENDING :                                   \
        (((pUART_A_t)(UARTPtr))->UTX =                                  \
            (Data) & (!(((pUART_A_t)(UARTPtr))->UCR2 & UCR2_WS) ?       \
            SEVEN_BIT_MASK : EIGHT_BIT_MASK),                           \
            DD_ERR_NONE)                                                \
    )                                                                   \
    :                                                                   \
       UART_A_Transmit_f(UARTPtr,Data)                                  \
)
#define UART_A_Transmit(UARTPtr,Data)                                   \
(                                                                       \
    (UART_A_PARAM_CHECKING) ?                                           \
    (                                                                   \
        ((UARTPtr) == NULL) ? DD_ERR_INVALID_HANDLE :                   \
        ((!(((pUART_A_t)(UARTPtr))->UCR2 & UCR2_WS)) && Data > 127) ?   \
            UART_A_ERR_INVALID_DATA_VALUE :                             \
        UART_A_Transmit_m(UARTPtr,Data)                                 \
    )                                                                   \
    :                                                                   \
        UART_A_Transmit_m(UARTPtr,Data)                                 \
)

Back to Article

Listing Four

#include "emusrvr.h"

HINSTANCE hLibrary=NULL; PSERVERCONNECT ESLConnect=NULL; PSERVERDISCONNECT ESLDisconnect=NULL; PSETMCUINFORMATION ESLSetMCU=NULL; PTARGETRESET ESLTargetReset=NULL; SERVER_RETVAL ret; BYTE bESLClientID;

MCUINFO MCUInfo={0x40, 0x00}; // Set CPUType = 0x40

BOOL fConnected = FALSE;

// Load API Library hLibrary = LoadLibrary("Esrv32.dll"); if (hLibrary) { // Get pointers to APIs ESLConnect = (PSERVERCONNECT)GetProcAddress( hLibrary, cszSERVERCONNECT ); ESLSetMCU = (PSETMCUINFORMATION)GetProcAddress( hLibrary, cszSETMCUINFORMATION ); ESLDisconnect = (PSERVERDISCONNECT)GetProcAddress( hLibrary, cszSERVERDISCONNECT ); ESLTargetReset = (PTARGETRESET)GetProcAddress( hLibrary, cszTARGETRESET ); if (ESLConnect && ESLSetMCU && ESLDisconnect && ESLTargetReset) { // Connect to EBDI ret = ESLConnect( NULL, "COM1", EBDI, &bESLClientID ); if (ret == SERVER_READY) { // Tell EBDI we want to do M*CORE fConnected = TRUE; ret = ESLSetMCU( &MCUInfo, 0, 0, bESLClientID ); } } }

Back to Article

Listing Five

RESETSTRUCT Reset = {0};        // Default= reset into debug modeASYNCSTRUCT Async = {0};        // Storage for event structure


ret = ESLTargetReset( &Reset, bESLClientID ); if ( ret == SERVER_COMPLETE ) { ret = SERVER_ASYNC; while (ret == SERVER_ASYNC) // process until No events { ret = ESLGetAsync( &Async, bESLClientID ); if ( ret == SERVER_ASYNC ) // got one { // Process event types } } }

Back to Article

Listing Six

while (wStatus == CSrecord::srecordOK){
    wStatus = pSrecord->GetNextSrecord( &dwSrecAddress, 
                                             srecBytes, 256, &nSrecLen );
    if (wStatus == CSrecord::srecordOK)
    {
        if (fFirst)
        {
            // Initialize current address for first s-record
            dwCurAddress = dwSrecAddress;
            fFirst = FALSE;
        }
        if (nSrecLen)
        {
            // s-record file not at end
            // check for address discontinuity or full buffer
            dwNextAddress = dwCurAddress + nTotal;
            nDatalen = nTotal + nSrecLen;       // buffer plus this s-record


if (( dwNextAddress != dwSrecAddress) || (nDatalen > 508)) { // Address discontinuity or buffer full - download buffer // before appending this s-record to buffer if (nTotal) { // buffer has data dwGrandTotal += nTotal; // running total of all bytes wESLStatus = ESLSetTargetMemory( 0x00, // bModifier = Target 0x00, // bAdderSpace = 0 (ignored) 0x02, // bSize = 32-bit writes dwCurAddress, // Address nTotal - 1, // bytes to write buffer, // bytes nTotal, // bytes in buffer &dwErrorAddress, // Error address ESLId); // ESL ID if (wESLStatus != SERVER_COMPLETE) { // Error writing target memory - notify user printf("\nError writing address %08.8lX, nBytes = %d, status = %d\n",dwCurAddress,nTotal,wESLStatus); DoExit(); return (-1); } // buffer now empty nTotal = 0; dwCurAddress = dwSrecAddress; } } // Add this s-record to buffer and adjust length of buffer memcpy( buffer+nTotal, srecBytes, nSrecLen ); nTotal += nSrecLen; } } }

Back to Article

DDJ


Copyright © 1999, Dr. Dobb's Journal
Mar99: Low-Level APIs for Embedded Systems

Low-Level APIs for Embedded Systems

By Tom Cunningham and Chad Peckham

Dr. Dobb's Journal March 1999

Figure 1: Level 1 service interrelationships.


Copyright © 1999, Dr. Dobb's Journal
Mar99: Low-Level APIs for Embedded Systems

Low-Level APIs for Embedded Systems

By Tom Cunningham and Chad Peckham

Dr. Dobb's Journal March 1999

Figure 2: Level 2 service interrelationships.


Copyright © 1999, Dr. Dobb's Journal
Mar99: Low-Level APIs for Embedded Systems

Low-Level APIs for Embedded Systems

By Tom Cunningham and Chad Peckham

Dr. Dobb's Journal March 1999

Figure 3: Peripherals library interrupt structure.


Copyright © 1999, Dr. Dobb's Journal
Mar99: Low-Level APIs for Embedded Systems

Low-Level APIs for Embedded Systems

By Tom Cunningham and Chad Peckham

Dr. Dobb's Journal March 1999

Figure 4: ESL system.


Copyright © 1999, Dr. Dobb's Journal
Mar99: Low-Level APIs for Embedded Systems

Low-Level APIs for Embedded Systems

By Tom Cunningham and Chad Peckham

Dr. Dobb's Journal March 1999

Figure 5: Initial connection to launcher.


Copyright © 1999, Dr. Dobb's Journal
Mar99: Low-Level APIs for Embedded Systems

Low-Level APIs for Embedded Systems

By Tom Cunningham and Chad Peckham

Dr. Dobb's Journal March 1999

Figure 6: Launcher spawns protocol process.


Copyright © 1999, Dr. Dobb's Journal
Mar99: Low-Level APIs for Embedded Systems

Low-Level APIs for Embedded Systems

By Tom Cunningham and Chad Peckham

Dr. Dobb's Journal March 1999

Figure 7: Connection made to protocol module and M-CORE evaluation board.


Copyright © 1999, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.