↓ Skip to main content

VxWorks 5.4 Device Driver Development: Character and Block Drivers

VxWorks 5.4 Device Driver Development: Character and Block Drivers

Developing device drivers for VxWorks requires a solid understanding of real-time scheduling, interrupt handling, task synchronization, and the operating system’s I/O subsystem. A driver must coordinate hardware access with concurrent tasks while meeting timing requirements and maintaining system stability.

This guide focuses on the classic VxWorks 5.4 and Tornado 2.0 development environment. It explains the RTOS mechanisms most relevant to driver development, the architecture of character and block drivers, interrupt service routines (ISRs), and practical implementation patterns using a UART and a RAM-backed disk as examples.

The APIs and design patterns described here belong primarily to the classic VxWorks driver model. They should not be assumed to apply unchanged to modern VxWorks releases, which may use different driver frameworks and hardware abstraction mechanisms.

⚙️ 1. Real-Time Operating Systems and VxWorks
#

Real-time scheduling fundamentals
#

A real-time operating system is designed to provide predictable responses to events within defined timing constraints. Its suitability for embedded systems depends not only on execution speed but also on scheduling behavior, interrupt latency, context-switch overhead, and resource-management policies.

Important evaluation criteria include:

  • Kernel footprint and memory requirements
  • Interrupt latency and interrupt response time
  • Interrupt handling and recovery behavior
  • Multitasking and task synchronization
  • Context-switch overhead
  • Memory-management and virtual-memory support

VxWorks provides a real-time kernel that schedules multiple tasks according to their priorities. Although tasks appear to execute concurrently, a single CPU executes only one task at a time unless the target has multiple processors.

Classic VxWorks uses a shared address space for kernel and task code by default. This can reduce address-space transition overhead, but it also means that an invalid pointer or memory overwrite can corrupt unrelated application data or kernel structures. Optional memory-management facilities and other execution environments may provide additional isolation, depending on the VxWorks version and configuration.

Tasks and task states
#

VxWorks Task Status

A VxWorks task is an independently scheduled execution context. Its task control block (TCB) maintains the state required by the kernel to manage execution.

A task’s context includes:

  • Program counter and CPU registers
  • Stack and function-call state
  • Signal-handling information
  • I/O-related state
  • Kernel scheduling and management information

A task can be created with taskInit() and activated with taskActivate(). The taskSpawn() routine combines task creation and activation into a single operation.

The scheduler moves tasks between states according to their execution and synchronization requirements. Common states include:

  • Ready: The task can execute but may be waiting for CPU time.
  • Executing: The task is currently running.
  • Pended: The task is waiting for a resource or synchronization event.
  • Delayed: The task is waiting for a specified time interval.
  • Suspended: The task has been explicitly suspended or otherwise prevented from running.

Exact state names and combinations depend on the relevant VxWorks release and diagnostic interfaces.

For driver developers, the distinction between a pended task and a suspended task is particularly important. A task that pends on a semaphore can resume when the semaphore becomes available. A suspended task generally requires an explicit resume operation or another applicable kernel action.

Scheduler and preemption
#

Classic VxWorks uses priority-based preemptive scheduling by default. When a higher-priority task becomes ready, it can preempt a lower-priority task that is currently executing.

Round-robin scheduling can also be enabled to provide time slicing among eligible tasks at the same priority. This can improve fairness, but it does not replace the need to assign priorities according to real-time deadlines.

Driver design must account for these scheduling rules. A task that holds a shared resource for too long can delay higher-priority tasks, while excessive work in an ISR can increase interrupt-processing latency and postpone normal task execution.

Mutual exclusion and semaphores
#

Shared driver state must be protected against concurrent access from tasks and interrupt handlers. Classic VxWorks provides several mechanisms:

  • Binary semaphores for event signaling and basic synchronization
  • Mutual-exclusion semaphores for protecting resources shared by tasks
  • Counting semaphores for managing multiple available resources
  • Interrupt locking for short, interrupt-sensitive critical sections
  • taskLock() and taskUnlock() for temporarily preventing task-level preemption

These mechanisms are not interchangeable.

Binary semaphores are useful when an ISR needs to notify a task that an event has occurred. Mutual-exclusion semaphores are designed for task-level resource protection and can support priority inheritance when configured and used appropriately. Counting semaphores can represent available buffers, queued work items, or other resources with a count greater than one.

Use intLock() and intUnlock() only for short critical sections where interrupt exclusion is required. Preserve and restore the interrupt-lock key returned by the API rather than assuming interrupts were enabled before the lock.

