文章目录
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 中文参考手册》。
关于串口设置的一般步骤可以总结为如下几个步骤:
- 串口时钟使能,GPIO 时钟使能。
- 设置引脚复用器映射:调用 GPIO_PinAFConfig 函数。
- GPIO 初始化设置:要设置模式为复用功能。
- 串口参数初始化:设置波特率,字长,奇偶校验等参数。
- 开启中断并且初始化 NVIC,使能中断(如果需要开启中断才需要这个步骤)。
- 使能串口。
- 编写中断处理函数:函数名格式为 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传输方式:
- 外设到内存
- 内存到外设
- 内存到内存
- 外设到外设
DMA传输参数:
我们知道,数据传输,首先需要的是
- 数据的源地址
- 数据传输位置的目标地址
- 传递数据多少的数据传输量
- 进行多少次传输的传输模式
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 的配置步骤:
- 使能 DMA1 时钟
__HAL_RCC_DMA1_CLK_ENABLE();
- 初始化 DMA1 数据流 4,包括配置通道,外设地址,存储器地址,传输数据量等。
在这里的图像化配置会让代码多一些关于DMA的东西,就比如
__HAL_LINKDMA(&UART1_Handler,hdmatx,UART1TxDMA_Handler);
这 句 话 的 含 义 就 是 把 UART1_Handler 句 柄 的 成 员 变 量 hdmatx
和 DMA 句 柄
UART1TxDMA_Handler
连接起来,是纯软件处理,没有任何硬件操作。
这里我们就点到为止,大家可以自行查阅相关HAL库代码和STM32寄存器。
- 使能串口 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);//恢复
- 使能 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 传输了。
- 查询 DMA 传输状态
在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的方法是:
__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TCIF3_7)
获取当前传输剩余数据量:
__HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler);
DMA 相关的库函数我们就讲解到这里,大家可以查看固件库中文手册详细了解。
- 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只能发送一次的原因之一
- 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
之前的章节,我们移植了稚晖君的重定向的printf
和scanf
,我们可以直接调用这两个函数来发送和接收,指定格式字符串,和我们平时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