https://blog.csdn.net/baidu_23187363/article/details/53811144
环境
STM32F103开发板
HAL库(标准库也没事换个串口输出函数就行)
MDK5.28
STM32CubeMX
前言
原本直接使用串口输出来debug调试的,但是添加FreeRTOS之后出现乱码的现象。所以决定做个线程安全的printf函数来打印输出方便调试。
原因
假设一个115200的波特率发送一个8位的数据、1个停止位、1个起始位、无奇偶校验位,需要大约87us,当发送大量数据的时候很容易被中断或者其他高优先级的任务打断,从而出现乱码。
串口的重映射
参考开发板的教程printf重映射函数是这么写的
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
这样使用printf函数就可以实现串口1输出的,由于没有任何保护。所以有概率会出现乱码。
printf函数实现
#include "stdio.h"
#include <stdarg.h>
void print_usart1(char *format, ...)
{
char buf[64];
va_list ap; //声明字符指针 ap
va_start(ap, format); //初始化 ap 变量
vsprintf(buf, format, ap); //使用参数列表发送格式化输出到字符串
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);//100ms内发送
va_end(ap);
}
C标准库<stdarg.h>
va_start()
void va_start(va_list ap, last_arg)
参数:
- ap – 这是一个 va_list 类型的对象,它用来存储通过 va_arg 获取额外参数时所必需的信息。
- last_arg – 最后一个传递给函数的已知的固定参数。“…”之前的参数
vsprintf
int vsprintf(char *str, const char *format, va_list arg)
参数:
- str – 这是指向一个字符数组的指针,该数组存储了 C 字符串。
- format – 这是字符串,包含了要被写入到字符串 str 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。
- arg – 一个表示可变参数列表的对象。这应被 中定义的 va_start 宏初始化。
返回值:
如果成功,则返回写入的字符总数,否则返回一个负数。
va_end
void va_end(va_list ap)
参数:
- ap – 这是之前由同一函数中的 va_start 初始化的 va_list 对象。
线程安全函数构建
方案一:
我们不希望在打印到一半的时候进入别的任务线程中,我们可以使用函数挂起所有线程,打印结束再回复任务调度。
那么可以使用进入临界区的方式来保证线程安全。
void vTaskSuspendAll( void )
BaseType_t xTaskResumeAll( void )
#include "stdio.h"
#include <stdarg.h>
void print_usart1(char *format, ...)
{
char buf[64];
va_list ap; //声明字符指针 ap
vTaskSuspendAll(); //挂起任务
va_start(ap, format); //初始化 ap 变量
vsprintf(buf, format, ap); //使用参数列表发送格式化输出到字符串
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);//100ms内发送
va_end(ap);
xTaskResumeAll(); //恢复任务调度
}
改良方案一:
但是这种方式只是挂起了任务和恢复任务,没法保证中断安全,若出现中断嵌套还是会出现线程不安全。
所以可以使用临界区和关闭中断来保证printf线程和中断安全。当然前提我们需要构造一个函数判断是否在中断中。
#include "stdio.h"
#include <stdarg.h>
static int inHandlerMode (void) //若在中断中__get_IPSR()返回1,否则返回0
{
return __get_IPSR();
}
void print_usart1(char *format, ...)
{
char buf[64];
if(inHandlerMode() != 0)
{
taskDISABLE_INTERRUPTS();//若在中断中调用则关闭中断,防止中断嵌套造成线程不安全
}
else
{
taskENTER_CRITICAL(); //若不在中断中则进入临界区关闭中断且禁止任务调度
}
va_list ap;
va_start(ap, format);
vsprintf(buf, format, ap);
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);
va_end(ap);
if(inHandlerMode() != 0)
{
taskENABLE_INTERRUPTS();//打开中断
}else{
taskEXIT_CRITICAL();//退出临界区
}
}
再次改良
上述方法虽然能解决线程安全和中断安全,但是需要挂起所有任务消耗资源。所以继续改进使用任务切换的方式,若串口忙时需要打印参数,则只挂起当前任务。
void print_usart1(char *format, ...)
{
char buf[64];
if(inHandlerMode() != 0)
{
taskDISABLE_INTERRUPTS();
}
else
{
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX)//若串口忙则挂起此任务
taskYIELD();
}
va_list ap;
va_start(ap, format);
vsprintf(buf, format, ap);
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);
va_end(ap);
if(inHandlerMode() != 0)
taskENABLE_INTERRUPTS();
}
#参考文章
Stone_Biny