taskLock() is not a substitute for a mutex. It prevents task-level preemption but does not necessarily prevent an ISR from accessing the same data. Long periods without task preemption can also damage real-time responsiveness.

Message queues and intertask communication
#

VxWorks message queues provide communication between tasks using messages of configurable lengths and queue capacities. Multiple tasks can wait on the same queue, and queue behavior can be configured to prioritize waiting tasks.

ISRs can send messages through supported ISR-safe APIs, but receiving messages and performing other potentially blocking operations must be handled by task context.

Other communication facilities include pipes, sockets, remote procedure calls (RPC), and signals. The appropriate mechanism depends on whether the driver needs event notification, structured data transfer, synchronization, or communication across a network.

⚡ 2. Interrupt Handling and ISR Design
#

Interrupt latency and response time
#

An interrupt allows a hardware device to request CPU attention. The associated interrupt service routine processes the event or records enough information for a task to complete the work.

Interrupt response time includes the delay between the hardware event and execution of the ISR’s first instruction. Depending on the target architecture and interrupt configuration, this can include interrupt masking, higher-priority interrupt activity, exception-entry processing, and register-save overhead.

The BSP and kernel participate in interrupt dispatch. The exact sequence is hardware- and release-dependent, so driver developers should consult the target’s BSP implementation and processor documentation.

Connecting an interrupt handler
#

In the classic VxWorks API, intConnect() associates an interrupt vector with a handler and an argument.

STATUS status;

status = intConnect(
    INUM_TO_IVEC(intNum),
    (VOIDFUNCPTR)myIntHandler,
    (int)argToHandler
);

if (status != OK)
{
    /* Handle interrupt registration failure. */
}

The exact function-pointer and argument types depend on the VxWorks headers and target architecture. Use the declarations supplied by the installed toolchain rather than assuming that the example’s casts are universally valid.

Before enabling a device’s interrupts, initialize the device registers, software state, synchronization objects, and ISR-visible buffers. The interrupt vector and any controller-specific configuration must also be correct for the target BSP.

Restrictions inside an ISR
#

VxWorks User Task and ISR

An ISR runs in interrupt context rather than ordinary task context. It must complete quickly and must not perform operations that require blocking or task scheduling.

Important rules include:

  • Do not call blocking functions or wait for a semaphore.
  • Do not use ordinary memory-allocation routines such as malloc() or free() unless a specific API is documented as ISR-safe.
  • Avoid lengthy processing, formatted output, and other operations with unpredictable execution time.
  • Use only APIs explicitly supported at interrupt level.
  • Protect shared data using a synchronization strategy appropriate for both task and interrupt contexts.
  • Avoid assuming that nested interrupts are supported on every target.

A common design is to let the ISR acknowledge the device, collect essential status or data, update a ring buffer, and signal a task. The task performs the remaining processing outside interrupt context.

An ISR can communicate with tasks through supported semaphore-give operations, shared buffers, message queues, pipes, or signals, subject to each API’s interrupt-safety restrictions.

Example interrupt-to-task signaling
#

The following illustrates the basic pattern. It omits device-specific register operations and error handling.

SEM_ID dataReadySem;

/* Initialize before enabling the device interrupt. */
dataReadySem = semBCreate(SEM_Q_PRIORITY, SEM_EMPTY);

void myIntHandler(int arg)
{
    /* Read device status and acknowledge the interrupt. */
    /* Copy or record data using an ISR-safe mechanism. */

    semGive(dataReadySem);
}

void myDeviceTask(void)
{
    for (;;)
    {
        if (semTake(dataReadySem, WAIT_FOREVER) == OK)
        {
            /* Process data collected by the ISR. */
        }
    }
}

The real implementation must account for repeated interrupts, buffer capacity, device-specific acknowledgement requirements, and possible event coalescing. A binary semaphore indicates that work is available; it does not count every interrupt. If every event must be preserved, use a counter, queue, or ring-buffer design that records the event count or data explicitly.

🧩 3. VxWorks Devices and the I/O Subsystem
#

Device-driver architecture
#

A device driver provides a software interface through which applications access a hardware or software device. The driver translates generic operations into device-specific behavior.

In classic VxWorks, the I/O subsystem provides a uniform interface for opening devices and performing operations such as reading, writing, and issuing control requests.

A device might represent a serial port, disk, pipe, or another I/O endpoint. For example, a serial channel may be exposed through a device name such as /tyCo/0, depending on the BSP and configuration.

The I/O subsystem resolves a device name to a registered device descriptor and invokes the corresponding driver routines. A driver can manage multiple devices, each with its own name and device-specific state.

