串口(含DMA)——跟我一起写STM32(第五期)

7 串口通信

7.1 认识串口

谈到单品机通信,不得不提及的三种通信方式:
UART、I2C、SPI
补课视频推荐(b站):

在STM32的参考手册中,串口被描述成通用同步异步收发器(USART)。
串口通信是一种设备间非常常用的串行通行方式,其简单便捷,大部分电子设备都支持。
串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,其重要性不言而喻。

普通串口一般两个接口,Rx,Tx。连接上就是Rx接上Tx,Tx接上Rx即可完成物理层面没有硬件流控制的连接。因为笔者一般也就是使用无硬件流控制的连接方式,所以我们的笔墨也会放在此处。

还有一点就是,

USART:通用同步和异步收发器
UART:通用异步收发器

当进行异步通信时,这两者是没有区别的。区别在于USART比UART多了同步通信功能。
这个同步通信功能可以把USART当作SPI来用,比如用USART来驱动SPI设备。

7.2 配置串口

在串口的协议中,有几个东西尤其重要:

1. 波特率
异步通信中由于没有时钟信号,所以2个通信设备需约定好波特率,常见的有4800、9600、115200等。

2. 通信的起始和停止信号
串口通信的一个数据包从起始信号开始,知道停止信号结束。数据包的起始信号由一个逻辑0的数据位表示,而数据包的停止信号可由0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可。
3. 有效数据
在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效
数据的长度常被约定为 5、6、7或 8位长
4. 数据校验
在有效数据之后,有一个可选的数据校验位。由于数据通信相对容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验(odd)、偶校验(even)、0校验(space)、1校验(mark)以及无校验(noparity)。
奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个 8 位长的有效数据为:01101001,此时总共有 4 个“1”,为达到奇校验效果,校验位为“1”,最后传输的数据将是 8 位的有效数据加上 1 位的校验位总共 9 位。
偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为 4 个,所以偶校验位为“0”。
0 校验是不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为“1”。

当然这些重要的配置都有相应的寄存器,像:

每个串口都有一个自己独立的波特率寄存器 USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的;

STM32 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是一个双寄存器,包含了 TDR 和 RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内;


可以看出,虽然是一个 32 位寄存器,但是只用了低 9 位(DR[8:0]),其他都是保留。
DR[8:0]为串口数据,包含了发送或接收的数据。由于它是由两个寄存器组成的,一个给发送用(TDR),一个给接收用(RDR),该寄存器兼具读和写的功能。TDR 寄存器提供了内部总线和输出移位寄存器之间的并行接口。RDR 寄存器提供了输入移位寄存器和内部总线之间的并行接口。
当使能校验位(USART_CR1 中 PCE 位被置位)进行发送时,写到 MSB 的值(根据数据的长
度不同,MSB 是第 7 位或者第 8 位)会被后来的校验位取代。
当使能校验位进行接收时,读到的 MSB 位是接收到的校验位。

串口的状态可以通过状态寄存器 USART_SR 读取。USART_SR 的各位描述:


这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。
RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并
且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将
该位清零,也可以向该位写 0,直接清除。
TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如
果设置了这个位的中断,则会产生中断。该位也有两种清零方式:1)读 USART_SR,写
USART_DR。2)直接向该位写 0。
通过以上一些寄存器的操作外加一下 IO 口的配置,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考《STM32 中文参考手册》。

关于串口设置的一般步骤可以总结为如下几个步骤:

  1. 串口时钟使能,GPIO 时钟使能。
  2. 设置引脚复用器映射:调用 GPIO_PinAFConfig 函数。
  3. GPIO 初始化设置:要设置模式为复用功能。
  4. 串口参数初始化:设置波特率,字长,奇偶校验等参数。
  5. 开启中断并且初始化 NVIC,使能中断(如果需要开启中断才需要这个步骤)。
  6. 使能串口。
  7. 编写中断处理函数:函数名格式为 USARTxIRQHandler(x 对应串口号)。

