一、 printf 和 fputc 的 关系

printffputc 的关联是通过 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 的实现即可适配不同硬件。

总结

printffputc 的关联是 “上层格式化”与“底层输出”的分工合作
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)接收并显示。

关键注意事项

  1. 串口必须先初始化:重定向前需确保串口硬件配置正确(波特率、校验位等需与电脑端一致),否则fputc调用串口发送函数会失败。
  2. 编译器兼容性:不同编译器(如Keil、IAR、GCC)对fputc重定向的要求可能不同。例如,Keil可能需要勾选“Use Micro LIB”才能支持重定向;GCC可能需要额外链接标准库。
  3. 缓冲问题printf默认有缓冲机制(数据积累到一定量才发送),可能导致输出不及时。可通过setbuf(stdout, NULL);关闭缓冲,让字符立即发送。

总结:重定向printf到串口的本质是通过重写fputc函数,将printf生成的字符流导向串口硬件,从而让单片机可以用熟悉的printf语法实现串口输出。

三、printf`函数使用方法

学习C语言中printf函数的使用方法,包括它的基本语法、格式控制符、常见用法和进阶技巧,以便能灵活输出各种类型的数据。下面我会从基础到进阶,一步步讲解printf的完整使用教程。

一、printf 基础认知

printf是C语言标准库<stdio.h>中的格式化输出函数(全称 print formatted),核心作用是将指定格式的数据输出到标准输出设备(通常是屏幕)。

前置条件

使用printf必须包含头文件,否则编译器会报错:

#include <stdio.h>
基本语法
printf("格式控制字符串", 参数列表);
  • 格式控制字符串:包含两部分内容
    1. 普通字符:会直接原样输出(比如文字、空格);
    2. 格式说明符:以%开头,用于占位,对应后面的参数(比如%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语言"
百分号:%

四、常见注意事项(避坑)

  1. 类型匹配:格式符和参数类型必须一致(比如%d对应int,不能用%d输出float),否则会输出乱码;
  2. 数量匹配:格式符数量要和参数数量一致,少了会读取内存随机值,多了会忽略多余参数;
  3. 字符串输出%s要求参数是字符串首地址(如字符数组名),不能直接传单个字符(比如printf("%s", 'A')会报错)。

总结

  1. 使用printf必须先包含<stdio.h>头文件,核心语法是printf("格式串", 参数列表)
  2. 关键是格式控制符的匹配:%d(整数)、%f(浮点数)、%c(字符)、%s(字符串)是最常用的,需注意类型和数量一致;
  3. 可通过修饰符(如%.2f%5d)控制输出的精度、宽度和对齐方式,满足个性化输出需求。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