Skip to main content

Designing an I2C Device Driver for VxWorks 7

·2402 words·12 mins
VxWorks 7 VxBus I2C Device Drivers BSP Device-Tree EEPROM Embedded Systems
Table of Contents
BSP - This article is part of a series.
Part 15: This Article

Designing an I2C Device Driver for VxWorks 7

Inter-Integrated Circuit (I²C) is one of the most widely used low-speed communication protocols in embedded systems. Its two-wire interface makes it a practical choice for connecting sensors, EEPROMs, ADCs, codecs, power-management ICs, and other peripherals to a processor or SoC.

In VxWorks 7, I²C drivers are integrated into the modern VxBus device framework. This architecture separates the hardware-specific controller implementation from peripheral drivers, while device-tree integration provides a standardized way to describe the hardware topology.

For BSP developers working with custom boards and SoCs, understanding this separation is important. A robust I²C implementation needs to handle more than register access: it must correctly manage bus transactions, device addressing, ACK/STOP conditions, timeouts, error handling, and integration with the VxBus lifecycle.

This article walks through the architecture of a generic I²C master controller driver and demonstrates how an I²C EEPROM peripheral can use the controller through the VxWorks 7 driver model.

The examples use a simplified memory-mapped I²C controller. They are intended as a framework for BSP development and should be adapted to the actual controller specification and VxWorks 7 SDK/API version used by the target platform.

🏗️ VxWorks 7 I²C Driver Architecture
#

VxBus and I²C
#

VxBus is the VxWorks device-driver framework responsible for connecting hardware descriptions with driver implementations. It provides a common lifecycle and abstraction layer for devices, including operations such as probing, attaching, resource management, and driver-to-device matching.

An I²C implementation can be viewed as two layers:

  • I²C controller driver — communicates directly with the memory-mapped I²C hardware and provides the bus-transfer interface.
  • I²C peripheral driver — communicates with a specific device connected to the I²C bus by using the controller’s transfer interface.

This separation is important because an EEPROM driver should not need to know how a particular SoC generates START, STOP, clock, or data signals. Those hardware-specific details belong in the controller driver.

Core VxBus Components
#

A typical VxWorks 7 I²C implementation involves several pieces of the VxBus and I²C infrastructure:

  • vxbI2cLib.h — I²C message and controller interfaces.
  • VXB_I2C_BUS_METHODS — method table used by an I²C bus/controller driver.
  • vxbFdtLib.h — interfaces for working with Flattened Device Tree (FDT) descriptions.
  • VxBus device and resource APIs — used to obtain controller resources and associate driver state with a device.

The exact API details can vary between VxWorks releases and Wind River SDK configurations, so production drivers should always be checked against the headers provided by the target VxWorks installation.

Typical Initialization Flow
#

A simplified initialization sequence looks like this:

  1. The device tree describes the I²C controller and its child devices.
  2. VxBus discovers the controller node.
  3. The controller driver is matched and probed.
  4. The controller driver attaches and maps its hardware resources.
  5. The driver registers the I²C bus.
  6. Peripheral drivers are associated with child devices.
  7. Peripheral drivers issue I²C transactions through the controller interface.

This architecture allows the same peripheral driver to work with different I²C controllers as long as they expose the required VxWorks I²C interface.

🔧 Building a Generic I²C Master Controller
#

To demonstrate the implementation, assume a simple memory-mapped I²C controller with the following registers:

Register Offset Description
CTRL 0x00 Control register
STATUS 0x04 Controller status
DATA 0x08 Transmit/receive data
CLK_DIV 0x0C I²C clock divider

A real controller will normally have considerably more state, including FIFO controls, interrupt status, error flags, arbitration status, bus recovery, and timing configuration. The simplified model is useful for illustrating the driver architecture.

Controller Bit Definitions
#

#define I2C_CTRL_START      (1 << 0)
#define I2C_CTRL_STOP       (1 << 1)
#define I2C_CTRL_READ       (1 << 2)
#define I2C_CTRL_WRITE      (1 << 3)

#define I2C_STATUS_BUSY     (1 << 0)
#define I2C_STATUS_ACK      (1 << 1)

#define I2C_REG_CTRL        0x00
#define I2C_REG_STATUS      0x04
#define I2C_REG_DATA        0x08
#define I2C_REG_CLK_DIV     0x0C

The exact bit definitions must, of course, be replaced with those specified by the actual hardware reference manual.

💻 Implementing the VxBus Controller Driver
#

Register Access Helpers
#

A simple controller can begin with register-access macros such as:

