freertos 外设同步

是的,多个任务共享 SPI、I2C、UART 等外设时,通常必须加锁,否则会出现资源竞争、数据错乱、通信失败等问题。这是 RTOS 编程中常见的同步问题。


为什么需要加锁?

外设通常是共享资源,比如:

  • SPI 是一个总线,发送和接收过程是连续的,不能被中断打断;
  • I2C 通常只有一个控制器(master),多任务如果同时发起通信,会造成仲裁冲突;
  • UART 发送过程可能被打断,导致拼接成一条乱码。

✅ 加锁的方法

1. 使用互斥锁(osMutex / xSemaphoreCreateMutex

每次访问前加锁,访问完释放:

osMutexAcquire(spiMutex, osWaitForever);
HAL_SPI_Transmit(...);
osMutexRelease(spiMutex);
2. 使用队列或事件驱动机制

让某一个任务专门处理 SPI/I2C 请求,其他任务通过消息队列通知它,属于串行化访问方案。


❗ 不加锁会怎样?

  • SPI/I2C 可能会被两个任务“抢着用”,造成数据冲突
  • 有些 HAL 函数不是线程安全的(如 HAL_I2C_Master_Transmit());
  • 如果任务 A 传输过程中被任务 B 打断,传输会失败。

建议策略:

资源类型 推荐加锁方式 说明
SPI osMutex 保护 HAL 传输函数
I2C osMutex 确保读写是原子操作
UART osMutex + RingBuf 可读写分离,但仍需写锁
ADC/DMA 一般加锁或用信号量 DMA 同步较特殊
外设访问任务 osMessageQueue + 专属线程 更复杂但线程安全(推荐在复杂项目中)

总结

是的,必须加锁或序列化访问,这是使用 RTOS 时避免“竞态条件”的基本要求。
你可以选:

  • 简单加 osMutex
  • 高级一点设计资源访问任务,用 osMessageQueue 分发。

序列化访问指的是:多个任务不直接操作某个共享资源(如 SPI/I2C/UART),而是通过“一个专门的任务”来统一访问该资源。

这样做的核心目的是 避免并发冲突,实现 线程安全


为何使用“序列化访问”而非简单加锁?

模式 优点 缺点
加锁访问 代码简单、快速实现 易滥用;多个任务仍需等待锁,可能嵌套死锁或优先级反转
序列化访问 明确职责,资源操作集中管理 实现复杂,需要设计消息格式/队列

✅ 序列化访问的经典做法

1. 创建一个专属任务,比如 Task_SPI_Manager
void Task_SPI_Manager(void *argument) {
    for (;;) {
        SPI_Request_t req;
        if (osMessageQueueGet(spiQueue, &req, NULL, osWaitForever) == osOK) {
            switch (req.cmd) {
                case SPI_CMD_WRITE:
                    HAL_SPI_Transmit(req.hspi, req.data, req.len, 100);
                    break;
                case SPI_CMD_READ:
                    HAL_SPI_Receive(req.hspi, req.data, req.len, 100);
                    break;
                // 其他操作...
            }

            // 可选:发信号通知请求完成
            if (req.doneSem != NULL)
                osSemaphoreRelease(req.doneSem);
        }
    }
}
2. 其他任务通过消息队列请求访问 SPI
SPI_Request_t req = {
    .cmd = SPI_CMD_WRITE,
    .hspi = &hspi1,
    .data = buf,
    .len = len,
    .doneSem = doneSemHandle // 可选,用于同步
};

osMessageQueuePut(spiQueue, &req, 0, 0);
osSemaphoreAcquire(doneSemHandle, osWaitForever); // 等待完成

✳️ 数据结构示例

typedef enum {
    SPI_CMD_WRITE,
    SPI_CMD_READ,
    // ...
} SPI_Command_t;

typedef struct {
    SPI_Command_t cmd;
    SPI_HandleTypeDef *hspi;
    uint8_t *data;
    uint16_t len;
    osSemaphoreId_t doneSem; // 可选
} SPI_Request_t;

应用场景推荐

使用情况 推荐方案
简单项目,偶尔访问 osMutex 加锁
复杂系统,多个任务频繁操作 SPI/I2C 序列化访问任务
UART 打印 RingBuffer + 信号
LCD/Flash/EEPROM 控制 序列化访问任务

小贴士

  • 这种结构类似驱动抽象层(Driver Thread),强烈推荐用于多任务系统;
  • 支持优先级管理、延时处理、DMA 回调等高级功能;
  • 可以方便地移植或单元测试。

以下是一个 CMSIS-RTOS2 + FreeRTOS + HAL 环境下的 SPI 访问序列化任务模板,用于多个任务安全共享一个 SPI 接口。


项目结构

Core/
 ├── Inc/
 │    └── spi_serial.h
 └── Src/
      └── spi_serial.c

spi_serial.h

#ifndef SPI_SERIAL_H
#define SPI_SERIAL_H

#include "cmsis_os2.h"
#include "main.h" // 包含 hspi 句柄

typedef enum {
    SPI_CMD_WRITE,
    SPI_CMD_READ
} SPI_Command_t;

typedef struct {
    SPI_Command_t cmd;
    SPI_HandleTypeDef *hspi;
    uint8_t *data;
    uint16_t len;
    osSemaphoreId_t doneSem; // 可用于同步请求完成
} SPI_Request_t;

void SPI_SerialInit(void);
osStatus_t SPI_AsyncRequest(SPI_Request_t *req); // 非阻塞
osStatus_t SPI_BlockingRequest(SPI_Request_t *req); // 阻塞等待完成

#endif // SPI_SERIAL_H

spi_serial.c

#include "spi_serial.h"

#define SPI_QUEUE_LENGTH 8

static osMessageQueueId_t spiQueue;
static osThreadId_t spiTaskHandle;

static void SPI_Task(void *argument) {
    SPI_Request_t req;
    for (;;) {
        if (osMessageQueueGet(spiQueue, &req, NULL, osWaitForever) == osOK) {
            switch (req.cmd) {
                case SPI_CMD_WRITE:
                    HAL_SPI_Transmit(req.hspi, req.data, req.len, HAL_MAX_DELAY);
                    break;
                case SPI_CMD_READ:
                    HAL_SPI_Receive(req.hspi, req.data, req.len, HAL_MAX_DELAY);
                    break;
                default:
                    break;
            }

            if (req.doneSem) {
                osSemaphoreRelease(req.doneSem);
            }
        }
    }
}

void SPI_SerialInit(void) {
    spiQueue = osMessageQueueNew(SPI_QUEUE_LENGTH, sizeof(SPI_Request_t), NULL);

    const osThreadAttr_t spiTaskAttr = {
        .name = "SPISerialTask",
        .priority = osPriorityNormal,
        .stack_size = 512
    };
    spiTaskHandle = osThreadNew(SPI_Task, NULL, &spiTaskAttr);
}

osStatus_t SPI_AsyncRequest(SPI_Request_t *req) {
    return osMessageQueuePut(spiQueue, req, 0, 0);
}

osStatus_t SPI_BlockingRequest(SPI_Request_t *req) {
    osSemaphoreId_t sem = osSemaphoreNew(1, 0, NULL);
    req->doneSem = sem;

    osStatus_t status = osMessageQueuePut(spiQueue, req, 0, 0);
    if (status == osOK) {
        osSemaphoreAcquire(sem, osWaitForever);
    }

    osSemaphoreDelete(sem);
    return status;
}

使用示例(任务中调用)

void SomeTask(void *argument) {
    uint8_t tx[2] = {0x9F, 0x00};

    SPI_Request_t req = {
        .cmd = SPI_CMD_WRITE,
        .hspi = &hspi1,
        .data = tx,
        .len = sizeof(tx),
        .doneSem = NULL
    };

    SPI_BlockingRequest(&req); // 等待传输完成

    osDelay(10);
}

注意事项

  1. 适配多个 SPI:可以让 .hspi 为不同句柄,如 &hspi1, &hspi2
  2. 可扩展 DMA、回调、超时机制
  3. 调试建议:加打印看哪个任务发送请求;
  4. 优先级反转保护:FreeRTOS 默认支持优先级继承;
  5. 你也可以使用 osMutex,但本方式适合高可靠性的设备资源抽象。

为适配 多个 SPI 外设(如 SPI1, SPI2, SPI3 等),你可以按照以下思路扩展现有框架,支持每个 SPI 接口有独立的队列与后台任务,从而安全地并发访问多个 SPI。


✅ 目标

为每个 SPI_HandleTypeDef 实例分配一个独立的任务与消息队列,实现访问串行化,支持多个 SPI。


设计核心结构

spi_serial.h 修改:

#ifndef SPI_SERIAL_H
#define SPI_SERIAL_H

#include "cmsis_os2.h"
#include "main.h" // HAL SPI 句柄

typedef enum {
    SPI_CMD_WRITE,
    SPI_CMD_READ
} SPI_Command_t;

typedef struct {
    SPI_Command_t cmd;
    uint8_t *data;
    uint16_t len;
    osSemaphoreId_t doneSem;
} SPI_Request_t;

// 初始化一个 SPI 序列化通道
void SPI_SerialInit(SPI_HandleTypeDef *hspi);

// 发送请求(阻塞/非阻塞)
osStatus_t SPI_BlockingRequest(SPI_HandleTypeDef *hspi, SPI_Request_t *req);
osStatus_t SPI_AsyncRequest(SPI_HandleTypeDef *hspi, SPI_Request_t *req);

#endif

spi_serial.c 关键实现:

#include "spi_serial.h"
#include 

#define MAX_SPI_INSTANCES     3
#define SPI_QUEUE_LENGTH      8

typedef struct {
    SPI_HandleTypeDef *hspi;
    osMessageQueueId_t queue;
    osThreadId_t thread;
} SPI_Context_t;

static SPI_Context_t spiContexts[MAX_SPI_INSTANCES];
static uint32_t spiContextCount = 0;

static SPI_Context_t* findContext(SPI_HandleTypeDef *hspi) {
    for (uint32_t i = 0; i < spiContextCount; ++i) {
        if (spiContexts[i].hspi == hspi)
            return &spiContexts[i];
    }
    return NULL;
}

static void SPI_SerialTask(void *argument) {
    SPI_Context_t *ctx = (SPI_Context_t *)argument;
    SPI_Request_t req;

    while (1) {
        if (osMessageQueueGet(ctx->queue, &req, NULL, osWaitForever) == osOK) {
            switch (req.cmd) {
                case SPI_CMD_WRITE:
                    HAL_SPI_Transmit(ctx->hspi, req.data, req.len, HAL_MAX_DELAY);
                    break;
                case SPI_CMD_READ:
                    HAL_SPI_Receive(ctx->hspi, req.data, req.len, HAL_MAX_DELAY);
                    break;
                default:
                    break;
            }

            if (req.doneSem) {
                osSemaphoreRelease(req.doneSem);
            }
        }
    }
}

void SPI_SerialInit(SPI_HandleTypeDef *hspi) {
    if (spiContextCount >= MAX_SPI_INSTANCES || findContext(hspi)) return;

    SPI_Context_t *ctx = &spiContexts[spiContextCount++];
    ctx->hspi = hspi;
    ctx->queue = osMessageQueueNew(SPI_QUEUE_LENGTH, sizeof(SPI_Request_t), NULL);

    char name[16];
    snprintf(name, sizeof(name), "SPI_Task_%ld", spiContextCount);
    const osThreadAttr_t attr = {
        .name = name,
        .stack_size = 512,
        .priority = osPriorityNormal,
    };
    ctx->thread = osThreadNew(SPI_SerialTask, ctx, &attr);
}

osStatus_t SPI_AsyncRequest(SPI_HandleTypeDef *hspi, SPI_Request_t *req) {
    SPI_Context_t *ctx = findContext(hspi);
    if (!ctx) return osErrorParameter;
    return osMessageQueuePut(ctx->queue, req, 0, 0);
}

osStatus_t SPI_BlockingRequest(SPI_HandleTypeDef *hspi, SPI_Request_t *req) {
    osSemaphoreId_t sem = osSemaphoreNew(1, 0, NULL);
    if (!sem) return osErrorNoMemory;

    req->doneSem = sem;

    SPI_Context_t *ctx = findContext(hspi);
    if (!ctx) {
        osSemaphoreDelete(sem);
        return osErrorParameter;
    }

    osStatus_t status = osMessageQueuePut(ctx->queue, req, 0, 0);
    if (status == osOK) {
        osSemaphoreAcquire(sem, osWaitForever);
    }

    osSemaphoreDelete(sem);
    return status;
}

使用示例(多 SPI)

void app_main(void) {
    SPI_SerialInit(&hspi1);
    SPI_SerialInit(&hspi2);
}
void Task1(void *arg) {
    uint8_t tx[2] = {0x9F, 0x00};
    SPI_Request_t req = {
        .cmd = SPI_CMD_WRITE,
        .data = tx,
        .len = sizeof(tx),
    };
    SPI_BlockingRequest(&hspi1, &req);
}

void Task2(void *arg) {
    uint8_t tx[1] = {0x90};
    SPI_Request_t req = {
        .cmd = SPI_CMD_WRITE,
        .data = tx,
        .len = 1,
    };
    SPI_BlockingRequest(&hspi2, &req);
}

✅ 优点

  • 支持任意多个 SPI 独立串行访问;
  • HAL 安全封装,无需每次手动上锁;
  • 扩展为 DMA、回调非常方便;
  • 不依赖 FreeRTOS 互斥量,不会优先级反转。

你可能感兴趣的:(stm32,arm开发)