Character devices and block devices
#

VxWorks distinguishes between character-oriented and block-oriented devices.

Property Character device Block device
Typical use UARTs, terminals, streams Disks and storage media
Data access Stream or byte-oriented operations Addressable blocks
Common interface open(), read(), write(), ioctl() Block-device callbacks used through a file system
File-system dependency Not inherently required Commonly accessed through a file system
Typical concerns Buffering, flow control, event signaling Block addressing, capacity, media state, consistency

Character drivers generally register their operations with the I/O subsystem. Block drivers expose block-access operations through structures such as BLK_DEV or SEQ_DEV, which are then used by the relevant file-system layer.

Drivers typically execute in the context of the task that invokes their entry points. Consequently, driver routines can be preempted and must handle concurrency correctly. ISR execution is different and requires stricter API and timing constraints.

Dynamic and static driver integration
#

Depending on the driver and system configuration, driver code may be linked into the VxWorks image or loaded dynamically.

Static integration is common when a driver is required during early boot or must always be available. Dynamic loading can simplify development and testing, provided the target image supports the required loading facilities and the driver can be initialized safely at runtime.

Regardless of integration method, the driver must initialize its private state, register the appropriate interfaces, configure hardware, and handle partial initialization failures without leaving interrupts or resources in an unsafe state.

🔧 4. Developing a Character Driver
#

Classic VxWorks character drivers can expose up to seven standard interfaces:

  • create()
  • remove()
  • open()
  • close()
  • read()
  • write()
  • ioctl()

Not every driver requires every operation. However, supported operations should have well-defined behavior, and unsupported operations should fail predictably.

Step 1: Install the driver interfaces
#

iosDrvInstall() registers the driver callbacks with the I/O subsystem and returns a driver number.

static int myDrvNum = ERROR;

STATUS myDrvInit(void)
{
    myDrvNum = iosDrvInstall(
        myDevCreate,
        myDevRemove,
        myDevOpen,
        myDevClose,
        myDevRead,
        myDevWrite,
        myDevIoctl
    );

    if (myDrvNum == ERROR)
    {
        return ERROR;
    }

    /* Initialize private state and hardware here. */
    return OK;
}

This example uses the classic driver API and assumes the callback prototypes match the installed VxWorks 5.4 headers. Production code should use the exact callback signatures required by that release.

Registering the callbacks does not automatically create a device instance. It establishes the driver entry in the I/O subsystem’s driver table.

Step 2: Define the device descriptor
#

A device descriptor stores the state required by the driver. For classic VxWorks I/O drivers, the structure begins with a DEV_HDR member.

typedef struct
{
    DEV_HDR devHdr;

    BOOL    initialized;
    SEM_ID  accessSem;

    char   *readBuf;
    int     readSize;
    int     readHead;
    int     readTail;
} MY_DEV;

The descriptor can also contain hardware register addresses, interrupt configuration, transmit and receive buffers, open-state information, and device-specific configuration.

The DEV_HDR member must remain at the beginning of the structure because the I/O subsystem uses it to manage the device. Initialize all required members before exposing the device to application tasks.

Step 3: Add the device to the I/O subsystem
#

iosDevAdd() associates a device descriptor with a name and a driver number.

STATUS myDevCreate(MY_DEV *pDev, char *name)
{
    if (pDev == NULL || name == NULL || myDrvNum == ERROR)
    {
        return ERROR;
    }

    if (iosDevAdd((DEV_HDR *)pDev, name, myDrvNum) == ERROR)
    {
        return ERROR;
    }

    return OK;
}

The exact declaration of the create callback depends on the classic I/O API. The example shows the registration operation rather than a complete callback implementation.

Once registered, the device can be resolved by name through the I/O subsystem. Device creation should fail cleanly if registration fails, and the driver should provide a corresponding removal path if runtime removal is supported.

Step 4: Implement read, write, and control operations
#

The read and write routines transfer data between the application and the device. They must validate arguments, check access permissions where applicable, and define how partial transfers and errors are reported.

STATUS myDevRead(MY_DEV *pDev, char *buffer, int nBytes)
{
    if (pDev == NULL || buffer == NULL || nBytes < 0)
    {
        return ERROR;
    }

    /*
     * Wait for data if blocking reads are supported.
     * Copy available data from the receive buffer.
     * Return the number of bytes transferred according
     * to the callback contract for this VxWorks release.
     */

    return OK;
}

