CAN Driver Implementation Guidelines

Target audience: Developers who need to write a custom CAN driver for CC27XX devices that are affected by the SYS‑211 advisory. Goal: Summarize the mandatory changes, explain why they are required, and provide ready‑to‑copy code snippets taken from the TI reference driver (CANCC27XXX10.c).

Background – What is SYS‑211?

SYS‑211 advisory details an issue that affects the CAN controller and other bus initiators on CC274XX-Q1 silicon. When a write is performed to CAN registers while other bus initiators (DMA, HSM, UART, SPI, I2S) are active, the CAN controller can ignore the HREADYIN control signal and generate back‑to‑back internal accesses with random data. This results in undefined behavior when it occurs on certain critical CAN registers.

CAN Driver Workaround (as defined by TI):

  • All writes to the critical CAN registers must be performed via DMA while all other bus initiators are disabled.

  • The DMA write routine must block on a semaphore, therefore it cannot be called from an ISR.

  • Interrupt handling must be deferred to a task/thread.

  • Any driver‑library function that writes a critical register must be replaced by the DMA‑based version.

  • Access to the CAN write functions must be protected with a semaphore.

The reference implementation that follows these rules is located in the SDK under /source/ti/can/drivers/CANCC27XXX10.c.

Summary of Key Requirements

To properly implement a CAN driver on silicon affected by SYS‑211:

  • Never write a critical CAN register directly. Use the DMA helper (CANCC27XX_writeRegDma).

  • Never call the DMA helper from an ISR. Defer the ISR to a task using a semaphore.

  • Wrap any Tx path with a binary semaphore.

  • Re‑implement any weak driver‑lib function that writes a critical register so it routes through the DMA helper.

  • Handle DMA power and resources correctly: Set dependencies on DMA during initialization and release them during cleanup.

Critical CAN Registers

The following registers are critical and must be written via DMA helper:

Register

Description

MCAN_TXBAR

Tx Buffer Add Request

MCAN_TXEFA

Tx Event FIFO Acknowledge

MCAN_RXF0A

Rx FIFO0 Acknowledge

MCAN_RXF1A

Rx FIFO1 Acknowledge

MCAN_TXBCR

Tx Buffer Cancellation Request

MCAN_CCCR

CAN Control

MCAN_NDAT1 / MCAN_NDAT2

New Data registers

MCANSS_CLKCTL

Clock control

MCANSS_EOI

End‑of‑Interrupt

CANFD_ICLR0 / CANFD_ICLR1

Interrupt clear registers

These are defined in the driver as an array:

/* Array of critical MCAN registers that require special handling due to errata
 * SYS_211.
 */
static uint32_t CANCC27XX_mcanCriticalRegs[] = {
    /* Most frequently accessed registers are listed first */
    MCAN_TXBAR,
    MCAN_TXEFA,
    MCAN_RXF0A,
    MCAN_RXF1A,
    MCAN_NDAT1,
    MCAN_NDAT2,
    MCAN_CCCR,
    MCAN_TXBCR,
};

This function searches the array to determine if a register is critical:

/*
 *  Returns true if the provided offset matches a critical MCAN register.
 */
static bool CANCC27XX_isCriticalMcanReg(uint32_t offset)
{
    bool found = false;
    size_t i;
    size_t numRegs = sizeof(CANCC27XX_mcanCriticalRegs) / sizeof(CANCC27XX_mcanCriticalRegs[0]);

    for (i = 0; i < numRegs; i++)
    {
        if (CANCC27XX_mcanCriticalRegs[i] == offset)
        {
            found = true;
            break;
        }
    }

    return found;
}

DMA‑Based Register Write

DMA Configuration for Register Writes

Allocate a dedicated DMA channel for critical CAN register writes:

#define CANCC27XX_REG_WRITE_DMA_CHANNEL_NUM   10U
#define CANCC27XX_WRITE_REG_UDMA_CHANNEL_MASK ((uint32_t)1UL << CANCC27XX_REG_WRITE_DMA_CHANNEL_NUM)

/* DMA Control Table Entries */
ALLOCATE_CONTROL_TABLE_ENTRY(dmaChannel10ControlTableEntry, CANCC27XX_REG_WRITE_DMA_CHANNEL_NUM);

static volatile uDMAControlTableEntry *CANCC27XX_regWriteDmaTableEntry = &dmaChannel10ControlTableEntry;

/* Variable to hold register value to be written by uDMA */
static uint32_t CANCC27XX_regVal;

DMA‑Based Register Write Helper Function

