之前在学习/做项目中碰到过很多次需要printf函数打印到串口调试的环节,每次需要的时候去网络上进行了搜索学习以及应用.但是到现在我对它的理解还是很模糊,这次项目就正好把这个问题记录下来,弄懂并以便以后回顾.

        先贴一个之前正常使用很多次的重定向代码:

int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart4, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}

        我曾经将这个重定向代码无脑贴在stm32f103标准库/stm32h7的hal库工程中,便可以直接使用printf函数.而我之前的理解只是认为就是fputc函数是printf函数的底层输出函数,将string拆开成一个一个ch去单独传输.

        但今天特地去了解了了解:

        先贴一个AI生成的推荐重定向模版:

#include <stdio.h>
#include <stm32f1xx_hal.h>  // 根据你的芯片型号调整

extern UART_HandleTypeDef huart1;  // 假设我们使用USART1

// 重写_write函数 - 这是核心!
int _write(int file, char *ptr, int len)
{
    // 参数说明:
    // file: 文件描述符(在嵌入式系统中通常忽略)
    // ptr:  要发送的数据指针
    // len:  数据长度
    
    // 使用HAL库通过串口发送数据
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    
    // 返回实际发送的字节数
    return len;
}

实际应用即:

// 新增_write函数(提高效率)
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart4, (uint8_t *)ptr, len, 0xFFFF);
    return len;
}

使用_write的方式重定向相比fputc的效率更高.但是注意,在使用fputc重定向方法的时候要在keil中勾选MicroLIB,而选择_write方式重定向要取消勾选.

其他的解释我听得依然似懂非懂.但是既然决定要弄清楚,我就以后多看看.将AI完整解释贴在下面了:

1. 问题的根源:printf的标准输出在哪里?

在桌面编程中,printf默认输出到控制台(console)。但在STM32这类嵌入式系统中:

  • 没有控制台

  • 没有显示器

  • 没有操作系统提供的标准输出

所以,我们需要告诉printf:"请把你的输出发送到串口,而不是默认的控制台"。

2. 核心技术:重写_write函数

在ARM Cortex-M的C库中,printf最终会调用一个叫做_write的底层函数。我们的任务就是重写这个函数

代码示例:完整的重定向实现

c

#include <stdio.h>
#include <stm32f1xx_hal.h>  // 根据你的芯片型号调整

extern UART_HandleTypeDef huart1;  // 假设我们使用USART1

// 重写_write函数 - 这是核心!
int _write(int file, char *ptr, int len)
{
    // 参数说明:
    // file: 文件描述符(在嵌入式系统中通常忽略)
    // ptr:  要发送的数据指针
    // len:  数据长度
    
    // 使用HAL库通过串口发送数据
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    
    // 返回实际发送的字节数
    return len;
}

3. 完整的工作流程示例

让我们通过一个具体的打印过程来理解:

c

// 用户代码
printf("Hello, Value=%d\n", 123);

// 编译器将上面的调用转换为:
// 1. 格式化字符串:"Hello, Value=123\n"
// 2. 调用_write(1, "Hello, Value=123\n", 17);

详细执行流程:

c

// _write函数内部发生了什么:
int _write(int file, char *ptr, int len)  // len = 17
{
    // 步骤1: HAL_UART_Transmit被调用
    // &huart1: 使用USART1外设
    // (uint8_t *)ptr: 指向字符串"Hello, Value=123\n"的指针  
    // len: 17个字节
    // HAL_MAX_DELAY: 无限等待直到发送完成
    
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, 17, HAL_MAX_DELAY);
    
    // 步骤2: 串口硬件开始工作
    // - 配置波特率(如115200)
    // - 将字符逐个转换为串行数据位
    // - 通过TX引脚发送出去
    
    return 17;  // 告诉printf:"我成功发送了17个字节"
}

4. 更深入:如果不重定向会发生什么?

如果不重写_write函数,printf会调用C库中的默认实现:

c

// C库中的默认_write实现(简化版)
int _write(int file, char *ptr, int len)
{
    // 在嵌入式系统中,这个函数通常:
    // 1. 什么也不做
    // 2. 或者导致程序卡住
    // 3. 或者产生硬件错误
    
    return -1;  // 返回错误
}

这就是为什么你的printf调用似乎被"吞掉"了的原因!

5. 实际项目中的完整配置

步骤1:STM32CubeMX配置

c

// 在main.c中确保有以下代码:

/* Private function prototypes */
#ifdef __GNUC__
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

