Bare-Metal CAN Driver Implementation for S32K148

The S32K148 microcontroller supports three primary methods for CAN message transmission and reception: mailbox mode, FIFO mode, and DMA mode. Among these, mailbox mode offers simplest conceptual model and serves as a practical starting point for understanding CAN communication on this platform.

Key registers involved in mailbox-based CAN operation include control registers (MCR, CTRL1), message buffer RAM (RAMn), individual mask registers (RXIMR), and global mask registers (RXMGMASK). Detailed register descriptions are available in the reference manual; this article focuses on functional implementation through example code.

Initialization of the CAN peripheral involves several critical steps:

  1. Enable the clock for the CAN module and associated GPIO via the Peripheral Clock Control (PCC).
  2. Configure the CAN TX and RX pins for alternate function.
  3. Place the CAN module into freeze mode to allow safe configuration.
  4. Set up timing parameters (bit rate), clear message buffers, configure acceptance filters, and define message buffer roles.
  5. Enable relevant interrupts for transmission, reception, and error conditions.
#define MSG_BUF_WORD_COUNT   4      // Each message buffer occupies 4 32-bit words
#define TX_MAILBOX_INDEX     16u    // Transmit mailbox index
#define RX_MAILBOX_INDEX     0u     // Receive mailbox index (used in interrupt handler)

void can2_init(void) {
    // Enable clock for PORTB
    PCC->PCCn[PCC_PORTB_INDEX] |= PCC_PCCn_CGC_MASK;

    // Configure PTB12 (CAN2_RX) and PTB13 (CAN2_TX) as CAN alternate function
    PORTB->PCR[12] = PORT_PCR_MUX(2);
    PORTB->PCR[13] = PORT_PCR_MUX(2);

    // Enable clock for FlexCAN2
    PCC->PCCn[PCC_FlexCAN2_INDEX] |= PCC_PCCn_CGC_MASK;

    // Disable module before configuration
    CAN2->MCR |= CAN_MCR_MDIS_MASK;
    CAN2->CTRL1 &= ~CAN_CTRL1_CLKSRC_MASK; // Use SOSCDIV2 as clock source
    CAN2->MCR &= ~CAN_MCR_MDIS_MASK;       // Re-enable module

    // Wait until freeze mode is acknowledged
    while (!(CAN2->MCR & CAN_MCR_FRZACK_MASK));

    // Configure bit timing for 250 kbps
    CAN2->CTRL1 = 0x01DB0006;

    // Clear all message buffer RAM (32 buffers × 4 words)
    for (uint32_t i = 0; i < 128; i++) {
        CAN2->RAMn[i] = 0;
    }

    // Set all individual RX masks to accept any ID (0xFFFFFFFF = match all bits)
    for (uint32_t i = 0; i < 32; i++) {
        CAN2->RXIMR[i] = 0xFFFFFFFF;
    }

    // Global mask: 0x0 → don't care about any ID bits → accept all messages
    CAN2->RXMGMASK = 0x00000000;

    // Configure RX mailbox (MB0) as inactive receiver (CODE=0b0100)
    CAN2->RAMn[RX_MAILBOX_INDEX * MSG_BUF_WORD_COUNT + 0] = 
        (4U << 24) |                 // CODE = RX_INACTIVE
        (0U << 21) |                 // IDE = standard ID
        (0U << 20);                  // Reserved/SRR = 0

    // Set maximum number of message buffers and disable self-reception
    CAN2->MCR = (31U << CAN_MCR_MAXMB_SHIFT) | CAN_MCR_SRXDIS_MASK;

    // Exit freeze mode
    CAN2->MCR &= ~CAN_MCR_HALT_MASK;

    // Enable interrupts: MB0 (RX) and MB16 (TX)
    CAN2->IMASK1 |= (1U << TX_MAILBOX_INDEX) | (1U << RX_MAILBOX_INDEX);
    // Enable bus-off interrupt
    CAN2->CTRL1 |= CAN_CTRL1_BOFFMSK_MASK;

    // Wait for module to exit freeze and become ready
    while (CAN2->MCR & CAN_MCR_FRZACK_MASK);
    while (CAN2->MCR & CAN_MCR_NOTRDY_MASK);

    // Enable NVIC interrupts
    S32_NVIC_EnableIRQ(CAN2_ORed_0_15_MB_IRQn);
    S32_NVIC_EnableIRQ(CAN2_ORed_16_31_MB_IRQn);
    S32_NVIC_EnableIRQ(CAN2_ORed_IRQn);
}

