CAN Programming in VxWorks: Driver Setup, Messaging, and Recovery
Controller Area Network (CAN) is a robust serial communication protocol widely used in automotive electronics, industrial automation, embedded controllers, and distributed control systems. It allows multiple electronic control units (ECUs) and embedded devices to exchange messages over a shared bus without requiring a central host computer.
VxWorks, Wind River’s real-time operating system (RTOS), supports CAN communication through hardware-specific drivers and platform-dependent integration interfaces. Developers can use these facilities to configure the CAN controller, transmit and receive frames, handle interrupts, monitor communication errors, and implement recovery procedures.
This guide presents a practical approach to CAN programming under VxWorks, covering device initialization, bit-timing configuration, message exchange, interrupt handling, and error recovery. It also explains how the device driver, Board Support Package (BSP), and application interact.
The code examples use a CAN API resembling the canDevCreate(), canIoctl(), canWrite(), and canRead() interfaces described in this guide. These names and their argument conventions are driver-specific rather than universal APIs guaranteed to exist in every VxWorks installation. Verify the relevant header files and driver documentation before using the examples with a real target.
For a driver-level reference, see Design and Optimization of a CAN Driver Under VxWorks, which describes a multi-channel CAN implementation based on the SJA1000T controller. The related guide to developing an SJA1000T CAN driver using VxBus provides additional background on driver structure, hardware resources, interrupts, and controller initialization.
🔌 Understanding CAN Support in VxWorks #
A CAN application depends on several software and hardware layers working together.
CAN Controller and BSP #
The CAN controller implements the hardware functions required to transmit, receive, and monitor CAN frames. It may be integrated into an SoC or provided by a separate controller connected through a peripheral bus.
The BSP describes the target hardware and provides platform-specific resources such as register addresses, interrupt mappings, clock configuration, and device initialization.
Before writing application code, verify that the BSP supports the intended CAN controller and that the correct driver is included in the VxWorks image or otherwise available to the target.
CAN Device Driver #
The driver translates application requests into controller operations.
Typical responsibilities include:
- Initializing controller registers and bit timing.
- Configuring standard or extended frame formats.
- Managing transmit and receive buffers.
- Handling controller interrupts.
- Reporting bus status and error counters.
- Applying acceptance filters.
- Recovering from controller faults, where supported.
In VxWorks versions and configurations that use VxBus, a CAN driver may integrate with the device framework through the appropriate driver registration and resource-management mechanisms.
The exact implementation depends on the controller, BSP, and VxWorks release. A legacy BSP-specific driver may follow a different architecture.
Application Interface #
The application interacts with the CAN device through the interface exposed by its driver. Depending on the implementation, this may use file descriptors, device-specific APIs, ioctl()-style configuration operations, or dedicated read/write functions.
The example in this article assumes a driver that exposes a device endpoint such as /can/0 and provides operations for configuring the controller, transmitting frames, receiving frames, and querying error status.
Do not assume that /can/0 or the functions shown below are available automatically in a default VxWorks image. The device name and API must match the driver installed on your target.
🛠️ Step 1: Initialize the CAN Device #
The first step is to create or open the CAN device using the interface provided by the driver.
The following example illustrates the initialization workflow:
#include <vxWorks.h>
#include <stdio.h>
#include <string.h>
#include <can.h>
/*
* API-specific example:
* Verify canDevCreate() and CAN_ID against the actual driver.
*/
int canInitialize(void)
{
CAN_ID canId;
int fd;
memset(&canId, 0, sizeof(canId));
/*
* Example device name. The actual path depends on
* the CAN driver and target configuration.
*/
fd = canDevCreate("/can/0", &canId);
if (fd < 0)
{
printf("Failed to initialize CAN device\n");
return ERROR;
}
printf("CAN device initialized successfully\n");
/*
* The caller should retain the returned device handle
* and use it for configuration and frame operations.
*/
return fd;
}The function returns the device handle on success and an error indication otherwise. The caller should keep this handle for subsequent operations and eventually release it through the corresponding driver API.
In a production application, also verify that the controller has completed initialization, the transceiver is enabled, and the physical bus is connected correctly.
Some CAN controllers require additional configuration before they can enter normal operating mode. Others may initialize during BSP startup. Follow the controller’s documented initialization sequence rather than assuming that opening a device always makes it ready to communicate.
⚙️ Step 2: Configure the CAN Bus #
CAN communication requires compatible bit timing across all participating nodes. Incorrect bit-rate, sample-point, or synchronization settings can produce communication errors even when the driver initializes successfully.
A configuration structure in the example API contains the following fields:
bitRate: The nominal CAN bit rate.samplePoint: The desired sample point, according to the driver’s representation.sjw: The synchronization jump width.
For example, the following settings describe a nominal bit rate of 500 kbit/s and a sample point of 87.5%.
#include <stdint.h>
#include <string.h>
#include <can.h>
/*
* Example only. Use the CAN_CONFIG layout and ioctl
* calling convention documented by the actual driver.
*/
STATUS canConfigure(int fd)
{
CAN_CONFIG config;
memset(&config, 0, sizeof(config));
config.bitRate = 500000;
config.samplePoint = 875;
config.sjw = 1;
/*
* Pass the configuration according to the real API.
* Do not cast a pointer to int if that can truncate
* the address on the target ABI.
*/
if (canIoctl(fd, CAN_CMD_SET_CONFIG,
(CAN_IOCTL_ARG)&config) != OK)
{
printf("Failed to configure CAN bus\n");
return ERROR;
}
return OK;
}CAN_IOCTL_ARG above represents the pointer-argument type defined by the example driver; it is not a universal VxWorks type. Replace it with the actual argument type and invocation required by the installed implementation. Some drivers use an integer argument for scalar values, while others use a pointer for configuration structures.
Select the Correct Bit Timing #
The example’s 500 kbit/s rate is common in some automotive and industrial networks, but it is not a universal setting.
Choose bit timing based on:
- The bit rate supported by every node on the bus.
- The oscillator or peripheral clock supplied to the controller.
- The physical bus length and cable characteristics.
- The transceiver’s propagation delay.
- The controller’s supported time-quanta and prescaler values.
- The network’s noise margin and sampling requirements.
The sample-point representation also varies by driver. A value of 875 might mean 87.5% in one interface, but this must be verified rather than assumed.
The synchronization jump width (SJW) is constrained by the CAN controller and selected timing configuration. It should not be copied from an example without checking those constraints.
Classical CAN and CAN FD #
Classical CAN supports up to 8 data bytes per frame. CAN FD supports larger payloads, up to 64 bytes, when the controller, transceiver, driver, and all relevant nodes support the required mode.
CAN FD can also use a separate data-phase bit rate when bit-rate switching is enabled. In that case, the nominal and data-phase timing parameters must be configured consistently.
A driver that exposes only an 8-byte CAN_FRAME may support Classical CAN only. Do not assume that setting a higher bit rate enables CAN FD.
📤 Step 3: Transmit CAN Frames #
After initializing and configuring the device, the application can construct a frame and submit it to the driver.
The following example creates a standard Classical CAN data frame with identifier 0x123 and eight payload bytes.
#include <stdio.h>
#include <can.h>
STATUS canSendExample(int fd)
{
CAN_FRAME txFrame;
int written;
memset(&txFrame, 0, sizeof(txFrame));
txFrame.id = 0x123;
txFrame.dlc = 8;
/*
* Payload values are for demonstration only.
* Define application-specific encoding and byte
* order in the actual protocol.
*/
for (int i = 0; i < 8; i++)
{
txFrame.data[i] = (unsigned char)i;
}
written = canWrite(fd, &txFrame, 1);
if (written != 1)
{
printf("Failed to submit CAN frame\n");
return ERROR;
}
return OK;
}The example assumes the driver’s canWrite() returns the number of frames accepted for transmission. Confirm its exact return semantics, because some APIs distinguish queueing a frame from completing its transmission on the physical bus.
Select the Frame Format #
A complete implementation should define the intended identifier format and frame type.
Important properties include:
- Standard identifier: Uses an 11-bit CAN identifier.
- Extended identifier: Uses a 29-bit CAN identifier.
- Data frame: Carries an application payload.
- Remote frame: Supported by Classical CAN but not used in the same way by CAN FD.
- Payload length: Must match the supported CAN mode and frame format.
The field names used to specify standard versus extended identifiers vary between drivers. Validate the identifier range before submitting a frame.
Handle Transmit Errors #
Transmission can fail because the transmit queue is full, the controller is bus-off, the network is disconnected, or the frame cannot be transmitted successfully.
A robust application should distinguish between failure to queue a frame and failure to complete transmission. The driver documentation may expose transmit-completion events, error status, or asynchronous notifications for this purpose.
Avoid retrying continuously without a bound. A disconnected or persistently faulty bus can otherwise consume CPU time and prevent other real-time tasks from executing.
📥 Step 4: Receive CAN Frames #
CAN reception is commonly implemented using hardware receive buffers, controller interrupts, and a driver-managed queue.
The application reads frames from the driver rather than accessing controller registers directly.
The following example uses a blocking or driver-managed read operation:
#include <stdio.h>
#include <can.h>
STATUS canReceiveLoop(int fd)
{
CAN_FRAME rxFrame;
for (;;)
{
int received = canRead(fd, &rxFrame, 1);
if (received == 1)
{
/*
* Validate the frame length according to the
* supported CAN format and driver definition.
*/
if (rxFrame.dlc > 8)
{
printf("Invalid Classical CAN payload length\n");
continue;
}
printf("Received CAN frame ID: 0x%X\n",
rxFrame.id);
for (int i = 0; i < rxFrame.dlc; i++)
{
printf("Data[%d] = 0x%02X\n",
i, rxFrame.data[i]);
}
/*
* Perform application processing here.
* Keep this path bounded if timing is critical.
*/
}
else if (received < 0)
{
printf("CAN receive error\n");
return ERROR;
}
else
{
/*
* A return value of zero may indicate that
* no frame is currently available, depending
* on the driver API.
*/
}
}
return OK;
}This example assumes canRead() returns the number of frames received. Its handling of empty reads, timeouts, and errors must be adjusted to the actual driver’s contract.
For a blocking receive function, ensure the task does not wait indefinitely if the application needs a bounded shutdown time. If the driver supports timeouts or nonblocking operations, use them according to the system’s responsiveness requirements.
Filtering Incoming Messages #
CAN identifiers often represent message types or priorities within an application’s protocol. Receiving every frame may waste CPU resources when the network carries high traffic.
Where supported, configure acceptance filters in the CAN controller or driver to admit only the relevant identifiers.
A correct filtering strategy can reduce interrupt load, receive-queue pressure, and the amount of application-level parsing.
However, filters must be configured carefully. Incorrect masks can exclude required messages or accept unintended traffic. Validate filter behavior with known frame identifiers and test both included and excluded messages.
⚡ Step 5: Handle CAN Interrupts Correctly #
CAN controllers typically generate interrupts for receive events, transmission completion, error conditions, and changes in controller state.
A suitable driver handles these events while keeping interrupt processing short and predictable.
Interrupt Service Routine Design #
The ISR should generally:
- Read the relevant controller status.
- Identify and acknowledge the interrupt source using the controller’s documented procedure.
- Capture essential status or frame information if required.
- Notify a deferred processing task using an ISR-safe mechanism.
- Return promptly.
The ISR should not perform lengthy printing, wait on blocking synchronization primitives, or run complex application-level protocol processing.
An illustrative design is shown below. This is pseudocode because the actual ISR signature, interrupt-vector setup, status registers, and notification mechanism depend on the controller and BSP.
/*
* Illustrative pseudocode, not a directly compilable ISR.
* Use the handler signature and APIs specified by the BSP.
*/
void canInterruptHandler(void *arg)
{
MY_CAN_DEVICE *dev = (MY_CAN_DEVICE *)arg;
unsigned int status;
status = canReadInterruptStatus(dev);
if (status == 0)
{
return;
}
/*
* Acknowledge the relevant hardware event according
* to the CAN controller's register semantics.
*/
canAcknowledgeInterrupt(dev, status);
/*
* Notify the processing task through an ISR-safe
* mechanism supported by the target configuration.
*/
canNotifyWorkerFromIsr(dev, status);
}Do not assume that a generic intConnect() call is sufficient for every VxWorks CAN driver. Some devices register interrupts through VxBus, while others use BSP-specific interrupt interfaces.
The driver must also handle shared interrupts correctly. If multiple devices share an interrupt vector, the handler must determine whether its own device generated the event before acknowledging it.
🚨 Step 6: Monitor CAN Errors and Recover from Bus-Off #
CAN controllers monitor communication errors and maintain error counters. These counters provide information about conditions such as bit errors, stuffing errors, CRC errors, and missing acknowledgements.
As errors accumulate, a controller may transition between error-active and error-passive states and eventually enter bus-off.
Query Controller Error Status #
A driver may expose an error-reporting structure through an ioctl-style API.
An illustrative example is:
#include <stdio.h>
#include <can.h>
STATUS canCheckErrors(int fd)
{
CAN_ERROR_STATUS status;
memset(&status, 0, sizeof(status));
if (canIoctl(fd, CAN_CMD_GET_ERROR_STATUS,
(CAN_IOCTL_ARG)&status) != OK)
{
printf("Failed to read CAN error status\n");
return ERROR;
}
printf("TX error counter: %u\n",
status.txErrorCounter);
printf("RX error counter: %u\n",
status.rxErrorCounter);
printf("Bus-off: %d\n", status.busOff);
return OK;
}As with configuration calls, CAN_IOCTL_ARG is a placeholder for the argument type required by the actual driver.
The CAN error counters should be interpreted according to the controller specification and the CAN protocol’s error-state rules. A threshold of 127 or 128 can be useful for detecting a transition toward error-passive behavior in many implementations, but a generic comparison is not a substitute for checking the controller’s actual status bits.
When repeated errors occur, investigate their cause rather than simply resetting the controller. Common causes include incorrect bit timing, missing or incorrect termination, electrical noise, transceiver faults, disconnected nodes, and mismatched frame configuration.
Implement Bus-Off Recovery #
When a controller enters bus-off, the application should stop treating the channel as healthy and initiate a controlled recovery procedure.
A recovery policy may include:
- Record the bus-off event and relevant status.
- Notify the application or supervisory controller.
- Stop or limit new transmissions.
- Allow the controller to recover according to the applicable protocol and hardware requirements.
- Reinitialize the controller or request recovery through the supported driver interface.
- Resume normal traffic only after the driver confirms that the channel is operational.
For some controllers, recovery requires observing the prescribed bus-idle conditions. A forced reset may discard pending messages or interfere with other nodes, so it should not be used indiscriminately.
The following pseudocode illustrates the policy:
/*
* Illustrative pseudocode. The actual recovery API and
* required bus-idle procedure are controller-specific.
*/
STATUS canHandleBusOff(int fd, CAN_ERROR_STATUS *status)
{
if (status == NULL)
{
return ERROR;
}
if (!status->busOff)
{
return OK;
}
printf("CAN controller entered bus-off state\n");
/*
* Notify the application and inhibit additional
* transmissions while recovery is in progress.
*/
canNotifyApplicationBusOff(fd);
/*
* Invoke only the recovery mechanism documented
* for the selected controller and driver.
*/
if (canRequestRecovery(fd) != OK)
{
printf("CAN recovery request failed\n");
return ERROR;
}
return OK;
}The function names in this example are conceptual and must be implemented or replaced using the actual driver API.
Production systems should prevent repeated recovery attempts from creating a busy loop. Use bounded retries, an appropriate delay or backoff strategy, diagnostic logging, and a clear policy for escalating persistent failures.
🧹 Step 7: Clean Up the CAN Device #
When the application no longer needs the CAN interface, release the device using the cleanup routine documented by the driver.
For the API used in the examples, cleanup may resemble the following:
#include <can.h>
STATUS canCleanup(int fd)
{
if (canDevDelete(fd) != OK)
{
return ERROR;
}
return OK;
}This example assumes canDevDelete() accepts the device handle and returns a VxWorks-style status value. Some drivers separate device creation, opening, closing, and device deletion into different operations. Use the correct lifecycle functions for your implementation.
Before releasing the device, stop application-level transmit and receive activity, ensure that worker tasks have terminated or detached safely, and release any allocated resources owned by the application.
The driver remains responsible for safely disabling interrupts, stopping DMA or controller activity, and releasing its own hardware resources according to the platform’s device lifecycle.
🧪 Testing and Troubleshooting #
A successful compilation does not establish that a CAN implementation works reliably on the physical bus. Functional and timing validation should cover both normal operation and failure conditions.
Verify the Hardware Setup #
Check the following before diagnosing application code:
- Confirm that every node uses compatible nominal bit timing.
- Verify that the transceivers support the selected CAN format.
- Check bus wiring, signal polarity, connector pinout, and grounding.
- Ensure the bus is terminated according to the physical network design.
- Confirm that each node is powered and leaves initialization mode correctly.
- Verify that the driver reports the correct device and controller status.
A controller that cannot receive acknowledgements may increment its transmit error counter even when the application appears to submit frames successfully.
Test Transmit and Receive Independently #
Begin with a simple test involving two CAN nodes and a known identifier.
Transmit a frame from one node and verify that the second node receives the expected identifier, frame format, payload length, and data bytes.
Then reverse the direction and repeat the test. This helps determine whether the problem originates in the driver, physical bus, controller configuration, or application logic.
A CAN analyzer or USB-to-CAN interface is useful for inspecting actual bus traffic and confirming whether the controller is transmitting frames correctly.
Validate Under Load #
After basic communication succeeds, test the target under representative traffic and system load.
Measure:
- Frame loss and receive-buffer overflow.
- Transmission queue saturation.
- ISR duration and receive-task latency.
- Error-counter behavior under electrical disturbance.
- Recovery after disconnection or bus-off.
- Performance when other high-priority tasks are executing.
For time-critical systems, also measure worst-case response behavior rather than relying exclusively on average throughput.
✅ Conclusion #
CAN programming under VxWorks requires coordination among the application, CAN driver, BSP, and physical controller. The development workflow includes device initialization, bit-timing configuration, frame transmission and reception, interrupt processing, error monitoring, and controlled bus-off recovery.
A reliable implementation depends on more than sending and receiving frames. It must use the correct controller-specific APIs, respect interrupt-context restrictions, validate all incoming messages, manage transmit and receive buffers safely, and recover predictably from faults.
For driver developers, the most important starting point is the hardware and driver documentation. The VxWorks CAN driver design and optimization reference and the VxBus SJA1000T driver development guide provide concrete examples of controller initialization, interrupt-driven reception, and hardware-specific integration.
By combining a well-defined driver interface with careful timing configuration, bounded interrupt handling, appropriate error policies, and systematic hardware testing, developers can build CAN communication systems that are responsive, maintainable, and suitable for demanding real-time embedded applications.