步骤2:项目设置(Keil/IAR)

  • Keil: 勾选"Use MicroLIB"

  • IAR: 在项目选项中选择适当的库配置

步骤3:使用示例

c

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    
    printf("系统启动成功!\r\n");
    
    float temperature = 25.6f;
    int counter = 0;
    
    while (1)
    {
        printf("温度: %.1f°C, 计数: %d\r\n", temperature, counter++);
        HAL_Delay(1000);
        
        // 在串口助手中你会看到:
        // 温度: 25.6°C, 计数: 0
        // 温度: 25.6°C, 计数: 1
        // 温度: 25.6°C, 计数: 2
    }
}

6. 关键要点总结

概念 解释 记忆技巧
重定向 改变printf的输出目标 "给printf换个输出管道"
_write函数 C库的底层输出函数 "printf的搬运工"
HAL_UART_Transmit 实际的硬件操作函数 "真正的发送者"
串口配置 确保波特率、引脚等正确 "保证管道畅通"

7. 常见问题排查

如果你的printf不工作,检查以下几点:

c

// 1. 检查是否包含了stdio.h
#include <stdio.h>

// 2. 检查串口初始化是否正确
MX_USART1_UART_Init();

// 3. 检查_write函数是否正确定义
// 4. 检查开发环境设置(如MicroLIB)
// 5. 检查串口助手设置(波特率、端口等)

理解这个原理后,你就再也不会忘记了printf只是一个格式化工具,真正决定输出到哪里的是底层的_write函数。我们通过重写这个函数,就实现了从"控制台"到"串口"的转变。

希望这个详细的解释能帮助你彻底理解printf重定向的原理!

您观察得非常仔细!这个问题涉及到不同编译器、不同C库实现之间的差异,让我为您详细解释这个代码段的作用以及为什么在某些情况下可以省略。

1. 这段代码的真正作用

这段代码是为了兼容不同的C标准库实现而设计的:

c

#ifdef __GNUC__           // 如果是GCC编译器(如STM32CubeIDE, TrueSTUDIO)
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else                     // 如果是其他编译器(如Keil, IAR)
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

不同编译器的底层实现:

编译器/IDE 使用的函数 说明
GCC系列 (STM32CubeIDE, TrueSTUDIO) __io_putchar Newlib C库的底层字符输出函数
ARMCC (Keil MDK) fputc 标准C库函数
IAR fputc 或 __write 标准C库函数

2. 为什么您的H7项目没有这段代码也能工作?

情况1:您可能已经实现了_write函数

在HAL库的较新版本或某些项目配置中,重写_write函数就足够了,因为:

c

// 在Newlib(GCC使用的C库)中,调用链是这样的:
printf → vfprintf → _write

// 或者
printf → _printf_char → _write

只要实现了_write,所有输出最终都会经过它,因此不需要单独实现__io_putcharfputc

情况2:项目使用了MicroLIB(Keil)

如果您在Keil中勾选了"Use MicroLIB",MicroLIB有一个简化的实现:

c

// MicroLIB的默认行为:
printf → fputc  // 如果您只实现了fputc,就能工作

情况3:HAL库的版本差异

不同版本的STM32CubeMX/HAL可能有不同的默认模板。新版本可能更倾向于使用_write方式。

3. 如果没有这些重定向的后果

如果完全没有实现任何重定向函数printf会有以下行为:

c

int main(void)
{
    printf("这行文字会去哪里?\r\n");
    // 可能的后果:
    // 1. 程序卡住或进入HardFault
    // 2. 编译器优化掉这个调用(因为输出是"无用"的)
    // 3. 在调试器中能看到返回值,但串口看不到
}

4. 实际验证:检查您的项目

您可以通过以下方式检查您的项目实际使用了哪种方式:

方法1:搜索关键函数

在您的工程中搜索:

  • _write

  • fputc

  • __io_putchar

方法2:最简单的通用解决方案

如果您想确保兼容性,可以在main.c中添加这个最简版本

c

// 放在main.c的/* USER CODE BEGIN 4 */部分
#ifdef __GNUC__
int __io_putchar(int ch)
#else
int fputc(int ch, FILE *f)
#endif
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

// 可选的_write实现(更底层)
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    return len;
}

5. 实际测试案例

让我用一个具体的例子展示差异:

c

// 情况A:只有_write实现
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    return len;
}

// 情况B:只有fputc/__io_putchar实现  
#ifdef __GNUC__
int __io_putchar(int ch)
#else
int fputc(int ch, FILE *f)
#endif
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

