STM32高级开发(12)-在GCC中使用printf打印串口数据

本文介绍如何在Keil和GNU环境下为STM32的printf函数重映射输出流到串口,实现调试信息的串口输出,并解释了不同工具链下标准C库的实现差异。

在大家使用keil或是iar开发stm32等arm芯片的时候,想来最不陌生的就是使用print通过串口输出一些数据,用来调试或是其他作用。但是要明确的是由于keil iar gcc 他们使用的标准C语言库虽然都遵循一个标准,但他们底层的函数实现方式都是不同的,那么在GCC中我们能否像在keil中一样重映射print的输出流到串口上呢?答案是肯定的。


keil中的重映射方式及原理

/* 
 * libc_printf.c 
 * 
 *  Created on: Dec 26, 2015 
 *      Author: Yang 
 * 
 *      使用标准C库时,重映射printf等输出函数的文件 
 *    添加在工程内即可生效(切勿选择semihost功能) 
 */  

#include <stdio.h>  
//include "stm32f10x.h"    


#pragma import(__use_no_semihosting)               
//标准库需要的支持函数                   
struct __FILE  
{  
    int handle;  

};  
FILE __stdout;  

//定义_sys_exit()以避免使用半主机模式      
_sys_exit(int x)  
{  
    x = x;  
}  

//重映射fputc函数,此函数为多个输出函数的基础函数  
int fputc(int ch, FILE *f)  
{  
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);  
    USART_SendData(USART1, (uint8_t) ch);  
    return ch;  
}  

在keil中的C库中,printf、scanf等输入输出流函数是通过fputc、fgetc来实现最底层操作的,所以我们只需要在我们的工程中重定义这两个函数的功能就可以实现printf、scanf等流函数的重映射。


GNU下的C流函数重映射方式

我们来看看前几篇中提供的样例工程中的usart_stdio例程中的代码片段:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

/*
 * To implement the STDIO functions you need to create
 * the _read and _write functions and hook them to the
 * USART you are using. This example also has a buffered
 * read function for basic line editing.
 */
int _write(int fd, char *ptr, int len);
int _read(int fd, char *ptr, int len);
void get_buffered_line(void);

/*
 * This is a pretty classic ring buffer for characters
 */
#define BUFLEN 127

static uint16_t start_ndx;
static uint16_t end_ndx;
static char buf[BUFLEN + 1];
#define buf_len ((end_ndx - start_ndx) % BUFLEN)
static inline int inc_ndx(int n) { return ((n + 1) % BUFLEN); }
static inline int dec_ndx(int n) { return (((n + BUFLEN) - 1) % BUFLEN); }

/* back up the cursor one space */
static inline void back_up(void)
{
    end_ndx = dec_ndx(end_ndx);
    usart_send_blocking(USART1, '\010');
    usart_send_blocking(USART1, ' ');
    usart_send_blocking(USART1, '\010');
}

/*
 * A buffered line editing function.
 */
void get_buffered_line(void)
{
    char c;

    if (start_ndx != end_ndx)
    {
        return;
    }

    while (1)
    {
        c = usart_recv_blocking(USART1);

        if (c == '\r')
        {
            buf[end_ndx] = '\n';
            end_ndx = inc_ndx(end_ndx);
            buf[end_ndx] = '\0';
            usart_send_blocking(USART1, '\r');
            usart_send_blocking(USART1, '\n');
            return;
        }

        /* or DEL erase a character */
        if ((c == '\010') || (c == '\177'))
        {
            if (buf_len == 0)
            {
                usart_send_blocking(USART1, '\a');
            }

            else
            {
                back_up();
            }

            /* erases a word */
        }

        else if (c == 0x17)
        {
            while ((buf_len > 0) &&
                    (!(isspace((int) buf[end_ndx]))))
            {
                back_up();
            }

            /* erases the line */
        }

        else if (c == 0x15)
        {
            while (buf_len > 0)
            {
                back_up();
            }

            /* Non-editing character so insert it */
        }

        else
        {
            if (buf_len == (BUFLEN - 1))
            {
                usart_send_blocking(USART1, '\a');
            }

            else
            {
                buf[end_ndx] = c;
                end_ndx = inc_ndx(end_ndx);
                usart_send_blocking(USART1, c);
            }
        }
    }
}

