在大家使用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數據的末尾不加入\n或fflush(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函數相同,但是他們各有各的實現方式,他們在底層實現是可能完全不同的樣子。所以在我們更換工具鏈後,一定要注意自己工程中的代碼不一定會適應新的工具鏈開發環境。