FreeRTOS 线程安全的printf输出(使用STM32F103)

news/2025/2/9 7:00:04 标签: stm32, 单片机, arm

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


http://www.niftyadmin.cn/n/520431.html

相关文章

Ymodem下载协议

背景 这里的Ymodem是YMODEM-1K&#xff08;除此还有Ymodem-g&#xff08;没有CRC校验&#xff0c;不常用&#xff09;&#xff09;&#xff0c;经常使用在IAP固件升级中。是X-modem协议升级过来的&#xff0c;每一包数据可以达到1024字节&#xff0c;比X-modem高效很多。这个协…

Intel HEX文件格式

原文地址http://bbs.netpu.net/viewthread.php?tid1690问题&#xff1a; 什么是Intel HEX文件格式&#xff1f;回答&#xff1a;Intel HEX文件是由一行行符合Intel HEX文件格式的文本所构成的ASCII文本文件。在Intel HEX文件中&#xff0c;每一行包含一个HEX记录。这些记录由对…

STM32的堆和栈

STM32 的堆和栈1.前言2.栈2.1栈的分类3.堆4.堆栈溢出参考文章1.前言 本博客讨论的堆栈是内存分配的堆和栈&#xff0c;并不是数据结构中的堆栈。 C语言内存分配可以看下这篇文章 2.栈 栈&#xff08;stack&#xff09;作用是用于局部变量、函数形参、函数调用时的现场保护和…

STM32 IAP固件升级 认知篇

1、环境 stm32f103zet6 MDK 5.28 2、芯片 2.1 Flash大小 我用的是stm32f103zet6属于高容量产品&#xff0c;flash大小512KB&#xff0c;每个Page2KB大小&#xff0c;一共256页&#xff08;这个可以根据自己的芯片去ST官网查询文档&#xff09;&#xff0c;如下图所示&#…

ultraedit 使用方法

(1) ultraedit 调用DEV-CPP编译安装DEV-CPP后&#xff0c;用命令行及UltraEdit调用devcpp中的mingw Gcc进行编译需要添加的环境变量有&#xff1a;1、PATH&#xff1a;C:/dev-cpp/bin;如果这个变量电脑上已有&#xff0c;在原有的变量上添加“;C:/dev-cpp/bin”就行了&#xff…

内核库函数Kernel32.exe提供的API

内核库函数Kernel32.exe提供的API函数名称 说明AddAtom 向本地原子表添加一个字符串 AllocConsole 为当前进程分配一个新控制台 AreFileApisANSI 确定一个WIN32文件函数集是否在使用ANSI或OEM字符集代码页 BackupRead 向一缓冲区读进与给定文件相关联的数据 BackupSeek 在访问…

结构体------对齐与压缩(#pragma, __packed)

结构体对齐 摘自原则&#xff1a; 结构体中元素是按照定义顺序一个一个放到内存中去的&#xff0c;但并不是紧密排列的。从结构体存储的首地址开始&#xff0c;每一个元素放置到内存中时&#xff0c;它都会认为内存是以它自己的大小来划分的&#xff0c;因此元素放置的位置一定…

避免窗口一闪而过

大家知道&#xff0c;调试C语言程序的时候&#xff0c;点下run后&#xff0c;运行结果往往闪了一下就消失了&#xff0c;这是因为我们调试的程序都不大&#xff0c;所以基本上都来不及看结果&#xff0c;窗口就自动关闭了&#xff0c;这时可以在主程序的返回之前加上getch(),就…