Implement Dual-Device Communication Using SPI with STM32

7 min readApr 17, 2025

This article is contributed by Chengxuyuan Charlie.

I have been working with RT-Thread for the past two to three months and have deeply appreciated the contributions made by many developers to this system. RT-Thread is powerful, stable, and convenient for development. Due to the need for resource expansion in our project, we chose the SPI dual-device communication solution, but found that relevant resources were scarce. Therefore, I wrote this article in the hope that we can discuss and refine the implementation methods and code quality together.

1. Development Environment

I am using STM32CubeMX along with Keil as my development environment. The master device is an STM32F407, while the slave device is an STM32F103.

2. Circuit Design

The SPI0_NSS interface is the enable interface for the master SPI, while SPI0_EN is the interface used by the slave to notify the master that it is ready to send data. In conventional uses, the slave typically passively receives data; however, in dual-device communication, the slave might need to actively send data. In this case, an interface is required to notify the master to receive the data being sent by the slave.

Note: It is advisable to design the SPI0_EN as a pull-down, as there may be a rising edge pulse during startup, which could lead to false message detection.

3. Project Configuration

The configuration principle is to set the master device to Master mode and the slave device to Slave mode. The parameter configurations must match, such as communication speed, etc. Since DMA will be used, it should also be configured accordingly.

3.1 Configuration of the Master Device

3.2 Configuration of the Slave Device

4. Menuconfig Configuration

Write the corresponding Kconfig and enable the SPI peripheral, ensuring it aligns with the official documentation. No further explanation will be provided.

5. Main Code

5.1 Code for the Master Device

Note: Some of the code includes additional functionalities, such as message queues, which are not part of the SPI communication content. Since it was copied from the project and not organized, please discern accordingly.

