07-BSP工程管理与封装.md 18 KB


title: BSP工程管理与封装 tags: [BSP, 工程管理, IMX6ULL, 裸机开发, Makefile] created: 2026-09-17 updated: 2026-09-17

pdf_ref: "正点原子I.MX6U嵌入式Linux驱动开发指南V2.0.1 第十三章"

BSP工程管理与封装

💡 关联知识: [[STM32学习笔记/21-附录:HAL库架构与Keil MDK深入]] | [[Linux+C+C++技术体系梳理/1. C语言基础/14-模块化编程]]

一、BSP概述

1.1 什么是BSP

BSP (Board Support Package) 即板级支持包,是介于硬件和操作系统之间的软件层。它的核心作用是:

  • 硬件抽象:屏蔽底层寄存器细节,提供统一API
  • 驱动封装:将外设驱动(LED、按键、串口等)封装为独立模块
  • 初始化代码:提供系统启动时的硬件初始化
  • 可移植性:更换硬件平台时只需修改BSP层,上层应用无需改动

1.2 BSP vs 驱动

对比项 BSP 驱动
范围 包含所有外设驱动 + 系统初始化 仅单个外设的控制代码
层次 板级完整支持 设备级功能实现
包含内容 时钟、GPIO、中断、外设驱动等 特定设备的寄存器操作
可复用性 跨项目复用 跨平台复用

1.3 BSP开发流程

┌─────────────────────────────────────────────────────────────┐
│  1. 分析硬件原理图,确定引脚分配和寄存器地址                  │
│  2. 编写底层寄存器操作函数                                   │
│  3. 封装为独立驱动模块(.c + .h)                           │
│  4. 编写BSP统一头文件,集成所有驱动                          │
│  5. 编写Makefile,管理多目录编译                             │
│  6. 测试验证,确保各模块独立工作                             │
└─────────────────────────────────────────────────────────────┘

1.4 为什么需要BSP工程管理

当工程文件较少时,全部放在根目录下无可厚非。但当源文件达到几十甚至数百个时,混乱的结构会导致:

  • 难以找到特定功能的代码
  • 编译依赖关系混乱
  • 无法多人协作开发
  • 移植和维护困难

BSP工程管理的核心思想:按功能模块分目录,按职责分离文件

二、工程目录结构

2.1 标准目录结构

以IMX6ULL裸机工程为例,推荐的目录结构如下:

5_ledc_bsp/
├── project/                    # 应用层
│   ├── start.S                 # 启动汇编文件
│   └── main.c                  # 主程序入口
├── bsp/                        # 板级支持包
│   ├── clk/                    # 时钟驱动
│   │   ├── bsp_clk.c
│   │   └── bsp_clk.h
│   ├── delay/                  # 延时驱动
│   │   ├── bsp_delay.c
│   │   └── bsp_delay.h
│   └── led/                    # LED驱动
│       ├── bsp_led.c
│       └── bsp_led.h
├── imx6ul/                     # 芯片相关文件
│   ├── imx6ul.h               # 统一包含头文件
│   ├── cc.h                   # 编译器相关定义
│   ├── MCIMX6Y2.h             # NXP官方寄存器定义
│   ├── fsl_common.h           # NXP通用函数声明
│   └── fsl_iomuxc.h           # IO复用配置函数
├── obj/                        # 编译输出目录
│   ├── start.o
│   ├── main.o
│   ├── bsp_clk.o
│   ├── bsp_led.o
│   └── bsp_delay.o
├── imx6ul.lds                  # 链接脚本
├── Makefile                    # 编译脚本
└── bsp.bin                     # 编译输出二进制文件

2.2 各目录职责说明

目录 用途 说明
project/ 应用程序 存放启动代码和main.c,是程序入口
bsp/ 板级驱动 按功能分子目录,每个外设独立模块
imx6ul/ 芯片头文件 NXP官方SDK头文件和芯片寄存器定义
obj/ 编译产物 存放编译生成的.o文件,保持源码目录整洁

2.3 文件命名规范

文件类型 命名规则 示例
BSP驱动源文件 bsp_<外设>.c bsp_led.c, bsp_clk.c
BSP驱动头文件 bsp_<外设>.h bsp_led.h, bsp_clk.h
芯片头文件 直接使用官方命名 MCIMX6Y2.h, fsl_common.h
主程序 main.c 固定名称
启动汇编 start.S 固定名称

三、Makefile编写

3.1 通用Makefile框架

这是正点原子裸机例程使用的通用Makefile,支持自动搜索源文件和目录管理:

