0%

实现自己的串口 Printf 函数

我们常常会用到串口,甚至 ST-Link 自带了一个虚拟串口,有时候串口时为了传输数据给上位机,但有时只是为了输出个结果供我们观察程序运行状态,对于后者,实现一个 printf 函数就十分有用了。网上通常的方案是修改fputc来实现。但我们可以利用标准库实现一个自己的 printf 函数,也可以是一个printf宏。

从HAL库的串口传输开始

串口传输可以是阻塞模式、中断模式、DMA模式。我们以阻塞模式为例。

1
2
3
4
5
6
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

串口发送数据的函数为HAL_UART_Transmit。我们的思路是将我们的数据转换成字符串然后发送出去。

函数版本

我们希望串口发送函数具备printf的特点,那么就需要用到可变参数。下面是与此相关的头文件。

1
2
#include <stdio.h>
#include <stdarg.h>

我们需要简单的了解一下 va_listva_startva_endvsprintf

既然需要用到可变参数,那不妨就顺便学习下最简单的用法。

可变参数简单用法

其中va_list需要被va_start初始化 va_end 最后释放内存。希望实现的串口printf,需要用到的是vsprintf

vsprintf用法

vsprintf的形参为

int vsprintf (char * s, const char * format, va_list arg );

其中arg应当用 char*来初始化。

1
2
3
char* format = something; 
va_list ap;
va_start(ap, format);

既然转换为了字符串,那么自然需要知道它的长度,因为字符串的长度即为发送的size。那么需要引入头文件 string.h 然后调用 strlen 函数。并且我们需要一个数组存放字符串。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
char uart_buffer[UART_BUFF_SIZE] = {0};
void UARTPrintf(UART_HandleTypeDef *handle, char *format, ...)
{
va_list ap;
va_start(ap, format);
vsprintf(uart_buffer, format, ap);
va_end(ap);
HAL_UART_Transmit(handle, uart_buffer, strlen(uart_buffer), 1000);
}

宏版本

实现了函数版本后,我们可以实现一个宏版本。同时思考到,函数版本有一个UART的句柄 UART_HandleTypeDef ,串口用作printf时通常我们是固定的一个串口,通常不会发生一会儿使用 uart1,一会儿使用 uart2的情况。我们可以将它固定下来。在宏版本中,我们来完成这一工作,你也可以修改对应的函数版本。

1
2
3
4
// 固定下串口print的句柄
// huart1 会在cubemx初始化代码的时候生成,我们只需声明 extern 来引用它。
#define UART_HANDLE huart1
extern UART_HandleTypeDef UART_HANDLE;

关键还是在可变参数中。这次是在宏中使用可变参数。对于宏的可变参数而言,有一个专门的宏__VA_ARGS__

我们需要转换为字符串,因此需要sprintf函数,用法非常简单。

1
2
3
4
5
6
7
#define uart_printf(...)                                                    \
{ \
char buf[UART_BUFF_SIZE] = {0}; \
sprintf(buf, __VA_ARGS__); \
HAL_UART_Transmit(&UART_HANDLE, (uint8_t *)buf, strlen(buf), 1000); \
}
#endif

注:__VA_ARGS__只能用于宏定义,从C99开始引入,不能用于函数实现。

宏定义加入括号,可以直接跟if语句而不加大括号。如

if (…)

​ uart_printf

else

​ do something