/*
 * Called by libc stdio fwrite functions
 */
int _write(int fd, char *ptr, int len)
{
    int i = 0;

    /*
     * write "len" of char from "ptr" to file id "fd"
     * Return number of char written.
     *
    * Only work for STDOUT, STDIN, and STDERR
     */
    if (fd > 2)
    {
        return -1;
    }

    while (*ptr && (i < len))
    {
        usart_send_blocking(USART1, *ptr);

        if (*ptr == '\n')
        {
            usart_send_blocking(USART1, '\r');
        }

        i++;
        ptr++;
    }

    return i;
}

/*
 * Called by the libc stdio fread fucntions
 *
 * Implements a buffered read with line editing.
 */
int _read(int fd, char *ptr, int len)
{
    int my_len;

    if (fd > 2)
    {
        return -1;
    }

    get_buffered_line();
    my_len = 0;

    while ((buf_len > 0) && (len > 0))
    {
        *ptr++ = buf[start_ndx];
        start_ndx = inc_ndx(start_ndx);
        my_len++;
        len--;
    }

    return my_len; /* return the length we got */
}

这个文件因为实现了scanf的功能同时还带有在串口上终端回显并支持backspace键所以显得有些长,我们来将其中的实现printf重映射的片段取出:

#include <stdio.h>
#include <stdlib.h>

int _write(int fd, char *ptr, int len)
{
    int i = 0;

    /*
     * write "len" of char from "ptr" to file id "fd"
     * Return number of char written.
     *
    * Only work for STDOUT, STDIN, and STDERR
     */
    if (fd > 2)
    {
        return -1;
    }

    while (*ptr && (i < len))
    {
        usart_send_blocking(USART1, *ptr);

        if (*ptr == '\n')
        {
            usart_send_blocking(USART1, '\r');
        }

        i++;
        ptr++;
    }

    return i;
}

与keil C库类似GNU C库下的流函数底层是通过_read、_write函数实现的,我们只要在工程中将他们重新定义就可以实现重映射的功能了。


补充

差点忘了最重要的。我们在使用GNU的printf时,一定要记住在发送的内容后添加 \n或者在printf后使用fflush(stdout),来立即刷新输出流。否则printf不会输出任何数据,而且会被后来的正确发送的printf数据覆盖。这是由于printf的数据流在扫描到 \n以前会被保存在缓存中,直到 \n出现或是fflush(stdout)强制刷新才会输出数据,如果我们在printf数据的末尾不加入\nfflush(stdout),这个printf数据就不会被发送出去,而且在新的printf语句也会重写printf的缓存内容,使得新的printf语句不会附带之前的内容一起输出,从而造成上一条错误的printf内容不显示且丢失。

/*methord1*/
printf("Enter the delay(ms) constant for blink : ");
fflush(stdout);

/*methord2*/
printf("Error: expected a delay > 0\n");

总结

这里需要大家明白的是,GNU C 与 KEIL C 下的标准库函数实际上都是各个不同的机构组织编写的,虽然他们符合不同时期的C标准,如C89、C99等,那也只是用户层的API相同(同时要明白他们这些标准库是属于编译器的一部分的,就储存在编译器路径下的lib文件夹中)。虽然上层被调用的标准C函数相同,但是他们各有各的实现方式,他们在底层实现是可能完全不同的样子。所以在我们更换工具链后,一定要注意自己工程中的代码不一定会适应新的工具链开发环境。

补充

