找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5497

积分

0

好友

726

主题
发表于 2 小时前 | 查看: 5| 回复: 0

嵌入式项目开发第一件事,先把目录结构定好——这一步走对了,后续移植和多人协作会省下成吨的时间。

目录结构

以一个 STM32F4 + FreeRTOS + LCD 的项目为例,完整的目录长这样:

project/
├── app/                    # 应用层:业务逻辑
│   ├── task_sensor.c       # 传感器采集任务
│   ├── task_sensor.h
│   ├── task_comm.c         # 通信任务
│   ├── task_comm.h
│   ├── task_display.c      # 显示任务
│   ├── task_display.h
│   └── app_init.c          # 应用初始化(创建任务、消息队列等)
│
├── bsp/                    # 板级支持包:与具体硬件绑定
│   ├── stm32f407/          # 按芯片型号分目录
│   │   ├── bsp_uart.c
│   │   ├── bsp_uart.h
│   │   ├── bsp_spi.c
│   │   ├── bsp_spi.h
│   │   ├── bsp_gpio.c
│   │   ├── bsp_gpio.h
│   │   └── bsp_conf.h     # 板级配置(时钟、引脚映射)
│   └── gd32f450/           # 移植时加新目录即可
│       ├── bsp_uart.c
│       ├── bsp_uart.h
│       └── ...
│
├── driver/                 # 外设驱动:与芯片无关,与器件绑定
│   ├── w25q256/            # SPI Flash
│   │   ├── w25q256.c
│   │   └── w25q256.h
│   ├── ssd1306/            # OLED显示屏
│   │   ├── ssd1306.c
│   │   └── ssd1306.h
│   ├── bme280/             # 温湿度传感器
│   │   ├── bme280.c
│   │   └── bme280.h
│   └── mcu_flash/          # MCU内部Flash操作
│       ├── mcu_flash.c
│       └── mcu_flash.h
│
├── middleware/              # 中间件:通用功能模块
│   ├── shell/              # 串口命令行
│   │   ├── shell.c
│   │   └── shell.h
│   ├── log/                # 日志系统
│   │   ├── log.c
│   │   └── log.h
│   ├── param/              # 参数管理(KV存储)
│   │   ├── param.c
│   │   └── param.h
│   └── protocol/           # 通信协议
│       ├── protocol.c
│       └── protocol.h
│
├── os/                     # RTOS适配层
│   ├── os_wrapper.c        # 统一封装RTOS API
│   └── os_wrapper.h
│
├── config/                 # 全局配置
│   ├── project_config.h    # 项目级开关和参数
│   └── FreeRTOSConfig.h    # RTOS配置
│
├── utils/                  # 工具函数
│   ├── crc16.c
│   ├── crc16.h
│   ├── ringbuf.c
│   └── ringbuf.h
│
├── startup/                # 启动相关
│   ├── main.c
│   └── stm32f4xx_it.c      # 中断服务(仅放钩子,不放逻辑)
│
└── doc/                    # 项目文档
    └── porting_guide.md    # 移植说明

分层原则

目录结构的核心是依赖方向单向流动

app → middleware → driver → bsp
                ↘ os ↗
  • app 只依赖 middleware 和 driver 的接口
  • middleware 只依赖 driver 的接口
  • driver 只依赖 bsp 的接口
  • bsp 不依赖任何人,它是最底层

违反这个方向就是耦合。比如 driver 里的 w25q256.c 直接调了 USART2->DR,那这个驱动就绑死在 STM32 上了,换芯片得改驱动代码。谁也不想每换一颗 MCU 就重写一遍外设驱动,对吧?

bsp 和 driver 的区别

这两个概念最容易搞混,一句话区分:bsp 跟 MCU 绑定,driver 跟外设芯片绑定

bsp driver
绑定对象 具体 MCU 具体外设芯片
移植时改不改 换 MCU 就要重写 换 MCU 不用改
典型内容 时钟配置、引脚初始化、UART 收发 W25Q256 读写、BME280 数据解析
举例 bsp_uart_init() 配 PA9/PA10 w25q256_read_id() 发 0x9F 读 ID

bsp 提供硬件能力,driver 使用 bsp 提供的接口操作外设。换芯片只改 bsp 目录,driver 和 app 层代码一行不动。

接口隔离

driver 调 bsp 的函数,不能直接 #include "bsp_uart.h"。中间要加一层抽象接口。

以 SPI Flash 驱动为例,w25q256.h 需要 SPI 收发能力,但它不应该知道底层是 STM32 的 SPI 还是 GPIO 模拟的 SPI。

w25q256.h — 定义接口类型:

#ifndef W25Q256_H
#define W25Q256_H

#include <stdint.h>
#include <stddef.h>