STATUS myDevWrite(MY_DEV *pDev, char *buffer, int nBytes)
{
    if (pDev == NULL || buffer == NULL || nBytes < 0)
    {
        return ERROR;
    }

    /*
     * Check transmit-buffer capacity.
     * Queue data or write it to the device.
     * Return the number of bytes transferred according
     * to the callback contract for this VxWorks release.
     */

    return OK;
}

These are implementation outlines, not complete drivers. In particular, the return type and error-reporting behavior must match the actual callback contract. A production implementation should not return OK unconditionally when no data was transferred.

The ioctl() operation provides device-specific control. It can support operations such as changing a baud rate, enabling or disabling features, querying status, or resetting hardware.

STATUS myDevIoctl(MY_DEV *pDev, int request, int arg)
{
    if (pDev == NULL)
    {
        return ERROR;
    }

    switch (request)
    {
        case SET_DEVICE:
            /* Apply device configuration. */
            return OK;

        case GET_DEVICE_STATUS:
            /* Return or record device status. */
            return OK;

        default:
            errnoSet(S_ioLib_UNKNOWN_REQUEST);
            return ERROR;
    }
}

The request codes and argument conventions are driver-defined in this example. Define them in a public header and document whether each request expects an integer, pointer, or structured argument.

Step 5: Coordinate the ISR and driver tasks
#

The ISR should perform only the operations needed to acknowledge the hardware event and preserve essential state. A task can then consume buffered data or refill a transmit queue.

Avoid using a single semaphore as both a mutual-exclusion lock and an event counter unless its behavior explicitly meets both requirements. Separate resource protection from event notification when that makes the concurrency model easier to reason about.

The driver should define ownership for each shared variable and buffer. For example, the ISR might update a receive head index while the reader task updates a receive tail index. This can reduce contention, but the design must still account for index wraparound, full-versus-empty detection, memory ordering, and possible nested interrupts.

💾 5. Developing a Block Device Driver
#

Block-device architecture
#

A block device exposes storage as a sequence of addressable blocks. Applications usually access it through a file system rather than calling low-level block operations directly.

Classic VxWorks uses structures such as BLK_DEV for direct-access block devices and SEQ_DEV for sequential devices. These structures describe the device and provide callbacks for the operations required by the file-system layer.

A direct-access device supports reading and writing addressable blocks. A sequential device generally restricts writes to the end of the written medium, although its read behavior can resemble that of other storage devices.

The specific interface requirements depend on the device type and the VxWorks file-system library in use.

The BLK_DEV structure
#

A BLK_DEV structure contains device parameters and callback pointers. Depending on the release, these include:

Field or callback Purpose
bd_blkRd Read one or more blocks
bd_blkWrt Write one or more blocks
bd_ioctl Handle device-control requests
bd_reset Reset the device, when supported
bd_statusChk Check device status, when supported
bd_removable Indicate whether the medium is removable
bd_nBlocks Report the number of blocks
bd_bytesPerBlk Report the block size
bd_retry Specify retry behavior where supported
bd_mode Describe the access mode

Field names and exact callback signatures can vary across versions. Always consult the BLK_DEV declaration and file-system documentation supplied with the target toolchain.

A driver may embed BLK_DEV as the first member of a private structure to associate standard block-device metadata with additional hardware state. If callbacks receive a pointer to the embedded structure, the driver can recover the enclosing object when the layout and API contract permit it.

Block read and write callbacks
#

A typical direct-access block driver needs routines that read and write a range of blocks.

STATUS myBlkDevRead(
    DEVICE *pDev,
    int startBlock,
    int numBlocks,
    char *buffer
)
{
    if (pDev == NULL || buffer == NULL ||
        startBlock < 0 || numBlocks < 0)
    {
        return ERROR;
    }

    /*
     * Validate the requested block range.
     * Transfer data from the device into buffer.
     * Handle hardware errors and partial transfers.
     */

    return OK;
}

STATUS myBlkDevWrite(
    DEVICE *pDev,
    int startBlock,
    int numBlocks,
    char *buffer
)
{
    if (pDev == NULL || buffer == NULL ||
        startBlock < 0 || numBlocks < 0)
    {
        return ERROR;
    }

    /*
     * Validate the requested block range.
     * Transfer data from buffer to the device.
     * Handle write protection, completion, and errors.
     */

    return OK;
}

These examples illustrate the responsibilities rather than provide drop-in implementations. Use the exact parameter types and return conventions declared by the target VxWorks headers.

Before accessing the hardware, validate that the requested range does not exceed the device’s capacity. A robust driver must also handle timeouts, retries, device removal, write protection, and media errors as appropriate.