#define I2C_READ_REG(base, offset) \
    (*(volatile UINT32 *)((UINT8 *)(base) + (offset)))

#define I2C_WRITE_REG(base, offset, value) \
    (*(volatile UINT32 *)((UINT8 *)(base) + (offset)) = (value))

For production code, it is preferable to use the platform’s VxBus/resource and register-access mechanisms where appropriate rather than assuming that a raw virtual address is always sufficient.

Driver State
#

The driver needs private state for the VxBus device and the controller’s mapped registers:

typedef struct i2cGenDrvCtrl
{
    VXB_DEV_ID       dev;
    void            *regBase;
    VXB_RESOURCE    *pRes;
} I2C_GEN_DRV_CTRL;

LOCAL VXB_DRV_METHOD i2cGenDrvMethods[] =
{
    { VXB_DEVMETHOD_CALL(vxbDevProbe),  i2cDrvProbe },
    { VXB_DEVMETHOD_CALL(vxbDevAttach), i2cDrvAttach },
    { 0, NULL }
};

A real driver will typically need additional state, such as:

  • Bus clock configuration
  • Interrupt resources
  • Synchronization primitives
  • Transfer timeout values
  • Current transaction state
  • DMA or FIFO state
  • Error and recovery state

Keeping these details inside the controller’s private state prevents peripheral drivers from becoming dependent on controller-specific implementation details.

Probe and Attach
#

The probe function determines whether the driver recognizes the hardware, while the attach function initializes the controller and registers it with the I²C subsystem.

A simplified example is:

LOCAL STATUS i2cDrvProbe(VXB_DEV_ID pDev)
{
    return vxbFdtDevMatch(pDev, NULL);
}

LOCAL STATUS i2cDrvAttach(VXB_DEV_ID pDev)
{
    I2C_GEN_DRV_CTRL *pDrvCtrl;
    void *regBase;

    pDrvCtrl = (I2C_GEN_DRV_CTRL *)
        vxbMemAlloc(sizeof(I2C_GEN_DRV_CTRL));

    if (pDrvCtrl == NULL)
        return ERROR;

    pDrvCtrl->dev = pDev;

    regBase = (void *)vxFdtRegGet(pDev, 0);

    if (regBase == NULL)
    {
        vxbMemFree(pDrvCtrl);
        return ERROR;
    }

    pDrvCtrl->regBase = regBase;

    vxbDevSoftcSet(pDev, pDrvCtrl);

    return vxbI2cBusDevRegister(pDev);
}

The exact resource acquisition and FDT APIs should be aligned with the VxWorks 7 version and BSP framework being used. In particular, production code should properly validate the reg resource, map it through the platform’s resource mechanism when required, and clean up all resources if attachment fails.

🔄 Implementing I²C Data Transfers
#

The central operation of an I²C controller driver is transferring one or more I²C messages.

A simplified transfer routine can be structured as follows:

LOCAL STATUS i2cDevXfer
    (
    VXB_DEV_ID      dev,
    VXB_I2C_MSG    *msgs,
    int             num
    )
{
    I2C_GEN_DRV_CTRL *pDrvCtrl = vxbDevSoftcGet(dev);
    void *base = pDrvCtrl->regBase;

    for (int i = 0; i < num; i++)
    {
        VXB_I2C_MSG *msg = &msgs[i];

        for (int j = 0; j < msg->len; j++)
        {
            UINT32 ctrl;

            if (msg->flags & VXB_I2C_M_RD)
            {
                ctrl = I2C_CTRL_READ;
            }
            else
            {
                ctrl = I2C_CTRL_WRITE;
                I2C_WRITE_REG(base, I2C_REG_DATA, msg->buf[j]);
            }

            if (j == 0)
                ctrl |= I2C_CTRL_START;

            if (j == msg->len - 1)
                ctrl |= I2C_CTRL_STOP;

            I2C_WRITE_REG(base, I2C_REG_CTRL, ctrl);

            while (I2C_READ_REG(base, I2C_REG_STATUS) &
                   I2C_STATUS_BUSY)
            {
                /* A production driver must implement a timeout. */
            }

            if (!(I2C_READ_REG(base, I2C_REG_STATUS) &
                  I2C_STATUS_ACK))
            {
                return ERROR;
            }

            if (msg->flags & VXB_I2C_M_RD)
                msg->buf[j] =
                    I2C_READ_REG(base, I2C_REG_DATA);
        }
    }

    return OK;
}

LOCAL VXB_I2C_BUS_METHODS i2cBusMethods =
{
    .i2cDevXfer        = i2cDevXfer,
    .i2cDevXferTimeout = NULL
};

