1. 项目概述
在嵌入式开发领域,STM32F103C8T6(俗称"蓝莓派")因其性价比高而广受欢迎。但这款芯片仅有20KB RAM,在移植实时操作系统(如uCOS-III)并创建多任务时,经常会遇到内存溢出的问题。本文将详细记录如何在资源受限的STM32F103C8T6上成功移植uCOS-III,并创建包含双LED闪烁和串口通信的三任务工程。
提示:本方案同样适用于其他RAM资源紧张的Cortex-M3内核MCU,如STM32F103C6T6等。
2. 环境准备与源码获取
2.1 硬件准备
- STM32F103C8T6最小系统板(核心板)
- USB-TTL串口模块(用于调试输出)
- 两个LED灯及限流电阻(建议220Ω)
- 杜邦线若干
2.2 软件工具
- Keil MDK-ARM(建议V5.25以上)
- STM32标准外设库(V3.5.0)
- uCOS-III源码(3.03版本)
2.3 源码获取
从野火电子官网下载《uCOS-III内核实现与应用开发实战指南》配套资料:
- 访问野火产品资料下载中心
- 找到"uCOS-III内核实现与应用开发"资料包
- 下载包含uCOS-III 3.03源码的压缩包
注意:务必选择3.03版本(适配Cortex-M3),而非3.04版本(适配Cortex-M4)
3. 工程架构设计
3.1 目录结构规划
建议采用以下目录结构:
code复制USER/
├── APP/ # uCOS-III核心源码
├── BSP/ # 板级支持包
│ ├── bsp.c
│ ├── bsp_led.c
│ └── bsp_serial.c
├── uC-CPU/ # CPU相关移植文件
├── uC-LIB/ # 库文件
└── uCOS-III/ # uCOS-III内核
3.2 内存分配策略
针对20KB RAM的限制,采用以下优化策略:
| 模块 | 原始配置 | 优化后配置 | 节省量 |
|---|---|---|---|
| 堆内存 | 27KB | 10KB | 17KB |
| 任务栈 | 默认512B | 统一256B | 50% |
| 内核功能 | 全开 | 仅基础功能 | 可变 |
4. 详细移植步骤
4.1 源码移植
-
从uCOS-III 3.03源码中复制以下文件:
Micrium/Software/uC-CPUMicrium/Software/uC-LIBMicrium/Software/uCOS-IIIMicrium/Software/EvalBoards/.../uCOS-III下的8个核心文件
-
清理不必要的文件:
- 删除所有
GNU和IAR文件夹 - 保留
ARM-Cortex-M3/Generic/RealView下的移植文件
- 删除所有
4.2 Keil工程配置
-
创建以下分组:
- USER:主程序文件
- APP:应用任务
- BSP:硬件驱动
- uC/CPU:CPU移植层
- uC/LIB:库文件
- uC/OS-III Source:内核源码
- uC/OS-III Port:移植文件
-
添加头文件路径:
.\USER\APP.\USER\BSP.\USER\uC-CPU.\USER\uC-LIB.\USER\uCOS-III\Source.\USER\uCOS-III\Ports\ARM-Cortex-M3\Generic\RealView
4.3 关键文件修改
4.3.1 启动文件修改
修改startup_stm32f10x_md.s:
assembly复制; 原代码
; DCD PendSV_Handler
; DCD SysTick_Handler
; 修改为
DCD OS_CPU_PendSVHandler
DCD OS_CPU_SysTickHandler
4.3.2 配置文件优化
os_cfg.h关键配置:
c复制#define OS_CFG_TASK_DEL_EN 1u // 启用任务删除功能
#define OS_CFG_PRIO_MAX 32u // 最大优先级数
#define OS_CFG_FLAG_EN 0u // 禁用事件标志
#define OS_CFG_MUTEX_EN 0u // 禁用互斥信号量
#define OS_CFG_Q_EN 0u // 禁用消息队列
#define OS_CFG_TMR_EN 0u // 禁用软件定时器
lib_cfg.h修改:
c复制#define LIB_MEM_CFG_HEAP_SIZE 10u * 1024u // 堆大小改为10KB
app_cfg.h任务配置:
c复制// 任务优先级
#define APP_TASK_START_PRIO 2
#define APP_TASK_LED1_PRIO 3
#define APP_TASK_LED2_PRIO 3
#define APP_TASK_UART_PRIO 4
// 任务栈大小(统一256字节)
#define APP_TASK_START_STK_SIZE 256
#define APP_TASK_LED1_STK_SIZE 256
#define APP_TASK_LED2_STK_SIZE 256
#define APP_TASK_UART_STK_SIZE 256
5. 驱动开发与任务实现
5.1 LED驱动实现
修改野火例程中的bsp_led.h,适配C8T6的GPIO:
c复制// LED1 -> PA1, LED2 -> PA2
#define LED1_GPIO_PORT GPIOA
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOA
#define LED1_GPIO_PIN GPIO_Pin_1
#define LED2_GPIO_PORT GPIOA
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOA
#define LED2_GPIO_PIN GPIO_Pin_2
5.2 串口驱动实现
bsp_serial.c关键代码:
c复制void Serial_Init(void)
{
// 初始化USART1 (PA9-TX, PA10-RX)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// ... 其他初始化代码
}
void Serial_SendString(char *String)
{
uint8_t i;
for(i = 0; String[i] != '\0'; i++) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, String[i]);
}
}
5.3 任务实现
app.c中的任务函数:
c复制// LED1任务 (每2秒翻转)
static void AppTaskLed1(void *p_arg)
{
OS_ERR err;
while(DEF_TRUE) {
LED1_TOGGLE;
OSTimeDly(2000, OS_OPT_TIME_DLY, &err);
}
}
// LED2任务 (每6秒翻转)
static void AppTaskLed2(void *p_arg)
{
OS_ERR err;
while(DEF_TRUE) {
LED2_TOGGLE;
OSTimeDly(6000, OS_OPT_TIME_DLY, &err);
}
}
// 串口任务 (每4秒发送消息)
static void AppTaskUart(void *p_arg)
{
OS_ERR err;
while(DEF_TRUE) {
Serial_SendString("uCOS-III Running\r\n");
OSTimeDly(4000, OS_OPT_TIME_DLY, &err);
}
}
6. 内存优化技巧
6.1 栈空间分配原则
- 每个任务栈大小 = 函数调用深度 × 栈帧大小 + 局部变量 + 安全余量
- 对于简单任务,256字节通常足够
- 可通过
OS_CFG_STK_SIZE_MIN设置最小栈限制
6.2 内核裁剪建议
- 关闭统计任务:
OS_CFG_STAT_TASK_EN 0u - 禁用不需要的内核对象:
c复制#define OS_CFG_FLAG_EN 0u // 事件标志 #define OS_CFG_MUTEX_EN 0u // 互斥量 #define OS_CFG_Q_EN 0u // 消息队列 - 减少优先级数量:
OS_CFG_PRIO_MAX 32u(默认值)
6.3 堆内存管理
- 修改
lib_cfg.h中的LIB_MEM_CFG_HEAP_SIZE - 内存分配算法选择:
- 首次适应(适合小内存系统)
- 最佳适应(减少碎片但开销大)
7. 常见问题与解决方案
7.1 编译错误排查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| L6406E内存溢出 | 1. 堆栈设置过大 2. 内核功能未裁剪 |
1. 减小LIB_MEM_CFG_HEAP_SIZE 2. 关闭不必要功能 |
| 任务无法创建 | 1. 栈空间不足 2. 优先级冲突 |
1. 增加栈大小 2. 检查优先级分配 |
| 串口无输出 | 1. 引脚配置错误 2. 波特率不匹配 |
1. 检查PA9/PA10连接 2. 确认双方波特率一致 |
7.2 调试技巧
- 使用
OS_OPT_TASK_STK_CHK选项检测栈溢出 - 通过
CPU_IntDisMeasMaxCurReset()测量中断关闭时间 - 在
app_cfg.h中启用OS_CFG_DBG_EN调试功能
7.3 性能优化建议
- 将频繁调用的函数声明为
static inline - 使用
OS_CFG_ISR_POST_DEFERRED_EN延迟中断处理 - 合理设置
OS_CFG_TICK_RATE_HZ(通常10-100Hz)
8. 项目验证与测试
8.1 功能测试清单
-
LED测试:
- LED1应每2秒切换状态
- LED2应每6秒切换状态
- 两者状态变化应独立不干扰
-
串口测试:
- 每4秒收到"hello world"消息
- 波特率9600无乱码
-
内存测试:
- 持续运行24小时无内存泄漏
- 任务创建/删除操作稳定
8.2 压力测试方法
-
内存压力测试:
c复制void *ptr[10]; for(int i=0; i<10; i++) { ptr[i] = malloc(100); if(ptr[i] == NULL) { // 内存不足处理 } } -
任务切换测试:
- 创建10个简单任务观察调度情况
- 监控
OSIdleTaskCtr判断CPU负载
-
中断响应测试:
- 使用GPIO外部中断测量响应延迟
- 确保关键中断不被长时间屏蔽
9. 工程源码结构说明
9.1 关键文件清单
code复制app.c - 主任务实现
bsp_led.c - LED驱动
bsp_serial.c - 串口驱动
os_cfg.h - 内核配置
lib_cfg.h - 内存配置
app_cfg.h - 任务参数配置
stm32f10x_it.c - 中断服务程序
9.2 代码版本管理建议
-
使用Git进行版本控制
-
推荐分支策略:
- master:稳定发布版
- dev:开发测试版
- feature/xxx:功能开发分支
-
.gitignore配置示例:
code复制*.uvgui.* *.uvopt *.uvproj *.lst *.map *.o
10. 扩展应用方向
10.1 功能扩展建议
- 添加按键中断唤醒功能
- 实现任务间通信(信号量/消息队列)
- 增加低功耗模式支持
10.2 其他MCU适配
-
STM32F103C6T6(10KB RAM):
- 进一步减小堆大小(5-8KB)
- 任务栈缩减至128字节
-
GD32F103系列:
- 注意时钟树差异
- 可能需要调整延时参数
-
CH32V103(RISC-V内核):
- 需要重新移植CPU相关代码
- 修改中断处理机制
11. 开发心得与建议
在实际移植过程中,针对小内存MCU的关键经验:
-
配置优先原则:先通过
os_cfg.h裁剪内核功能,再调整内存分配 -
栈空间估算:
- 简单任务:256字节
- 中等复杂度:512字节
- 复杂任务:768字节以上
-
调试技巧:
- 使用
OS_CPU_SysTickHandler()钩子函数监控系统健康状态 - 定期检查
OSMemUsage()返回值
- 使用
-
优化方向:
- 将频繁访问的变量声明为
register - 使用
OS_CFG_TICK_RATE_HZ降低系统节拍(如从100Hz降至50Hz)
- 将频繁访问的变量声明为
-
资源监控:
c复制void MonitorTask(void *p_arg) { while(1) { printf("Free Heap: %d\r\n", LIB_MEM_CFG_HEAP_SIZE - OSMemUsage()); OSTimeDly(1000, OS_OPT_TIME_DLY, &err); } }
对于初次接触uCOS-III的开发者,建议先从功能完备的开发板(如STM32F407)入手,熟悉内核机制后再移植到资源受限的C8T6平台。在项目时间允许的情况下,可以尝试逐步启用更多内核功能,找到功能与资源占用的最佳平衡点。