Creating a RAM disk
#

VxWorks provides a RAM-disk driver that stores data in memory rather than on physical storage. It is useful for temporary storage, test environments, and applications that need a file-system interface without a physical disk.

A classic VxWorks 5.4 example using the DOS file system is:

#include <vxWorks.h>
#include <ramDrv.h>
#include <dosFsLib.h>

BLK_DEV *pBlkDev;
DOS_VOL_DESC *pVolDesc;

/* Allocate a RAM-backed block device. */
pBlkDev = ramDevCreate(
    0,      /* Allocate backing memory dynamically. */
    512,    /* Bytes per block. */
    32,     /* Blocks per track. */
    400,    /* Number of blocks. */
    0       /* Block offset. */
);

if (pBlkDev == NULL)
{
    /* Handle allocation or initialization failure. */
}

/* Create and initialize a DOS file system on the device. */
pVolDesc = dosFsMkfs("DEV1:", pBlkDev);

if (pVolDesc == NULL)
{
    /* Handle file-system initialization failure. */
}

This example follows the classic API model and must be checked against the installed VxWorks 5.4 headers. In particular, confirm the function declarations, return values, and file-system initialization requirements for the exact maintenance release.

The parameters describe the RAM-disk backing memory and geometry. Passing zero for the memory address requests allocation by the driver in versions that support this convention. The example creates a device with 400 blocks of 512 bytes each, or 204,800 bytes of nominal storage capacity.

The dosFsMkfs() routine initializes a DOS file system on the supplied block device. It is a formatting operation, not simply a mount operation. Do not use it on an existing volume whose data must be preserved.

For a device that already contains a valid DOS file system, use the appropriate initialization or mounting path documented for that release, such as dosFsDevInit() with a suitable configuration. The block-device structure provides the low-level callbacks that the file-system layer invokes when block access is required.

🖥️ 6. Real-World Example: A UART Character Driver
#

Hardware and interrupt-controller model
#

VxWorks Device Driver Interaction

A UART driver illustrates the interaction between device registers, interrupt delivery, buffering, and the VxWorks I/O subsystem.

The original example uses a PowerPC 8245 DUART with an NS16550-compatible programming model. The exact register offsets, interrupt routing, and controller behavior are hardware-specific. The values below should therefore be treated as an example of the design approach, not as portable constants for arbitrary UART hardware.

The processor’s Embedded Utilities Block (EUMB) provides access to integrated peripherals and interrupt-controller registers. In a typical interrupt flow:

  1. The driver configures the UART’s interrupt conditions and the interrupt controller.
  2. The UART asserts an interrupt when a receive or transmit event occurs.
  3. The BSP and kernel dispatch the interrupt through the configured vector.
  4. The ISR reads device status, handles the event, and acknowledges the device as required.
  5. The interrupt controller’s end-of-interrupt handling is performed according to the platform’s interrupt architecture.

Some BSPs perform parts of this work automatically. Do not duplicate interrupt-controller acknowledgement or end-of-interrupt handling unless the platform’s documented interrupt model requires the driver to do so.

UART registers and initialization
#

An NS16550-compatible UART typically exposes registers for receive data, transmit data, interrupt enable, interrupt identification, line control, line status, and FIFO control. Some registers share addresses and are selected through configuration bits.

A driver must configure the divisor, data format, FIFO behavior, interrupt enables, and controller routing in the correct sequence. The correct baud-rate divisor depends on the UART input clock and the hardware’s divisor formula.

A robust initialization routine should:

  • Verify that the device is accessible.
  • Disable or mask interrupts while configuring registers.
  • Set the baud-rate divisor and line format.
  • Clear pending status where required.
  • Configure transmit and receive FIFOs.
  • Initialize software buffers and synchronization objects.
  • Register the ISR.
  • Enable device interrupts only after all ISR-visible state is ready.

The original example uses a FIFO capacity of 14 bytes and a ring-buffer size of 141 bytes. Such values are hardware- and implementation-specific. Buffer sizing should be based on expected data rates, task scheduling delays, maximum interrupt latency, and the consequences of overflow.

Driver state and ring buffers
#

A simplified private structure might look like this:

#define BUF_SIZE 256

typedef struct
{
    DEV_HDR devHdr;

    char readBuf[BUF_SIZE];
    char writeBuf[BUF_SIZE];

    unsigned int readHead;
    unsigned int readTail;
    unsigned int writeHead;
    unsigned int writeTail;

    SEM_ID rxReadySem;
    SEM_ID txSpaceSem;
    SEM_ID mutex;

    BOOL initialized;
} UART_DEV;

