STM32的 syscalls.c 和 sysmem.c
syscalls.c
是 STM32CubeIDE 自动生成的标准系统调用适配文件,用于裸机环境下支持 newlib
标准库(如 printf
, scanf
, malloc
)的运行。这份文件提供了标准库运行所需的最小系统调用实现。现在我来逐段解析其作用,并补充你可能需要修改或关注的地方。
🧩 主要用途
该文件让 标准C库函数 在没有操作系统的 STM32 环境中可以正常工作。特别是:
printf()
调用_write()
(需要重定向到串口)scanf()
调用_read()
(需要接收串口输入)malloc()
依赖_sbrk()
(位于另一个文件sysmem.c
中)
📄 文件结构解析
🔹 引入的头文件
#include <sys/stat.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/times.h>
用于提供标准类型定义和错误码,支持不同系统调用的原型和实现。
🔹 环境变量(通常忽略)
char *__env[1] = { 0 };
char **environ = __env;
🔹 核心系统调用函数(重点)
1. _write()
: 用于 printf
__attribute__((weak)) int _write(int file, char *ptr, int len)
{(void)file;for (int i = 0; i < len; i++)__io_putchar(*ptr++);return len;
}
⚠️
__io_putchar()
是 弱函数(weak),你需要在项目中自己实现这个函数,常用于串口重定向:
int __io_putchar(int ch)
{HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);return ch;
}
2. _read()
: 用于 scanf
__attribute__((weak)) int _read(int file, char *ptr, int len)
{for (int i = 0; i < len; i++)*ptr++ = __io_getchar();return len;
}
你也需实现 __io_getchar()
,如:
int __io_getchar(void)
{uint8_t ch;HAL_UART_Receive(&huart2, &ch, 1, HAL_MAX_DELAY);return ch;
}
🔹 其他系统调用(通常不使用,但需要实现以避免链接错误)
这些函数都是 dummy 实现(返回错误码),用于满足编译要求:
函数 | 用途/说明 |
---|---|
_exit() | 程序退出,死循环防止退出 |
_kill() | 假设无信号机制,返回错误 |
_getpid() | 返回 PID 1,占位 |
_close() | 文件关闭,占位 |
_fstat() | 标识为字符设备 |
_isatty() | 判断文件是否为终端,返回1 |
_lseek() | 文件定位,占位 |
_open() | 打开文件,占位 |
_unlink() | 删除文件,占位 |
_fork() | 无进程管理,占位 |
_execve() | 不支持执行程序 |
_wait() | 无子进程支持,占位 |
_times() | 占位 |
_link() | 占位 |
_stat() | 占位 |
✅ 如何启用 UART 输出功能?
- 确保开启 UART(比如
USART2
)并启用HAL_UART_Transmit
。 - 添加以下函数实现(在
main.c
或独立retarget.c
文件):
int __io_putchar(int ch)
{HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);return ch;
}int __io_getchar(void)
{uint8_t ch;HAL_UART_Receive(&huart2, &ch, 1, HAL_MAX_DELAY);return ch;
}
📝 如果你想改为 fputc()
重定向,也可以用以下方式:
int fputc(int ch, FILE *f)
{HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);return ch;
}
不过 __io_putchar
更适合与 CubeIDE 生成的 syscalls.c
搭配使用。
🔚 总结
方面 | 内容 |
---|---|
是否需要改动? | 如果你要使用 printf/scanf ,是的! |
推荐修改点 | 实现 __io_putchar 和 __io_getchar |
使用 HAL_UART 示例? | 是,可用于 STM32 串口输出 |
你提供的 sysmem.c
是 STM32CubeIDE 自动生成的内存分配支持文件,主要目的是实现标准 C 库 malloc
系列函数所依赖的 _sbrk()
系统调用。
这是嵌入式裸机编程中用于支持动态内存分配的 标准方法。
🔍 核心作用:实现 _sbrk()
✅ _sbrk(ptrdiff_t incr)
说明
该函数负责从 RAM 中划出堆空间。
void *_sbrk(ptrdiff_t incr)
incr
: 要增加的堆大小(单位:字节)- 返回值: 成功时返回旧的堆尾地址,失败返回
(void *) -1
并设置errno = ENOMEM
🧠 工作原理
它通过链接脚本中的符号 _end
, _estack
, _Min_Stack_Size
来判断内存边界:
RAM 起始地址 → [.data][.bss][heap → → →] ... [stack] ← ← ← ← ← _estack(RAM末端)↑ ↑_end _estack
__sbrk_heap_end
:当前堆的“末尾指针”_end
:链接脚本中定义的 静态数据段结束地址(堆的起始)_estack
:RAM 顶部(栈的起点)_Min_Stack_Size
:为主堆栈指针(MSP)保留的空间,避免栈和堆冲突
⚠️ 如果你用的是默认
STM32CubeIDE
生成的.ld
链接脚本,这些符号是自动生成的,你无需手动添加。
🧩 链接脚本中通常有以下内容(例如):
._user_heap_stack :{. = ALIGN(8);PROVIDE ( end = . );PROVIDE ( _end = . );. = . + _Min_Heap_Size;. = . + _Min_Stack_Size;. = ALIGN(8);} >RAM
⚠️ 如果你使用 malloc()
报错或崩溃…
-
确保链接器脚本中定义了
_end
,_estack
,_Min_Stack_Size
-
确保在
sysmem.c
中_sbrk()
没有超出max_heap
-
调整堆栈大小:STM32CubeIDE 中修改:
Project -> Properties -> C/C++ Build -> Settings -> Tool Settings -> MCU Settings
- 增大
Heap size
- 调整
Stack size
- 增大
✅ 示例:在 STM32 中使用 malloc()
#include <stdlib.h>char *buf = malloc(100);
if (!buf) {// 分配失败,内存不足
}
该调用会通过
malloc()
→sbrk()
来从 RAM 中申请空间
📝 总结
项目 | 内容 |
---|---|
文件作用 | 支持 malloc 的底层实现 |
关键函数 | _sbrk() |
内存边界判断依据 | _end , _estack , _Min_Stack_Size |
错误处理 | 堆超出栈保留区时返回 ENOMEM |
Makefile 中用于链接(linking)的部分配置,主要用来指导 GCC 如何链接目标文件、库文件和链接脚本。
我们来逐项解释这几行内容。
🔍 内容逐行解析
# libraries
LIBS = -lc -lm -lnosys
-lc
:链接标准 C 库(libc)-lm
:链接数学库(libm),例如支持sin
,cos
,sqrt
-lnosys
:链接libnosys.a
,这是一个提供最小系统调用桩函数的库(避免链接错误)
LIBDIR =
- 没设置额外的库搜索目录
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) \-Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref \-Wl,--gc-sections
这是构造链接参数的完整语句,分解如下:
参数 | 说明 |
---|---|
$(MCU) | MCU 相关的 GCC 参数(如 -mcpu=cortex-m4 -mthumb ) |
-specs=nano.specs | 使用轻量级 nano newlib(newlib-nano ),适合嵌入式系统 |
-T$(LDSCRIPT) | 指定链接脚本路径,例如 stm32f4xx.ld |
$(LIBDIR) | 库目录参数(未定义时留空) |
$(LIBS) | 链接所需的库,如 -lc , -lm , -lnosys |
-Wl,-Map=... | 让 ld 生成 .map 文件,帮助查看内存布局和符号 |
--cref | 在 .map 文件中启用交叉引用 |
--gc-sections | 丢弃未使用的段,减小最终二进制体积 |
🔧 举例:使用这些参数生成固件
假设你运行 arm-none-eabi-gcc
手动链接:
arm-none-eabi-gcc main.o startup_stm32f4xx.o \-mcpu=cortex-m4 -mthumb \-specs=nano.specs \-Tstm32f4xx.ld \-lc -lm -lnosys \-Wl,-Map=output.map,--cref \-Wl,--gc-sections \-o firmware.elf
✅ 关于 -lnosys
的补充
libnosys.a
是newlib
提供的最小系统调用实现库。- 它中断了
exit()
,_kill()
,_getpid()
等函数的调用链。 - 通常你会通过
syscalls.c
或自己实现_write()
,_sbrk()
来覆盖它。
✅ 关于 -specs=nano.specs
- 它会让
gcc
使用libc_nano.a
和libg_nano.a
- 这是为嵌入式优化的版本,更小但功能有限,适用于 STM32、ARM Cortex-M 等资源受限环境。
🧠 提示
如果你想使用 printf()
的浮点功能(在 nano.specs
下默认关闭),你需要额外加:
-Wl,-u,_printf_float
或在 LDFLAGS
中加:
LDFLAGS += -Wl,-u,_printf_float
✅ 总结
项 | 用途 |
---|---|
-lnosys | 提供默认的空系统调用,避免链接错误(可被用户定义的 _write 等函数替代) |
-specs=nano.specs | 使用轻量 newlib 版本,适合嵌入式 |
-T$(LDSCRIPT) | 指定 .ld 链接脚本路径 |
-Wl,--gc-sections | 去掉未用段,减小大小 |
-Map=... | 生成 map 文件,调试内存和符号使用情况 |
USE_NEWLIB_REENTRANT
(或在 FreeRTOSConfig.h
中叫 configUSE_NEWLIB_REENTRANT
)是一个和 FreeRTOS + newlib 标准 C 库 集成相关的重要配置项,确实需要谨慎使用。
🧠 什么是 USE_NEWLIB_REENTRANT
?
简单说:
它表示是否为每个 FreeRTOS 任务分配一个 独立的 newlib 重入结构体 struct _reent
,以支持 线程安全的标准 C 函数(如 printf
, malloc
等)。
🔧 背景:newlib 是单线程库
标准的 newlib
(ARM GCC 默认使用的 C 标准库)不是线程安全的。比如:
- 全局变量
errno
- 全局堆指针
malloc()
/free()
如果多任务同时调用这些函数,可能发生内存损坏或数据错乱。
✅ 设置 configUSE_NEWLIB_REENTRANT = 1
的作用
启用后:
- FreeRTOS 会为每个任务分配一个独立的
struct _reent
- 标准库中的函数会使用当前任务的
reent
结构 - 保证
malloc()
、errno
、printf()
等的线程安全性
⚠️ 警告与注意事项
❗ 1. 你必须使用 newlib-nano
或 newlib
启用 configUSE_NEWLIB_REENTRANT = 1
的前提是你项目确实在使用 newlib,否则会报错:
cannot open source file "reent.h"
如果你没有用
-specs=nano.specs
或-lc
之类的 newlib 库,启用这个配置毫无意义还会出错。
❗ 2. 你必须提供必要的系统调用(_sbrk
, _write
, 等)
CubeMX 会生成
syscalls.c
/sysmem.c
,你必须保留它们。
否则 malloc()
、printf()
就无法工作。
❗ 3. 你可能还需要实现 __malloc_lock()
和 __malloc_unlock()
如果你使用的是 full newlib(不是 nano 版),malloc 默认是非线程安全的,除非你实现:
void __malloc_lock(struct _reent *r) {taskENTER_CRITICAL(); // 或者使用 mutex
}void __malloc_unlock(struct _reent *r) {taskEXIT_CRITICAL();
}
🔍 启用方式
在 FreeRTOSConfig.h
中加上:
#define configUSE_NEWLIB_REENTRANT 1
然后确保:
- 编译器链接了
newlib
/newlib-nano
(一般-lc -lrdimon -lnosys
) - 有
syscalls.c
、sysmem.c
- 编译器路径中包含了
reent.h
(通常自动包含)
❌ 不建议随意启用的原因
官方警告你很清楚:
⚠️ “FreeRTOS does not use newlib itself. If you enable reentrancy, you are responsible for making it work safely.”
启用后:
- 系统内存占用增加(每个任务增加
struct _reent
) - 锁管理交给你自己实现
- 如果你不理解
malloc
、errno
的底层行为,很容易出错
✅ 总结
配置项 | 作用 |
---|---|
configUSE_NEWLIB_REENTRANT = 1 | 每个任务都有独立 reent 结构,支持线程安全 newlib |
启用前需满足 | 说明 |
---|---|
使用 newlib 库 | 链接 -lc (默认是)或 -specs=nano.specs |
包含 reent.h 头文件 | 通常自动包含于 newlib |
提供系统调用(如 _sbrk() ) | 用于 malloc 等 |
了解线程安全需求 | 特别是 __malloc_lock() 等 |
当你在 STM32CubeMX 中启用了 FreeRTOS
,它可能会在“Project Report”或生成代码时提示你:
提示:configUSE_NEWLIB_REENTRANT is enabled. You must ensure newlib is thread-safe and syscalls are implemented properly.
这是一个 重要但非致命 的提示,意思是你需要为启用 configUSE_NEWLIB_REENTRANT = 1
做额外准备工作。下面是详细解释和应对方法:
🔍 提示含义详解
当你在 CubeMX 中启用:
Use Newlib reentrant
它就会在生成的 FreeRTOSConfig.h
中添加:
#define configUSE_NEWLIB_REENTRANT 1
并提示你:你必须确保系统环境满足 Newlib 多线程安全运行的条件,否则可能引发运行时错误。
✅ 要做的准备(否则会踩坑)
条件 | 说明 |
---|---|
✅ 使用 newlib 标准版 而非 nano | nano 是精简版,不完整支持线程安全结构 |
✅ 正确实现了 syscalls,如 _sbrk() , _write() 等 | 否则 malloc() 、printf() 等会失效 |
✅ 每个任务堆栈空间足够(通常 ≥ 256 字节) | 否则 newlib 的 _reent 结构体无法分配 |
✅ heap_3.c 要求线程锁保护 malloc() | 推荐改用 heap_useNewlib.c 或 heap_4.c |
🛠 关键实现步骤
① 项目设置中使用 full newlib
在 STM32CubeIDE 中:
Project > Properties > C/C++ Build > Settings
- 进入
Tool Settings > MCU GCC Linker > Libraries
- ❌ 取消勾选
Use newlib-nano (--specs=nano.specs)
- ✔️ 保留或添加
--specs=nosys.specs
② 实现系统调用 syscalls.c
(如果还没有)
你需要提供如下函数,至少要包括:
void *_sbrk(ptrdiff_t incr); // 用于 malloc 内存扩展
int _write(int file, char *ptr, int len); // 用于 printf 输出
✅ 示例 _sbrk()
实现:
extern char _end; // Defined in linker script
static char *heap_end;void *_sbrk(ptrdiff_t incr) {char *prev_heap_end;if (heap_end == 0)heap_end = &_end;prev_heap_end = heap_end;heap_end += incr;return (void *) prev_heap_end;
}
③ 使用线程安全的 malloc()
方案(可选)
如果你使用 heap_3.c
(基于标准 malloc
),你需要启用锁支持,否则多个任务调用会冲突。
✅ 更好的方式:
- 使用
heap_useNewlib.c
(基于 Dave Nadler 的线程安全 malloc) - 或使用
heap_4.c
+pvPortMalloc()
替代系统 malloc
✅ 示例配置(FreeRTOSConfig.h)
#define configUSE_NEWLIB_REENTRANT 1
#define configTOTAL_HEAP_SIZE (10 * 1024)
✅ 示例任务(带 printf)
void TaskPrint(void *pvParameters) {while (1) {printf("Hello from task %s\n", pcTaskGetName(NULL));vTaskDelay(pdMS_TO_TICKS(1000));}
}
✅ 如何验证配置正确
测试 | 正常现象 |
---|---|
printf() 在多个任务中输出 | 没有乱码、不会崩溃 |
malloc() 在任务中使用 | 分配正常、无崩溃 |
errno 在任务间隔离 | 每个任务 errno 不冲突 |
📌 总结
这条提示的真正含义是:
启用了线程安全支持后,你必须确保 newlib 运行环境完整、堆/栈足够、syscalls 正确、malloc 线程安全,否则系统将不可预期。
当启用configUSE_NEWLIB_REENTRANT
后,每个任务的栈空间需求会显著增加,因为需要为每个任务分配独立的C库上下文。
栈空间需求
基本推荐值
// 最小栈大小建议
#define configMINIMAL_STACK_SIZE ((unsigned short)512) // 2KB (512 * 4字节)// 实际任务栈大小建议
xTaskCreate(TaskFunction, "TaskName", 1024, // 4KB栈空间NULL, Priority, &TaskHandle);
不同使用场景的栈大小
1. 简单任务(只有基本操作)
#define SIMPLE_TASK_STACK_SIZE 512 // 2KB
2. 使用printf/sprintf的任务
#define PRINTF_TASK_STACK_SIZE 1024 // 4KB
3. 使用malloc/free的任务
#define MALLOC_TASK_STACK_SIZE 1024 // 4KB
4. 使用文件系统操作的任务
#define FILE_TASK_STACK_SIZE 2048 // 8KB
栈空间增加的原因
启用configUSE_NEWLIB_REENTRANT
后增加的开销包括:
- struct _reent结构体:约400-600字节
- I/O缓冲区:printf等函数的缓冲区
- malloc堆管理:每个任务的堆状态信息
- 错误处理:errno等错误状态
实际测试建议
// 栈使用情况检查
void vTaskStackCheck(void)
{UBaseType_t uxHighWaterMark;// 获取任务剩余栈空间uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);printf("Stack remaining: %d words\n", uxHighWaterMark);
}
优化建议
1. 根据实际需求调整
// 不同任务使用不同栈大小
xTaskCreate(SimpleTask, "Simple", 512, NULL, 1, NULL); // 2KB
xTaskCreate(PrintfTask, "Printf", 1024, NULL, 1, NULL); // 4KB
xTaskCreate(FileTask, "File", 2048, NULL, 1, NULL); // 8KB
2. 使用栈监控
// 在FreeRTOSConfig.h中启用
#define configCHECK_FOR_STACK_OVERFLOW 2
3. 考虑关闭可重入支持
如果RAM紧张,可以考虑:
#define configUSE_NEWLIB_REENTRANT 0
然后使用互斥锁保护共享资源。
总结
- 最小推荐:512 words (2KB)
- 安全推荐:1024 words (4KB)
- 重度使用:2048 words (8KB)
建议从4KB开始,然后根据实际的栈使用情况进行调整。记住要定期检查栈的高水位标记,确保没有栈溢出的风险。