/* SPI接口抽象:驱动只认这三个函数指针 */
typedef struct {
    void    (*cs_low)(void);
    void    (*cs_high)(void);
    uint8_t (*transfer)(uint8_t tx_byte);
} w25q256_spi_t;

int w25q256_init(const w25q256_spi_t *spi);
int w25q256_read_id(uint8_t id[3]);
int w25q256_read(uint32_t addr, uint8_t *buf, size_t len);
int w25q256_write(uint32_t addr, const uint8_t *buf, size_t len);
int w25q256_erase_sector(uint32_t addr);

#endif

bsp 层注册接口 — 在 app_init.c 或单独的 board.c 中:

#include "w25q256.h"
#include "bsp_spi.h"

/* bsp提供具体实现 */
static void spi1_cs_low(void)
{
    GPIOA->BSRR = GPIO_BSRR_BR4;  /* PA4低电平 */
}

static void spi1_cs_high(void)
{
    GPIOA->BSRR = GPIO_BSRR_BS4;  /* PA4高电平 */
}

static uint8_t spi1_transfer(uint8_t tx)
{
    SPI1->DR = tx;
    while (!(SPI1->SR & SPI_SR_RXNE));
    return SPI1->DR;
}

/* 注册到驱动 */
static const w25q256_spi_t spi1_for_flash = {
    .cs_low   = spi1_cs_low,
    .cs_high  = spi1_cs_high,
    .transfer = spi1_transfer,
};

void board_init(void)
{
    bsp_spi1_init();                /* 先初始化SPI硬件 */
    w25q256_init(&spi1_for_flash);  /* 再把接口注册给驱动 */
}

这样 w25q256.c 里没有任何 STM32 寄存器操作,换到 GD32 或 RISC-V,只需要写新的 bsp 函数并重新注册。这种写法在 C/C++ 的嵌入式项目中非常实用,把依赖反转过来,维护成本直线下降。

OS 适配层

RTOS 的 API 各家不一样。FreeRTOS 是 xTaskCreate,RT-Thread 是 rt_thread_create,裸机就是函数指针 + 定时器中断。如果 app 层直接调 FreeRTOS 的 API,换 RTOS 就是灾难。

os_wrapper.h — 统一接口:

#ifndef OS_WRAPPER_H
#define OS_WRAPPER_H

#include <stdint.h>
#include <stdbool.h>

typedef void (*os_task_func_t)(void *arg);

/* 任务 */
int os_task_create(os_task_func_t func, const char *name,
                   uint32_t stack_size, void *arg, uint32_t prio,
                   void **handle);

/* 延时 */
void os_delay_ms(uint32_t ms);

/* 互斥锁 */
typedef void *os_mutex_t;
int os_mutex_create(os_mutex_t *mutex);
int os_mutex_lock(os_mutex_t mutex, uint32_t timeout_ms);
int os_mutex_unlock(os_mutex_t mutex);

/* 消息队列 */
typedef void *os_queue_t;
int os_queue_create(os_queue_t *queue, uint32_t item_size, uint32_t capacity);
int os_queue_send(os_queue_t queue, const void *item, uint32_t timeout_ms);
int os_queue_recv(os_queue_t queue, void *item, uint32_t timeout_ms);

#endif

os_wrapper.c — FreeRTOS 实现:

#include "os_wrapper.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

int os_task_create(os_task_func_t func, const char *name,
                   uint32_t stack_size, void *arg, uint32_t prio,
                   void **handle)
{
    TaskHandle_t xHandle;
    BaseType_t ret = xTaskCreate(func, name, stack_size, arg, prio, &xHandle);
    if (ret != pdPASS) return -1;
    if (handle) *handle = xHandle;
    return 0;
}

void os_delay_ms(uint32_t ms)
{
    vTaskDelay(pdMS_TO_TICKS(ms));
}

int os_mutex_create(os_mutex_t *mutex)
{
    SemaphoreHandle_t m = xSemaphoreCreateMutex();
    if (!m) return -1;
    *mutex = (os_mutex_t)m;
    return 0;
}

int os_mutex_lock(os_mutex_t mutex, uint32_t timeout_ms)
{
    return xSemaphoreTake((SemaphoreHandle_t)mutex,
                          pdMS_TO_TICKS(timeout_ms)) == pdTRUE ? 0 : -1;
}

int os_mutex_unlock(os_mutex_t mutex)
{
    return xSemaphoreGive((SemaphoreHandle_t)mutex) == pdTRUE ? 0 : -1;
}

int os_queue_create(os_queue_t *queue, uint32_t item_size, uint32_t capacity)
{
    QueueHandle_t q = xQueueCreate(capacity, item_size);
    if (!q) return -1;
    *queue = (os_queue_t)q;
    return 0;
}

int os_queue_send(os_queue_t queue, const void *item, uint32_t timeout_ms)
{
    return xQueueSend((QueueHandle_t)queue, item,
                      pdMS_TO_TICKS(timeout_ms)) == pdTRUE ? 0 : -1;
}