The ring buffers decouple the rate at which the UART produces or consumes bytes from the rate at which application tasks process them.

The ISR typically appends received bytes to the receive ring and removes bytes from the transmit ring when the hardware can accept more data. The reader task consumes bytes from the receive ring, while the writer task appends bytes to the transmit ring.

The example uses separate synchronization objects for receive and transmit operations. A production design should explicitly define which context owns each index, how full and empty states are distinguished, and how buffer overflow or underflow is handled.

If an ISR and a task concurrently update the same buffer metadata, merely using different indices is not always sufficient. Correctness depends on the architecture’s atomic access guarantees, the compiler’s memory model, interrupt nesting behavior, and the chosen synchronization strategy.

Receive processing
#

When receive data becomes available, the ISR should read the UART’s status and data registers according to the hardware documentation. It can then append received bytes to the ring buffer and notify the reader task.

The reader should wait for data only in task context. Once notified, it can copy the available bytes to the caller’s buffer and return the actual number of bytes read.

Important design decisions include:

  • Whether reads block when the receive buffer is empty
  • Whether a read returns immediately with available bytes or waits for a requested count
  • How receive errors are reported
  • What happens when the ring buffer becomes full
  • Whether flow control or dropped-byte counters are required

The driver must also account for wraparound when the buffer head or tail reaches the end of the array. A well-defined ring-buffer implementation is easier to verify than separate branches that rely on comparisons between raw indices.

Transmit processing
#

A task should normally enqueue transmit data rather than poll the UART indefinitely. The ISR can drain the queue when the transmit FIFO has capacity and signal the writer when more space becomes available.

A robust transmit path should specify whether write() waits for buffer space, returns a partial count, or fails when the buffer is full. It should also distinguish between data accepted into the software buffer and data physically transmitted by the UART.

For example, a successful write() may mean that bytes have been queued, not that the last stop bit has already left the hardware. If the application needs a transmission-complete guarantee, expose an explicit control operation or completion mechanism.

Configuring the baud rate through ioctl()
#

The ioctl() callback can provide a control interface for changing the UART’s baud rate or line settings.

For an NS16550-compatible UART, the divisor is generally calculated from the UART input clock and the hardware’s baud-rate formula. The exact calculation depends on the clock frequency and UART configuration.

Before changing the divisor, the driver should coordinate with active readers and writers, preserve any register bits that must remain unchanged, and follow the hardware’s documented sequence for accessing divisor registers. Do not assume that a divisor always fits in one byte or that writing a low byte and a high byte can be performed without first selecting the divisor-latch registers.

ISR implementation considerations
#

The ISR should determine the interrupt cause, process the required receive or transmit event, handle error status, and acknowledge the hardware in the documented order.

Avoid calling logMsg() repeatedly from the interrupt handler. Logging can add unpredictable latency and may not be appropriate in interrupt context. Record an error counter or status flag instead, then report it from task context.

Similarly, use intLock() only when required to protect a short critical section. The interrupt-lock key should be preserved and restored correctly. Disabling interrupts around an entire receive or transmit loop can cause unnecessary latency and may interfere with the very events the driver is designed to process.

Registering and using the UART device
#

After initializing the driver and registering the device, an application can use the standard I/O interface:

#include <vxWorks.h>
#include <ioLib.h>
#include <fcntl.h>

int fd;
char buffer[128];
int nRead;

/* The driver initialization must already have registered /duart0. */
fd = open("/duart0", O_RDWR, 0);

if (fd == ERROR)
{
    /* Handle open failure. */
}
else
{
    static const char message[] = "UART test\r\n";

    if (write(fd, (char *)message, sizeof(message) - 1) == ERROR)
    {
        /* Handle write failure. */
    }

    nRead = read(fd, buffer, sizeof(buffer));

    if (nRead == ERROR)
    {
        /* Handle read failure. */
    }

    close(fd);
}

The exact behavior of read() and write() depends on the driver’s callback implementation. The application should check return values and handle partial transfers rather than assuming that every request completes in full.

During development, Tornado’s debugging tools can be used to inspect driver state, task execution, and fault conditions. Hardware breakpoints and debugger interaction can affect real-time timing, so timing-sensitive behavior should also be tested without intrusive debugging.

🧠 7. Advanced Driver Engineering Topics
#

Context switching and execution context
#

A context switch saves the state of the currently executing task and restores the state of another task selected by the scheduler. It can occur when a task blocks, yields, or is preempted by a higher-priority task.

