本帖最后由 lulugl 于 2023-7-18 13:52 编辑
#申请原创# #有奖活动#@21小跑堂
一、前言
什么是重定向?重定向是指将fputc里面的输出指向目标设备。因printf函数调用了fputc,而fputc输出有默认指向的目标,且不同库中的fputc输出指向不同,所以需要重写fputc标准库实现重定向到串口
二、若需要printf输出到串口,则需要将fputc里面的输出指向串口,这一过程称为重定向。在我们的CW32L052_StandardPeripheralLib_V1.0的demo中官方给出了我们在mdk下面的串口重定向的示例,路径为\CW32L052_StandardPeripheralLib_V1.0\Utilities。其log.c中定义来设置了板载的UART2为printf输出串口。根据原理图其TXD与RXD分别为PD02与PC12。
在log.c中函数static void SerialInit(uint32_t BaudRate)为初始化指定的波特率的UART2,代码如下:
static void SerialInit(uint32_t BaudRate)
{
uint32_t PCLK_Freq;
GPIO_InitTypeDef GPIO_InitStructure = {0};
UART_InitTypeDef UART_InitStructure = {0};
PCLK_Freq = SystemCoreClock >> pow2_table[CW_SYSCTRL->CR0_f.HCLKPRS];
PCLK_Freq >>= pow2_table[CW_SYSCTRL->CR0_f.PCLKPRS];
// 调试串口使用UART2
// PC12->TX
// PD02<-RX
// 时钟使能
__RCC_GPIOD_CLK_ENABLE();
__RCC_GPIOC_CLK_ENABLE();
__RCC_UART2_CLK_ENABLE();
// 先设置UART TX RX 复用,后设置GPIO的属性,避免口线上出现毛刺
PD02_AFx_UART2RXD();
PC12_AFx_UART2TXD();
GPIO_InitStructure.Pins = GPIO_PIN_12;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_Init(CW_GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.Pins = GPIO_PIN_2;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_Init(CW_GPIOD, &GPIO_InitStructure);
UART_InitStructure.UART_BaudRate = BaudRate;
UART_InitStructure.UART_Over = UART_Over_16;
UART_InitStructure.UART_Source = UART_Source_PCLK;
UART_InitStructure.UART_UclkFreq = PCLK_Freq;
UART_InitStructure.UART_StartBit = UART_StartBit_FE;
UART_InitStructure.UART_StopBits = UART_StopBits_1;
UART_InitStructure.UART_Parity = UART_Parity_No;
UART_InitStructure.UART_HardwareFlowControl = UART_HardwareFlowControl_None;
UART_InitStructure.UART_Mode = UART_Mode_Rx | UART_Mode_Tx;
UART_Init(CW_UART2, &UART_InitStructure);
}
函数static void SerialSend(uint8_t Data)的功能为发送一个字节
static void SerialSend(uint8_t Data)
{
UART_SendData_8bit(CW_UART2, Data);
while (UART_GetFlagStatus(CW_UART2, UART_FLAG_TXE) == RESET);
}
【串口重定向】在最后的fputc中重构了标准输出函数。int fputc(int ch, FILE *f)
{
SerialSend(ch);
return ch;
}
然后重写的pirntf(即write),其代码如下:
size_t __write(int handle, const unsigned char * buffer, size_t size)
{
size_t nChars = 0;
if (buffer == 0)
{
/*
* This means that we should flush internal buffers. Since we
* don't we just return. (Remember, "handle" == -1 means that all
* handles should be flushed.)
*/
return 0;
}
for (/* Empty */; size != 0; --size)
{
SerialSend(*buffer++);
++nChars;
}
return nChars;
}
【注意】
1、调用printf需要先导入stdio.h头文件。
2、在MDK的设置中需要勾选Target中的use MicroLIB选项
如果我们在gcc的编译环境下,那printf的重定向函数需要修改为:
int _write (int fd, char *pBuffer, int size)
{
for (int i = 0; i < size; i++)
{
SerialSend(*buffer++);
}
return size;
}
以上就是串口重定向固件的串口的实现。我们就可以实现用printf("%d%s",num,string)来实现打印。
三、如果我们需要同时操作一个,或者多个串口,比如同时使用UART2来做日志打印,UART1来与WIFI模块通讯,UART3用来与NB-IOT来通信,同时我也需要用printf来组装参数,那么就必需使用到多串口的重定向功能。下面介绍如何实现这个功能的方法:
1、为了实现功能,我们需要用到C标准库中的stdarg.h这个库。
stdarg.h 头文件定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数。可变参数的函数通在参数列表的末尾是使用省略号(,...)定义的。
其中val_list 是一个适用于 va_start()、va_arg() 和 va_end() 这三个宏存储信息的类型。
- void va_start(va_list ap, last_arg)
这个宏初始化 ap 变量,它与 va_arg 和 va_end 宏是一起使用的。last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。- type va_arg(va_list ap, type)
这个宏检索函数参数列表中类型为 type 的下一个参数。
这个宏允许使用了 va_start 宏的带有可变参数的函数返回。如果在从函数返回之前没有调用 va_end,则结果为未定义。
2、先定义 需要用到串口的和发送字符串口组数:
#define UART1_TXBUFF_SIZE 128
__align(8) char Uart1_TxBuff[UART1_TXBUFF_SIZE];
3、新建u1_printf函数,流程为先创建一个valist 变量ap,然后获取可变参数地址 fmt地址赋给ap,使用参数列表发送格式化输出到字符串,函数功能将可变参数格式化输出到一个字符数组。最近通过指定的UART_SendString函数发送到指定的串口。代码如下:
void u1_printf(char* fmt,...)
{
va_list ap;
//va_list 可变参数列表,存参数地址
va_start(ap, fmt); //获取可变参数地址 fmt地址赋给ap
/*
* 使用参数列表发送格式化输出到字符串,函数功能将可变参数格式化输出到一个字符数组
*/
vsprintf(Uart1_TxBuff, fmt, ap);
va_end(ap); //清空参数列表
UART_SendString(CW_UART1, Uart1_TxBuff);
}
这样,我们可以创建多个u1_prntf的函数来实现多个串口定向的功能了。
【测试】
我们在主程序中添加u1_printf("rcv:%s\r\n",uart1_rx_buff);编译下载后就可以实现基功能了:
|