STM32 HAL库:使用C标准库或MicroLIB实现printf | 多串口使用printf输出 | 结合DMA实现printf | 常见问题总结

综合多篇文章对实现printf进行总结,本篇博客包含内容如下:

  • 使用MicroLIB实现printf(Windows环境下和Linux环境下)
  • 使用C标准库实现printf(也就是不需要配置工程打开MicroLIB)
  • 结合DMA实现printf(针对大量数据、OS任务间使用printf冲突)
  • 多串口通信(当有多个串口通信设备时)
  • printf使用常见问题

如果本篇博客对你有帮助的话,记得点个赞哦!谢谢大家!😀😀😀😀😀😀

一、需打开MicroLIB版

打开MicroLIB,后面的步骤基于此(重点!!!!!!!)

鉴于很多作者忘记在文章中提醒读者打开MicroLIB,所以我在最开始就把这个步骤列出来。
在这里插入图片描述

如果不打开MicroLIB,可能会在stm32进行调试时,进入 LDR R0, =SystemInit卡死。参考文章

1.1 方案一

1.1.1 修改usart.c

Ⅰ添加头文件stdio.h

在usart.c头文件定义段加入stdio.h

#include <stdio.h>
Ⅱ 定义结构体FILE

这步不要也行,我把它注释掉后,照样能够正常输出汉字、整数、浮点数、字符、字符串。

现在自己知识不够,解释不了这个结构体的意义,后面知道了回来把这个坑补上。

struct FILE { 
	int handle; 
}; 
Ⅲ 重写fputc() & fgetc函数
int fputc(int ch, FILE * f){
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);///<普通串口发送数据
  	while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET){}///<等待发送完成
  	return ch;
}


int fgetc(FILE * F) {
	uint8_t ch = 0;
	HAL_UART_Receive(&huart1,&ch, 1, 0xffff);///<普通串口接收数据
    while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET){}///<等待发送完成
	return ch; 
}
Ⅳ 修改后usart.c

切记将这些内容写在沙盒段(USER BEGIN 和 USER END)之间
在这里插入图片描述

1.1.2 使用演示

在需要printf的c文件#include <stdio.h>,然后就可以使用printf了。

测试输出整数、浮点数、字符、字符串
在这里插入图片描述

1.2 方案二(Linux环境下有用)

1.2.1 添加头文件并修改usart.c

#include <stdio.h>  /* 包含头文件!!!!! */


#ifdef __GNUC__
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
 
PUTCHAR_PROTOTYPE
{
  HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,0xFFFF);//阻塞方式打印
  return ch;
}

有的同志可能发现了方案二其实和方案一一样,确实,如果你在Windows上使用keil5方案一和方案二没什么区别,因为keil5默认使用ARM Compiler 5编译器,最终都是重定向了fputs函数。但如果你在Linux上使用GNUC编译器进行编译,那么这两种方案就有区别了。

1.2.2 使用演示

经过验证,可用

二、标准库(不需要打开MicroLIB)

2.1 修改usart.c文件

#include <stdio.h>

/* 告知连接器不从C库链接使用半主机的函数 */
#pragma import(__use_no_semihosting)

/* 定义 _sys_exit() 以避免使用半主机模式 */
void _sys_exit(int x)
{
    x = x;
}

/* 标准库需要的支持类型 */
struct __FILE
{
    int handle;
};

FILE __stdout;

/*  */
int fputc(int ch, FILE *stream)
{
	/* 不同芯片的串口标志位不一定相同! */
    while((USART1->SR & 0X40) == 0);
    /* 串口发送完成,将该字符发送 */
    USART1->DR = (uint8_t) ch;
    return ch;
}

2.2 使用演示

与前面使用方法相同,不再赘述。

MicroLIB库和C标准库有什么区别?

详情链接

三、大量数据传输(DMA)

3.1 什么时候需要

  • 需要传输大量数据时,为了减缓CPU的压力
  • FreeRTOS多任务之间使用printf冲突

3.2 Cube配置DMA

在这里插入图片描述

3.3 代码移植

3.3.1 修改usart.c

添加以下代码

#include <stdarg.h>
void printf_DMA(UART_HandleTypeDef *husart, char *fmt,...)
{
	uint8_t n=0;
	for(int i=0;i<sizeof(Sendbuf);i++)  //清空发送缓存
	{
		Sendbuf[i]=0;
	}
  va_list arg;
  va_start(arg,fmt);//将...中的输入与fmt初始化到arg
  vsprintf((char*)Sendbuf,fmt,arg);//将输出存到发送缓存中
  va_end(arg);
	
	for(int i=0;i<sizeof(Sendbuf);i++)  //计算发送缓存中的非零字符数
	{
		if(Sendbuf[i]!=0) n++;
	}
	HAL_UART_Transmit_DMA(husart,(uint8_t*)&Sendbuf,n);  //通过DMA发送字符串
}

3.3.2 修改usart.h