ISRs execute in a transient interrupt context rather than as ordinary scheduled tasks. They can preempt tasks independently of task priority, subject to the processor and interrupt-controller configuration. Some targets support interrupt nesting, while others restrict it or require explicit configuration.

This distinction affects every driver path. Blocking operations belong in task context, and interrupt handlers should avoid lengthy work that delays the scheduler from running ready tasks.

Reentrancy
#

A function is reentrant when it can be interrupted and safely invoked again before the previous invocation completes, without corrupting shared state.

Reentrancy is important for drivers because multiple tasks may call the same entry point, or an ISR may access data also used by a task. Local variables help because each invocation receives its own stack storage, but local variables alone do not protect shared hardware registers or global buffers.

To make a driver reentrant:

  • Minimize shared mutable state.
  • Protect shared resources using an appropriate synchronization mechanism.
  • Define ownership of shared buffers and indices.
  • Avoid calling non-reentrant library routines from concurrent contexts.
  • Document which APIs can be called from tasks and which can be called from ISRs.

Do not infer reentrancy solely from a function name. Check the documentation for the installed VxWorks library and the specific release.

Priority inversion and priority inheritance
#

Priority inversion occurs when a high-priority task is blocked by a lower-priority task holding a resource. The problem can become more severe if medium-priority tasks prevent the lower-priority task from running and releasing that resource.

For example, suppose task T3 has low priority and holds a mutex, task T1 has high priority and needs the same mutex, and task T2 has intermediate priority. T1 blocks on T3’s mutex. If T2 repeatedly preempts T3, T1 can remain blocked longer than expected.

A mutex with priority inheritance can temporarily raise the priority of the task holding the resource, allowing it to finish the critical section and release the mutex sooner.

Priority inheritance does not eliminate all timing problems. Keep critical sections short, avoid blocking while holding a resource unnecessarily, and analyze worst-case resource contention against application deadlines.

Address-space protection
#

In the classic shared-address-space model, faulty driver code can corrupt kernel data or other application state. This makes bounds checking, pointer validation, and careful memory ownership especially important.

Virtual-memory support does not automatically eliminate driver memory errors. Drivers must still respect the target’s mapping rules and distinguish between CPU virtual addresses, physical addresses, and device-visible addresses where applicable.

Systems configured with additional memory protection or different execution environments may provide stronger isolation, but their behavior should be verified against the relevant VxWorks release and configuration.

Cache coherency and DMA
#

VxWorks CPU Cache and DMA

Processors may cache instructions and data to improve performance. If a device uses direct memory access (DMA), the CPU’s cache and the device’s view of RAM can become inconsistent unless the driver follows the platform’s DMA-coherency rules.

For example, when a device reads data from RAM, the CPU may need to flush the relevant cache lines before starting DMA. When a device writes data into RAM, the CPU may need to invalidate relevant cached data before reading the results.

Classic VxWorks provides cache-management routines such as cacheFlush() and cacheInvalidate() on supported targets. Their correct use depends on the processor, cache architecture, BSP, and memory attributes. The required address alignment and range constraints must be checked in the platform documentation.

Where available, prefer the BSP’s documented DMA-safe allocation and mapping mechanisms. A non-cacheable memory region may be appropriate for some systems, but it can also have performance costs. Cache maintenance must be coordinated with DMA ownership and completion to avoid discarding valid CPU data or exposing stale device data.

Supporting select()
#

The select() interface allows a task to wait for I/O readiness across multiple devices. A driver that supports this interface must integrate with the VxWorks selection mechanism rather than merely implement read() and write().

In the classic model, a driver may use SEL_WAKEUP_LIST, SEL_WAKEUP_NODE, and the relevant ioctl() requests to register waiting tasks and notify them when read or write readiness changes.

A typical implementation needs to:

  1. Initialize the selection wake-up list during device creation.
  2. Register or remove selection nodes when the I/O subsystem requests it.
  3. Determine whether the task is waiting for read or write readiness.
  4. Notify waiting tasks when the device becomes ready.
  5. Remove registrations when a task no longer waits on the device.

The exact request codes and helper-function signatures must be verified against the VxWorks 5.4 headers. The driver must also avoid notifying waiters unnecessarily and must handle races between readiness checks and event registration.

🧪 8. Driver Validation and Debugging
#

Driver testing should cover both normal operation and failure conditions. Successful reads and writes are not sufficient evidence that a driver is correct under load or during unusual hardware events.

Functional testing
#