All critical registers are written using DMA via ``CANCC27XX_writeRegDma()``. The function does the following:

  1. Acquires the CommonResource lock (prevents use of other bus initiators).

  2. Disables interrupts (PRIMASK = 1).

  3. Programs a one‑shot uDMA channel that copies the value from SRAM to the CAN register address.

  4. Waits (polling the DMA‑done interrupt) without touching the bus.

  5. Restores the interrupt mask and releases the CommonResource lock.

The following function must be used verbatim without any modifications:

/*
 *  This function writes a CAN register using the uDMA after ensuring other bus
 *  masters are inactive and all interrupts are disabled. This function cannot
 *  be called from a context where blocking is not allowed.
 */
static void CANCC27XX_writeRegDma(uint32_t addr, uint32_t value)
{
    uint32_t dmaDoneMask;
    uint32_t primask;

    /* Due to SYS_211, acquire lock to arbitrate access to the HSM, CAN,
     * and APU.
     */
    CommonResourceXXF3_acquireLock(SemaphoreP_WAIT_FOREVER);

    /* Store the value in SRAM */
    CANCC27XX_regVal = value;

    /* Store current PRIMASK and set PRIMASK=1 to disable interrupts.
     * HwiP_disable() is not used here since it may write BASEPRI which leaves
     * high-priority interrupts enabled.
     */
    primask = __get_PRIMASK();
    __set_PRIMASK(1);

    /* Configure the CAN reg write DMA control structure for a single 32-bit
     * (word-aligned) transaction in auto-request mode with no address
     * incrementing.
     */
    CANCC27XX_regWriteDmaTableEntry->control = (UDMA_SIZE_32 | UDMA_SRC_INC_NONE | UDMA_DST_INC_NONE |
                                                UDMA_ARB_1 | UDMA_MODE_AUTO);

    /* Set DMA channel to high priority to minimize the critical section duration.
     * It is assumed no other drivers in the system are using this DMA channel and
     * thus we do not need to save and restore the previous priority.
     */
    uDMASetChannelPriority(CANCC27XX_WRITE_REG_UDMA_CHANNEL_MASK);

    /* Configure the DMA transfer source and destination end addresses. The end
     * addresses are the same as the start addresses since address incrementing
     * is disabled in the control struct.
     */
    CANCC27XX_regWriteDmaTableEntry->pSrcEndAddr = (void *)(&CANCC27XX_regVal);
    CANCC27XX_regWriteDmaTableEntry->pDstEndAddr = (void *)(addr);

    /* Make sure all DMA interrupts are disabled except for the channel being
     * used. Save the mask to restore it when the transaction is done.
     */
    dmaDoneMask                      = HWREG(DMA_BASE + DMA_O_DONEMASK);
    HWREG(DMA_BASE + DMA_O_DONEMASK) = CANCC27XX_WRITE_REG_UDMA_CHANNEL_MASK;

    /* Clear potential DMA channel request done for the channel being used */
    uDMAClearInt(CANCC27XX_WRITE_REG_UDMA_CHANNEL_MASK);

    /* Read any DMA register to ensure the DMA channel request done clear above
     * has completed before clearing the DMA interrupt.
     */
    HWREG(DMA_BASE + DMA_O_DONEMASK);

    /* Clear potential pending DMA interrupt */
    HwiP_clearInterrupt(INT_DMA_DONE_COMB);

    /* Enable the DMA channel; it will be disabled automatically by the uDMA
     * controller when the transfer is complete.
     */
    uDMAEnableChannel(CANCC27XX_WRITE_REG_UDMA_CHANNEL_MASK);

    /* Start the uDMA transfer. Due to SYS_211, there cannot be any
     * system bus accesses from this point until the transfer is done.
     */
    uDMARequestChannel(CANCC27XX_WRITE_REG_UDMA_CHANNEL_MASK);

    /* Wait until the uDMA transaction is finished by polling the NVIC interrupt
     * set pending register. This code is written in assembly using only CPU
     * registers to prevent accessing system bus while the DMA transaction is
     * ongoing. The assembly is equivalent to:
     *     while (IntGetPend(INT_DMA_DONE_COMB) == false) {};
     */
    __asm volatile("1:\n\t"                    /* Numerical local label */
                   "ldr r0, =0xE000E200\n\t"   /* Load NVIC ISPR (Interrupt Set-Pending Register) address */
                   "ldr r0, [r0]\n\t"          /* Read NVIC ISPR */
                   "ands.w r0, r0, #0x100\n\t" /* Mask NVIC (1 << (24-16 = 8) -> 0x100) */
                   "beq 1b\n\t"                /* Branch back to label if result is 0 */
                   :                           /* No output operands */
                   :                           /* No input operands */
                   : "r0", "cc", "memory");    /* Clobber list: r0, condition codes, memory */

    /* Clear the DMA channel request done */
    uDMAClearInt(CANCC27XX_WRITE_REG_UDMA_CHANNEL_MASK);

    /* Read any DMA register to ensure the DMA channel request done clear above
     * has completed before clearing the DMA interrupt.
     */
    HWREG(DMA_BASE + DMA_O_DONEMASK);

    /* Clear the pending DMA interrupt */
    HwiP_clearInterrupt(INT_DMA_DONE_COMB);

    /* Read the pending interrupt status to ensure the write above has taken
     * effect before re-enabling interrupts.
     */
    (void)IntGetPend(INT_DMA_DONE_COMB);

    /* Restore DMA interrupt mask */
    HWREG(DMA_BASE + DMA_O_DONEMASK) = dmaDoneMask;

    /* Restore PRIMASK */
    __set_PRIMASK(primask);

    CommonResourceXXF3_releaseLock();
}