//////////////////////////////////////华丽分割线/////////////////////////////////////#include <rtthread.h>#include <rtdevice.h>#include <drv_spi.h>#include <board.h>#include "app_spi1.h"#define SPI_BUS_NAME     "spi1"#define MASTER_SPI_DEVICE_NAME     "spi10"struct rt_spi_device *spiMaster;static uint8_t g_spiIrqFlag = 0;  // 用作中断通知标志/************************************************* 函数:spi_exirq_callback* 功能:spi中断回调函数************************************************/void spi_exirq_callback(void *args)// 回调函数{    g_spiIrqFlag++;//    rt_kprintf("g_spiIrqFlag=%d\n", g_spiIrqFlag);    if (g_spiIrqFlag >= 3) { /* 规避主/从上电启动时可能有检测到突变的上升沿 */        g_spiIrqFlag = 0;    }}/************************************************* 函数:SpiMasterHardwareConfiguration* 功能:主SPI硬件信息配置************************************************/static void SpiMasterHardwareConfiguration(struct rt_spi_device *spiDev){    struct rt_spi_configuration cfg;    cfg.data_width = 8;  // 8数据位    cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_0 | RT_SPI_MSB;  // 主模式0, MSB    cfg.max_hz = 10.5 * 1000 *1000; /* 10.5MBits/s速率 */    if (rt_spi_configure(spiDev, &cfg) != RT_EOK) {        rt_kprintf("master spi configure fail.\n");    }    }/************************************************* 函数:spi1_tx_thread* 功能:SPI1发送任务************************************************/static void spi1_tx_thread(void* parameter){    uint8_t canBuf[12] = {0}; // 数据接收缓冲区接收12字节的数据    uint8_t spiBuf[13] = {0};  // spi发送的13个字节数据缓冲区    while(1) {        /* 从消息队列中接收消息 */        if (rt_mq_recv(&g_can2MsgQueue, &canBuf, sizeof(canBuf), RT_WAITING_FOREVER) == RT_EOK) {            rt_kprintf("Spi1 Rcv QueMsg From can2: ");            for (int i = 0; i < sizeof(canBuf) / sizeof(uint8_t); ++i) {                rt_kprintf(" %2x", canBuf[i]);                spiBuf[i] = canBuf[i];  // copy data            }            rt_kprintf("\n");            spiBuf[12] = GetDataChecksum(canBuf, 12); // 0~12,最后一个数据是校验和            SendQueMsgBySpi(spiBuf, sizeof(spiBuf) / sizeof(uint8_t));        } else {            rt_kprintf("master spi recv msg queue fail.\n");        }           rt_thread_mdelay(10);    }}/************************************************* 函数:SpiSendTaskCreate* 功能:发送任务创建************************************************/static rt_err_t SpiSendTaskCreate(void){    rt_err_t ret = RT_EOK;    rt_thread_t thread_ptr;    /* 动态创建线程 */    /* 线程入口函数是SpiSendTask, 参数是RT_NULL    * 栈空间是1024, 优先级25, 时间片10个OS Tick    */    thread_ptr = rt_thread_create("spi1_tx", spi1_tx_thread, RT_NULL, 512, 25, 10);    /* 启动线程 */    if (thread_ptr != RT_NULL) {        rt_thread_startup(thread_ptr);    } else {        ret = RT_ERROR;    }    return ret;}/************************************************* 函数:spi1_rx_thread* 功能:SPI接收任务************************************************/static void spi1_rx_thread(void* parameter){    uint8_t spiRcvBuf[13] = {0};    while(1) {        uint8_t spiRcvLen = 0;        if (g_spiIrqFlag != 0) {            spiRcvLen = rt_spi_recv(spiMaster, spiRcvBuf, sizeof(spiRcvBuf) / sizeof(uint8_t));            if ((spiRcvLen != 0) &&(spiRcvBuf[0] != 0) && (spiRcvBuf[0] != 0xFF)) { /* 接收到数据 */                if (spiRcvBuf[12] == GetDataChecksum(spiRcvBuf, 12)) {  // 校验和                    /* 写消息队列发送给CAN */                    BmsSendQueMsgToNorthward(spiRcvBuf, 12);                    g_spiIrqFlag = 0;                }            }        }                rt_thread_mdelay(10);    }}/************************************************* 函数:SpiReceiveTaskCreate* 功能:SPI总线接收任务初始化************************************************/static rt_err_t SpiReceiveTaskCreate(void){    rt_err_t ret = RT_EOK;    rt_thread_t thread_ptr;    /* 动态创建线程 */    /* 线程入口函数是SpiSendTask, 参数是RT_NULL    * 栈空间是512, 优先级25, 时间片10个OS Tick    */    thread_ptr = rt_thread_create("spi1_rx", spi1_rx_thread, RT_NULL, 512, 25, 10);    /* 启动线程 */    if (thread_ptr != RT_NULL) {        rt_thread_startup(thread_ptr);    } else {        ret = RT_ERROR;    }    /* SPI1消息队列初始化 */    ret = Spi1MsgQueInit();    RT_ASSERT(ret == RT_EOK);    return ret;}/************************************************* 函数:SpiExtInterruptIint* 功能:SPI外部中断输入引脚初始化,通知Master SPI接收数据************************************************/static rt_err_t SpiExtInterruptIint(void){    rt_err_t ret = RT_EOK;    rt_pin_mode(SPI_EXT_INTERRUPT_PIN, PIN_MODE_INPUT_PULLDOWN); /* 下拉输入 */    /* 下降和上升都检测 */    rt_pin_attach_irq(SPI_EXT_INTERRUPT_PIN, PIN_IRQ_MODE_RISING_FALLING, spi_exirq_callback, RT_NULL);    rt_pin_irq_enable(SPI_EXT_INTERRUPT_PIN, PIN_IRQ_ENABLE); /* 使能中断 */    return ret;}/************************************************* 函数:APP_SpiMasterInit* 功能:初始化SPI总线配置以及收发任务初始化************************************************/rt_err_t APP_SpiMasterInit(void){      rt_err_t ret = RT_EOK;    char name[RT_NAME_MAX];    rt_strncpy(name, MASTER_SPI_DEVICE_NAME, RT_NAME_MAX);    /* 查找设备前挂接设备,CS=PA15 */    rt_hw_spi_device_attach("spi1", "spi10", GET_PIN(A, 15));    /* 查找spi设备获取设备句柄 */    spiMaster = (struct rt_spi_device *)rt_device_find(name);    if (!spiMaster) {        rt_kprintf("spi can't find %s device!\n", name);    } else {        SpiMasterHardwareConfiguration(spiMaster);  /* find之后要配置SPI */    }    /* 创建收发任务 */    ret = SpiSendTaskCreate();    if (ret != RT_EOK) {        rt_kprintf("SpiTxTask fail.\n");        return ret;    }    ret = SpiReceiveTaskCreate();    if (ret != RT_EOK) {        rt_kprintf("SpiRxTask fail.\n");        return ret;    }    rt_kprintf("SpiInit   ok.\n");    /* 配置SPI外部中断 */    if (SpiExtInterruptIint() != RT_EOK) {        rt_kprintf("Spi master exirq inti fail.\n");    }     return ret;}MSH_CMD_EXPORT(APP_SpiMasterInit, spi sample);## 5.1 从机主要代码#include <rtthread.h>#include <rtdevice.h>#include <drv_spi.h>#include <board.h>#include "app_spi2.h"#define SPI_BUS_NAME     "spi2"#define SPI_DEVICE_NAME     "spi20"static struct rt_spi_device *spiSlaveDev;/************************************************* 函数:SpiSlaveHardwareConfiguration* 功能:从SPI硬件信息配置************************************************/static rt_err_t SpiSlaveHardwareConfiguration(struct rt_spi_device *spiDev){    rt_err_t result = RT_EOK;    struct rt_spi_configuration cfg;    cfg.data_width = 8; // 数据位宽    /* 从模式, Mode 0:CPOL=0&CPHA=0. MSB: */    cfg.mode = RT_SPI_SLAVE | RT_SPI_MODE_0 | RT_SPI_MSB; // SPI从模式    cfg.max_hz = 10.5 * 1000 *1000; /* 10.5MBits/s */    if (rt_spi_configure(spiDev, &cfg) != RT_EOK) {        return RT_ERROR;    }    return result;}/************************************************* 函数:AuxMcuSendQueMsgBySpi* 功能:辅助MCU通过SPI将数据发送给主MCU************************************************/static rt_err_t AuxMcuSendQueMsgBySpi(const uint8_t *data, const uint8_t len){    rt_err_t ret = RT_EOK;    struct rt_spi_message msg1;    msg1.send_buf = data;    msg1.recv_buf = RT_NULL;    msg1.length = len; // len字节数据    msg1.cs_take = 1;  /* 选中片选 */    msg1.cs_release = 1; /* 函数返回时释放片选 */     msg1.next = RT_NULL;    rt_pin_write(SPI_INFORM_MASTER_PIN, PIN_HIGH); /* 给主MCU发送数据接收通知信号 */    if (rt_spi_transfer_message(spiSlaveDev, &msg1) == RT_NULL){              rt_spi_release(spiSlaveDev); /* 传输完成后释放片选 */        rt_spi_release_bus(spiSlaveDev);  /* 尽快释放总线 */        rt_thread_delay(2); // 2ms             rt_pin_write(SPI_INFORM_MASTER_PIN, PIN_LOW);  /* 给主MCU发送数据发送完信号 */    } else {        rt_thread_delay(2); // 2ms         /* 传输失败也要拉低信号,以便spi总线恢复的时候能恢复正常的数据传输 */        rt_pin_write(SPI_INFORM_MASTER_PIN, PIN_LOW);        return RT_ERROR;    }    return ret;}/************************************************* 函数:spi_tx_thread* 功能:SPI数据发送任务函数************************************************/static void spi_tx_thread(void *parameter){    uint8_t buf[12] = {0}; // 数据接收缓冲区接收12字节的数据    uint8_t spiBuf[13] = {0};      while(1)     {#ifdef USING_SPI_LOOP_SEND        SpiLoopSendData();#else                /* 从消息队列中接收消息 */        if (rt_mq_recv(&g_canMsgQueue, &buf, sizeof(buf), RT_WAITING_FOREVER) == RT_EOK) {            rt_kprintf("Aux Spi Rcv can QueMsg: ");            for (int i = 0; i < sizeof(buf) / sizeof(uint8_t); ++i) {                rt_kprintf(" %2x", buf[i]);                spiBuf[i] = buf[i]; // copy data            }            rt_kprintf("\n");            spiBuf[12] = GetDataChecksum(buf, 12);            AuxMcuSendQueMsgBySpi(spiBuf, sizeof(spiBuf) / sizeof(uint8_t));            rt_kprintf("Aux Spi send Msg.\n");        } else {            rt_kprintf("SPI recv msg queue fail(timeout).\n");        }    #endif           rt_thread_delay(5); // 5ms            }}/************************************************* 函数:APP_SpiSendTaskCreate* 功能:SPI发送任务创建************************************************/rt_err_t APP_SpiSendTaskCreate(void){    rt_err_t res = 0;    rt_thread_t thread;    /* 创建数据发送线程 */    thread = rt_thread_create("spi_tx", spi_tx_thread, RT_NULL, 1024, 28, 10); // 栈空间1024,28优先级,10ms时间片    if (thread != RT_NULL) {        rt_thread_startup(thread);    } else {        rt_kprintf("create spi_tx thread failed!\n");    }    return res;}/************************************************* 函数:spi_rx_thread* 功能:SPI数据接收任务函数************************************************/static void spi_rx_thread(void *parameter){    uint8_t spiRcvBuf[13] = {0};     uint8_t spiRcvLen = 0;       while(1)     {        rt_err_t ret = RT_EOK;        spiRcvLen = rt_spi_recv(spiSlaveDev, spiRcvBuf, sizeof(spiRcvBuf) / sizeof(uint8_t));        if ((spiRcvLen != 0) && ((spiRcvBuf[0] != 0) && (spiRcvBuf[0] != 0xFF))) { // 接收到数据            if (spiRcvBuf[12] == GetDataChecksum(spiRcvBuf, 12)) { // 校验和                      /* 写消息队列发送给CAN */                ret = rt_mq_send(&g_spiMsgQueue, spiRcvBuf, 12); // 只需要发送前12个数据,第十三个是校验和                if (ret != RT_EOK) {                    rt_kprintf("Spi send queMsg fail.\n");                    ret = RT_ERROR;                } else {                    rt_kprintf("Spi->QueMsg->Can.\n");                }            } else {                rt_kprintf("Slave Spi rcv data checksum fail.\n");            }        }        rt_thread_delay(10);    }}/************************************************* 函数:APP_SpiReceiveTaskCreate* 功能:SPI接收任务创建************************************************/rt_err_t APP_SpiReceiveTaskCreate(void){    rt_err_t res = 0;    rt_thread_t thread;    /* 创建数据接收线程 */    thread = rt_thread_create("spi_rx", spi_rx_thread, RT_NULL, 1024, 28, 10);    if (thread != RT_NULL) {        rt_thread_startup(thread);    } else {        rt_kprintf("create spi_rx thread failed!\n");    }    return res;}/************************************************* 函数:APP_SpiInformGpioInit* 功能:SPI通知主MCU接收数据接口初始化************************************************/rt_err_t APP_SpiInformGpioInit(void){    /* set pin mode to output */    rt_pin_mode(SPI_INFORM_MASTER_PIN, PIN_MODE_OUTPUT);    /* output low*/    rt_pin_write(SPI_INFORM_MASTER_PIN, PIN_LOW);    return RT_EOK;}/************************************************* 函数:APP_SpiDeviceInit* 功能:SPI设备初始化************************************************/rt_err_t APP_SpiDeviceInit(void){    rt_err_t ret = RT_EOK;    char name[RT_NAME_MAX];    rt_strncpy(name, SPI_DEVICE_NAME, RT_NAME_MAX);    /* 查找之前要挂接设备,CS = PB12 */    rt_hw_spi_device_attach("spi2", "spi20", GET_PIN(B, 12));    /* 查找spi设备获取设备句柄 */    spiSlaveDev = (struct rt_spi_device *)rt_device_find(name);    if (!spiSlaveDev){        rt_kprintf("Aux spi can't find %s device!\n", name);    } else {        // find之后要配置SPI        if (SpiSlaveHardwareConfiguration(spiSlaveDev) != RT_EOK) {            rt_kprintf("SpiConfigure fail.\n");            return ret;        }            }    /* SPI消息队列初始化 */    ret = SpiMsgQueInit();    RT_ASSERT(ret == RT_EOK);    /* 创建收发任务 */    ret = APP_SpiReceiveTaskCreate();    if (ret != RT_EOK) {        rt_kprintf("SpiRxTask fail.\n");        return ret;    }     ret = APP_SpiSendTaskCreate();    if (ret != RT_EOK) {        rt_kprintf("SpiTxTask fail.\n");        return ret;    }    rt_kprintf("SpiInit  ok.\n");      return ret;}MSH_CMD_EXPORT(APP_SpiDeviceInit, spi2 sample);

6. Testing Conditions

6.1 Master Sending and Slave Receiving

(1) Sending

6.2 Slave Sending and Master Receiving

(1) Sending

(2) Receiving

--

--

RT-Thread IoT OS
RT-Thread IoT OS

Written by RT-Thread IoT OS

An Open-Source Community-Powered Real-Time Operating System (RTOS) Project! Let’s develop, DIY, create, share, and explore this new IoT World together!

No responses yet