Validate at least the following behaviors:

  • Device initialization and registration
  • Open and close operations
  • Valid and invalid control requests
  • Partial reads and writes
  • Buffer wraparound and full-buffer conditions
  • Interrupt delivery and acknowledgement
  • Timeout and error handling
  • Device reset and reinitialization
  • Concurrent access from multiple tasks
  • Behavior during task preemption and heavy system load

For block drivers, also test block-range validation, media errors, write protection, file-system initialization, and preservation of data when formatting is not intended.

Timing and concurrency testing
#

Measure interrupt latency and ISR execution time on the actual target. Debugger breakpoints and verbose logging can distort these measurements, so perform timing tests under realistic operating conditions.

Exercise the driver with tasks at different priorities. Confirm that a blocked reader wakes correctly, that transmit buffers do not overflow silently, and that resource ownership remains consistent under repeated interrupts.

For DMA-capable devices, test cache maintenance and buffer alignment using the target’s documented rules. A driver that works with caches disabled may fail when caching is enabled.

Debugging common failures
#

Symptom Possible cause Investigation
Device cannot be opened Incorrect device name or failed registration Check iosDrvInstall(), iosDevAdd(), and initialization status
No interrupts received Incorrect vector, routing, mask, or device configuration Inspect BSP interrupt setup and hardware status registers
Read blocks indefinitely No event notification or incorrect semaphore initialization Trace ISR execution, buffer updates, and semaphore operations
Lost serial data Receive buffer overflow or excessive interrupt latency Measure service time and add overflow detection
Corrupted buffer contents Incorrect ring-buffer logic or concurrent access Verify index ownership, wraparound, and synchronization
Random DMA corruption Cache-coherency or mapping errors Review DMA buffer allocation, cache operations, and address translation
Unexpected task delays Priority inversion or long critical sections Inspect task priorities and resource contention
File-system initialization fails Invalid block-device geometry or configuration Check block size, capacity, callback results, and file-system setup

Integration into the system image
#

A driver that must be available at boot should be initialized at the appropriate point in the BSP or system initialization sequence. Ensure that the hardware, memory, interrupt controller, and required kernel facilities are ready before the driver enables device interrupts.

The initialization order must also account for dependencies such as semaphore services, I/O subsystem setup, and file-system libraries. If the driver is loaded dynamically, verify that its initialization and cleanup paths are safe to invoke repeatedly and that the system does not unload code while interrupts or tasks can still call into it.

For background on the classic driver model, see VxWorks 5.4 Device Driver Development. Developers working with newer VxWorks releases should also review VxBus Driver Development: A Complete Guide for VxWorks Developers and verify which driver framework applies to their target.

📌 9. Key Design Principles
#

Reliable VxWorks drivers are built around clear execution boundaries, explicit resource ownership, and predictable failure handling.

The central principles are:

  1. Keep ISRs short. Acknowledge hardware, preserve essential state, and defer lengthy processing to tasks.
  2. Choose synchronization primitives deliberately. Use semaphores for the synchronization pattern they are designed to support, and do not treat event signaling as equivalent to mutual exclusion.
  3. Define buffer ownership. Specify which context updates each index and how overflow, wraparound, and concurrent access are handled.
  4. Validate all device requests. Check pointers, lengths, block ranges, modes, and control arguments before touching hardware.
  5. Respect platform-specific behavior. Interrupt routing, register layout, cache maintenance, DMA mapping, and callback signatures depend on the target and VxWorks release.
  6. Design for failure. Handle initialization errors, timeouts, device faults, and partial transfers without leaving resources in an inconsistent state.
  7. Measure real-time behavior. Verify latency and scheduling behavior on the target under representative load.

Conclusion
#

VxWorks 5.4 driver development combines the classic I/O subsystem with real-time task scheduling, interrupt handling, and hardware-specific programming. Character drivers commonly expose stream-oriented operations through callbacks registered with iosDrvInstall() and devices added through iosDevAdd(). Block drivers expose storage operations through structures such as BLK_DEV and integrate with a file-system layer.

The UART example demonstrates how device registers, interrupt handling, ring buffers, and task-level synchronization fit together. The RAM-disk example illustrates how a block device can be connected to the DOS file system. Advanced topics—including reentrancy, priority inversion, cache coherency, and select() support—show why correct driver design requires more than simply reading and writing hardware registers.

These examples use legacy APIs and should be validated against the exact VxWorks 5.4 maintenance release, Tornado toolchain, BSP, and target hardware. With clearly defined synchronization, careful interrupt design, and systematic validation, drivers can provide predictable and maintainable access to embedded devices.

Related