# 交叉编译器前缀,更换编译器时只需修改此行
CROSS_COMPILE ?= arm-linux-gnueabihf-

# 目标文件名,不同例程修改此行
TARGET ?= bsp

# 编译工具链
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump

# 头文件目录列表(所有包含.h的目录)
INCDIRS := imx6ul \
           bsp/clk \
           bsp/led \
           bsp/delay

# 源文件目录列表(所有包含.c和.S的目录)
SRCDIRS := project \
           bsp/clk \
           bsp/led \
           bsp/delay

# 为每个目录添加 -I 前缀
INCLUDE := $(patsubst %, -I %, $(INCDIRS))

# 自动搜索所有汇编文件
SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S))
# 自动搜索所有C文件
CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))

# 提取文件名(去掉路径)
SFILENDIR := $(notdir $(SFILES))
CFILENDIR := $(notdir $(CFILES))

# 生成.o文件列表,统一放到obj目录
SOBJS := $(patsubst %, obj/%, $(SFILENDIR:.S=.o))
COBJS := $(patsubst %, obj/%, $(CFILENDIR:.c=.o))
OBJS := $(SOBJS) $(COBJS)

# 指定源文件搜索路径
VPATH := $(SRCDIRS)

# 伪目标声明
.PHONY: clean

# 最终目标:生成.bin文件
$(TARGET).bin : $(OBJS)
	$(LD) -Timx6ul.lds -o $(TARGET).elf $^
	$(OBJCOPY) -O binary -S $(TARGET).elf $@
	$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis

# 汇编文件编译规则
$(SOBJS) : obj/%.o : %.S
	$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<

# C文件编译规则
$(COBJS) : obj/%.o : %.c
	$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<

# 清理目标
clean:
	rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)

3.2 Makefile核心语法解析

3.2.1 变量定义

变量 作用 示例值
CROSS_COMPILE 交叉编译器前缀 arm-linux-gnueabihf-
TARGET 输出文件名 bsp
INCDIRS 头文件目录列表 imx6ul bsp/clk bsp/led
SRCDIRS 源文件目录列表 project bsp/clk bsp/led
INCLUDE 编译器头文件参数 -I imx6ul -I bsp/clk
OBJS 所有目标文件 obj/start.o obj/main.o ...

3.2.2 常用函数

函数 语法 作用
patsubst $(patsubst 匹配, 替换, 文本) 模式替换,如添加-I前缀
wildcard $(wildcard 模式) 文件搜索,如$(dir)/*.c
foreach $(foreach 变量, 列表, 表达式) 循环遍历,如遍历目录
notdir $(notdir 路径) 提取文件名,去掉目录路径

3.2.3 编译流程详解

源文件(.c/.S)
    ↓  GCC编译 (-c选项)
目标文件(.o)
    ↓  LD链接 (-T指定链接脚本)
可执行文件(.elf)
    ↓  objcopy转换
二进制文件(.bin)
    ↓  imxdownload烧写
SD卡启动

3.3 编译选项说明

选项 作用
-Wall 开启所有警告信息
-nostdlib 不链接标准库
-O2 二级优化
-c 只编译不链接
-I 指定头文件搜索路径
-T 指定链接脚本

四、工程管理脚本

4.1 一键编译脚本

创建build.sh脚本,一键完成编译和清理:

#!/bin/bash
# 编译脚本 - build.sh

echo "=== 开始编译BSP工程 ==="

# 清理旧文件
make clean

# 编译新工程
make -j$(nproc)

if [ $? -eq 0 ]; then
    echo "=== 编译成功 ==="
    ls -lh bsp.bin
else
    echo "=== 编译失败 ==="
    exit 1
fi

4.2 烧写脚本

创建burn.sh脚本,自动烧写到SD卡:

#!/bin/bash
# 烧写脚本 - burn.sh

if [ $# -ne 1 ]; then
    echo "Usage: $0 /dev/sdX"
    echo "Example: $0 /dev/sdd"
    exit 1
fi

SD_DEVICE=$1
BIN_FILE="bsp.bin"

# 检查文件是否存在
if [ ! -f "$BIN_FILE" ]; then
    echo "错误: 找不到 $BIN_FILE,请先编译"
    exit 1
fi

# 检查设备是否存在
if [ ! -e "$SD_DEVICE" ]; then
    echo "错误: 设备 $SD_DEVICE 不存在"
    exit 1
fi

echo "正在烧写 $BIN_FILE 到 $SD_DEVICE ..."
chmod 777 imxdownload
./imxdownload "$BIN_FILE" "$SD_DEVICE"

if [ $? -eq 0 ]; then
    echo "=== 烧写成功 ==="
else
    echo "=== 烧写失败 ==="
fi

4.3 清理脚本

#!/bin/bash
# 清理脚本 - clean.sh

echo "=== 清理编译产物 ==="
make clean
echo "=== 清理完成 ==="

五、完整工程示例

5.1 工程结构图

5_ledc_bsp/
├── project/
│   ├── start.S              # 启动汇编,初始化栈和BSS段
│   └── main.c               # 主程序,LED闪烁逻辑
├── bsp/
│   ├── clk/
│   │   ├── bsp_clk.c        # 使能所有外设时钟
│   │   └── bsp_clk.h
│   ├── delay/
│   │   ├── bsp_delay.c      # 延时函数
│   │   └── bsp_delay.h
│   └── led/
│       ├── bsp_led.c        # LED初始化和控制
│       └── bsp_led.h
├── imx6ul/
│   ├── imx6ul.h             # 统一头文件
│   ├── cc.h
│   ├── MCIMX6Y2.h
│   ├── fsl_common.h
│   └── fsl_iomuxc.h
├── obj/
├── imx6ul.lds
├── Makefile
└── bsp.bin

5.2 关键源码

5.2.1 imx6ul.h(统一头文件)

#ifndef __IMX6UL_H
#define __IMX6UL_H

#include "cc.h"
#include "MCIMX6Y2.h"
#include "fsl_common.h"
#include "fsl_iomuxc.h"

#endif

5.2.2 bsp_led.h

#ifndef __BSP_LED_H
#define __BSP_LED_H

#include "imx6ul.h"

#define LED0    0
#define ON      1
#define OFF     0

void led_init(void);
void led_switch(int led, int status);

#endif

5.2.3 bsp_led.c

#include "bsp_led.h"

/*
 * @description : 初始化LED对应的GPIO
 */
void led_init(void)
{
    /* 1、初始化IO复用 */
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);

    /* 2、配置GPIO1_IO03的IO属性 */
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0X10B0);

    /* 3、初始化GPIO, GPIO1_IO03设置为输出 */
    GPIO1->GDIR |= (1 << 3);

    /* 4、设置GPIO1_IO03输出低电平,打开LED0 */
    GPIO1->DR &= ~(1 << 3);
}

/*
 * @description : LED控制函数
 * @param - led : 要控制的LED灯编号
 * @param - status : 0关闭, 1打开
 */
void led_switch(int led, int status)
{
    switch(led)
    {
        case LED0:
            if(status == ON)
                GPIO1->DR &= ~(1<<3);
            else if(status == OFF)
                GPIO1->DR |= (1<<3);
            break;
    }
}

5.2.4 bsp_clk.c

#include "bsp_clk.h"

/*
 * @description : 使能I.MX6U所有外设时钟
 */
void clk_enable(void)
{
    CCM->CCGR0 = 0XFFFFFFFF;
    CCM->CCGR1 = 0XFFFFFFFF;
    CCM->CCGR2 = 0XFFFFFFFF;
    CCM->CCGR3 = 0XFFFFFFFF;
    CCM->CCGR4 = 0XFFFFFFFF;
    CCM->CCGR5 = 0XFFFFFFFF;
    CCM->CCGR6 = 0XFFFFFFFF;
}

5.2.5 bsp_delay.c

#include "bsp_delay.h"

/*
 * @description : 短时间延时
 */
void delay_short(volatile unsigned int n)
{
    while(n--){}
}

/*
 * @description : 延时函数,396MHz主频下约1ms
 */
void delay(volatile unsigned int n)
{
    while(n--)
    {
        delay_short(0x7ff);
    }
}

5.2.6 main.c

#include "bsp_clk.h"
#include "bsp_delay.h"
#include "bsp_led.h"

int main(void)
{
    clk_enable();       /* 使能所有的时钟 */
    led_init();         /* 初始化led */

    while(1)
    {
        led_switch(LED0, ON);
        delay(500);

        led_switch(LED0, OFF);
        delay(500);
    }

    return 0;
}

5.3 链接脚本 imx6ul.lds

SECTIONS{
    . = 0X87800000;            /* 链接地址,IMX6ULL启动地址 */

    .text :                    /* 代码段 */
    {
        obj/start.o            /* 启动文件必须第一个链接 */
        *(.text)
    }

    .rodata ALIGN(4) : {*(.rodata*)}   /* 只读数据段 */
    .data ALIGN(4) : { *(.data) }      /* 已初始化数据段 */

    __bss_start = .;           /* BSS段起始地址 */
    .bss ALIGN(4) : { *(.bss) *(COMMON) }
    __bss_end = .;             /* BSS段结束地址 */
}