Important: This routine blocks on the common resource semaphore. Consequently, it must never be called from an ISR.

Register Write Wrapper Function

Implement a wrapper function for all CAN register writes which will check for critical registers and write them using DMA-based helper function.

void MCAN_writeReg(uint32_t offset, uint32_t value)
{
    uint32_t addr;

    /* Determine if the offset is for an MCAN register or message RAM */
    if (offset < CANFD_SRAM_BASE)
    {
        /* MCAN register: add the offset to CAN-FD peripheral base address */
        addr = CANFD_BASE + offset;

        if (CANCC27XX_isCriticalMcanReg(offset))
        {
            /* Due to SYS_211, use DMA to write the register */
            CANCC27XX_writeRegDma(addr, value);
        }
        else
        {
            HWREG(addr) = value;
        }
    }
    else
    {
        /* MCAN message RAM location: directly use the offset as the address */
        addr = offset;

        HWREG(addr) = value;
    }
}

Deferred Interrupt Handling

Because the ISR cannot call CANCC27XX_writeRegDma(), the driver moves the work to a RTOS task:

  • The hardware interrupt (INT_CAN_IRQ) runs a tiny HWI (CANCC27XX_hwiFxn) that only disables the interrupt and posts a semaphore.

  • A dedicated task (CANCC27XX_taskFxn) waits on that semaphore, then calls the full driver ISR (CANCC27XX_irqHandler) where any DMA‑based register writes may occur.

/* CANCC27XXX10.c – HWI that defers work */
void CANCC27XX_hwiFxn(uintptr_t arg)
{
    (void)arg; /* unused arg */

    /* Due to SYS_211, IRQ handling must be deferred to a task to avoid
     * system deadlock, missed interrupts, and priority inversion.
     */

    /* Disable the CAN interrupt until the task can process it */
    IntDisable(INT_CAN_IRQ);

    /* Unblock task to handle the interrupt */
    SemaphoreP_post(&canIrqSemaphore);
}

/* CANCC27XXX10.c – task that processes the interrupt */
static void CANCC27XX_taskFxn(void *arg)
{
    while (1)
    {
        /* Wait for interrupt handler to post semaphore */
        (void)SemaphoreP_pend(&canIrqSemaphore, SemaphoreP_WAIT_FOREVER);

        /* Call actual interrupt handling function */
        handleCanInterrupt(arg);

        /* Re-enable the CAN interrupt */
        IntEnable(INT_CAN_IRQ);
    }
}

Take‑away: All CAN‑related ISR work (including calls to CANCC27XX_writeRegDma()) must be performed in a task context.

Overriding Weak DriverLib Functions

The TI driver library provides a few weak functions that write directly to critical CAN HW registers: CANSetEndOfInt, CANClearInt, CANSSSetClkStopCtrl, CANSSClearClkStopCtrl). In the reference driver they are re‑implemented to route the write through CANCC27XX_writeRegDma() as a workaround for SYS‑211.

/* CANCC27XXX10.c - new driver library functions */

void CANSSSetEndOfInt(uint32_t eoi)
{
    CANCC27XX_writeRegDma(CANFD_BASE + CANFD_O_MCANSS_EOI, eoi);
}

void CANClearInt(uint8_t lineNum, uint32_t flags)
{
    uint32_t offset = (lineNum == CAN_INT_LINE0) ? CANFD_O_ICLR0 : CANFD_O_ICLR1;

    CANCC27XX_writeRegDma(CANFD_BASE + offset, flags);
}

