STM32-USART串口

概念

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++下也可以编译。这样就可以进行多文件编写了。

相关推荐
hongmai6668881 小时前
耐高温小钢炮:ESP32-C3-MINI-1U-H4模组选型分享
笔记·单片机·嵌入式硬件·物联网·risc-v
a1117762 小时前
工业视觉软件入门:VisionMaster(VM)、HALCON、VisionPro
嵌入式硬件·数码相机
Kisorge3 小时前
【电机控制器】 基于STSPIN32G4的FOC控制
stm32·嵌入式硬件·算法
LCMICRO-133108477463 小时前
国产长芯微LD3092完全pin-pin替代LT3092,是一款可编程两端电流源
arm开发·单片机·嵌入式硬件·fpga开发·硬件工程·恒流源·lt3092
nuoxin1143 小时前
BL-M8812CU3 无线 WiFi 模块-富利威
人工智能·嵌入式硬件·fpga开发·硬件工程·dsp开发
z20348315204 小时前
Livox Mid-360S点云图录制
嵌入式硬件·雷达·ros2
JNX_SEMI5 小时前
Hi5000Q/H 6.5~75V宽输入——与H5227A完全对等,无缝适配现有电源方案-聚能芯-智芯官方授权一级代理
驱动开发·单片机·嵌入式硬件·物联网·硬件工程
哥不想学算法5 小时前
STM32零基础教程(五):UART串口通信与协议设计
stm32·单片机·嵌入式硬件
WWJA王文举5 小时前
STM32时钟树怎么看?一步理解SYSCLK、HCLK、PCLK和外设时钟配置
stm32·单片机·嵌入式硬件