概念
USART是一种通用异步收发器。用于设备间的串行通信。USART一共有两根线,一根为Tx,用于发送数据,一根为Rx,用于接收数据。
USART是进行设备间通信的,可以是和板子上面的外设,也可以连接上位机进行通信。只要对方设备支持串口通信就可以。在连接中,一方的Tx连接另一方的Rx。双方GND都接地
USART通信特点
USART为串行,异步,全双工,1对1进行通信的。
串行和并行:比如现在有八个bite要进行发送,并行可以把八个bite放在八条线上之间一次性发送,串行则需要把八个bite一个个的放在一条线上进行发送。
同步和异步:同步通信可以通过时钟信号来规定,某一时刻接收什么样的数据,异步则是需要通过定义好,波特率,帧格式来进行限定。

我们在stm32中,可以之间观察是否有时钟信号,就可以判断为同步还是异步。注意这里的时钟信号和RCC配置的时钟树的时钟信号,是两个东西。
1v1,1v多和多v多:这里的为1对1,这个很好理解。后面我们还会学到I2C,SPI为一主多从的场景。
全双工,半双工,单工:这里的概念和计算机网络相同。全双工为双方没有限制,可以互发数据,半双工为可以互发数据,但是不能同时互相发送,单工为只能由一方向另一方发送数据。
MCU和串口
串口属于MCU上的片上外设。
我们可以看到,在APB1和APB2上均接有串口。但是我们也可以看到,有的叫USART,有的叫做UART,这两个有什么区别呢?
UART和USART
UART为通用异步收发器,USART为通用同步/异步收发器
但是我们在串口一般不使用同步,所以我们后续学习,认为两个相同。
4方关系
串口基本通信过程
这里我们就通过这条红线进行发送,但是有两个问题,第一,我怎么你到你发多块,MCU端数据已经发完了,但串口设备端才接收了两个比特。第二,我怎么知道你什么时候开始发送,什么时候结束。
对于第一个问题,我们就使用波特率来进行约束,约束它的发送速度是多少。什么是波特率呢,就是传码率,我一秒发送了多少个信号,但是一个信号不一定代表一个bite啊,所以波特率>=比特率但是在USART中,使用的是二进制码元,所以波特率=比特率。
对于第二个问题,就可以使用帧格式来进行约束。
我们设置空闲时为高电平,第一位起始位拉低电平一位,后开始发送数据,最后校验位可选,随后拉高电平,1,1.5,2电平的停止位。在实际开发中,一般是八位数据位加一位校验位。这里校验位分为奇校验,和偶校验。规则就是,数据位加校验位1的个数是奇数还是偶数。
USART简化原理结构图
这里我们主要关注5这个部分就行。其中1就是一些引脚,2是波特率控制相关,3是一些控制寄存器,4是状态寄存器相关,5是数据收发,6是智能卡模式才会使用。
我们把5简化一下就是这样的一个逻辑。然后TX和RX接到GPIO上,在接出外面。
发送的时候,数据先会放到TDR寄存器当中,然后由内部移到转移到移位寄存器,然后在发送控制器的控制下发送出去。接收的时候,先接收到接收移位寄存器,在由内部转移到RDR当中,随后读走数据。接收和发送都要靠时钟来进行控制。这里注意,TDR和RDR实际只有一共共用的DR寄存器。
与之前讲的一样,USART也是内部存在寄存器,我们还是通过控制寄存器来控制串口。
这个DR寄存器也解释了我们上面所说的内容。
这里有一个情况是,发送方确实按照约定好的波特率和帧格式进行发送,但接收方没有及时的取走数据,就会造成数据的丢失,比如发送方连续发送五个字节,接收方接受了一个字节后,代码停止了一秒,这个时候当第二字节把输入移位寄存器写完后,会把三四五字节丢弃,造成数据丢失。这种问题我们后面通过代码把全双工改为半双工就可以避免。这个问题不能靠寄存器来进行控制。还有一种问题如下。
发送方一直在往TDR中写,第一字节写完,移位到发送移位寄存器中,但没有完全发送,会不会第二字节给移上去,产生了覆盖,第二点,会不会在TDR中,直接产生覆盖,第三个问题,在读数据时,它怎么能确定,现在读的数据是不是接收的数据呢?显然这些问题发生在串口内部,可以通过寄存器来进行控制。
我们关注好5,6,7位,就可以解决上述问题。
第五位来解决第三个问题。当为0,表示没有数据不能读取。当为1,表示可以读取。第七位若为0,则表示现在DR中有数据,不能写入,1则表示现在可以写。第六位为0则表示没有发送完成,DR中数据不能移位。为1则表示发送完成,可以写入。像这样进行发送和接收,就是阻塞式发送和接收。
轮询模式数据收发代码和源码