void CANSSSetClkStopCtrl(uint32_t flags)
{
    CANCC27XX_writeRegDma(CANFD_BASE + CANFD_O_MCANSS_CLKCTL, flags);
}

void CANSSClearClkStopCtrl(uint32_t flags)
{
    CANCC27XX_writeRegDma(CANFD_BASE + CANFD_O_MCANSS_CLKCTL, (uint32_t)~flags);
}

Rule: Any function that ends up writing to a register in the critical list must be rewritten to call CANCC27XX_writeRegDma() (or a wrapper that does so).

Protecting the Tx Buffer Access

The Tx path (CAN_write and CAN_writeBuffer) both write to the Tx buffers and add the Tx request using a DMA‑based register write to MCAN_TXBAR). Therefore, the driver protects the whole operation with a binary semaphore (CANCC27XX_writeSemaphore) to ensure exclusive access between any tasks and ISR.

int_fast16_t CAN_write(CAN_Handle handle, const MCAN_TxBufElement *elem)
{
    CAN_Object *object = handle->object;
    int_fast16_t status = CAN_STATUS_ERROR;
    MCAN_TxFifoQStatus fifoQStatus = {0};

    if (object->txFifoQNum != 0U) {
        /* 1) Grab exclusive access to the Tx buffer */
        SemaphoreP_pend(&CANCC27XX_writeSemaphore, SemaphoreP_WAIT_FOREVER);

        MCAN_getTxFifoQStatus(&fifoQStatus);
        if (fifoQStatus.fifoFull == 0U) {
            MCAN_writeTxMsg(fifoQStatus.putIdx, elem);
            MCAN_setTxBufAddReq(fifoQStatus.putIdx);   /* <-- DMA‑protected register */
            status = CAN_STATUS_SUCCESS;
        } else {
            /* Queue is full – fall back to software ring buffer */
            if (StructRingBuf_put(&object->txStructRingBuf, elem) < 0)
                status = CAN_STATUS_TX_BUF_FULL;
            else
                status = CAN_STATUS_SUCCESS;
        }

        /* 2) Release the semaphore */
        SemaphoreP_post(&CANCC27XX_writeSemaphore);
    }
    return status;
}

/* CAN_writeBuffer follows the same pattern */
int_fast16_t CAN_writeBuffer(CAN_Handle handle, uint32_t bufIdx, const MCAN_TxBufElement *elem)
{
    CAN_Object *object  = handle->object;
    int_fast16_t status = CAN_STATUS_ERROR;
    uint32_t pendingTx;

    if (bufIdx < object->txBufNum)
    {
        /* Ensure exclusive access to the Tx buffer until the Tx request is added.
         * No need to check return value when waiting forever.
         */
        (void)SemaphoreP_pend(&CANCC27XX_writeSemaphore, SemaphoreP_WAIT_FOREVER);

        pendingTx = MCAN_getTxBufReqPend();

        if ((((uint32_t)1U << bufIdx) & pendingTx) == 0U)
        {
            MCAN_writeTxMsg(bufIdx, elem);

            MCAN_setTxBufAddReq(bufIdx);

            status = CAN_STATUS_SUCCESS;
        }

        SemaphoreP_post(&CANCC27XX_writeSemaphore);
    }

    return status;
}

Take‑away: All code that writes to the CAN Tx buffers should be protected by a semaphore to ensure exclusive access to the buffer until after the transmit request pending register is written via DMA. A critical section cannot be used as protection since the DMA‑protected register write blocks on a semaphore and which could result in a dead‑lock if interrupts are disabled.

Driver Initialization and Cleanup

Initialization

When initializing your CAN driver, ensure you:

  1. Initialize the CommonResource access semaphore for SYS_211 advisory

  2. Create binary semaphores for interrupt handling and write protection

  3. Create a task for handling CAN interrupts

  4. Set power dependency on DMA