STM32F030上使用`printf`通过串口打印数据,需完成以下关键步骤: 1. **重定向`printf`到串口**(通过实现`_write`或`fputc`函数)。 2. **初始化USART外设**(时钟、GPIO、波特率等)。 3. **启用微库(MicroLIB)或标准库支持**(确保`printf`支持浮点数)。 --- ### **完整代码示例** #### **1. 硬件初始化与`printf`重定向** ```c #include "stm32f0xx.h" #include <stdio.h> // 必须包含此头文件 // 初始化USART1(PA9-TX, PA10-RX) void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; USART_InitTypeDef USART_InitStruct; // 1. 启用时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 2. 配置GPIO(PA9-TX) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_Level_1; GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. 配置复用功能(USART1_TX) GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_1); // 4. 配置USART参数 USART_InitStruct.USART_BaudRate = 9600; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode = USART_Mode_Tx; USART_Init(USART1, &USART_InitStruct); // 5. 启用USART USART_Cmd(USART1, ENABLE); } // 重定向printf串口(方法1:使用_write) int _write(int fd, char *ptr, int len) { for (int i = 0; i < len; i++) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)*ptr++); } return len; } // 或重定向printf串口(方法2:使用fputc) // int fputc(int ch, FILE *f) { // while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // USART_SendData(USART1, (uint8_t)ch); // return ch; // } ``` #### **2. 主程序(打印变量)** ```c int main(void) { USART1_Init(); // 初始化串口 int counter = 0; float voltage = 3.3; char *message = "Hello STM32F030!"; while (1) { printf("Counter: %d, Voltage: %.2fV, Message: %s\r\n", counter, voltage, message); counter++; voltage -= 0.1; for (volatile int i = 0; i < 1000000; i++); // 简单延时 } } ``` --- ### **关键配置说明** 1. **重定向`printf`** - **方法1**:实现`_write`函数(适用于Newlib库)。 - **方法2**:实现`fputc`函数(需启用`Use MicroLIB`选项)。 2. **浮点数支持** - 在Keil中: 1. 勾选 `Options for Target → Target → Use MicroLIB`。 2. 在 `Options for Target → C/C++` 中添加 `--specs=nano.specs --specs=nosys.specs`(若使用ARM GCC)。 -GCC中:编译时添加 `-u _printf_float` 选项。 3. **波特率匹配** - 确保USART的波特率(如9600)与终端工具(如Putty、Tera Term)一致。 --- ### **常见问题解决** 1. **无输出** - 检查是否调用了`USART1_Init()`。 - 确认GPIO引脚(PA9)和复用功能(AF1)配置正确。 - 验证时钟是否启用(`RCC_APB2Periph_USART1`)。 2. **乱码** - 波特率不匹配。 - 时钟源配置错误(默认使用HSI 8MHz,若需更高精度,可配置HSE)。 3. **浮点数不显示** - 未启用MicroLIB或未链接浮点格式化库。 - 尝试用`%.2f`强制输出浮点数(避免编译器优化)。 4. **代码体积过大** - 改用简化函数(如`itoa` + 手动发送)替代`printf`。 - 在Keil中勾选 `Options for Target → C/C++ → Optimization → Level -O1`。 --- ### **优化建议** 1. **使用DMA发送** - 提高连续数据发送效率(参考`USART_DMACmd`)。 2. **中断方式接收** - 通过`USART_ITConfig`启用接收中断,避免轮询。 3. **低功耗设计** - 在空闲时关闭USART时钟(`RCC_APB2PeriphClockCmd`)。 --- ### **相关问题** 1. 如何启用STM32F030的硬件浮点单元(FPU)? 2. 在Keil中如何配置`printf`以支持十六进制输出(如`%04X`)? 3. 串口打印时如何避免数据覆盖(如使用环形缓冲区)? 4. 如何测量`printf`函数的执行时间? 5. 在多任务环境中(如FreeRTOS),如何安全使用`printf`?
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值