不过,这里笔者还是使用图形化配置的方式。

这里配置模式为异步
比特率为115200Bits/s(常用)
无校验
停止位为1(常设置为1)
然后我们生成工程

7.3 简单的串口收发

我们来认识一下API

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

UART_HandleTypeDef *huart :结构体指针,这个结构体的成员的值已经被HAL库通过刚刚我们的图形化配置配置好了,这里是/Core/Src/usart.c里面的内容

UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void){
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK){
    Error_Handler();
  }

可以看出,我们只需要把得到的huart1的值传给这个函数HAL_UART_Transmit即可,不过因为是指针,所以不要忘记加上&哦
其他的参数就是
pData:要传入的数据
Size: 数据的大小
Timeout:轮询等候的时间
这里我不得不再介绍再一下轮询的概念了。
轮询就是程序运行到了这里,一直等,直到数据完整发送或者轮询时间超过了,才执行下一条语句。
相应的方式就是后面带_DMA的DMA运输方式和后面带_IT中断方式。
在没学DMA之前,一般发送用轮询,接收用中断。
于是我们有了下面的代码

#define TIMEOUT 0xffff
/* USER CODE BEGIN WHILE */
uint8_t tx_dat[] = {"Hello World!"}; 
  while (1)
  {
	  	HAL_UART_Transmit(&huart1,tx_dat, strlen((char *)tx_dat), TIMEOUT);
		HAL_Delay(1000);
/* USER CODE END WHILE */
  }

然后,我们烧入程序测试一下
测试方法的话:
首先,需要朋友们下载一个串口调试助手,这种东西网上一搜一大堆。(这里笔者使用的是野火多功能调试助手)
并把参数调整至和我们程序设计的一样

然后,准备一个CH340,将CH340的Rx,Tx与STM32的Rx,Tx交叉连接,连接电脑。

电脑上下载CH340的驱动并安装。

CH340串口驱动的官网下载链接地址(包含各大操作系统平台)

接下来,让我们来看看结果吧


看来是可以成功发送了

我们试试收信息呢
打开中断,重建工程
在这里插入图片描述
在/Core/Src/stm32xxxx.it中找到USART1_IRQHandler
然后逐层深入,
在这里插入图片描述
这里我们不选择事件中断,我们选择了完成中断(即UART接收完成再触发中断),并选择自己重写回调函数。

uint8_t rx_dat = 0;
... //省略代码
int main(void)
{
... //省略代码
HAL_UART_Receive_IT(&huart1, &rx_dat, 1);
... //省略代码
}
... //省略代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)	//开启
{
	if(huart->Instance == USART1){
		HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
		HAL_UART_Transmit(&huart1,&rx_dat, 1, TIMEOUT);
		HAL_UART_Receive_IT(&huart1, &rx_dat, 1);//接收下一次
	}
}

这样我们就做好了一个串口控制灯。

7.4 接收不定长的数据

HAL库最让笔者头疼的就是这个接收函数只能接收固定长度的数据,而现实中,我们接收数据往往是不固定长度的。
STM32里支持空闲中断,于是笔者想到可以改进一下我们的代码。
/Core/Src/main.c

HAL_UART_Receive_IT(&huart1, &rx_dat, 1);

/Core/Src/stm32xxxxx_it.c

void UART_IDLE_Handler(UART_HandleTypeDef *huart);

void USART1_IRQHandler(void)
{

  HAL_UART_IRQHandler(&huart1);
  UART_IDLE_Handler(&huart1); //检测空闲事件中断并进行处理
  
}
void UART_IDLE_Handler(UART_HandleTypeDef *huart)
{

	UART_IDLE_Callback(huart);
	
}

/Core/Src/usart.h

extern uint8_t rx_dat;
void UART_IDLE_Callback(UART_HandleTypeDef *huart);

/Core/Src/usart.c

