title: BSP工程管理与封装 tags: [BSP, 工程管理, IMX6ULL, 裸机开发, Makefile] created: 2026-09-17 updated: 2026-09-17
💡 关联知识: [[STM32学习笔记/21-附录:HAL库架构与Keil MDK深入]] | [[Linux+C+C++技术体系梳理/1. C语言基础/14-模块化编程]]
BSP (Board Support Package) 即板级支持包,是介于硬件和操作系统之间的软件层。它的核心作用是:
| 对比项 | BSP | 驱动 |
|---|---|---|
| 范围 | 包含所有外设驱动 + 系统初始化 | 仅单个外设的控制代码 |
| 层次 | 板级完整支持 | 设备级功能实现 |
| 包含内容 | 时钟、GPIO、中断、外设驱动等 | 特定设备的寄存器操作 |
| 可复用性 | 跨项目复用 | 跨平台复用 |
┌─────────────────────────────────────────────────────────────┐
│ 1. 分析硬件原理图,确定引脚分配和寄存器地址 │
│ 2. 编写底层寄存器操作函数 │
│ 3. 封装为独立驱动模块(.c + .h) │
│ 4. 编写BSP统一头文件,集成所有驱动 │
│ 5. 编写Makefile,管理多目录编译 │
│ 6. 测试验证,确保各模块独立工作 │
└─────────────────────────────────────────────────────────────┘
当工程文件较少时,全部放在根目录下无可厚非。但当源文件达到几十甚至数百个时,混乱的结构会导致:
BSP工程管理的核心思想:按功能模块分目录,按职责分离文件。
以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 # 编译输出二进制文件
| 目录 | 用途 | 说明 |
|---|---|---|
project/ |
应用程序 | 存放启动代码和main.c,是程序入口 |
bsp/ |
板级驱动 | 按功能分子目录,每个外设独立模块 |
imx6ul/ |
芯片头文件 | NXP官方SDK头文件和芯片寄存器定义 |
obj/ |
编译产物 | 存放编译生成的.o文件,保持源码目录整洁 |
| 文件类型 | 命名规则 | 示例 |
|---|---|---|
| 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,支持自动搜索源文件和目录管理:
# 交叉编译器前缀,更换编译器时只需修改此行
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)
| 变量 | 作用 | 示例值 |
|---|---|---|
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 ... |
| 函数 | 语法 | 作用 |
|---|---|---|
patsubst |
$(patsubst 匹配, 替换, 文本) |
模式替换,如添加-I前缀 |
wildcard |
$(wildcard 模式) |
文件搜索,如$(dir)/*.c |
foreach |
$(foreach 变量, 列表, 表达式) |
循环遍历,如遍历目录 |
notdir |
$(notdir 路径) |
提取文件名,去掉目录路径 |
源文件(.c/.S)
↓ GCC编译 (-c选项)
目标文件(.o)
↓ LD链接 (-T指定链接脚本)
可执行文件(.elf)
↓ objcopy转换
二进制文件(.bin)
↓ imxdownload烧写
SD卡启动
| 选项 | 作用 |
|---|---|
-Wall |
开启所有警告信息 |
-nostdlib |
不链接标准库 |
-O2 |
二级优化 |
-c |
只编译不链接 |
-I |
指定头文件搜索路径 |
-T |
指定链接脚本 |
创建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
创建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
#!/bin/bash
# 清理脚本 - clean.sh
echo "=== 清理编译产物 ==="
make clean
echo "=== 清理完成 ==="
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
#ifndef __IMX6UL_H
#define __IMX6UL_H
#include "cc.h"
#include "MCIMX6Y2.h"
#include "fsl_common.h"
#include "fsl_iomuxc.h"
#endif
#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
#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;
}
}
#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;
}
#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);
}
}
#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;
}
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段结束地址 */
}
# 进入工程目录
cd 5_ledc_bsp
# 编译工程
make
# 清理编译产物
make clean
编译成功后会生成:
bsp.elf - 可执行文件bsp.bin - 二进制文件bsp.dis - 反汇编文件obj/ - 编译中间文件# 1. 给imxdownload执行权限(仅首次)
chmod 777 imxdownload
# 2. 烧写到SD卡(注意设备号)
./imxdownload bsp.bin /dev/sdd
注意:
/dev/sda或/dev/sda1lsblk命令查看SD卡设备号| 特性 | 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 |
| 特性 | IMX6ULL | STM32 HAL | STM32 LL |
|---|---|---|---|
| 抽象程度 | 高(自定义) | 高(官方) | 中 |
| 代码量 | 精简 | 较多 | 较少 |
| 性能 | 高 | 中 | 高 |
| 可移植性 | 中 | 高 | 高 |
| 学习曲线 | 低 | 中 | 高 |
考察点:BSP概念理解
参考答案:
led_init()、led_switch()等函数,上层无需关心具体寄存器操作考察点:驱动设计能力
参考答案:
bsp_<外设>.c/h,职责单一xxx_init(), xxx_read(), xxx_write()#ifdef支持不同平台VPATH的作用是什么?为什么需要它?考察点:Makefile理解
参考答案:
VPATH指定源文件的搜索路径,当源文件分布在多个目录时使用VPATH := project bsp/clk bsp/led,Make会在这些目录中查找.c和.S文件__bss_start和__bss_end的作用是什么?考察点:链接脚本理解
参考答案:
启动代码中的使用:
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
bss_loop:
cmp r0, r1
strlt r2, [r0], #4
blt bss_loop
考察点:驱动架构对比
参考答案:
| 特性 | 裸机 | RTOS | Linux |
|---|---|---|---|
| 并发模型 | 无(轮询/中断) | 任务调度+同步原语 | 进程/线程+内核框架 |
| 资源管理 | 直接操作寄存器 | 通过RTOS API | 通过内核驱动框架 |
| 设备注册 | 无需注册 | 无需注册 | 需要注册到内核 |
| 设备树 | 不支持 | 不支持 | 支持(描述硬件资源) |
| 典型场景 | 简单控制、学习 | 实时控制 | 复杂系统、网络应用 |
代码来源: 裸机例程 5_ledc_bsp 最后更新: 2026-09-17