Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Embedded Systems

Low-Level APIs for Embedded Systems


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:

  • A device completion flag to signal the application program logic.
  • A completion code accompanying the flag.
  • A buffer for a single datum.
  • A queue of data.
  • A structured message.

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:

  • Sorting out among individual device hardware requests coming through the same vector.
  • Specifying the relevant hardware device address or device descriptor for the given instance of a peripheral. This allows an interrupt service function to be written independently of the hardware or descriptor address, and is therefore capable of servicing any number of like devices in the same system.
  • Serving as a wrapper for the interrupt service function -- performing a normal C-function call, receiving control back from the ISF when it is finished, and performing any postprocessing that may be necessary.

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:

  • A set of generic APIs that cover all basic debugging operations.
  • Communication over TCP/IP sockets with a low-level protocol module thus allowing cross-machine debugging.
  • Ability of more than one application to communicate with the same hardware simultaneously.
  • A Protocol Module Kit to enable third parties to construct custom low-level protocol modules.
  • A Protocol Module that is modularized, object-oriented designed, and easy to extend.

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.

  • API library interface. The ESL, linked to the client application, consists of two interfaces -- the API front end and TCP/IP back end. The API front end contains all the functions the application needs to communicate to the emulation hardware. The TCP/IP back end contains two sockets for communicating with the launcher and the protocol module. All data contained in the APIs are reformatted into TCP/IP packets and sent to the Protocol Module.
  • The protocol launcher daemon is a process that runs on the target machine. When the API library requests a protocol process, the launcher first checks for a running module of the desired type. If found, the port number is returned to the API library; if not, the launcher spawns a new protocol module and passes back the port number.
  • This process consists of two interfaces, one that interfaces with the API library, and another that interfaces with the protocol module. When the protocol module is configured, all communications from the API layer go directly to it. The launcher monitors when new clients are connecting and when protocols need to be destroyed.
  • The protocol modules are where all the real work is done. They consist of three interfaces -- a socketed front end, target interface back end, and interface to the launcher. The front-end socket receives API messages from the API library, makes calls to the corresponding functions in the protocol, and returns results back to the library. The launcher interface allows the process to notify when startup initialization has completed and when the process is shutting down. The target interface back end handles all communication with the development board.
  • This ESL layer is extensible. A Protocol Module Building Kit (available from Motorola) allows third parties to add protocol modules to communicate with different development boards. It provides a socketed front-end binary object, base class for entry points, stubs for all API corresponding calls, and instructions on how to build custom protocols.

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"

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


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


</p>
BOOL fConnected = FALSE;


</p>
// 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


</p>
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


</p>
            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

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.