STM32的 syscalls.c 和 sysmem.c

syscalls.c 是 STM32CubeIDE 自动生成的标准系统调用适配文件,用于裸机环境下支持 newlib 标准库(如 printf, scanf, malloc)的运行。这份文件提供了标准库运行所需的最小系统调用实现。现在我来逐段解析其作用,并补充你可能需要修改或关注的地方。


🧩 主要用途

该文件让 标准C库函数 在没有操作系统的 STM32 环境中可以正常工作。特别是:

  • printf() 调用 _write()(需要重定向到串口)
  • scanf() 调用 _read()(需要接收串口输入)
  • malloc() 依赖 _sbrk()(位于另一个文件 sysmem.c 中)

📄 文件结构解析

🔹 引入的头文件

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>

用于提供标准类型定义和错误码,支持不同系统调用的原型和实现。


🔹 环境变量(通常忽略)

c 复制代码
char *__env[1] = { 0 };
char **environ = __env;

🔹 核心系统调用函数(重点)

1. _write(): 用于 printf
c 复制代码
__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),你需要在项目中自己实现这个函数,常用于串口重定向:

c 复制代码
int __io_putchar(int ch)
{
  HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}
2. _read(): 用于 scanf
c 复制代码
__attribute__((weak)) int _read(int file, char *ptr, int len)
{
  for (int i = 0; i < len; i++)
    *ptr++ = __io_getchar();
  return len;
}

你也需实现 __io_getchar(),如:

c 复制代码
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 输出功能?

  1. 确保开启 UART (比如 USART2)并启用 HAL_UART_Transmit
  2. 添加以下函数实现(在 main.c 或独立 retarget.c 文件)
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() 重定向,也可以用以下方式:

c 复制代码
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 中划出堆空间。

c 复制代码
void *_sbrk(ptrdiff_t incr)
  • incr: 要增加的堆大小(单位:字节)
  • 返回值: 成功时返回旧的堆尾地址,失败返回 (void *) -1 并设置 errno = ENOMEM

🧠 工作原理

它通过链接脚本中的符号 _end, _estack, _Min_Stack_Size 来判断内存边界:

c 复制代码
 RAM 起始地址 → [.data][.bss][heap → → →] ... [stack] ← ← ← ← ← _estack(RAM末端)
                                   ↑       ↑
                                 _end   _estack
  • __sbrk_heap_end:当前堆的"末尾指针"
  • _end:链接脚本中定义的 静态数据段结束地址(堆的起始)
  • _estack:RAM 顶部(栈的起点)
  • _Min_Stack_Size:为主堆栈指针(MSP)保留的空间,避免栈和堆冲突

⚠️ 如果你用的是默认 STM32CubeIDE 生成的 .ld 链接脚本,这些符号是自动生成的,你无需手动添加。


🧩 链接脚本中通常有以下内容(例如):

ld 复制代码
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

⚠️ 如果你使用 malloc() 报错或崩溃...

  1. 确保链接器脚本中定义了 _end, _estack, _Min_Stack_Size

  2. 确保在 sysmem.c_sbrk() 没有超出 max_heap

  3. 调整堆栈大小:STM32CubeIDE 中修改:

    复制代码
    Project -> Properties -> C/C++ Build -> Settings -> Tool Settings -> MCU Settings
    • 增大 Heap size
    • 调整 Stack size

✅ 示例:在 STM32 中使用 malloc()

c 复制代码
#include <stdlib.h>

char *buf = malloc(100);
if (!buf) {
  // 分配失败,内存不足
}

该调用会通过 malloc()sbrk() 来从 RAM 中申请空间


📝 总结

项目 内容
文件作用 支持 malloc 的底层实现
关键函数 _sbrk()
内存边界判断依据 _end, _estack, _Min_Stack_Size
错误处理 堆超出栈保留区时返回 ENOMEM

Makefile 中用于链接(linking)的部分配置,主要用来指导 GCC 如何链接目标文件、库文件和链接脚本。

我们来逐项解释这几行内容。


🔍 内容逐行解析

makefile 复制代码
# libraries
LIBS = -lc -lm -lnosys 
  • -lc:链接标准 C 库(libc)
  • -lm:链接数学库(libm),例如支持 sin, cos, sqrt
  • -lnosys:链接 libnosys.a,这是一个提供最小系统调用桩函数的库(避免链接错误)
makefile 复制代码
LIBDIR = 
  • 没设置额外的库搜索目录
makefile 复制代码
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 手动链接:

bash 复制代码
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.anewlib 提供的最小系统调用实现库。
  • 它中断了 exit(), _kill(), _getpid() 等函数的调用链。
  • 通常你会通过 syscalls.c 或自己实现 _write(), _sbrk() 来覆盖它。

✅ 关于 -specs=nano.specs

  • 它会让 gcc 使用 libc_nano.alibg_nano.a
  • 这是为嵌入式优化的版本,更小但功能有限,适用于 STM32、ARM Cortex-M 等资源受限环境。

🧠 提示

如果你想使用 printf() 的浮点功能(在 nano.specs 下默认关闭),你需要额外加:

makefile 复制代码
-Wl,-u,_printf_float

或在 LDFLAGS 中加:

makefile 复制代码
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()errnoprintf() 等的线程安全性

⚠️ 警告与注意事项

❗ 1. 你必须使用 newlib-nanonewlib

启用 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 默认是非线程安全的,除非你实现:

c 复制代码
void __malloc_lock(struct _reent *r) {
  taskENTER_CRITICAL(); // 或者使用 mutex
}

void __malloc_unlock(struct _reent *r) {
  taskEXIT_CRITICAL();
}

