Table of Contents

    Introduction to Inter-Processor Communication (IPC)


    The following diagram is a screenshot from Texas Instruments user documentation. It shows that each CPU has its own dedicated resources, such as RAM and Flash, while the peripherals are shared among the CPUs. The IPC acts as a communication channel that allows the CPUs to exchange data and information with each other.

    IPC communication uses dedicated RAM regions for reading and writing on each CPU to avoid memory collisions. For example, communication from CPU1 to CPU2 through the IPC uses MSGRAM0, while communication from CPU2 to CPU1 uses MSGRAM1.

    In this article, we will focus on the IPC peripheral and how it can be used to implement multitasking (multithreaded) operations between CPU1 and CPU2.

    F2838x Block Diagram

    Why IPC Is Needed


    Inter-Processor Communication (IPC) is used to enable safe and reliable data exchange between multiple processors or cores within a system. It allows tasks running on different CPUs to communicate, synchronize their actions, and share information without causing data corruption or timing issues.

    For the TMS320F28388D used in this article, IPC is supported by dedicated hardware interrupts: 4 interrupts between CPU1 and CPU2, and 8 interrupts between each CPU and the Cortex-M4 microcontroller.

    This hardware peripheral support ensures fast signaling, low latency, and deterministic communication between all processing units.

    IPC Architecture Between CPU1, CPU2, and CM


    CPU1_TO_CPU2 IPC Module

    CPUx_to_CM IPC Module

    IPC Flags (CPU1 → CPU2): LED Toggle Example


    Looking at the CPU1 to CPU2 IPC module diagram, we can easily understand that the IPC has 32 “pages” for communication. Each page has a flag with set, clear, and acknowledge mechanisms. For example, if we want to synchronize CPU1 and CPU2, we can use one of the 32 flags, which provides set, clear, and acknowledge operations to ensure proper synchronization between the two CPUs.

    In my examples, I use the DriverLib from Texas Instruments. This allows anyone to download the C2000Ware SDK and consult the related documentation. You can find it at the link below:
    C2000Ware SDK.

    One more important point when using IPC is boot synchronization. Using DriverLib, this can be done with the following code:

    // CPU1
    // Synchronize both cores
    IPC_sync(IPC_CPU1_L_CPU2_R, IPC_FLAG0);
    
    // CPU2
    // Synchronize both cores
    IPC_sync(IPC_CPU2_L_CPU1_R, IPC_FLAG0);
    

    You can see that the same routine is used on CPU1 and CPU2, with the same flag (IPC_FLAG0). Once the synchronization is completed, this flag can be reused later for another task. The only requirement is to check whether the flag is already in use.

    In this code snippet, IPC_FLAG0 is used, which can also trigger an interrupt. One recommendation is that if a flag is not used with an interrupt, you may use another flag and reserve flags 0, 1, 2, and 3 exclusively for interrupt-based tasks. Flags 0 to 7 should be used when communication is required between the TI CPUs and the ARM Cortex-M4.

    To indicate that CPU1 is attempting to communicate with CPU2, CPU1 must set an IPC flag. If interrupts are enabled, setting IPC Flag 1 will trigger an interrupt on CPU2. In the following code, however, interrupts are not used.

    CPU1 Code:

    
    volatile static bool toggleLed0 = false;
    while(1)
    {
        if (true == toggleLed0)
        {
            IPC_setFlagLtoR(IPC_CPU1_L_CPU2_R, IPC_FLAG1);   //<------- set IPC Flag
    
            toggleLed0 = false;     
            DEVICE_DELAY_US(50000); 
        }
    
    }
    

    CPU2 Code:

    while(1)
    {
        //Wait for flag
        IPC_waitForFlag(IPC_CPU2_L_CPU1_R,IPC_FLAG1);
    
        //toggle an led
        GPIO_togglePin(myBoardLED0_GPIO);
    
        // Acknowledge the flag
        IPC_ackFlagRtoL(IPC_CPU2_L_CPU1_R, IPC_FLAG1);
    
        DEVICE_DELAY_US(50000);   
    
    }
    

    In this example, CPU1 sets Flag 0, and CPU2 reads it and toggles an LED.

    Data Exchange Using IPC and MSGRAM


    In this step, we will use the IPC RAM dedicated to data exchange in a multi-CPU architecture. These memory regions are defined in the linker command file as follows:

    CPU1TOCPU2RAM    : origin = 0x03A000, length = 0x000800
    CPU2TOCPU1RAM    : origin = 0x03B000, length = 0x000800
    

    In this example, a 32-bit word is used as a counter shared between CPU1 and CPU2. Although the IPC module provides its own 64-bit registers that can be accessed by both CPUs in a more efficient way, this example focuses on using the dedicated IPC RAM.

    We define a uint32_t variable adresse as follows:

    #pragma DATA_SECTION (cpu1Tocpu2Data, "CPU1TOCPU2RAM")
    #define IPC_CPU1_COMPTEUR_ADDR  ((volatile uint32_t*)0x03A000)
    #define IPC_CPU2_COMPTEUR_ADDR  ((volatile uint32_t*)0x03B000)
    

    CPU1 code:

    while(1)
    {
        // Write a value to the shared counter
        *IPC_CPU1_COMPTEUR_ADDR = 1234;
    
        // Set a flag to notify CPU2 that new data is available
        IPC_setFlagLtoR(IPC_CPU1_L_CPU2_R, IPC_FLAG2);
    }
    

    CPU2 code:

    while(1)
    {
        // Wait for data from CPU1
        IPC_waitForFlag(IPC_CPU2_L_CPU1_R, IPC_FLAG2);
    
        // Read, modify, and write back the value
        uint32_t value = *IPC_CPU1_COMPTEUR_ADDR;
        value++;
        *IPC_CPU2_COMPTEUR_ADDR = value;
    
        // Acknowledge the flag
        IPC_ackFlagRtoL(IPC_CPU2_L_CPU1_R, IPC_FLAG2);
    }
    

    IPC Command-Based Communication


    A request sent from CPU1 to CPU2 can be simplified into a command and an address. To illustrate this mechanism, we will use an example with three commands sent from CPU1 to CPU2:

    1. Toggle an LED
    2. Read an ADC value
    3. Put CPU2 into the idle state

    Each task associated with these commands can go through several states: IDLE, NEW, BUSY, DONE, or ERROR.

    The command itself is represented as a 32-bit word divided into three fields: parameter, status, and command ID.
    These enums must be shared between CPU1 and CPU2. In my case, I use a common header file in both projects. This file serves as the interface contract between the two CPUs.

    /* Command IDs */
    typedef enum
    {
        CMD_NONE        = 0,
        CMD_TOGGLE_LED  = 1,
        CMD_READ_ADC    = 2,
        CMD_CPU2_IDLE   = 3,
    } cmd_id_t;
    
    /* Status values */
    typedef enum
    {
        CMD_STATUS_IDLE  = 0x00,
        CMD_STATUS_NEW   = 0x01,
        CMD_STATUS_BUSY  = 0x02,
        CMD_STATUS_DONE  = 0x03,
        CMD_STATUS_ERROR = 0xFF,
    } cmd_status_t;
    
    /* Command register layout */
    typedef union
    {
        uint32_t value;
        struct {
            uint16_t param;
            uint16_t status : 8;
            uint16_t cmd_id : 8;
        } field;
    } ipc_cmd_reg_t;
    

    The following C code example handles commands sent from CPU1, executes them on CPU2, and updates the status of each task accordingly whether toggling an LED, reading an ADC value, or placing CPU2 into the idle state.

    CPU1 Code:

    //
    // ===== IPC Task CPU1 =====
    //
    void CPU1_IPC_Task(void)
    {
        static bool toggleLed0 = false;
        static bool readAdc    = false;
        static bool cpu2Idle   = false;
    
        // Task 1 : Toggle LED
        if (toggleLed0)
        {
            toggleLed0 = false;
            pass = CPU1_SendToggleLED() ? 1 : 0;
        }
    
        // Task 2 : Read ADC
        if (readAdc)
        {
            readAdc = false;
            pass = CPU1_SendReadADC(2) ? 1 : 0;
        }
    
        // Task 3 : CPU2 Idle
        if (cpu2Idle)
        {
            cpu2Idle = false;
            pass = CPU1_SendCpu2Idle() ? 1 : 0;
        }
    }
    
    
    //
    // ===== Generic IPC send =====
    //
    static bool CPU1_SendCommand(ipc_cmd_reg_t *cmd, uint32_t *response)
    {
        bool ret;
    
        ret = IPC_sendCommand(IPC_CPU1_L_CPU2_R,
                              IPC_FLAG5,
                              IPC_ADDR_CORRECTION_ENABLE,
                              cmd->value,
                              0,
                              cmd->field.param);
    
        if (!ret)
            return false;
    
        IPC_waitForAck(IPC_CPU1_L_CPU2_R, IPC_FLAG5);
    
        *response = IPC_getResponse(IPC_CPU1_L_CPU2_R);
    
        return true;
    }
    
    //
    // ===== Command wrappers =====
    //
    static bool CPU1_SendToggleLED(void)
    {
        ipc_cmd_reg_t cmd;
        uint32_t response;
    
        cmd.field.cmd_id = CMD_TOGGLE_LED;
        cmd.field.param  = 0;
        cmd.field.status = CMD_STATUS_NEW;
    
        if (!CPU1_SendCommand(&cmd, &response))
            return false;
    
        return (response == TEST_PASS);
    }
    
    static bool CPU1_SendReadADC(uint16_t channel)
    {
        ipc_cmd_reg_t cmd;
        uint32_t response;
    
        cmd.field.cmd_id = CMD_READ_ADC;
        cmd.field.param  = channel;
        cmd.field.status = CMD_STATUS_NEW;
    
        if (!CPU1_SendCommand(&cmd, &response))
            return false;
    
        return (response == 1234);
    }
    
    static bool CPU1_SendCpu2Idle(void)
    {
        ipc_cmd_reg_t cmd;
        uint32_t response;
    
        cmd.field.cmd_id = CMD_CPU2_IDLE;
        cmd.field.param  = 0;
        cmd.field.status = CMD_STATUS_NEW;
    
        if (!CPU1_SendCommand(&cmd, &response))
            return false;
    
        return true;
    }
    

    CPU2 Code:

    //
    // ===== IPC Task CPU2 =====
    //
    void CPU2_IPC_Task(void)
    {
        uint32_t raw_cmd;
        uint32_t addr;
        uint32_t data;
        uint32_t response = 0;
        bool     cmd_found = false;
    
        if (!IPC_readCommand(IPC_CPU2_L_CPU1_R,
                             IPC_FLAG5,
                             IPC_ADDR_CORRECTION_ENABLE,
                             &raw_cmd,
                             &addr,
                             &data))
        {
            return;
        }
    
        ipc_cmd_reg_t cmd;
        cmd.value = raw_cmd;
        cmd.field.status = CMD_STATUS_BUSY;
    
        uint16_t i;
        for (i = 0; i < (sizeof(cmd_table) / sizeof(cmd_table[0])); i++)
        {
            if (cmd_table[i].id == cmd.field.cmd_id)
            {
                cmd.field.param  = (uint16_t)data;
                cmd.field.status = cmd_table[i].handler(&cmd, &response);
                cmd_found = true;
                break;
            }
        }
    
        if (!cmd_found)
        {
            cmd.field.status = CMD_STATUS_ERROR;
            response = TEST_FAIL;
        }
    
        IPC_sendResponse(IPC_CPU2_L_CPU1_R, response);
        IPC_ackFlagRtoL(IPC_CPU2_L_CPU1_R, IPC_FLAG5);
    }
    
    //
    // ===== Command Handlers =====
    //
    static cmd_status_t CMD_HandleToggleLED(ipc_cmd_reg_t *cmd, uint32_t *response)
    {
        LED_Toggle();
        *response = TEST_PASS;
        return CMD_STATUS_DONE;
    }
    
    static cmd_status_t CMD_HandleReadADC(ipc_cmd_reg_t *cmd, uint32_t *response)
    {
        uint16_t channel = cmd->field.param;
    
        *response = ADC_Read(channel);
        return CMD_STATUS_DONE;
    }
    
    static cmd_status_t CMD_HandleCPU2Idle(ipc_cmd_reg_t *cmd, uint32_t *response)
    {
        *response = TEST_PASS;
    
        while (1)
        {
            asm(" IDLE");
        }
    }
    
    //
    // ===== Hardware functions =====
    //
    void LED_Toggle(void)
    {
        GPIO_togglePin(myBoardLED0_GPIO);
    }
    
    uint16_t ADC_Read(uint16_t channel)
    {
        // Implémentation ADC réel à ajouter
        return 1234;
    }
    

    Bidirectional IPC Communication Using Queues


    In this example, CPU2 is assumed to be controlling a motor, while CPU1 provides the required speed and direction. CPU1 uses an IPC queue to send this information to CPU2. The command header (contract) file remains the same, and CPU2 decode the received data, applying the command to the motor, and returning a status using another IPC queue.

    The interrupt is triggered only from CPU1 to CPU2.

    To use the DriverLib queue functions, interrupts must be enabled and the queue must be initialized. Therefore, before sending a queue command, the IPC interrupt must be enabled on CPU2, and the queue must be initialized on both CPUs.

    CPU1:
    ...
    {
        // Initialize message queue
    IPC_initMessageQueue(IPC_CPU1_L_CPU2_R, &g_messageQueue, IPC_INT1, IPC_INT1);
    }
    ....
    
    CPU2:
    ...
    {
        // Enable IPC interrupts
        IPC_registerInterrupt(IPC_CPU2_L_CPU1_R, IPC_INT1, INT_CPU2_IPC_Queue_Task);
    
        // Initialize message queue
        IPC_initMessageQueue(IPC_CPU2_L_CPU1_R, &g_messageQueue, IPC_INT1, IPC_INT1);
    }
    ...
    

    CPU1 Code:

    //
    // ===== IPC Queue Task CPU1 =====
    //
    void CPU1_IPC_Queue_Task_CPU1(IPC_MessageQueue_t *g_messageQueue)
    {
        static uint16_t cmdPeriod = 0;
    
        // --- Send command to cpu1
        cmdPeriod++;
        if(cmdPeriod >= 10)
        {
            cmdPeriod = 0;
    
            motorCmd.speed_rpm = 1500;
            motorCmd.direction = 1;
            IPC_sendMotorCommand(&motorCmd, g_messageQueue);
    
            // --- Read Status from cpu2
            MotorStatus_t status;
            if(IPC_receiveMotorStatus(&status, g_messageQueue))
            {
                HMI_UpdateSpeed(status.speed_rpm);
                HMI_UpdateFault(status.fault_code);
            }
        }
    }
    

    I use inline function and i put them in a header file:

    #pragma DATA_SECTION(motorCmd, "MSGRAM_CPU1_TO_CPU2");
    volatile MotorCommand_t motorCmd;
    
    static inline void IPC_packMotorCommand(IPC_Message_t *msg,
                                            const volatile MotorCommand_t *cmd)
    {
        msg->command = CMD_SET_SPEED;
        msg->dataw1  = ((uint32_t)cmd->direction << 16) |
            (uint32_t)cmd->speed_rpm;
        msg->dataw2  = 0;
    }
    
    static inline bool IPC_sendMotorCommand(const volatile MotorCommand_t *cmd, IPC_MessageQueue_t *messageQueue)
    {
        IPC_Message_t msg;
        IPC_packMotorCommand(&msg, cmd);
    
        if(IPC_sendMessageToQueue(IPC_CPU1_L_CPU2_R, messageQueue,
                                  IPC_ADDR_CORRECTION_ENABLE,
                                  &msg,
                                  IPC_BLOCKING_CALL))
        {
            return true;
        }
    
        return false;
    }
    
    static inline void IPC_unpackMotorStatus(const IPC_Message_t *msg,
                                             MotorStatus_t *status)
    {
        status->speed_rpm = (uint16_t)(msg->dataw1 & 0xFFFF);
        status->fault_code = (uint16_t)(msg->dataw1 >> 16);
    }
    
    static inline bool IPC_receiveMotorStatus(MotorStatus_t *status, IPC_MessageQueue_t *messageQueue)
    {
        IPC_Message_t msg;
    
        if(IPC_readMessageFromQueue(IPC_CPU1_L_CPU2_R,
                                    messageQueue,
                                    IPC_ADDR_CORRECTION_ENABLE,
                                    &msg,
                                    IPC_NONBLOCKING_CALL))
        {
            if(msg.command == CMD_GET_STATUS)
            {
                IPC_unpackMotorStatus(&msg, status);
                return true;
            }
        }
        return false;
    }
    
    

    CPU2 Code:

    The Queue must use in interrpt in the remote core, so before runing the following code, make sure that interrupt are activated and connect to cpu2.

    __interrupt void INT_CPU2_IPC_Queue_Task(void)
    {
        MotorCommand_t rxCmd;
        static uint16_t motorSpeedRef;
        static uint16_t motorDirection;
        static uint16_t motorFault;
    
        if(IPC_receiveMotorCommand(&rxCmd))
        {
            // Variables internes CPU2
            motorSpeedRef = rxCmd.speed_rpm;
            motorDirection = rxCmd.direction;
        }
    
        // --- Envoi périodique du status (ex: toutes les 1 ms)
        static uint16_t statusPeriod = 0;
        statusPeriod++;
        if(statusPeriod >= 1)
        {
            statusPeriod = 0;
    
            MotorStatus_t status;
            status.speed_rpm  =motorSpeedRef+1;
            status.fault_code = motorFault+1;
    
            IPC_sendMotorStatus(&status);
        }
    
        // Acknowledge the flag
        IPC_ackFlagRtoL(IPC_CPU2_L_CPU1_R, IPC_FLAG1);
    
        // Acknowledge the PIE interrupt.
        Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP1);
    }
    

    And my inline functions are in the inline header file like cpu1:

    
    
    extern IPC_MessageQueue_t g_messageQueue;
    static inline void IPC_unpackMotorCommand(const IPC_Message_t *msg,
                                              MotorCommand_t *cmd)
    {
        cmd->speed_rpm = (uint16_t)(msg->dataw1 & 0xFFFF);
        cmd->direction = (uint16_t)((msg->dataw1 >> 16) & 0xFF);
    }
    
    static inline bool IPC_receiveMotorCommand(MotorCommand_t *cmd)
    {
        IPC_Message_t msg;
    
        if(IPC_readMessageFromQueue(IPC_CPU2_L_CPU1_R,
                                    &g_messageQueue,
                                    IPC_ADDR_CORRECTION_ENABLE,
                                    &msg,
                                    IPC_NONBLOCKING_CALL))
        {
            if(msg.command == CMD_SET_SPEED)
            {
                IPC_unpackMotorCommand(&msg, cmd);
                return true;
            }
        }
        return false;
    }
    
    static inline void IPC_packMotorStatus(IPC_Message_t *msg,
                                           const MotorStatus_t *status)
    {
        msg->command = CMD_GET_STATUS;
        msg->dataw1  = ((uint32_t)status->fault_code << 16) |
            (uint32_t)status->speed_rpm;
        msg->dataw2  = 0;
    }
    
    static inline void IPC_sendMotorStatus(const MotorStatus_t *status)
    {
        IPC_Message_t msg;
    
        IPC_packMotorStatus(&msg, status);
    
        IPC_sendMessageToQueue(IPC_CPU2_L_CPU1_R,
                               &g_messageQueue,
                               IPC_ADDR_CORRECTION_DISABLE,
                               &msg,
                               IPC_NONBLOCKING_CALL);
    
    }
    
    
    📝 Article Author : SEMRADE Tarik
    🏷️ Author position : Senior Embedded Software Engineer
    🔗 Author LinkedIn : LinkedIn profile

    Comments