syscalls.c
是 STM32CubeIDE 自动生成的标准系统调用适配文件,用于裸机环境下支持 newlib
标准库(如 printf
, scanf
, malloc
)的运行。这份文件提供了标准库运行所需的最小系统调用实现。现在我来逐段解析其作用,并补充你可能需要修改或关注的地方。
该文件让 标准C库函数 在没有操作系统的 STM32 环境中可以正常工作。特别是:
printf()
调用 _write()
(需要重定向到串口)scanf()
调用 _read()
(需要接收串口输入)malloc()
依赖 _sbrk()
(位于另一个文件 sysmem.c
中)#include
#include
#include
#include
#include
#include
#include
#include
用于提供标准类型定义和错误码,支持不同系统调用的原型和实现。
char *__env[1] = { 0 };
char **environ = __env;
_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;
}
_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() |
占位 |
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
malloc()
#include
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
如果你想使用 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
(ARM GCC 默认使用的 C 标准库)不是线程安全的。比如:
errno
malloc()
/ free()
如果多任务同时调用这些函数,可能发生内存损坏或数据错乱。
configUSE_NEWLIB_REENTRANT = 1
的作用启用后:
struct _reent
reent
结构malloc()
、errno
、printf()
等的线程安全性newlib-nano
或 newlib
启用 configUSE_NEWLIB_REENTRANT = 1
的前提是你项目确实在使用 newlib,否则会报错:
cannot open source file "reent.h"
如果你没有用
-specs=nano.specs
或-lc
之类的 newlib 库,启用这个配置毫无意义还会出错。
_sbrk
, _write
, 等)CubeMX 会生成
syscalls.c
/sysmem.c
,你必须保留它们。
否则 malloc()
、printf()
就无法工作。
__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 |
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#define configUSE_NEWLIB_REENTRANT 1
#define configTOTAL_HEAP_SIZE (10 * 1024)
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
后增加的开销包括:
// 栈使用情况检查
void vTaskStackCheck(void)
{
UBaseType_t uxHighWaterMark;
// 获取任务剩余栈空间
uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
printf("Stack remaining: %d words\n", uxHighWaterMark);
}
// 不同任务使用不同栈大小
xTaskCreate(SimpleTask, "Simple", 512, NULL, 1, NULL); // 2KB
xTaskCreate(PrintfTask, "Printf", 1024, NULL, 1, NULL); // 4KB
xTaskCreate(FileTask, "File", 2048, NULL, 1, NULL); // 8KB
// 在FreeRTOSConfig.h中启用
#define configCHECK_FOR_STACK_OVERFLOW 2
如果RAM紧张,可以考虑:
#define configUSE_NEWLIB_REENTRANT 0
然后使用互斥锁保护共享资源。
建议从4KB开始,然后根据实际的栈使用情况进行调整。记住要定期检查栈的高水位标记,确保没有栈溢出的风险。