一、USART与UART
在印象中uart和usart好像都是串口,但是为什么有分这两个东西呢?
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步串行接收/发送器相较于UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter)多了一个S,即synchronous(同步)。也就是说UART相较于USART只是少了一个同步方式而已,而串口在嵌入式中经常使用,但是我们一般使用UART就够了。
常见用途有:
1.作为调试口,打印程序运行的状态信息
2.连接串口接口的模块(比如GPS模块),传输数据
3. 通过电平转换芯片变为RS232/RS485电平,连接工控设备
二、USART框图
数据的接收与发送
1. 数据的接收:从Rx引脚进入的数据经过块再到达接收移位寄存器,接收到了8位数据一次性将数据发送到接收数据寄存器中,然后CPU就可以读取数据了。
2. 数据的发送:CPU将数据写入到发送数据寄存器中,写完后发送数据寄存器将8位数据传送给发送位移寄存器中,然后移位寄存器将这些数据一位的一位的发送出去。
时钟的来源:
时钟源 会先经过框1将时钟分割,再经过采样除法器进一步将时钟分频作为发送器时钟以及接收器时钟连接到接收器以及发送控制。而接收器控制和发送器分别控制接收位移寄存器以及发送移位寄存器达到控制传输数据速度。
框1是由传统波特率生成器控制的,其可以产生小数去分割时钟。
三、波特率的生成与计算
上述公式16是由于F1系列其OVER8为0:8*(2-OVER8)
这里介绍一下波特率寄存器:
其中4~15位用于设置USARTDIV的尾数也就是整数部分,比如37.5:将12位的值设置为37就可以了。而0.5则由低四位设置,也就是设置为8就行。为什么是8呢?0.5*16 =8,而16代表低四位也就2的四次方也就是是16的分辨率。注意,当OVER8为1时第3位必须为0,也就是低四位的最高位为0!!!
举个例子:假如串口时钟为90M,我们波特率设置为115200;
由上公式可以得: USARTDIV=90000000/(115200*16)= 48.828;
那么:
DIV_Fraction = 16*0.828 = 9 = 0x0D; DIV_Mantissa = 48 = 0x30;
这样我们就得到了USART1->BRR的值为0X30D
四、状态寄存器
五、串口收发
我们打开Cubemx,关于时钟以及下载口的设置这里就不说了,我的为F405芯片,时钟设置为84MHz。我们设置为异步模式,
为了方便观察,这里将PB1和PB2设置为输出模式控制LED,我们生成工程。
5.1.串口发送和接收一个数据
首先定义如上变量用来收发;再编写中断回调函数,判断是串口1的中断并进行相关操作(比如翻转LED)。最后记得重新开启中断(回调函数每调用一次,库的设置会自动将中断关闭,所以要再次开启),这里不需要清除中断标志位,进入中断回调函数前HALL库已经清零了。
在while前记得开启接收中断否则无法接收数据(CubeMx配置的串口初始化中并没有开启串口中断)!我们在while中当接收到数据也就是rx_flag为1时发送数据也就是字符a,我们等待发送完成并清除接收完成标志位(为什么是TC标志位可以看上文关于状态寄存器的介绍)。编译烧录下载就可以看见现象了。
串口重定向
/*--------------- 串口重定向函数 ---------------*/
//重定向c库函数printf到串口DEBUG_USART,重定向后可使用printf函数
int fputc(int c,FILE *f)
{
uint8_t ch[1]={c};
//发送一个字节数据到串口DEBUG_USART
HAL_UART_Transmit(&huart1,(uint8_t *)ch,1,0xFFFF);
return c;
}
//重定向c库函数scanf到串口DEBUG_USART,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
uint8_t ch;
//scanf()此函数读取数据应该以空格为结束
HAL_UART_Receive(&huart1,(uint8_t *)&ch,1,HAL_MAX_DELAY);
return ch;
}
注意:如果使用的不是串口1,则修改相应的串口句柄就可以了。使用串口重定向还需加头文件#include "stdio.h"以及勾选Use MicriLIB。
为什么要串口重定向?
因为单片机编程条件下没有现成的从串口发送出数据的函数,单片机是硬件,不同单片机由不同的寄存器配置方法将数据从串口发送出去。所以我们要对其中某些部分进行重写,把正在使用的单片机的串口输出的代码和printf融合在一起。printf其实底层也是调用了fputc来一个一个char进行输出的,所以我们要重写这个程序,这样printf调用fputc的时候,fputc就会执行我们写在里面的程序一个一个char的从单片机串口输出了。
5.2. 串口发送和接收字符串
串口每次接收到一个数据都会进入中断,而只有huart->RxXferCount减到0了才会进入中断回调函数。下面来介绍串口接收和发送字符串,
5.2.1串口发送字符串
char tx_buffer[11] = {"Hello World"};
HAL_UART_Transmit(&huart1,(uint8_t *)tx_buffer,sizeof(tx_buffer),50);//发送数据
HAL_Delay(500);
串口发送字符串很简单,定义一个数组为要发送的数据,然后利用阻塞式发送就可以了(其中sizeof可以获取数组长度)。或使用Printf发送:
uint8_t time=123;
printf("Time=%d\r\n",time);
printf("发送完成!\r\n");
//HAL_UART_Transmit(&huart1,(uint8_t *)tx_buffer,sizeof(tx_buffer),50);//发送数据
HAL_Delay(500);
5.2.2 串口接收字符串
将接收到的数据发送出去:
uint8_t rx_buffer[12];
HAL_UART_Receive_IT(&huart1,rx_buffer,sizeof(rx_buffer));
先在主函数外定义第一行代码,然后再main函数的while前开启中断(第二行),然后编写中断代码如下:可以发现实验结果并不是电脑发送一串字符串后立马收到单片机发过来的代码,我们定义的发送中断为12个字符,也就是我们发完6个字符并不会进入中断回调函数,只有接收到12个字符也就是我们设置中断的字符数才能正确收发,也就是使用前必须规定发送的字符长度!!!
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
HAL_UART_Transmit(&huart1,(uint8_t *)rx_buffer,sizeof(rx_buffer),0xffff);//发送数据
HAL_UART_Receive_IT(&huart1,rx_buffer,sizeof(rx_buffer));
}
}
5.2.3 串口接收任意字长字符串
我们修改接收数组长度,开启串口为空闲中断(关于串口的空闲中断:发送的两个字符之间间隔非常短,故在两个字符之间不叫空闲。空闲的定义是总线上在一个字节时间内没有再接收到数据。空闲中断就是检测到数据被接收后,总线上在一个字节时间内没有再接收到数据的时候发生的),并编写中断函数:
uint8_t rx_buffer[30];
HAL_UARTEx_ReceiveToIdle_IT(&huart1,rx_buffer,sizeof(rx_buffer));
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart->Instance == USART1)
{
HAL_UART_Transmit(&huart1,(uint8_t *)rx_buffer,Size,0xffff);//发送数据
HAL_UARTEx_ReceiveToIdle_IT(&huart1,rx_buffer,sizeof(rx_buffer));
}
}
六、串口DMA收发
6.1 Cubemx的配置
串口配置和上边一样,只是在串口位置开启了DMA,其DMA的传输通道和方向是默认值不能修改
传输请求优先级:
有低(Low)、中等(Medium)、高(High)、很高(Very High),请求优先级根据自己需要设置,优先权相等时由硬件决定(例:请求0优先于请求1,依次类推)。
关于DMA优先级:
软件阶段:寄存器配置, 有低(Low)、中等(Medium)、高(High)、很高(Very High)。
硬件阶段:编号低获得更高优先级。DMA1优先于DMA2,其次再是编号。
传输模式:
Normal:正常模式,当DMA数据传输完成后就会停止DMA传送,也就是只传输一次。
Circular:循环模式,当传输结束时,硬件自动会将传输数据量寄存器进行重装进行下一轮的数据传输,也就是多次传输
Increment Address:地址指针递增
Src Memory(外设地址寄存器):设置传输数据的时候外设地址是不变还是递增。如果设置为递增,那么下一次传输的时候地址加 Data Width个字节。
Dst Memory (内存地址寄存器):设置传输数据时候内存地址是否递增。如果设置为递增,那么下一次传输的时候地址加 Data Width个字节。这个Src Memory一样,只不过针对的是内存。
串口发送数据是将数据不断存进固定外设地址串口的发送数据寄存器(USARTx_TDR)。所以外设的地址是不递增。
而内存储器存储的是要发送的数据,所以地址指针要递增,保证数据依次被发出。
传输数据类型:
位(Bit): 比特为计算机的最小信息单位,只能储存0和1。
字节(Byte): 一个字节就是八位。
半字(Half Word): stm32为32位处理器,所以half word对应16位。
字(Word): 对于stm32为32位。两个字节为一个字,汉字的储存单位都是一个字。
我们根据数据大小选择对应Data Width,这里都选择Byte(串口发送寄存器只能存储8Bit)。假如我使用ADC进行数据采集,ADC精度位12位,那么我Data Width就选择Half Word就够用了。
1个字节(Byte)=8位(Bit)数据 一个字(Word)=4个字节(Byte)(32位系统中)
6.2 DMA发送和接收一个字符串
6.2.1 DMA发送字符串
将上述配置好后我们生成工程,定义发送数组变量,并将下列代码整合到工程中就可以看到现象:
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
char tx_buf[20];
/* USER CODE END PV */
//在main函数的while前调用,用于保存字符串到数组
strcpy(tx_buf,"Hello World");
//在while中调用此发送函数
HAL_UART_Transmit_DMA(&huart1,(uint8_t *)tx_buf,sizeof(tx_buf));
HAL_Delay(500);
6.2.2 DMA接收字符串
我们在中断回调函数中将接收到的数据发送出去,我们定义的长度为5,所以电脑发送Hello,然后电脑接收到也是Hello。注意接收完得将串口DMA接收开启,否则只能接收一次!因为上边RX DMA接收设置的为正常模式,换成循环模式就不用写HAL_UART_Receive_DMA(&huart1,(uint8_t *)rx_buf,sizeof(rx_buf));这行开启代码。
//在main函数外定义变量
char tx_buf[20];
char rx_buf[5];
//在while前写下面一行代码,开启DMA转运
HAL_UART_Receive_DMA(&huart1,(uint8_t *)rx_buf,sizeof(rx_buf));
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
HAL_UART_Transmit_DMA(&huart1,(uint8_t *)rx_buf,sizeof(rx_buf));
HAL_UART_Receive_DMA(&huart1,(uint8_t *)rx_buf,sizeof(rx_buf));
}
}
6.3 DMA不定长接收
对于Cubemx的配置还是不变,在stm32f4xx.h中添加如下代码
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
extern uint16_t rx_len;
extern uint8_t recv_end_flag;
/* USER CODE END PV */
/**
* @brief This function handles USART1 global interrupt.
*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
uint32_t temp;
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)==SET)//获取空闲中断标志位
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除空闲中断标志位
HAL_UART_DMAStop(&huart1);
// temp = __HAL_DMA_GET_COUNTER(&hdma_uart1_rx);//获取DMA中未传输数据的个数
temp = hdma_usart1_rx.Instance->NDTR;//读取NDTR寄存器获取DMA中未传输的数据个数
rx_len = 50 - temp;//获取数据长度
recv_end_flag = 1;//接收完成标志位
}
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
在main.c中:在main函数外定义如下变量,然后在while中添加下面(第二段)代码编译并下载。
/* USER CODE BEGIN PV */
char tx_buf[20];
char rx_buf[50];
uint16_t rx_len;
uint8_t recv_end_flag;
/* USER CODE END PV */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(recv_end_flag)
{
recv_end_flag = 0;
HAL_UART_Transmit_DMA(&huart1,(uint8_t *)rx_buf,rx_len);
rx_len = 0;
memset(rx_buf,0,rx_len);
HAL_UART_Receive_DMA(&huart1,(uint8_t *)rx_buf,sizeof(rx_buf));
}
}
/* USER CODE END 3 */
}
现象如下: