前言 我在查看他人的项目源码时,发现了一个名为 syscalls.c 的文件。查阅资料后了解到,这个文件是 STM32CubeMX 针对 arm-none-eabi-gcc(即嵌入式版的 GCC)的底层实现。平时在网上搜索重定向 printf 到串口的方法时,大多数教程都是针对 ARM 环境的。本文将介绍 GCC 环境下如何重定向 printf 到串口,以及两者的实现细节差异。
不同开发环境使用的编译器和 C 标准库对比如下:
IDE/工具链
编译器
C 标准库
Keil MDK (AC5)
ARMCC
ARM C Library / MicroLib
Keil MDK (AC6)
ARMCLANG
ARM C Library / MicroLib
STM32CubeIDE
GCC (arm-none-eabi-gcc)
Newlib
System Workbench
GCC (arm-none-eabi-gcc)
Newlib
IAR EWARM
ICCARM
DLIB / CLIB
printf 重定向的原理 在上位机(自己的电脑)编写 C 语言程序时,会使用很多标准库函数:
1 2 3 printf ("Hello" ); malloc (100 ); strcpy (a, b);
不同编译器对标准库都有各自的底层实现,但实现效果相同,因为它们遵循同一套标准。
各编译器使用的 C 标准库如下:
编译器
使用的 C 标准库
Keil AC5
ARM C Library 或 MicroLib
GCC
Newlib
电脑上的 GCC
glibc(太大,嵌入式不适用)
调用 printf 时的执行流程如下:
1 2 3 4 5 6 7 用户代码:printf("Hello %d", 123) ↓ 【标准库层】处理格式化字符串 → "Hello 123" ↓ 【底层接口】逐字符输出 ↓ 【硬件层】UART 发送数据
不同的标准库在”底层接口”这一层使用不同的函数名和机制,这就是问题的根源。两者的主要区别如下:
Keil (ARMCC):实现 fputc() 函数
GCC (Newlib):实现 _write() 或 __io_putchar() 函数
Keil MDK 实现方法 使用 MicroLib(最简单) MicroLib 是 ARM C Library 的精简版本,去除了文件操作等复杂功能。可以在 Keil 中直接启用,只需勾选相应选项即可。
启用 MicroLib 后,只需实现相应的底层接口即可,这里是 fputc 函数(串口可自行更改):
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> #include "stm32xxx_hal.h" extern UART_HandleTypeDef huart1;int fputc (int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1 , HAL_MAX_DELAY); return ch; }
使用标准库 使用标准库需要定义的内容较多,但都是固定的。可以参考正点原子的教程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <stdio.h> #include "stm32f0xx_hal.h" extern UART_HandleTypeDef huart1;int fputc (int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1 , HAL_MAX_DELAY); return ch; } int fgetc (FILE *f) { uint8_t ch; HAL_UART_Receive(&huart1, &ch, 1 , HAL_MAX_DELAY); return ch; } #pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; FILE __stdin; void _sys_exit(int x) { x = x; }
GCC (Newlib) 实现方法 GCC 的实现流程如下:printf() → 格式化 → _write() → __io_putchar() → UART 硬件。通过 _write() 系统调用实现输出。
直接实现 _write 1 2 3 4 5 6 7 8 9 10 #include <unistd.h> #include "stm32xxx_hal.h" extern UART_HandleTypeDef huart1;int _write(int file, char *ptr, int len){ HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY); return len; }
使用 syscalls.c 这是 STM32CubeIDE 和 System Workbench 的标准做法。由于这个文件是自动生成的,这里不再赘述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 #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> extern int errno;extern int __io_putchar(int ch) __attribute__((weak));extern int __io_getchar(void ) __attribute__((weak));register char * stack_ptr asm ("sp" ) ;char *__env[1 ] = { 0 };char **environ = __env;void initialise_monitor_handles () { } int _getpid(void ){ return 1 ; } int _kill(int pid, int sig){ errno = EINVAL; return -1 ; } void _exit (int status){ _kill(status, -1 ); while (1 ) {} } __attribute__((weak)) int _read(int file, char *ptr, int len) { int DataIdx; for (DataIdx = 0 ; DataIdx < len; DataIdx++) { *ptr++ = __io_getchar(); } return len; } __attribute__((weak)) int _write(int file, char *ptr, int len) { int DataIdx; for (DataIdx = 0 ; DataIdx < len; DataIdx++) { __io_putchar(*ptr++); } return len; } caddr_t _sbrk(int incr){ extern char end asm ("end" ) ; static char *heap_end; char *prev_heap_end; if (heap_end == 0 ) heap_end = &end; prev_heap_end = heap_end; if (heap_end + incr > stack_ptr) { errno = ENOMEM; return (caddr_t ) -1 ; } heap_end += incr; return (caddr_t ) prev_heap_end; } int _close(int file){ return -1 ; } int _fstat(int file, struct stat *st){ st->st_mode = S_IFCHR; return 0 ; } int _isatty(int file){ return 1 ; } int _lseek(int file, int ptr, int dir){ return 0 ; } int _open(char *path, int flags, ...){ return -1 ; } int _wait(int *status){ errno = ECHILD; return -1 ; } int _unlink(char *name){ errno = ENOENT; return -1 ; } int _times(struct tms *buf){ return -1 ; } int _stat(char *file, struct stat *st){ st->st_mode = S_IFCHR; return 0 ; } int _link(char *old, char *new){ errno = EMLINK; return -1 ; } int _fork(void ){ errno = EAGAIN; return -1 ; } int _execve(char *name, char **argv, char **env){ errno = ENOMEM; return -1 ; }
附录 名词解释 本文涉及的一些专业术语解释如下。
半主机模式(Semihosting)
半主机模式是让单片机通过调试器(比如 ST-Link、J-Link)借用电脑的功能(比如在电脑屏幕上打印信息、读写电脑的文件)。当程序需要脱离调试器独立运行时(比如烧录后拔掉调试器),必须禁用半主机模式,否则在运行 printf 时,程序会一直等待调试器,导致卡死。
arm-none-eabi-gcc
这是嵌入式 ARM 版本的 GCC。none 表示没有操作系统(裸机运行),eabi 是 Embedded Application Binary Interface(嵌入式应用程序二进制接口)的缩写。
Newlib
一个专为嵌入式系统设计的 C 标准库,适用于 GCC 工具链(STM32CubeIDE、PlatformIO 等)。syscalls.c 就是为 Newlib 准备的底层接口文件。
AC5 和 AC6
Keil MDK 使用的两代编译器:
项目
AC5
AC6
全名
ARM Compiler 5
ARM Compiler 6
核心
ARMCC
ARMCLANG(基于 LLVM)
年代
老版本(已停止更新)
新版本(推荐使用)
C 库
ARM C Library
ARM C Library
两者都不使用 Newlib,所以都不需要 syscalls.c。
glibc
Linux 电脑上使用的标准 C 库(体积较大,嵌入式系统不适用)。
总结对比
项目
Keil
GCC
函数名
fputc()
_write() / __io_putchar()
syscalls.c
不需要
推荐使用
半主机模式处理
#pragma 禁用
syscalls.c 已处理