单片机开发笔记--- 如何理解重定向C库函数 printf ( )
一、 printf 和 fputc 的 关系
printf 与 fputc 的关联是通过 C标准库的底层设计 实现的,本质上是一种「分层调用」关系:printf 负责复杂的格式化处理,而 fputc 负责最基础的单个字符输出,二者通过标准库的内部逻辑绑定。
/* retarget the C library printf function to the USART */
#include <stdio.h>
int fputc(int ch, FILE *f)
{
while (RESET == usart_flag_get(USART0, USART_FLAG_TBE))
;
usart_data_transmit(USART0, (uint8_t)ch);
return ch;
}
具体关联过程可以拆解为3步:
1. printf 的核心功能是“格式化转换”
printf 的主要工作是解析格式化字符串(如 "%d, %.2f"),将传入的参数(整数、浮点数等)按照格式转换为对应的字符序列。
例如:printf("Num: %d", 123) 会先将整数 123 转换为字符序列 '1','2','3',再拼接成完整的字符串 Num: 123。
2. printf 依赖 fputc 输出字符
printf 本身不直接操作硬件,它生成字符序列后,会 逐个调用 fputc 函数,将每个字符发送出去。
简化的逻辑类似这样(伪代码):
// printf的内部工作流程(简化版)
int printf(const char *format, ...) {
// 1. 解析format和参数,生成字符流(存到临时缓冲区)
char buffer[1024];
format_to_buffer(buffer, format, ...); // 格式化转换
// 2. 逐个字符调用fputc输出
for (int i = 0; buffer[i] != '\0'; i++) {
fputc(buffer[i], stdout); // 调用fputc发送每个字符
}
return 已输出的字符数;
}
其中 stdout 是标准输出流(C库预定义的 FILE 类型对象),fputc 的第二个参数就是这个流对象。
3. fputc 是“设备无关层”的接口
fputc 是C标准库定义的 设备无关输出函数,它的作用是“将单个字符写入指定的流(FILE *f)”。
- 在PC端,
fputc默认的实现是将字符写入控制台(stdout对应的设备); - 在单片机中,我们重写
fputc后,它会将字符写入串口(或其他硬件)。
为什么要通过 fputc 关联?
这种设计体现了C语言的 “设备无关性” 思想:
printf只负责“生成要输出的内容”,不关心输出到哪里;fputc只负责“将字符输出到指定设备”,不关心内容来源;- 二者通过标准库的约定关联,使得同一个
printf函数可以在不同设备(PC、单片机、嵌入式系统)上工作,只需修改fputc的实现即可适配不同硬件。
总结
printf 与 fputc 的关联是 “上层格式化”与“底层输出”的分工合作:printf 负责复杂的格式化转换,生成字符序列;fputc 负责将这些字符逐个发送到具体设备;
C标准库通过内部逻辑将二者绑定,使得重写 fputc 就能改变 printf 的输出目标(如从控制台改为串口)。
二、printf 和 scanf 串口使用
//重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到串口 */
USART_SendData(DEBUG_USART, (uint8_t) ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(DEBUG_USART, USART_FLAG_TXE) == RESET);
return (ch);
}
//重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_GetFlagStatus(DEBUG_USART, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(DEBUG_USART);
}
要理解单片机中重定向printf到串口的原理,以及fputc函数的作用,我们可以从「C库函数工作机制」和「单片机硬件特性」两个角度拆解:
1. 先理解printf的底层依赖
printf是C标准库中的格式化输出函数,它的核心功能是将数据按照指定格式转换为字符流,但它本身并不负责“将字符发送到具体设备”。
printf的输出行为依赖于一个更底层的函数——fputc(file put character)。其工作流程是:printf将格式化后的字符逐个交给fputc,由fputc负责将单个字符发送到目标设备(如屏幕、文件、串口等)。
2. 单片机为什么需要“重定向”?
在PC端,fputc默认会将字符发送到“标准输出设备”(如控制台窗口);但单片机没有“控制台”,最常用的输出方式是串口(通过串口线连接到电脑,用串口助手查看数据)。
因此,我们需要修改fputc的行为,让它不再输出到默认设备,而是通过串口发送字符——这就是“重定向”。
3. fputc函数的作用与重写逻辑
fputc的函数原型是:
int fputc(int ch, FILE *f);
- 参数
ch:要发送的单个字符(虽然是int类型,但实际只用低8位表示字符ASCII码)。 - 参数
FILE *f:目标文件流(在单片机中通常忽略,因为我们只需要固定输出到串口)。 - 返回值:成功发送则返回该字符,失败返回EOF。
重定向的核心就是重新实现fputc函数,让它在内部调用串口的发送函数。例如,在STM32单片机中(使用HAL库),重写后的fputc可能是这样:
#include <stdio.h> // 包含printf和fputc的声明
// 重写fputc,将字符通过串口1发送
int fputc(int ch, FILE *f) {
// 调用HAL库的串口发送函数,发送单个字符ch
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 0xFFFF);
return ch; // 发送成功,返回字符
}
4. 重定向后如何使用printf?
完成fputc的重写后,直接调用printf即可实现串口输出。例如:
// 初始化串口(必须先配置好波特率、数据位等参数)
MX_USART1_UART_Init();
// 调用printf,内容会通过串口1发送
printf("Hello, UART! \r\n"); // \r\n是换行符,方便串口助手查看
printf("Num: %d, Float: %.2f \r\n", 123, 3.14f);
此时,单片机通过串口发送的数据,可在电脑端用串口助手(如SSCOM、XCOM)接收并显示。
关键注意事项
- 串口必须先初始化:重定向前需确保串口硬件配置正确(波特率、校验位等需与电脑端一致),否则
fputc调用串口发送函数会失败。 - 编译器兼容性:不同编译器(如Keil、IAR、GCC)对
fputc重定向的要求可能不同。例如,Keil可能需要勾选“Use Micro LIB”才能支持重定向;GCC可能需要额外链接标准库。 - 缓冲问题:
printf默认有缓冲机制(数据积累到一定量才发送),可能导致输出不及时。可通过setbuf(stdout, NULL);关闭缓冲,让字符立即发送。
总结:重定向printf到串口的本质是通过重写fputc函数,将printf生成的字符流导向串口硬件,从而让单片机可以用熟悉的printf语法实现串口输出。
三、printf`函数使用方法
学习C语言中printf函数的使用方法,包括它的基本语法、格式控制符、常见用法和进阶技巧,以便能灵活输出各种类型的数据。下面我会从基础到进阶,一步步讲解printf的完整使用教程。
一、printf 基础认知
printf是C语言标准库<stdio.h>中的格式化输出函数(全称 print formatted),核心作用是将指定格式的数据输出到标准输出设备(通常是屏幕)。
前置条件
使用printf前必须包含头文件,否则编译器会报错:
#include <stdio.h>
基本语法
printf("格式控制字符串", 参数列表);
- 格式控制字符串:包含两部分内容
- 普通字符:会直接原样输出(比如文字、空格);
- 格式说明符:以
%开头,用于占位,对应后面的参数(比如%d表示整数占位)。
- 参数列表:需要输出的变量/常量,数量、类型必须和格式说明符一一对应。
二、核心:常用格式控制符
格式控制符是printf的核心,不同类型的数据需要匹配对应的符号,常见的如下:
| 格式符 | 说明 | 适用类型 |
|---|---|---|
| %d | 十进制整数(int) | int |
| %f | 浮点数(默认保留6位小数) | float/double |
| %c | 单个字符 | char |
| %s | 字符串 | char数组/字符串常量 |
| %x/%X | 十六进制整数(小写/大写) | int |
| %o | 八进制整数 | int |
| %p | 指针地址 | 任意类型指针 |
| %% | 输出百分号本身 | 无 |
三、实战示例(从简单到复杂)
1. 无参数:仅输出普通文本
#include <stdio.h>
int main() {
// \n 是换行符,让输出后自动换行
printf("Hello, C语言!\n");
return 0;
}
输出结果:
Hello, C语言!
2. 输出单个整数/浮点数
#include <stdio.h>
int main() {
int age = 20;
float score = 95.5;
// %d 匹配int类型,%f 匹配float类型
printf("年龄:%d\n", age);
printf("成绩:%f\n", score); // 默认保留6位小数,输出95.500000
return 0;
}
输出结果:
年龄:20
成绩:95.500000
3. 输出字符和字符串
#include <stdio.h>
int main() {
char ch = 'A'; // 单个字符
char name[] = "小明"; // 字符串(字符数组)
printf("字符:%c\n", ch);
printf("姓名:%s\n", name);
return 0;
}
输出结果:
字符:A
姓名:小明
4. 输出多个参数
#include <stdio.h>
int main() {
int a = 10, b = 20;
// 多个格式符对应多个参数,顺序要一致
printf("a = %d, b = %d, a+b = %d\n", a, b, a+b);
return 0;
}
输出结果:
a = 10, b = 20, a+b = 30
5. 进阶:控制输出精度、宽度和对齐
通过格式符的修饰符,可以自定义输出样式(新手高频需求):
%.nf:保留n位小数(浮点数);%nd:占n个字符宽度,右对齐(默认);%-nd:占n个字符宽度,左对齐;
#include <stdio.h>
int main() {
int num = 123;
float pi = 3.1415926;
// 宽度5,右对齐(前面补空格)
printf("宽度5的整数:%5d\n", num); // 输出: 123(2个空格+123)
// 宽度5,左对齐(后面补空格)
printf("左对齐宽度5:%-5d\n", num); // 输出:123 (123+2个空格)
// 保留3位小数
printf("保留3位小数:%.3f\n", pi); // 输出:3.142(四舍五入)
// 总宽度8,保留2位小数,右对齐
printf("宽度8保留2位:%8.2f\n", pi); // 输出: 3.14(3个空格+3.14)
return 0;
}
输出结果:
宽度5的整数: 123
左对齐宽度5:123
保留3位小数:3.142
宽度8保留2位: 3.14
6. 转义字符的配合使用
printf常和转义字符搭配,实现特殊输出效果:
| 转义字符 | 说明 |
|---|---|
| \n | 换行 |
| \t | 制表符(Tab) |
| \ | 输出反斜杠 |
| " | 输出双引号 |
#include <stdio.h>
int main() {
printf("第一列\t第二列\n"); // 制表符分隔列
printf("反斜杠:\\\n"); // 输出:反斜杠:\
printf("双引号:\"C语言\"\n"); // 输出:双引号:"C语言"
printf("百分号:%%\n"); // 输出:百分号:%
return 0;
}
输出结果:
第一列 第二列
反斜杠:\
双引号:"C语言"
百分号:%
四、常见注意事项(避坑)
- 类型匹配:格式符和参数类型必须一致(比如
%d对应int,不能用%d输出float),否则会输出乱码; - 数量匹配:格式符数量要和参数数量一致,少了会读取内存随机值,多了会忽略多余参数;
- 字符串输出:
%s要求参数是字符串首地址(如字符数组名),不能直接传单个字符(比如printf("%s", 'A')会报错)。
总结
- 使用
printf必须先包含<stdio.h>头文件,核心语法是printf("格式串", 参数列表); - 关键是格式控制符的匹配:
%d(整数)、%f(浮点数)、%c(字符)、%s(字符串)是最常用的,需注意类型和数量一致; - 可通过修饰符(如
%.2f、%5d)控制输出的精度、宽度和对齐方式,满足个性化输出需求。
更多推荐


所有评论(0)