This example is intentionally simplified. Actual I²C transaction handling requires careful consideration of message boundaries and bus protocol semantics.

Read Transactions Need Special Handling
#

One important detail is that an I²C read operation normally does not simply mean “set a READ bit and read the DATA register.”

Many peripherals require a sequence such as:

  1. START
  2. Send the device address with the write direction
  3. Send a register or memory address
  4. Issue a repeated START
  5. Send the device address with the read direction
  6. Receive one or more bytes
  7. Send ACK for intermediate bytes
  8. Send NACK for the final byte
  9. STOP

Therefore, a production controller driver must interpret the VXB_I2C_MSG sequence correctly rather than assuming that every message can be independently terminated with STOP.

Never Poll Without a Timeout
#

The simplified example contains a polling loop:

while (I2C_READ_REG(base, I2C_REG_STATUS) & I2C_STATUS_BUSY)
{
}

This is acceptable only as pseudocode.

A production driver must use a timeout. If the peripheral holds SDA low, the controller becomes stuck, or the hardware encounters an unexpected state, an infinite polling loop can block the calling task indefinitely.

A robust implementation should therefore:

  • Record a transaction deadline.
  • Poll or wait for completion.
  • Abort when the timeout expires.
  • Clear controller state.
  • Report the appropriate error.
  • Attempt bus recovery when supported.

For high-performance or latency-sensitive systems, interrupt-driven or DMA-based transfers are generally preferable to long polling loops.

🌳 Device Tree Integration
#

The device tree describes the I²C controller and the peripherals connected to it.

A simplified example is:

i2c@4000f000 {
    compatible = "generic,i2c-master";
    reg = <0x4000f000 0x1000>;

    #address-cells = <1>;
    #size-cells = <0>;

    status = "okay";

    eeprom@50 {
        compatible = "atmel,24c32";
        reg = <0x50>;
    };
};

The controller node identifies the I²C hardware, while the child node describes the EEPROM attached to the bus.

The reg = <0x50> property represents the EEPROM’s 7-bit I²C address in this example.

For a production BSP, the device tree may also need properties for:

  • Input clock frequency
  • Bus speed
  • Interrupts
  • DMA channels
  • Reset controls
  • Pin multiplexing
  • Power supplies
  • Controller-specific timing parameters

The exact bindings depend on the controller hardware and the VxWorks BSP implementation.

💾 Developing an I²C EEPROM Driver
#

Once the controller driver has registered the bus, a peripheral driver can communicate with an EEPROM through the VxWorks I²C interface.

A simple read operation can be represented by two messages:

LOCAL STATUS eepromReadByte
    (
    VXB_DEV_ID pDev
    )
{
    VXB_I2C_MSG msg[2];
    UINT8 addr = 0x00;
    UINT8 data = 0;

    msg[0].addr  = 0x50;
    msg[0].flags = 0;
    msg[0].buf   = &addr;
    msg[0].len   = 1;

    msg[1].addr  = 0x50;
    msg[1].flags = VXB_I2C_M_RD;
    msg[1].buf   = &data;
    msg[1].len   = 1;

    if (vxbI2cDevXfer(pDev, msg, 2) == OK)
    {
        printf("EEPROM read success: 0x%02x\n", data);
        return OK;
    }

    printf("EEPROM read failed\n");
    return ERROR;
}

The important concept is that the peripheral driver does not directly manipulate the controller’s registers.

Instead, it constructs I²C messages and passes them to vxbI2cDevXfer(). The controller driver translates those messages into hardware-specific operations.

EEPROM Addressing and Write Cycles
#

Real EEPROM drivers require more than a single-byte read.

Depending on the EEPROM family, the driver may need to handle:

  • 8-bit or 16-bit memory addresses
  • Page-write boundaries
  • Write-cycle delays
  • ACK polling
  • Sequential reads
  • Device-specific address bits
  • Multiple I²C addresses

For example, after an EEPROM write, the device may temporarily stop acknowledging its address while internally programming the nonvolatile memory. A robust driver should use the device’s documented write-cycle behavior rather than relying on an arbitrary fixed delay.

🧪 Testing and Debugging
#

I²C problems are often easier to diagnose by examining both the software state and the electrical bus signals.

Useful VxWorks Tools
#

Depending on the BSP and VxWorks configuration, commands such as the following can help inspect the system:

  • i2cShow — inspect registered I²C buses and devices when available.
  • vxbDevShow — inspect VxBus devices and their relationships.

The exact availability and output of these commands depends on the VxWorks image and included components.

Verify Device Tree Matching
#

If the controller never attaches, start with the device tree.