uint8_t rx_dat = 0;
void UART_IDLE_Callback(UART_HandleTypeDef *huart)
{
	if(__HAL_UART_GET_IT_SOURCE(huart,UART_IT_IDLE) == RESET)//判断IDLE中断是否开启
		return;
	__HAL_UART_CLEAR_IDLEFLAG(huart);			//清除IDLE中断标志位
	__HAL_UART_DISABLE_IT(huart,UART_IT_IDLE);	//禁止IDLE事件中断
	HAL_UART_Transmit(&huart1, &rx_dat, 1, 0xffff);
	HAL_UART_Receive_IT(&huart1, &rx_dat, 1);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart->Instance == USART1)
	{
		//FlagCompleted = RESET;
		__HAL_UART_ENABLE_IT(huart,UART_IT_IDLE);
	}
}

不过,这段代码写的非常差,笔者是打算利用每次发一个字符然后空闲处理,然后再继续接收,但是笔者没有考虑到数据传输的问题,以至于一旦数据过多,就会有一定的数据接收不到。于是接下来笔者将引入DMA来帮我们来处理这个问题。

7.5 用DMA来不定长接收

7.5.1 认识DMA

DMA基本介绍:
DMA,全称Direct Memory Access,即直接存储器访问。
众所周知,CPU无时不刻的在处理着大量的事务,但有些事情却没有那么重要,比方说数据的复制和存储数据,如果我们把这部分的CPU资源拿出来,让CPU去处理其他的复杂计算事务,是不是能够更好的利用CPU的资源呢?
因此:转移数据(尤其是转移大量数据)是可以不需要CPU参与。比如希望外设A的数据拷贝到外设B,只要给两种外设提供一条数据通路,直接让数据由A拷贝到B 不经过CPU的处理

DMA就是基于以上设想设计的,它的作用就是解决大量数据转移过度消耗CPU资源的问题。有了DMA使CPU更专注于更加实用的操作–计算、控制等。

DMA传输方式:

  1. 外设到内存
  2. 内存到外设
  3. 内存到内存
  4. 外设到外设

DMA传输参数:
我们知道,数据传输,首先需要的是

  1. 数据的源地址
  2. 数据传输位置的目标地址
  3. 传递数据多少的数据传输量
  4. 进行多少次传输的传输模式

DMA所需要的核心参数,便是这四个。当用户将参数设置好,主要涉及源地址、目标地址、传输数据量这三个,DMA控制器就会启动数据传输,当剩余传输数据量为0时 达到传输终点,结束DMA传输 ,当然,DMA 还有循环传输模式 当到达传输终点时会重新启动DMA传输。也就是说只要剩余传输数据量不是0,而且DMA是启动状态,那么就会发生数据传输。

DMA的主要特征

  • 每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置;

  • 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);

  • 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐; 支持循环的缓冲器管理;

  • 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;

  • 存储器和存储器间的传输、外设和存储器、存储器和外设之间的传输;

  • 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标; 可编程的数据传输数目:最大为65535。

7.5.2 配置DMA

从外设(TIMx、ADC、SPIx、I2Cx 和 USARTx)产生的 DMA 请求,通过逻辑或输入到
DMA 控制器,这就意味着同时只能有一个请求有效。外设的 DMA 请求,可以通过设置相应的
外设寄存器中的控制位,被独立地开启或关闭。

这里解释一下上面说的逻辑或,例如通道 1 的几个 DMA1 请求(ADC1、TIM2_CH3、TIM4_CH1),
这几个是通过逻辑或到通道 1 的,这样我们在同一时间,就只能使用其中的一个。其他通道也
是类似的。

例如我们要用到串口 1 的发送,属于 DMA1 的通道 4,
接下来我们就介绍下 DMA1 通道 4 的配置步骤:

  1. 使能 DMA1 时钟
__HAL_RCC_DMA1_CLK_ENABLE();
  1. 初始化 DMA1 数据流 4,包括配置通道,外设地址,存储器地址,传输数据量等。