int_fast16_t CANCC27XXXX_init(const CAN_Config *config)
{
    int_fast16_t status = CAN_STATUS_SUCCESS;
    TaskP_Params taskParams;

    /* Initialize CommonResource access semaphore, needed due to SYS_211 */
    CommonResourceXXF3_constructRTOSObjects();

    /* Create binary semaphore for IRQ handling */
    if (SemaphoreP_constructBinary(&CANCC27XX_irqSemaphore, 0) == NULL)
    {
        status = CAN_STATUS_ERROR;
    }

    if (status == CAN_STATUS_SUCCESS)
    {
        /* Create binary semaphore for CAN_write() */
        if (SemaphoreP_constructBinary(&CANCC27XX_writeSemaphore, 1) == NULL)
        {
            SemaphoreP_destruct(&CANCC27XX_irqSemaphore);
            status = CAN_STATUS_ERROR;
        }
    }

    if (status == CAN_STATUS_SUCCESS)
    {
        /* Initialize task params */
        TaskP_Params_init(&taskParams);
        taskParams.name      = "CANCC27XX";
        taskParams.priority  = CANCC27XXX10_config.taskPri;
        taskParams.stack     = CANCC27XXX10_config.taskStack;
        taskParams.stackSize = CANCC27XXX10_config.taskStackSize;
        taskParams.arg       = (void *)config;

        /* Construct a task for handling CANCC27XX interrupts */
        if (TaskP_construct(&CANCC27XX_task, CANCC27XX_taskFxn, &taskParams) == NULL)
        {
            SemaphoreP_destruct(&CANCC27XX_irqSemaphore);
            SemaphoreP_destruct(&CANCC27XX_writeSemaphore);
            status = CAN_STATUS_ERROR;
        }
    }

    if (status == CAN_STATUS_SUCCESS)
    {
        /* Set a power dependency on DMA */
        Power_setDependency(PowerLPF3_PERIPH_DMA);
    }

    return status;
}

Cleanup

When closing the driver, ensure you:

  1. Destroy the IRQ handling task

  2. Destroy semaphores

  3. Release power dependencies

void CANCC27XXXX_close(CAN_Handle handle)
{
    (void)handle; /* unused arg */

    /* Destroy the IRQ handling task */
    TaskP_destruct(&CANCC27XX_task);

    /* Destroy the IRQ handling semaphore */
    SemaphoreP_destruct(&CANCC27XX_irqSemaphore);

    /* Destroy the write semaphore */
    SemaphoreP_destruct(&CANCC27XX_writeSemaphore);

    /* Release the DMA power dependency */
    Power_releaseDependency(PowerLPF3_PERIPH_DMA);
}

Putting It All Together – What a New Driver Must Implement

Requ irement

Implementation in the reference driver

What you must do in a custom driver

I dentify c ritical regi sters

CA NCC27XX_mcanCriticalRegs[]

Replicate the array and use it in a helper that decides whether a write needs DMA.

DMA write h elper

CANCC27XX_writeRegDma()

Use the exact code sequence provided in CANCC27XXX10.c: acquire CommonResource lock, disable interrupts, configure a one‑shot uDMA channel, poll for completion, restore state.

Defer ISR work

HWI (CANCC27XX_hwiFxn) + task (CANCC27XX_taskFxn)

Create a binary semaphore, post it from the ISR, and run a task that calls the full CAN ISR.

O verride weak dr iverlib func tions

CANClearInt, CANSSSetClkStopCtrl, etc

Re‑implement every function that writes a critical register so it calls the DMA helper.

Se rialize Tx buffer/ request a ccess

CAN_write, CAN_writeBuffer with CANCC27XX_writeSemaphore

Add a binary semaphore around the whole Tx path (including any DMA‑protected register writes).

Ini tialize reso urces

CANCC27XXXX_init() creates the semaphores, task and DMA dependency

Follow the same init sequence: create binary semaphores, construct the task, enable DMA power, and call CommonResourceXXF 3_constructRTOSObjects().

Quick Checklist for Your Custom Driver

  1. Implement a special CAN register write function which will redirect all critical register writes to the DMA-based write helper function.

  2. Use ``CANCC27XX_writeRegDma()`` without any modifications – keep the lock, PRIMASK handling, DMA config, and poll loop exactly as shown.

  3. Replace all direct register writes (HWREG(addr) = value;) that target a critical register with a call to the DMA helper.

  4. Create a binary semaphore (CANCC27XX_writeSemaphore) and use it in CAN write functions.

  5. Implement a deferred‑interrupt task:

    • HWI only disables the IRQ and posts a semaphore.

    • Task waits on that semaphore, then runs the existing ISR body (you can copy CANCC27XX_irqHandler).

  6. Override the weak driver library functions (e.g., CANClearInt, CANSSSetClkStopCtrl, CANSSClearClkStopCtrl, CANSSSetEndOfInt).

  7. Initialize everything in the driver’s init routine (semaphores, task, DMA power dependency).

  8. Cleanup everything that was initialized during init when closing the driver.

References

TI SYS-211