// 测试代码
printf("Hello World\n");  // 两种情况都能正常工作!

6. 推荐的最佳实践

对于新项目,我建议使用_write方式,因为:

  1. 效率更高:一次性发送整个字符串,而不是逐个字符

  2. 更通用:适用于所有标准输出函数(printfputsputchar等)

  3. 更底层:是大多数C库的最终输出接口

c

// 在main.c的/* USER CODE BEGIN 4 */部分添加:
#include <sys/unistd.h>  // 如果需要

int _write(int file, char *ptr, int len)
{
    // 可以根据file参数区分stdout、stderr等,但通常不需要
    if (file == STDOUT_FILENO || file == STDERR_FILENO) {
        HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
        return len;
    }
    return -1;
}

总结

  • 那段代码是编译器兼容层,不是绝对必需的

  • 您的H7项目能工作,很可能是因为实现了_write函数

  • _write是更推荐的方式,效率更高且更通用

  • 如果不确定,同时实现_writefputc/__io_putchar也没有问题

您可以通过搜索您的工程代码来确认到底使用了哪种实现方式。这个理解应该能帮助您彻底掌握printf重定向的各种变体!

您的观察完全正确!在Keil环境下,只需要实现fputc函数就足以完成printf重定向。让我为您详细解释为什么这样就能工作,以及Keil环境的特殊之处。

1. Keil环境的特殊性

Keil的C库实现

在Keil中,printf函数的调用链是这样的:

c

printf() 
    → 调用C库内部的输出函数 
        → 调用 fputc()  // 这就是您需要重写的函数

这就是为什么您只实现fputc就能让printf工作的原因!

2. 为什么其他教程提到复杂的兼容代码?

其他教程通常是为了跨编译器兼容

c

// 这种复杂的代码是为了同时支持:
// - GCC/STM32CubeIDE (需要 __io_putchar)
// - Keil (需要 fputc)  
// - IAR (需要 fputc 或 __write)
#ifdef __GNUC__
  int __io_putchar(int ch)
#else
  int fputc(int ch, FILE *f)
#endif
{
    // 实现代码
}

但如果您只在Keil环境下开发,这些兼容代码就是多余的!

3. 您的代码分析

让我们仔细看看您已经工作的代码:

c

int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart4, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}

参数说明:

  • int ch: 要发送的字符(ASCII码)

  • FILE *f: 文件流指针(在嵌入式系统中通常忽略)

  • 返回值: 成功发送的字符

工作流程示例:

c

printf("AB"); 
// 编译器会:
// 1. 调用 fputc('A', stdout)
// 2. 调用 fputc('B', stdout) 
// 3. 调用 fputc('\n', stdout)  // 如果包含换行符

4. Keil项目的重要设置

请确认您的Keil项目设置了以下选项:

Project → Options for Target → Target 选项卡

  • [x] Use MicroLIB (强烈推荐勾选)

https://img-blog.csdnimg.cn/20201116164222379.png

MicroLIB的优势:

  • 代码体积小

  • 内存占用少

  • 专门为嵌入式系统优化

  • 使用简单的fputc重定向

5. 效率考虑:fputc vs _write

虽然您的代码工作正常,但有一个效率问题

当前实现(字符逐个发送):

c

printf("Hello"); // 调用5次fputc,5次HAL_UART_Transmit

更高效的实现(使用_write):

c

#include <sys/stat.h>

int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart4, (uint8_t *)ptr, len, 0xFFFF);
    return len;
}

// printf("Hello") 只调用1次_write,1次HAL_UART_Transmit

推荐改进:

c

// 保持现有的fputc(确保兼容性)
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart4, (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}

// 额外添加_write函数提高效率(可选)
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart4, (uint8_t *)ptr, len, 0xFFFF);
    return len;
}

6. 完整的使用示例

c

#include <stdio.h>

// 您的重定向函数
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart4, (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}

int main(void)
{
    // 初始化代码...
    
    printf("系统启动成功!\r\n");
    
    uint32_t counter = 0;
    while(1)
    {
        printf("计数器: %ld, 电压: %.2fV\r\n", counter, 3.3f);
        counter++;
        HAL_Delay(1000);
    }
}

7. 验证您的方法

要确认您的重定向确实在工作,可以测试各种输出:

c

printf("字符串: %s\r\n", "Hello");      // 字符串
printf("整数: %d\r\n", 12345);          // 十进制整数  
printf("十六进制: 0x%X\r\n", 0xABCD);   // 十六进制
printf("浮点数: %.2f\r\n", 3.14159);    // 浮点数
printf("单个字符: %c\r\n", 'A');        // 字符