在这里的图像化配置会让代码多一些关于DMA的东西,就比如

__HAL_LINKDMA(&UART1_Handler,hdmatx,UART1TxDMA_Handler);

这 句 话 的 含 义 就 是 把 UART1_Handler 句 柄 的 成 员 变 量 hdmatx 和 DMA 句 柄
UART1TxDMA_Handler 连接起来,是纯软件处理,没有任何硬件操作。
这里我们就点到为止,大家可以自行查阅相关HAL库代码和STM32寄存器。

  1. 使能串口 1 的 DMA 发送

在实验中,开启一次 DMA 传输传输函数如下:

//开启一次 DMA 传输
//huart:串口句柄
//pData:传输的数据指针
//Size:传输的数据量
void MYDMA_USART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
HAL_DMA_Start(huart->hdmatx, (u32)pData, (uint32_t)&huart->Instance->DR, Size);
//开启 DMA 传输
 huart->Instance->CR3 |= USART_CR3_DMAT;//使能串口 DMA 发送
} 

HAL 库还提供了对串口的 DMA 发送的停止,暂停,继续等操作函数

HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart); //停止
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart); //暂停
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);//恢复
  1. 使能 DMA1 数据流 4,启动传输。

使能 DMA 数据流的函数为:

HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress,
 uint32_t DstAddress, uint32_t DataLength);
	hdma: 				DMA 句柄,
	SrcAddress: 		传输源地址,
	DstAddress:		传输目标
	DataLength:		传输的数据长度。

通过以上 4 步设置,我们就可以启动一次 USART1 的 DMA 传输了。

  1. 查询 DMA 传输状态

在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的方法是:

__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TCIF3_7)

获取当前传输剩余数据量:

__HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler);

DMA 相关的库函数我们就讲解到这里,大家可以查看固件库中文手册详细了解。

  1. DMA 中断使用方法

DMA 中断对于每个流都有一个中断服务函数,比如 DMA1_Channel4 的中断服务函数为
DMA1_Channel4_IRQHandler。同样,HAL 库也提供了一个通用的 DMA 中断处理函数
HAL_DMA_IRQHandler,在该函数内部,会对 DMA 传输状态进行分析,然后调用相应的中断处理回调函数:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//发送完成回调函数
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);/发送一半回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收完成回调函数
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//接收一半回调函数
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);//传输出错回调函数

对于串口 DMA 开启,使能数据流,启动传输,这些步骤,如果使用了中断,可以直接调
用 HAL 库函数 HAL_USART_Transmit_DMA,该函数声明如下:

HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

不过使用这个函数的时候,要注意:

由于配置顺序不一样,用STM32CubeMX生成初始化代码之后,导致DMA的初始化函数被软件默认放置在了串口1初始化函数的后面,DMA还未初始化,就在串口一初始化函数里面配置了与DMA有关的参数,这就是串口1只能发送一次的原因。
就是

MX_USART1_UART_Init();
MX_DMA_Init();

改成

MX_DMA_Init();
MX_USART1_UART_Init();

参考:
HAL库——串口DMA发送函数HAL_UART_Transmit_DMA只能发送一次的原因之一

  1. DMA 中断相关宏
  • 使能中断的宏定义
__HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__)
  • 判断是触发哪个中断的宏定义
__HAL_UART_GET_FLAG(__HANDLE__, __IT__)
  • 清除IDLE的中断标志位的宏定义
__HAL_UART_CLEAR_IDLEFLAG(__HANDLE__)

7.5.3 DMA实现UART接收

这里我们重写之前没写好的不定长接收:

/Core/Src/main.c

... //省略代码
__HAL_UART_ENABLE_IT(&huart1 , UART_IT_IDLE);// 使能串口空闲中断
... //省略代码

空闲中断默认关闭,只要自己手动打开。
我们的核心逻辑就是每次空闲的时候就说明发完了,我们就去处理缓存区里的数据。

/Core/Src/stm32xxxxx_it.c

... //省略代码
void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
  UART_IDLE_Handler(&huart1); //检测空闲事件中断并进行处理
}
void UART_IDLE_Handler(UART_HandleTypeDef *huart)
{
	if(huart->Instance == USART1)
	{
		// 判断是否是空闲中断
		if(__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE) != RESET)
		{
		// 清除空闲中断标志(否则会一直不断进入中断)
		__HAL_UART_CLEAR_IDLEFLAG(huart);
		//调用中断处理函数
		UART_IDLE_Callback(huart);
		}
	}
}
... //省略代码

这段代码应该要熟悉,这就是一个HAL库的没有封装完的一个原始代码的框架,也是我们处理中断的一般流程。判断中断标志位->清除中断标志位->回调函数,本来这里可以直接实现的,不过笔者还是模仿HAL库的方式作了一个回调函数,也方便以后我们调用。

/Core/Src/usart.h

... //省略代码
#define RX_STR_LEN 255
extern char Rx_str[RX_STR_LEN];
extern uint8_t data_length;
void UART_IDLE_Callback(UART_HandleTypeDef *huart);
... //省略代码

/Core/Src/usart.c

#include <string.h>
char Rx_str[RX_STR_LEN] = {0};
... //省略代码
void UART_IDLE_Callback(UART_HandleTypeDef *huart)
{
	HAL_UART_DMAStop(huart);// 停止本次DMA传输
	uint8_t data_length  = RX_STR_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);//计算收到字符长度
	HAL_UART_Transmit(&huart1, (uint8_t *)Rx_str, data_length,0xffff);
	memset(Rx_str, 0, data_length);//初始化Rx_str
	data_length  = 0;
	HAL_UART_Receive_DMA(huart, (uint8_t *)Rx_str, RX_STR_LEN); //重新打开DMA接收
}

这里就是空闲中断函数了,就是我们处理不定长数据的核心了。这里我们先停止DMA的传输,以免数据被覆盖。然后我们就直接处理我们DMA接收到了的数据。之后不要忘记重新初始化缓存器和长度和重新打开DMA接收了哦。

7.6 改写printf 进入变参函数的世界

7.6.1 自己的print函数

朋友们有没有发现,我们每次发送一个数据的时候,数据里面不能含有变量,这十分的不方便。回想我们曾经在c语言中学习过一个叫printf的变参函数,这里我们可以想想能不能重写这个printf来方便我们数据的收发呢。
重写printf之前,让我补习一下c语言的一些基础知识。
总所周知,函数参数是以栈的形式存取的,从右往左入栈。
首先是参数存放的格式:参数存放在内存的堆栈里,在执行函数的时候,从最后一个开始入栈。因此栈底高地址,栈顶低地址。举个例子,

void fun(int x, float y, char z);

那么,我们在调用函数的时候就是z先进栈,然后y,然后z,内存中存放顺序为x->y->z,因此,从理论上说,我们只要探测到一个变量的地址,知道其他变量的类型,就能通过指针移位运算,顺藤摸瓜找到其他变量,这就是变参函数的核心。
下面是<stdarg.h>的几个重要的宏

typedef char *  va_list; 

va-list是一个char类型的指针,当被调用函数使用一个可变参数时,它声明一个类型为va-list的变量,该变量用来指向va-arg和va-end所需信息的位置。

#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) ) 

va-start它使va-list类型变量ap指向被传递给函数的可变参数表中的第一个参数,在第一次调用va-arg和va-end之前,必须首先调用该宏。va-start的第二个参数lastfix是传递给被调用函数的最后一个固定参数的标识符。va-start使ap只指向lastfix之外的可变参数表中的第一个参数,很明显它先得到第一个参数内存地址,然后又加上这个参数的内存大小,就是下个参数的内存地址了。

#define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )

type va-arg(va-list ap,type)也是一个宏,其使用有双重目的,第一个是返回ap所指对象的值,第二个是修改参数指针ap使其增加以指向表中下一个参数。va-arg的第二个参数提供了修改参数指针所必需的信息。在第一次使用va-arg时,它返回可变参数表中的第一个参数,后续的调用都返回表中的下一个参数。

#define va_end(ap)      ( ap = (va_list)0 )

va-end必须在va-arg读完所有参数后再调用,否则会产生意想不到的后果。特别地,当可变参数表函数在程序执行过程中不止一次被调用时,在函数体每次处理完可变参数表之后必须调用一次va-end,以保证正确地恢复栈。
我们先照猫画虎地写一下,然后测试一下

void print(char *Data, ...)
{
	const char *s;
	va_list ap;
	va_start(ap, Data);
	s = va_arg(ap,const char *);
	for ( ; *s; s++)
	{
		HAL_UART_Transmit(&huart1,(uint8_t *)s,1,0xFF);
	}
	s = va_arg(ap,const char *);
	for ( ; *s; s++)
	{
		HAL_UART_Transmit(&huart1,(uint8_t *)s,1,0xFF);
	}
	va_end(ap);
}
print("","hello\n","Judge\n");

看来我们成功了

明白了变参函数的本质,那我们就编写一个可以用于串口的print吧

void print(char *Data, ... )
{
	 const char *s;
	int d;
	char buf[16];
	uint8_t txdata;
	va_list ap;
	va_start(ap, Data);
	while (*Data != 0) // 判断是否到达字符串结束符
	{
		if (*Data == 0x5c) //'\'
		{
				switch (*++Data)
				{
				case 'r': // 回车符
					txdata = 0x0d;
					HAL_UART_Transmit(&huart1, &txdata, 1, 0xFF);
					Data++;
					break;
				case 'n': // 换行符
					txdata = 0x0a;
					HAL_UART_Transmit(&huart1, &txdata, 1, 0xFF);
					Data++;
					break;
				default:
					Data++;
					break;
				}
}
		else if (*Data == '%')
		{
			switch (*++Data)
			{
				case 's': // 字符串
				s = va_arg(ap, const char *);
				for (; *s; s++)
				{
					HAL_UART_Transmit(&huart1, (uint8_t *)s, 1, 0xFF);
					while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == 0);
				}
				Data++;
				break;
				case 'd': // 十进制
				d = va_arg(ap, int);
				itoa(d, buf, 10);
				for (s = buf; *s; s++)
				{
					HAL_UART_Transmit(&huart1, (uint8_t *)s, 1, 0xFF);
					while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == 0);
				}
				Data++;
				break;
				default:
				Data++;
				break;
				}
			}
			else
			{
					HAL_UART_Transmit(&huart1, (uint8_t *)Data, 1, 0xFF);
					Data++;
					while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == 0);
			}
		}
}

注:

char *itoa( int value, char *string,int radix);
value:欲转换的数据。
string:目标字符串的地址。
radix:转换后的进制数,可以是10进制、16进制等。

这里笔者只做了%d%s\n\r的处理,其他的功能或者更还原的printf还需要朋友们自行修改。

7.6.2 调用重定向的printf和scanf

之前的章节,我们移植了稚晖君的重定向的printfscanf,我们可以直接调用这两个函数来发送和接收,指定格式字符串,和我们平时c语言的用法基本无异。

#include "retarget.h"
RetargetInit(&huart1);

huart1是我们在HAL库图形化界面配置的。
接下来直接愉快地使用printf和scanf:

char buf[100];
printf("\r\nYour name: ");
scanf("%s", buf);
printf("\r\nHello, %s!\r\n", buf);

也可以在断言机制里添加错误反馈了:

#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t* file, uint32_t line)
{
    printf("Wrong parameters value: file %s on line %d\r\n", file, line);
    while(1);
}
#end
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值