Check:

  • compatible strings
  • reg address and size
  • Controller status
  • Child-node hierarchy
  • I²C device addresses
  • Required clocks and interrupts
  • Pin multiplexing configuration

A mismatch between the hardware description and the driver’s matching table can prevent the driver from ever reaching its attach routine.

Check ACK and STOP Behavior
#

If the controller attaches but transactions fail, inspect the actual bus protocol.

A logic analyzer can reveal:

  • START conditions
  • STOP conditions
  • Device addresses
  • Read/write direction bits
  • ACK/NACK responses
  • Clock frequency
  • Clock stretching
  • Unexpected bus stalls

This is often much faster than debugging register values alone.

Watch for Bus Lockups
#

A common I²C failure mode occurs when a transaction is interrupted or a peripheral holds SDA low.

A production driver should consider implementing bus recovery, typically by generating clock pulses and restoring the bus to an idle state when the controller and board design permit it.

Timeout handling is equally important. No transaction should be able to trap a VxWorks task in an unbounded polling loop.

🚀 Extending the Driver for Production Hardware
#

The generic driver above provides the basic architecture, but production-grade BSP code normally needs substantially more functionality.

Interrupt-Driven Transfers
#

Instead of polling the status register after every byte, the controller can generate interrupts when:

  • A byte has been transmitted
  • A byte has been received
  • The controller becomes idle
  • An ACK error occurs
  • Arbitration is lost
  • A bus error occurs

An interrupt-driven implementation can reduce CPU consumption and prevent long transactions from blocking execution.

DMA and FIFO Support
#

Controllers with FIFOs or DMA engines can transfer larger blocks of data more efficiently.

This is particularly useful for:

  • EEPROM transfers
  • Audio codecs
  • Sensors with large register windows
  • Display-related peripherals
  • High-volume management traffic

Multi-Bus Support
#

A BSP may contain multiple I²C controllers. The driver architecture should therefore keep controller-specific state in per-device structures rather than relying on global variables.

Each VxBus controller instance can then maintain its own:

  • Register base
  • Clock configuration
  • Interrupt resources
  • Transfer state
  • Synchronization objects
  • Error state

Repeated START
#

Repeated START support is especially important for register-based peripherals.

Many devices require a write of the register address followed immediately by a read without releasing the bus:

START
  Device Address + Write
  Register Address
REPEATED START
  Device Address + Read
  Data
STOP

A controller driver that inserts STOP between every message may fail with such peripherals.

Concurrency and Synchronization
#

Multiple VxWorks tasks may attempt to access the same I²C controller concurrently.

The driver therefore needs a synchronization strategy to ensure that one transaction cannot interfere with another. A controller-level mutex or equivalent synchronization mechanism can serialize access to the hardware while preserving the message-based interface exposed to peripheral drivers.

🏁 Conclusion
#

Designing an I²C driver for VxWorks 7 is primarily an exercise in separating hardware-specific bus control from peripheral-specific functionality.

The VxBus architecture provides the foundation for this separation:

  • The controller driver manages the physical I²C hardware.
  • The I²C bus interface provides message-based transfers.
  • The device tree describes controllers and connected peripherals.
  • Peripheral drivers use the bus interface without directly accessing controller registers.

A minimal implementation can begin with memory-mapped register access, VxBus probe/attach callbacks, an I²C transfer method, and device-tree integration. From there, it can evolve into a production driver with interrupt handling, DMA, FIFO support, transaction timeouts, repeated START operations, bus recovery, and concurrency control.

The generic controller and EEPROM examples in this article are intentionally simplified, but they provide a useful starting architecture for custom VxWorks 7 BSPs. When adapting them to real hardware, the most important step is to align every register operation, transaction state, device-tree property, and VxWorks API call with the actual controller documentation and SDK version.

With that foundation in place, the same driver architecture can be extended to more complex I²C peripherals, including sensors, codecs, ADCs, PMICs, EEPROMs, and other board-management devices.

BSP - This article is part of a series.
Part 15: This Article

Related

NXP QorIQ P1010 VxWorks 6.6 BSP Architecture Guide
·2397 words·12 mins
NXP QorIQ P1010 VxWorks VxWorks 6.6 BSP Power Architecture E500-V2 VxBus Embedded Systems
Practical PCIe Device Driver Development on VxWorks 7 with VxBus 2.0
·515 words·3 mins
PCIe VxWorks 7 VxBus Device Driver
Kontron D0801 BSP for Wind River VxWorks
·1557 words·8 mins
Kontron D0801 VxWorks BSP Wind River QorIQ P4080 FlashFX Pro SMP Embedded Systems