我们这里使用CH340K来实现串口间的上位机和板子通信。
这里我们按照如上来进行配置。
cpp
while (1)
{
if (HAL_OK == HAL_UART_Receive(&huart1, &recv_byte, 1, HAL_MAX_DELAY))
{
// HAL_Delay(100); // bug测试
// 这里有坑,为了看到效果,不建议设置为HAL_MAX_DELAY,因为发送要等待TC==1,这样会
// 导致HAL_UART_Receive接收变慢,但是发送端是一直在发送的
// 这样可能会丢失数据,设置为0,直接交给硬件然后立即返回进行接收
HAL_UART_Transmit(&huart1, &recv_byte, 1, 0);
}
cnt++;
if (cnt >= 10) // 收到10个字节就翻转一下
{
cnt = 0;
HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
上位机我们使用comtool来进行通信,接收到什么就给返回什么。
首先我们需要明确usart依然是一个地址,然后转为了结构体,里面存着寄存器,不同的是,这里HAL对其还进行了一个封装,使用了句柄结构体进行了定义。
cpp
typedef struct
{
__IO uint32_t SR; /*!< USART Status register, Address offset: 0x00 */
__IO uint32_t DR; /*!< USART Data register, Address offset: 0x04 */
__IO uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */
__IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */
__IO uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */
__IO uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */
__IO uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;

这个就是句柄的定义,第一个参数就是usaet结构体,第二个是初始化配置信息,包含波特率,数据长度,停止位,校验位,收发模式等待。第三个是发送缓冲区指针,发送缓冲区要发送大小,还剩多少,和接收缓冲区指针,接收缓冲区接收总大小,还有多少要接收,接收类型,接收事件。
cpp
typedef enum
{
HAL_OK = 0x00U,
HAL_ERROR = 0x01U,
HAL_BUSY = 0x02U,
HAL_TIMEOUT = 0x03U
} HAL_StatusTypeDef;
这是四种返回数据,代表请求的类型。
cpp
/*-------------------------- USART CR2 Configuration -----------------------*/
/* Configure the UART Stop Bits: Set STOP[13:12] bits
according to huart->Init.StopBits value */
MODIFY_REG(huart->Instance->CR2, USART_CR2_STOP, huart->Init.StopBits);
初始化源码
cpp
#define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
#define USART_CR2_STOP_Pos (12U)
#define USART_CR2_STOP_Msk (0x3UL << USART_CR2_STOP_Pos) /*!< 0x00003000 */
#define USART_CR2_STOP USART_CR2_STOP_Msk
这个代码我们之前讲过,相当于把0,设置到12,13位
12,13位为00,表示停止位只有一个。配置的逻辑和之前基本是一样的。
HAL_UART_Receive源码
cpp
/* Init tickstart for timeout management */
tickstart = HAL_GetTick();
huart->RxXferSize = Size;
huart->RxXferCount = Size;
/* In case of 9bits/No Parity transfer, pRxData needs to be handled as a uint16_t pointer */
if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
{
pdata8bits = NULL;
pdata16bits = (uint16_t *) pData;
}
else
{
pdata8bits = pData;
pdata16bits = NULL;
}
这里,首先获得一个时间戳,来为后面超时作准备,随后把要接收的总大小和还没有接收到的大小设为传进来的值,随后,如果传入九位全为数据,则八个bit位装不下。
cpp
/* Check the remain data to be received */
while (huart->RxXferCount > 0U)
{
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK)
{
huart->RxState = HAL_UART_STATE_READY;
return HAL_TIMEOUT;
}
if (pdata8bits == NULL)
{
*pdata16bits = (uint16_t)(huart->Instance->DR & 0x01FF);
pdata16bits++;
}
else
{
if ((huart->Init.WordLength == UART_WORDLENGTH_9B) || ((huart->Init.WordLength == UART_WORDLENGTH_8B) && (huart->Init.Parity == UART_PARITY_NONE)))
{
*pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
}
else
{
*pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
}
pdata8bits++;
}
huart->RxXferCount--;
}
这里循环,首先看有没有接收完,然后进入等待。
cpp
#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR & (__FLAG__)) == (__FLAG__))

这里Flag就是RXNE这一位,要看能否读取数据,这一位自己为1,然后与上寄存器,如果相等,说明寄存器为1,则循环为0,跳出循环,下面检查超时,第一个为检查是否死等,还是定时,或者直接不等三种情况。然后这个代码出去,如果是不是HAL_OK则超时,否则读取DR寄存器数据,随后指针++,读取到下一缓存,然后最后huart->RxXferCount--;
我们之前说,外设都需要RCC配置时钟树来进行使能,当然,这个也不例外
对fgetc和fputc进行改写
我们对这两个进行改写后,我们就可以直接调用printf,scanf,fgets等直接进行串口上的通信。
cpp
int fputc(int ch, FILE *fp)
{
(void)fp;
if (ghusart != NULL)
{
if (HAL_UART_Transmit(ghusart, (uint8_t *)&ch, 1, HAL_MAX_DELAY))
{
return ch;
}
}
return EOF;
}
int fgetc(FILE *fp)
{
uint8_t ch;
UNUSED(fp);
if (ghusart != NULL)
{
while (1)
{
if (HAL_UART_Receive(ghusart, &ch, 1, HAL_MAX_DELAY) != HAL_OK)
{
return EOF;
}
if (ch != '\r')
{
return (int)ch;
}
}
}
else
{
return EOF;
}
}
这里记得启用MicroLIB
模块化多文件
我们前面编写代码都是只在main里面编写,现在我们要把外设给拉出来。



我们要先自己建立好这个模板,随后在Keil中进行配置
命名可以随意,但要符合逻辑
随后找到刚才的.c加入进来,在.c中包含.h
这里需要添加头文件搜索路径,因为我们这里的组,是一个逻辑概念,它和之前的那些idea不同,它不是按照windows下的文件夹顺序来排的,它是你自己组的逻辑结构,比如你在这里删除文件,但是文件其实还是存在的,只是在这个逻辑中删除了。此时直接编译.h文件就加进来了。
cpp
#ifndef __BSP_USART_REDIR_H
#define __BSP_USART_REDIR_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif
在头文件中加入这个代码,一个解决重复包含,第二个保证在c++下也可以编译。这样就可以进行多文件编写了。