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開始,然后根據實際的棧使用情況進行調整。記住要定期檢查棧的高水位標記,確保沒有棧溢出的風險。