int os_queue_recv(os_queue_t queue, void *item, uint32_t timeout_ms)
{
    return xQueueReceive((QueueHandle_t)queue, item,
                         pdMS_TO_TICKS(timeout_ms)) == pdTRUE ? 0 : -1;
}

app 层只调 os_delay_ms()os_queue_send() 这些函数。从 FreeRTOS 切到 RT-Thread,只改 os_wrapper.c 一个文件。

全局配置管理

project_config.h 集中管理功能开关和板级参数,不要到处写 #define

#ifndef PROJECT_CONFIG_H
#define PROJECT_CONFIG_H

/* 功能开关 */
#define CFG_USE_FREERTOS        1
#define CFG_USE_SHELL           1
#define CFG_USE_LOG             1
#define CFG_SENSOR_BME280       1
#define CFG_DISPLAY_SSD1306     1

/* 串口配置 */
#define CFG_DEBUG_UART_BAUD     115200
#define CFG_COMM_UART_BAUD      9600

/* 传感器参数 */
#define CFG_SENSOR_SAMPLE_MS    1000
#define CFG_SENSOR_REPORT_MS    5000

/* Flash分区(扇区为单位,W25Q256扇区4KB) */
#define CFG_FLASH_PARAM_SECTOR  0       /* 参数存储区:扇区0 */
#define CFG_FLASH_LOG_SECTOR    1       /* 日志存储区:扇区1~3 */
#define CFG_FLASH_LOG_COUNT     3

#endif

模块内部根据宏决定是否编译:

/* task_sensor.c */
#include "project_config.h"

#if CFG_SENSOR_BME280
#include "bme280.h"

void sensor_task(void *arg)
{
    bme280_init();
    while (1) {
        bme280_data_t data;
        bme280_read(&data);
        /* ... */
        os_delay_ms(CFG_SENSOR_SAMPLE_MS);
    }
}
#endif

换项目时改 project_config.h 就行,不用翻遍代码找散落的宏定义。

头文件包含规则

头文件乱包含是编译依赖的万恶之源。规则很简单:

  1. .c 文件可以包含任何需要的头文件
  2. .h 文件只包含自身声明需要的头文件 —— 能用前向声明就用前向声明
  3. 所有 .h 必须有 include guard
/* 错误:头文件里包含了一堆不需要的东西 */
#ifndef TASK_SENSOR_H
#define TASK_SENSOR_H

#include "bme280.h"   /* 不需要,.c里包含就行 */
#include "FreeRTOS.h" /* 不需要 */
#include "task.h"     /* 不需要 */

void sensor_task(void *arg);
#endif

/* 正确:头文件只声明接口 */
#ifndef TASK_SENSOR_H
#define TASK_SENSOR_H

void sensor_task(void *arg);  /* void*不需要额外头文件 */

#endif

前向声明替代结构体包含:

/* 错误 */
#include "protocol.h"  /* 只为了用 protocol_frame_t */

void sensor_send(const protocol_frame_t *frame);

/* 正确 */
typedef struct protocol_frame protocol_frame_t;  /* 前向声明 */

void sensor_send(const protocol_frame_t *frame);

中断服务的处理

中断服务函数放在 startup/stm32f4xx_it.c,但里面不放业务逻辑,只调钩子:

/* stm32f4xx_it.c */
#include "bsp_uart.h"

void USART2_IRQHandler(void)
{
    bsp_uart2_irq_handler();  /* 转发给bsp处理 */
}
/* bsp_uart.c */
#include "bsp_uart.h"
#include "ringbuf.h"

static ringbuf_t uart2_rx_buf;

void bsp_uart2_irq_handler(void)
{
    if (USART2->SR & USART_SR_RXNE) {
        uint8_t ch = USART2->DR;
        ringbuf_put(&uart2_rx_buf, ch);
    }
}

int bsp_uart2_read(uint8_t *buf, size_t len, uint32_t timeout_ms)
{
    /* 从ringbuf取数据,超时由os层处理 */
    size_t got = 0;
    while (got < len) {
        if (ringbuf_get(&uart2_rx_buf, &buf[got])) {
            got++;
        } else if (timeout_ms == 0) {
            break;
        } else {
            os_delay_ms(1);
            timeout_ms--;
        }
    }
    return (int)got;
}

中断里只做最少的事(收字节进环形缓冲区),处理逻辑放在任务里。这样换 RTOS 时中断部分不用改,只改 bsp_uart2_read 里的等待方式。

如果对目录结构还有更多想法,欢迎到 云栈社区 和更多嵌入式开发者一起交流,很多踩过的坑,聊一聊就能绕过去。




上一篇:告别Linux旧印象:安装、软件、驱动与日常使用4大误区解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-25 07:43 , Processed in 0.721569 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表