Message transmission requires populating the designated transmit mailbox with data and control fields. The ID format (standard or extended) affects both the ID field placement and the IDE bit.

typedef enum { StandardID = 0, ExtendedID = 1 } IdType;

void can2_transmit(uint8_t data[], uint8_t len, uint32_t id, IdType type) {
    uint32_t mb_base = TX_MAILBOX_INDEX * MSG_BUF_WORD_COUNT;

    // Write data bytes into message buffer words 2 and 3
    CAN2->RAMn[mb_base + 2] = 
        ((uint32_t)data[0] << 24) |
        ((uint32_t)data[1] << 16) |
        ((uint32_t)data[2] << 8)  |
        ((uint32_t)data[3]);
    CAN2->RAMn[mb_base + 3] = 
        ((uint32_t)data[4] << 24) |
        ((uint32_t)data[5] << 16) |
        ((uint32_t)data[6] << 8)  |
        ((uint32_t)data[7]);

    // Configure ID and control word
    if (type == StandardID) {
        CAN2->RAMn[mb_base + 1] = id << 18;
        CAN2->RAMn[mb_base + 0] = 
            (0xCU << 24) |          // CODE = TX_ONCE (0b1100)
            (1U << 22) |            // SRR = 1
            (0U << 21) |            // IDE = 0 (standard)
            (0U << 20) |            // RTR = 0 (data frame)
            ((uint32_t)len << 16);  // DLC
    } else {
        CAN2->RAMn[mb_base + 1] = id;
        CAN2->RAMn[mb_base + 0] = 
            (0xCU << 24) |
            (1U << 22) |
            (1U << 21) |            // IDE = 1 (extended)
            (0U << 20) |
            ((uint32_t)len << 16);
    }
}

Reception is handled in the interrupt service routine corresponding to the configured receive mailbox (MB0 falls under CAN2_ORed_0_15_MB_IRQn). The received message is parsed from the mailbox RAM.

typedef struct {
    uint32_t id;
    uint8_t length;
    uint8_t data[8];
} CanMessage;

CanMessage rx_msg;

void can2_receive(IdType type) {
    uint32_t mb_base = RX_MAILBOX_INDEX * MSG_BUF_WORD_COUNT;
    uint32_t ctrl_word = CAN2->RAMn[mb_base + 0];
    uint32_t id_word   = CAN2->RAMn[mb_base + 1];
    uint32_t data0     = CAN2->RAMn[mb_base + 2];
    uint32_t data1     = CAN2->RAMn[mb_base + 3];

    rx_msg.length = (ctrl_word >> 16) & 0xF;

    if (type == StandardID) {
        rx_msg.id = (id_word >> 18) & 0x7FF;
    } else {
        rx_msg.id = id_word & 0x1FFFFFFF;
    }

    rx_msg.data[0] = (data0 >> 24) & 0xFF;
    rx_msg.data[1] = (data0 >> 16) & 0xFF;
    rx_msg.data[2] = (data0 >> 8)  & 0xFF;
    rx_msg.data[3] = data0 & 0xFF;
    rx_msg.data[4] = (data1 >> 24) & 0xFF;
    rx_msg.data[5] = (data1 >> 16) & 0xFF;
    rx_msg.data[6] = (data1 >> 8)  & 0xFF;
    rx_msg.data[7] = data1 & 0xFF;
}

The interrupt handler reads the message and clears the corresponding interrupt flag to allow subsequent receptions:

void CAN2_ORed_0_15_MB_IRQHandler(void) {
    // Optional: toggle debug LED
    PTE->PTOR = 1U << 22;

    can2_receive(StandardID);

    // Clear interrupt flag for MB0
    CAN2->IFLAG1 = (1U << RX_MAILBOX_INDEX);
}

With the global mask set to zero and the individual mask set to accept all IDs, the configured receive mailbox will capture any standard identifier CAN frame on the bus. Proper flag clearing in the ISR ensures continuous reception capability.

Tags: S32K148 FlexCAN can-bus bare-metal ARM-Cortex-M

Posted on Tue, 16 Jun 2026 16:34:56 +0000 by etones