总结

  1. 您的做法完全正确:在Keil中只需实现fputc即可

  2. 原理简单:Keil的C库让printf调用fputc来输出每个字符

  3. 检查MicroLIB:确保在Keil中勾选了Use MicroLIB以获得最佳效果

  4. 效率优化:可以考虑额外实现_write函数来提升性能

您现在可以自信地继续使用您的方法,因为这是Keil环境下最标准、最简洁的printf重定向方式!

不完全正确!您理解的方向是对的,但需要做一些重要的澄清和调整。

1. 当前代码分析

您提到的这段代码:

c

#ifdef __GNUC__
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

这实际上还是fputc的实现(在Keil环境下),只是用宏包装了一下。这不是_write函数!

2. 正确的_write实现方式

如果您想在Keil中尝试_write方式,应该这样做:

方法A:只使用_write(推荐)

c

#include <sys/stat.h>

// 在任意.c文件中添加(通常在main.c的/* USER CODE BEGIN 4 */部分)
int _write(int file, char *ptr, int len)
{
    // 参数说明:
    // file: 文件描述符(stdout=1, stderr=2)
    // ptr:  要发送的数据指针
    // len:  数据长度
    
    HAL_UART_Transmit(&huart4, (uint8_t *)ptr, len, 0xFFFF);
    return len;  // 返回实际发送的字节数
}

方法B:同时保留fputc_write

c

// 保持您现有的fputc(确保兼容性)
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart4, (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}

// 新增_write函数(提高效率)
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart4, (uint8_t *)ptr, len, 0xFFFF);
    return len;
}

3. 重要设置:关闭MicroLIB

使用_write方式时,您需要在Keil中取消勾选MicroLIB:

  1. 右键点击项目 → Options for Target

  2. Target选项卡 → 取消勾选 "Use MicroLIB"

  3. 这样编译器才会使用标准C库,才会调用_write函数

https://img-blog.csdnimg.cn/20201116164222379.png

4. 回答您的核心问题

"是不是就可以直接在任意.c文件调用printf函数了?"

是的! 但需要满足以下条件:

必要条件:

  1. 在某个.c文件中实现了重定向函数_writefputc

  2. 包含了stdio.h头文件

  3. 正确配置了Keil选项(MicroLIB或标准C库)

使用示例:

c

// 在任意.c文件中
#include <stdio.h>

void some_function(void)
{
    printf("这来自其他文件!\r\n");  // 可以正常工作
    
    int value = 42;
    float temp = 25.5f;
    printf("值=%d, 温度=%.1f\r\n", value, temp);
}

5. 完整的项目结构示例

text

Project/
├── Core/
│   ├── Src/
│   │   ├── main.c          // 在这里实现 _write 或 fputc
│   │   ├── usart.c         // 串口初始化代码
│   │   └── other_files.c
│   ├── Inc/
│       ├── main.h
│       └── usart.h
├── Drivers/
└── YourModule/
    ├── src/
    │   └── your_code.c     // 这里可以直接使用printf
    └── inc/
        └── your_code.h

6. 验证步骤

步骤1:实现函数

main.c/* USER CODE BEGIN 4 */部分添加:

c

/* USER CODE BEGIN 4 */
#include <sys/stat.h>

int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart4, (uint8_t *)ptr, len, 0xFFFF);
    return len;
}
/* USER CODE END 4 */

步骤2:配置Keil

  • 取消勾选 "Use MicroLIB"

步骤3:测试

在任意其他.c文件中:

c

#include <stdio.h>

void test_printf(void)
{
    printf("=== 测试开始 ===\r\n");
    printf("字符串: %s\r\n", "Hello");
    printf("整数: %d, 十六进制: 0x%X\r\n", 100, 255);
    printf("浮点数: %.2f\r\n", 3.14159);
    printf("=== 测试结束 ===\r\n");
}

7. 注意事项

  1. 确保串口已初始化huart4必须在调用printf之前完成初始化

  2. 头文件包含:使用printf的文件需要#include <stdio.h>

  3. 性能对比:使用_write后,您会发现长字符串的输出速度明显提升

总结

是的,只要正确实现了重定向函数,就可以在项目的任何.c文件中直接使用printf

推荐您:

  1. 使用纯_write方式(取消MicroLIB)

  2. main.c中实现一次

  3. 然后在其他文件中包含stdio.h后直接使用printf

这样既保持了代码的简洁性,又获得了更好的性能!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