void printf_DMA(UART_HandleTypeDef *husart, char *fmt,...);

四、多串口通信(满足与ESP8266、ESP32等进行通信)

4.1写法一

4.1.1 修改usart.c

添加以下代码到usart.c中,注意将代码放到USER段

#include <stdarg.h>
#include <string.h>
#include <stdio.h>
void usartPrintf(UART_HandleTypeDef USARTx, char *fmt,...)
{
 
	unsigned char UsartPrintfBuf[296];
	va_list ap;
	unsigned char *pStr = UsartPrintfBuf;
	
	va_start(ap, fmt);
	vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap);							//格式化
	va_end(ap);
	
	while(*pStr != NULL)
	{
        HAL_UART_Transmit (&USARTx ,(uint8_t *)pStr++,1,HAL_MAX_DELAY );		
	}
 
}

4.1.2修改usart.h

添加以下代码到usart.h中

void usartPrintf(UART_HandleTypeDef USARTx, char *fmt,...);

4.1.3 使用演示

与前面不同的是,如果这里要使用usartPrintf()包含的头文件是usart.h,不是stdio.h,usartPrintf的用法和printf一样。

在这里插入图片描述

4.2 写法二

地址:

4.2.1 修改usart.c

#include <stdarg.h>
uint8_t XL_Printf(UART_HandleTypeDef *huart,const char *format, ...)
{
	char buf[512]; //定义临时数组,根据实际发送大小微调
    va_list args;
    va_start(args, format);
    uint16_t len = vsnprintf((char *)buf, sizeof(buf), (char *)format, args);
    va_end(args);
    return HAL_UART_Transmit(huart,buf,len,1000); //串口打印函数,可以更换为中断发送或者DMA发送
}

4.2.2 修改usart.h

在usart.h中添加以下代码

uint8_t XL_Printf(UART_HandleTypeDef *huart,const char *format, ...);

4.2.3 使用演示

包含头文件#include <usart.h>

在这里插入图片描述

五、关于printf的问题总结

5.1 不加\r\n串口助手不显示(两种情况)

5.1.1 串口不发送

这个我没遇到过,是在浏览相关文章的时候看到的,有点没看明白,他说的解决方法我没看到,感觉没啥变化。

文章地址

5.1.2 串口助手程序的问题

有的串口助手在接收字符串时必须以\r\n结尾才会显示,否者不会在屏幕上显示。例如我的就是在vofa+上无法显示,在串口调试助手上就能正常显示。

5.2 debug时卡死在LDR R0, =SystemInit

这个是因为没有打开微库MicroLIB的原因造成的。

5.3 FreeRTOS堆栈不足,导致printf函数无法使用

这个是我在使用FreeRTOS时遇到的问题,如果任务中出现加上printf就卡死,那么就很有可能时任务堆栈不足。

5.4 不修改文件实现printf的效果

利用sprintf完成字符格式化,在通过HAL_UART_Transmit发送。

具体可以参考:在HAL库中的使用printf()函数和sprintf()函数

### 比较HAL标准库函数在嵌入式系统编程中的易用性 #### HAL的优势 HAL (Hardware Abstraction Layer) 提供了一种抽象层,使得开发者无需深入了解硬件细节即可操作微控制器的各种外设。这种设计显著简化了开发过程,尤其是在多平台项目中。通过使用一致的API接口,HAL允许程序更易于维护和移植。 对于STM32系列MCU而言,在采用HAL进行ADC配置时,通常会利用DMA实现高效的数据传输[^2]。这种方式不仅提高了效率,还减少了CPU占用率,从而让应用能够处理更多实时任务。 ```c // 使用HAL初始化并启动ADC转换的例子 static void MX_ADC_Init(void) { hadc.Instance = ADC1; hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc.Init.Resolution = ADC_RESOLUTION_12B; hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc.Init.ScanConvMode = DISABLE; // 单次模式 hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; hadc.Init.LowPowerAutoWait = DISABLE; hadc.Init.ContinuousConvMode = ENABLE; hadc.Init.NbrOfConversion = 1; hadc.Init.DiscontinuousConvMode = DISABLE; hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; if (HAL_ADC_Init(&hadc) != HAL_OK) { Error_Handler(); } } ``` #### 标准库函数的特点 相比之下,标准库(也称为固件LL)提供了更低级别的访问权限给底层硬件资源。这意味着虽然它可能更加灵活,但也要求程序员具备更多的专业知识才能正确设置寄存器参数以及理解具体的硬件行为。因此,当涉及到复杂的外设控制时,可能会增加项目的复杂度和调试难度。 然而,在某些情况下,如果目标是最优化性能者最小化代码尺寸,则直接调用标准库可能是更好的选择。因为在这种场景下可以直接操控硬件特性而不需要额外的功能封装所带来的开销。 综上所述,HAL更适合那些希望快速原型制作、跨平台迁移的应用场合;而对于追求极致性能是有特殊需求的小型设备来说,标准库许能带来更大的灵活性。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值