STM32自定义串口printf

前言

硬件的调试通常可分为两大类,一种是用调试器将芯片和电脑连接通过单步等方式逐步运行进行调试,另一种就是printf大法在程序运行过程中输出调试信息。第一种比较深入,可以了解每一个变量,栈堆,函数等等的变化是比较系统化的调试。但我通常都是用printf来调试的,因为快速简单属于非介入的方式,调试完毕直接注释掉相关部分就行了。


重定向 printf 函数

对于STM32来说现在串口例程一般都配有一个printf重定向到串口1的代码,直接可以使用printf很方便的输出信息到串口上。

1
2
3
4
5
6
7
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}

自定义printf函数

但是我们如果使用多个串口或者说串口输出不一定总从串口1输出怎么办呢。这时候这样就没法实现,后来上网查一下我们可以自己实现printf这个函数。命名为uprintf,接收至少两个参数,第1个参数是串口号;第2个是格式化字符串;第3-n ‘ … ‘ 是参数匹配列表, 实现变长的参数列表接收。。

1
2
3
4
5
6
7
8
9
10
11
//用串口写一个printf函数
void uprintf(USART_TypeDef *USARTx, const char *fmt, ...)
{
va_list ap; //typedef char *va_list; va_list是char型的指针
char *s_string = malloc(300); //申请缓冲区
va_start(ap, fmt); //找第一个可变形参的地址,并把地址赋给ap
vsprintf(s_string, fmt, ap); //类似sprintf函数
USART_String(USARTx, s_string); //发送和整个字符串
va_end(ap); //结束
free(s_string);
}

上面这个函数里的过程:

  1. 通过 va_start 函数获得参数列表里每个项和格式化字符里对应的位置,形成一个列表 ap 。
  2. 通过 vsprintf 把列表里每个参数转为字符串写到 s_string 字符串里。
  3. 通过 USART_String 发送到串口。
  4. 释放列表 ap 和 发送缓冲 s_string 。

完整的一个自定义printf实现文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "uart_x.h"
#include "stdlib.h"
#include "stdarg.h"
//发送一个字节
void SendByte(USART_TypeDef *USARTx, unsigned char dat)
{
while((USARTx->SR & 0X40) == 0) {}; //循环发送,直到发送完毕
USARTx->DR = (u8) dat;
}
//发送一个字符串
void USART_String(USART_TypeDef *USARTx, char *s)
{
while(*s != '\0') // '\0' 表示字符串结束标志
{
SendByte(USARTx, *s);
s++;
}
}
//用串口写一个printf函数
void uprintf(USART_TypeDef *USARTx, const char *fmt, ...)
{
va_list ap; //typedef char *va_list; va_list是char型的指针
char *s_string = malloc(300); //申请空间
va_start(ap, fmt); /找第一个可变形参的地址,并把地址赋给ap
vsprintf(s_string, fmt, ap); //类似sprintf函数
USART_String(USARTx, s_string); //发送和整个字符串
va_end(ap); //结束
free(s_string);
}

上面的函数已经能满足多个串口同时使用printf,根据自己的需求可以修改申请的缓冲区大小即可。

安全版本的自定义 printf 实现

但上面的实现并不是安全的,如果你发送的字符串长度大于缓冲区长度就会造成内存溢出。所以你可以更改为一个安全的版本,接收一个缓冲区长度参数从而申请一个合适大小的空间。

1
2
3
4
5
6
7
8
9
10
11
//用串口写一个printf函数
void uprintf_s(USART_TypeDef *USARTx, uint32_t BuffSize,const char *fmt, ...)
{
va_list ap; //typedef char *va_list; va_list是char型的指针
char *s_string = malloc(BuffSize); //申请空间
va_start(ap, fmt); //找第一个可变形参的地址,并把地址赋给ap
vsprintf_s(s_string,BuffSize, fmt, ap); //类似sprintf_s函数
USART_String(USARTx, s_string); //发送和整个字符串
va_end(ap); //结束
free(s_string);
}

后记

  1. 使用 printf 大法非常方便,但是尽量避免在中断里调用该函数(执行 printf 需要的时间比较久),以及调试完毕之后注释掉相关的代码,缩短功能代码的执行时间。
  2. 有兴趣的可以写 printf 到 OLED 之类的函数,原理都是一样,实现屏幕位置控制和最后的写一个字节的函数就能完成移植。

STM32自定义串口printf
http://example.com/2018/03/01/STM32-UART-CustomPrintf/
作者
Ekko bao
发布于
2018年3月1日
许可协议