🔍 启用方式

FreeRTOSConfig.h 中加上:

c 复制代码
#define configUSE_NEWLIB_REENTRANT 1

然后确保:

  • 编译器链接了 newlib/newlib-nano(一般 -lc -lrdimon -lnosys
  • syscalls.csysmem.c
  • 编译器路径中包含了 reent.h(通常自动包含)

❌ 不建议随意启用的原因

官方警告你很清楚:

⚠️ "FreeRTOS does not use newlib itself. If you enable reentrancy, you are responsible for making it work safely."

启用后:

  • 系统内存占用增加(每个任务增加 struct _reent
  • 锁管理交给你自己实现
  • 如果你不理解 mallocerrno 的底层行为,很容易出错

✅ 总结

配置项 作用
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 中添加:

c 复制代码
#define configUSE_NEWLIB_REENTRANT 1

并提示你:你必须确保系统环境满足 Newlib 多线程安全运行的条件,否则可能引发运行时错误。


✅ 要做的准备(否则会踩坑)

条件 说明
✅ 使用 newlib 标准版 而非 nano nano 是精简版,不完整支持线程安全结构
✅ 正确实现了 syscalls,如 _sbrk(), _write() 否则 malloc()printf() 等会失效
✅ 每个任务堆栈空间足够(通常 ≥ 256 字节) 否则 newlib 的 _reent 结构体无法分配
heap_3.c 要求线程锁保护 malloc() 推荐改用 heap_useNewlib.cheap_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(如果还没有)

你需要提供如下函数,至少要包括:

c 复制代码
void *_sbrk(ptrdiff_t incr);     // 用于 malloc 内存扩展
int _write(int file, char *ptr, int len);  // 用于 printf 输出

✅ 示例 _sbrk() 实现:

c 复制代码
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)

c 复制代码
#define configUSE_NEWLIB_REENTRANT 1
#define configTOTAL_HEAP_SIZE     (10 * 1024)

✅ 示例任务(带 printf)

c 复制代码
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库上下文。

栈空间需求

基本推荐值

c 复制代码
// 最小栈大小建议
#define configMINIMAL_STACK_SIZE    ((unsigned short)512)  // 2KB (512 * 4字节)

// 实际任务栈大小建议
xTaskCreate(TaskFunction, 
            "TaskName", 
            1024,        // 4KB栈空间
            NULL, 
            Priority, 
            &TaskHandle);

不同使用场景的栈大小

1. 简单任务(只有基本操作)

c 复制代码
#define SIMPLE_TASK_STACK_SIZE    512   // 2KB

2. 使用printf/sprintf的任务

c 复制代码
#define PRINTF_TASK_STACK_SIZE    1024  // 4KB

3. 使用malloc/free的任务

c 复制代码
#define MALLOC_TASK_STACK_SIZE    1024  // 4KB

4. 使用文件系统操作的任务

c 复制代码
#define FILE_TASK_STACK_SIZE      2048  // 8KB

栈空间增加的原因

启用configUSE_NEWLIB_REENTRANT后增加的开销包括:

  1. struct _reent结构体:约400-600字节
  2. I/O缓冲区:printf等函数的缓冲区
  3. malloc堆管理:每个任务的堆状态信息
  4. 错误处理:errno等错误状态

实际测试建议

c 复制代码
// 栈使用情况检查
void vTaskStackCheck(void)
{
    UBaseType_t uxHighWaterMark;
    
    // 获取任务剩余栈空间
    uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
    
    printf("Stack remaining: %d words\n", uxHighWaterMark);
}

优化建议

1. 根据实际需求调整

c 复制代码
// 不同任务使用不同栈大小
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. 使用栈监控

c 复制代码
// 在FreeRTOSConfig.h中启用
#define configCHECK_FOR_STACK_OVERFLOW  2

3. 考虑关闭可重入支持

如果RAM紧张,可以考虑:

c 复制代码
#define configUSE_NEWLIB_REENTRANT 0

然后使用互斥锁保护共享资源。

总结

  • 最小推荐:512 words (2KB)
  • 安全推荐:1024 words (4KB)
  • 重度使用:2048 words (8KB)

建议从4KB开始,然后根据实际的栈使用情况进行调整。记住要定期检查栈的高水位标记,确保没有栈溢出的风险。

相关推荐
竹照煜_ysn2 小时前
蓝桥杯51单片机设计
单片机·嵌入式硬件·51单片机
Do vis8243 小时前
STM32第十六天蓝牙模块
stm32·单片机·嵌入式硬件
学不动CV了3 小时前
ARM单片机启动流程(二)(详细解析)
c语言·arm开发·stm32·单片机·51单片机
猫猫的小茶馆5 小时前
【STM32】通用定时器基本原理
c语言·stm32·单片机·嵌入式硬件·mcu·51单片机
jingshaoqi_ccc6 小时前
stm32的USART使用DMA配置成循环模式时发送和接收有着本质区别
stm32·单片机·嵌入式硬件
pumpkin845146 小时前
Rust 调用 C 函数的 FFI
c语言·算法·rust
2401_858286117 小时前
125.【C语言】数据结构之归并排序递归解法
c语言·开发语言·数据结构·算法·排序算法·归并排序
双叶8368 小时前
(C++)学生管理系统(正式版)(map数组的应用)(string应用)(引用)(文件储存的应用)(C++教学)(C++项目)
c语言·开发语言·数据结构·c++
MingYue_SSS9 小时前
开关电源抄板学习
经验分享·笔记·嵌入式硬件·学习
玉树临风江流儿9 小时前
炸鸡派-定时器基础例程
单片机·嵌入式硬件