六、实验验证

6.1 编译方法

# 进入工程目录
cd 5_ledc_bsp

# 编译工程
make

# 清理编译产物
make clean

编译成功后会生成:

  • bsp.elf - 可执行文件
  • bsp.bin - 二进制文件
  • bsp.dis - 反汇编文件
  • obj/ - 编译中间文件

6.2 烧写方法

# 1. 给imxdownload执行权限(仅首次)
chmod 777 imxdownload

# 2. 烧写到SD卡(注意设备号)
./imxdownload bsp.bin /dev/sdd

注意

  • 不能烧写到/dev/sda/dev/sda1
  • 使用lsblk命令查看SD卡设备号
  • 烧写前确保SD卡已插入读卡器

6.3 运行验证

  1. 将烧写好的SD卡插入开发板SD卡槽
  2. 设置启动模式为SD卡启动
  3. 复位开发板
  4. 观察LED0以500ms间隔亮灭

七、跨平台对比

7.1 BSP工程结构对比

特性 IMX6ULL STM32 RK3568
启动地址 0x87800000 0x08000000 0x00200000
编译工具 arm-linux-gnueabihf-gcc arm-none-eabi-gcc aarch64-linux-gnu-gcc
启动文件 start.S startup_stm32f103xx.s start.S
链接脚本 .lds .ld .lds
烧写工具 imxdownload ST-Link/J-Link RKDevTool

7.2 驱动封装对比

特性 IMX6ULL STM32 HAL STM32 LL
抽象程度 高(自定义) 高(官方)
代码量 精简 较多 较少
性能
可移植性
学习曲线

八、面试精选

题目1:什么是BSP?它在嵌入式系统中的作用是什么?

考察点:BSP概念理解

参考答案

  • 定义:BSP (Board Support Package) 是板级支持包,是介于硬件和操作系统之间的软件层
  • 核心作用
    • 提供硬件抽象,屏蔽底层寄存器细节
    • 提供统一的驱动接口,方便上层调用
    • 实现系统初始化,配置时钟、内存等
    • 支持跨平台移植,更换硬件只需修改BSP层
  • 实际示例:LED驱动封装led_init()led_switch()等函数,上层无需关心具体寄存器操作

题目2:如何设计一个可移植的BSP驱动?请描述设计原则。

考察点:驱动设计能力

参考答案

  1. 模块化设计:每个外设独立成bsp_<外设>.c/h,职责单一
  2. 硬件配置与逻辑分离:引脚号、基地址等用宏定义,逻辑代码保持通用
  3. 统一API命名:如xxx_init(), xxx_read(), xxx_write()
  4. 使用条件编译#ifdef支持不同平台
  5. 提供默认配置:允许用户覆盖默认参数
  6. 文档化接口:每个函数说明参数、返回值、使用场景

题目3:Makefile中VPATH的作用是什么?为什么需要它?

考察点:Makefile理解

参考答案

  • 作用VPATH指定源文件的搜索路径,当源文件分布在多个目录时使用
  • 解决的问题
    • 避免在编译规则中写死源文件路径
    • 支持自动搜索多个目录的源文件
    • 保持编译规则的通用性
  • 示例VPATH := project bsp/clk bsp/led,Make会在这些目录中查找.c和.S文件

题目4:链接脚本中__bss_start__bss_end的作用是什么?

考察点:链接脚本理解

参考答案

  • 定义:在链接脚本中定义的符号,标记BSS段的起始和结束地址
  • 作用
    • 程序启动时根据这两个地址清零BSS段
    • 避免使用未初始化的全局变量(C标准要求全局变量初始化为0)
  • 启动代码中的使用

    ldr r0, =__bss_start
    ldr r1, =__bss_end
    mov r2, #0
    bss_loop:
      cmp r0, r1
      strlt r2, [r0], #4
      blt bss_loop
    

题目5:裸机程序和RTOS/Linux驱动有什么本质区别?

考察点:驱动架构对比

参考答案

特性 裸机 RTOS Linux
并发模型 无(轮询/中断) 任务调度+同步原语 进程/线程+内核框架
资源管理 直接操作寄存器 通过RTOS API 通过内核驱动框架
设备注册 无需注册 无需注册 需要注册到内核
设备树 不支持 不支持 支持(描述硬件资源)
典型场景 简单控制、学习 实时控制 复杂系统、网络应用

代码来源: 裸机例程 5_ledc_bsp 最后更新: 2026-09-17