串口通信操作方法及三种实现方式(基于百问网DshanMCU-F103)

UART全称为通用异步收发器,英文全称(Universal Asynchronous Receiver/Transmitter)。是一种串行、异步、全双工的通信协议。

一、使用方法

UART首先将接收到的并行数据转换成串行数据来传输,消息帧从一个低位起始位开始,后面是5-8个数据位,一个可用的奇偶位和一个或几个高低停止位。

数据传输模拟图

发送过程:接收器发现开始位时它就知道数据准备发送,并尝试与发送器时钟频率同步。如果选择了奇偶,UART就在数据位后面加上奇偶位。奇偶位可用来帮助校验错误。

接收过程:UART从消息帧中去掉起始位和结束位,对进来的字节进行奇偶校验,并将数据字节从串行转换成并行。UART也产生额外的信号来指示发送和接收的状态。例如,如果产生一个奇偶错误,UART就置位奇偶标志。

在UART通讯协议中信号线上的状态位高电平表示‘1’低电平代表‘0’,特点是:通信线路简单,只要一对传输线就可以实现双向通信,大大降低了成本,但传输速度慢,传输距离短。

二、UART数据协议

在串口通信中,最主要的是数据流以及波特率。在传输过程中,需要关注的是起始位、数据位、校验位、停止位、波特率

起始位:通信线路空闲时为“1”,当检测到下降沿时,认为数据开始传输;

有效数据位:传输开始后传递需要发送和接收的数据值,可以是指令或者数据;

校验位:一般为奇偶校验,即通过来校验传输数据中‘1’的个数为奇数个奇校验)或者是偶数个偶校验)来表示传输的数据正确与否;

停止位:数据传输结束后,传输线恢复成‘1’状态;

波特率:1秒内传输信号的状态数(波形数)。比特率:1秒内传输数据的bit。如果是一个波形,能表示N个bit,那么,波特率 * N = 比特率。

三、STM32CubeMX配置USART

在CubeMX新建项目选择STM32F103C6开发板,配置USART1,配置使用Asynchronous异步模式,如下图:

实现中断方式需要选择NVIC配置,将Enabled选项勾选,如下图:

实现DMA方式,需要在usart1中作如下设置:

四、代码实现

/*定义一个huart1变量*/
UART_HandleTypeDef huart1;
​
/* USART1 init function */
​
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();
  }
​
}
​
/*PA9为复用    推挽输出模式、PA10为浮空输入模式*/
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
​
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */
​
  /* USER CODE END USART1_MspInit 0 */
    /* USART1 clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();
​
    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
​
    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
​
  }
}
​
​

1、轮询收发方式

在主函数中调用HAL_UART_Transmit即可在串口调试助手显示出我们定义的字符串变量。

/*函数原型HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)*/
​
  /* USER CODE BEGIN 1 */
    extern UART_HandleTypeDef huart1;
    char *str1 = "I from www.100ask.net\r\n";
    char *message = "I am DshanMCU-F103\r\n";
    char c;
  /* USER CODE END 1 */
​
HAL_UART_Transmit(&huart1, (uint8_t *)message, strlen(message), 100);
HAL_UART_Transmit(&huart1, (uint8_t *)str1, strlen(str1), 100);
​
​
实验现象

2、中断收发方式

中断方式实现串口通信需要对NVIC进行设置,对内置函数不能之间修改,我们需要自己作函数定义,通过使用环形缓冲区来实现多数据的传输,避免数据丢失,将数据存储入环形缓冲区,从而实现串口通信。

uart.c

/* USER CODE BEGIN 0 */
#include "circle_buffer.h"
/* USER CODE END 0 */
​
/* USER CODE BEGIN 1 */
static volatile int g_tx_cplt = 0;
static volatile int g_rx_cplt = 0;
static uint8_t g_RecvBuf[100];
static uint8_t g_RecvChar;
static circle_buf g_uart1_rx_bufs;
​
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    g_tx_cplt=1;
}
​
void Wait_Tx_Complete(void)
{
    while(g_tx_cplt == 0);
    g_tx_cplt = 0;
}
​
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    g_rx_cplt=1;
    
    circle_buf_write(&g_uart1_rx_bufs, g_RecvChar);
    
    /*re-enable rxne interrupt*/
    HAL_UART_Receive_IT(&huart1, &g_RecvChar, 1);
}
​
void Wait_Rx_Complete(void)
{
    while(g_rx_cplt == 0);
    g_rx_cplt = 0;
}
​
void StartUARTRecv(void)
{
    /*init circle_buffer 用于存储数据 */
    circle_buf_init(&g_uart1_rx_bufs, 100, g_RecvBuf);
    /*enable rxne interrupt*/
    HAL_UART_Receive_IT(&huart1, &g_RecvChar, 1);
}
​
int UARTGetChar(uint8_t *pVal)
{
    return circle_buf_read(&g_uart1_rx_bufs, pVal);
}
​
/* USER CODE END 1 */
​

circle_buffer.h

#ifndef _CIRCLE_BUF_H
#define _CIRCLE_BUF_H
​
#include <stdint.h>
​
typedef struct circle_huf{
        uint32_t r;
        uint32_t w;
        uint32_t len;
        uint8_t *buf;
}circle_buf,*p_circle_buf;
​
void circle_buf_init(p_circle_buf pCircleBuf,uint32_t len,uint8_t *buf);
​
int circle_buf_read(p_circle_buf pCircleBuf,uint8_t *pVal);
​
int circle_buf_write(p_circle_buf pCircleBuf,uint8_t val);
#endif 
​

circle_buffer.c

#include "stdint.h"
#include "circle_buffer.h"
/*
环形缓冲区初始化,起始为空
三个参数分别是指向环形缓冲区结构体的指针 缓冲区长度 指向缓冲区内存的的指针
配置传入的长度和缓冲区地址赋值给结构体成员变量
*/
void circle_buf_init(p_circle_buf pCircleBuf,uint32_t len,uint8_t *buf)
{
    pCircleBuf->r = pCircleBuf->w = 0;
    pCircleBuf->len = len;
    pCircleBuf->buf = buf;
}
/*
函数功能:从环形缓冲区中读取数据的函数
参数:指向环形缓冲区结构体的指针,存储读取数据的指针pVal
功能解释:r!=w时 有数据可读,读取当前位置数据,读指针向后移动一位
成功读取返回为0,返回-1缓冲区为空
**/
int circle_buf_read(p_circle_buf pCircleBuf,uint8_t *pVal)
{
    if(pCircleBuf->r != pCircleBuf->w){
        *pVal = pCircleBuf->buf[pCircleBuf->r];
        
        pCircleBuf->r++;
        
        if(pCircleBuf->r == pCircleBuf ->len)
            pCircleBuf->r=0;
        return 0;
    }
    else
    {
        return -1;
    }
    
}
/**
函数功能:向缓冲区写入数据函数
参数:指向缓冲区的指针  写入的数据val
功能解释:如果next_w==w,表示缓冲区已满,无法写入返回-1
若返回0表示写入成功
**/
int circle_buf_write(p_circle_buf pCircleBuf,uint8_t val)
{
    uint32_t next_w;
    
    next_w = pCircleBuf->w + 1;
    if(next_w == pCircleBuf->len)
        next_w = 0;
    if(next_w != pCircleBuf->r)
    {
        pCircleBuf->buf[pCircleBuf->w] = val;
        pCircleBuf->w = next_w;
        return 0;
    }
    else
    {
        return -1;
    }
}

main.c

/* USER CODE BEGIN 1 */
extern UART_HandleTypeDef huart1;
char *str2 = "please enter a char:\r\n";
char c;
/* USER CODE END 1 */
​
/* USER CODE BEGIN PV */
/*函数声明*/
void Wait_Tx_Complete(void);
void Wait_Rx_Complete(void);
void StartUARTRecv(void);
int UARTGetChar(uint8_t *pVal);
/* USER CODE END PV */
​
/*enable rxne interrupt*/
StartUARTRecv();
​
while (1)
  {
      HAL_UART_Transmit_IT(&huart1, (uint8_t *)str2, strlen(str2));
      /*wait for tc*/
      Wait_Tx_Complete();
      
      while(UARTGetChar(&c) != 0);
    
      c+=1;
      
      HAL_UART_Transmit(&huart1, &c, 1, 100);
      HAL_UART_Transmit(&huart1, "\r\n", 2, 100);
    /* USER CODE END WHILE */
  }
实验现象

3、DMA方式

只是简单使用DMA方式的话,在配置好USART的DMA设置之后,更改函数HAL_UART_Transmit_IT为HAL_UART_Transmit_DMA就可以实现函数功能了。使用DMA方式配合IDLE空闲函数的话,能够实现它的功能。

IDLE,空闲的定义是:总线上在一个字节的时间内没有再接收到数据。 UART 的 IDLE 中断何时发生?RxD 引脚一开始就是空闲的啊,难道 IDLE 中断一直产生? 不是的。当我们使能 IDLE 中断后,它并不会立刻产生,而是:至少收到 1 个数据后,发现 在一个字节的时间里,都没有接收到新数据,才会产生 IDLE 中断。

uart.c

/* USER CODE BEGIN 1 */
static volatile int g_tx_cplt = 0;
static volatile int g_rx_cplt = 0;
//static uint8_t g_RecvChar;
static uint8_t g_RecvTmpBuf[10];
static uint8_t g_RecvBuf[100];
static circle_buf g_uart1_rx_bufs;
​
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    g_tx_cplt = 1;
}
​
void Wait_Tx_Complete(void)
{
    while (g_tx_cplt == 0);
    g_tx_cplt = 0;
}
​
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    g_rx_cplt = 1;
    
    /* put data to circle buffer */
    for (int i = 0; i < 10; i++)
    {
        circle_buf_write(&g_uart1_rx_bufs, g_RecvTmpBuf[i]);
    }
    
    /* re-use dma+idle to recv */
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_RecvTmpBuf, 10);
}
void Wait_Rx_Complete(void)
{
    while (g_rx_cplt == 0);
    g_rx_cplt = 0;
}
​
void StartUART1Recv(void)
{
    /* init circle buffer */
    circle_buf_init(&g_uart1_rx_bufs, 100, g_RecvBuf);
    
    /* use dma+idle to recv */
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_RecvTmpBuf, 10);
}
​
int UART1GetChar(uint8_t *pVal)
{
    return circle_buf_read(&g_uart1_rx_bufs, pVal);
}
​
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    for (int i = 0; i < Size; i++)
    {
        circle_buf_write(&g_uart1_rx_bufs, g_RecvTmpBuf[i]);
    }
​
    /* re-use dma+idle to recv */
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_RecvTmpBuf, 10);
}
​
/* USER CODE END 1 */
​

main.c

int main(void)
{
  /* USER CODE BEGIN 1 */
    extern UART_HandleTypeDef huart1;
    char *str1 = "I from www.100ask.net\r\n";
    char *message = "I am DshanMCU-F103\r\n";
    char *str2 = "please enter a char:\r\n";
    char c;
    
  /* USER CODE END 1 */
​
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
​
  /* USER CODE BEGIN Init */
​
  /* USER CODE END Init */
​
  /* Configure the system clock */
  SystemClock_Config();
​
  /* USER CODE BEGIN SysInit */
​
  /* USER CODE END SysInit */
​
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_I2C1_Init();
  MX_USART1_UART_Init();
​
​
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  
    /*enable rxne interrupt*/
    StartUART1Recv();
  
    HAL_UART_Transmit(&huart1, (uint8_t *)message, strlen(message), 100);
    HAL_UART_Transmit(&huart1, (uint8_t *)str1, strlen(str1), 100);
    
  while (1)
  {
      HAL_UART_Transmit_DMA(&huart1, (uint8_t *)str2, strlen(str2));
      
      while(0 != UART1GetChar(&c));
      
      c+=1;
      
      HAL_UART_Transmit(&huart1, &c, 1, 100);
      HAL_UART_Transmit(&huart1, "\r\n", 2, 100);
}
实验现象

五、总结

①.UART协议的优点

  • 简单的硬件需求:只需要两根数据线,一个用来发送(TX),一个用来接收(RX),由于是异步通信,不需要额外的时钟信号线,进一步减少了硬件需求。

  • 全双工通信能力:UART允许设备在一根线上发送数据,同时在另一根线上接收数据,实现全双工通信,提高了数据传输效率。

  • 配置灵活性:用户可以根据需求配置不同的波特率和数据格式,提高了灵活的通信速率和适应不同通信场景的能力。

  • 错误检测和处理机制:UART通过奇偶校验机制能够检测单个比特的错误,确保数据传输的正确性,并能够指示帧错误、奇偶校验错误等多种错误状态,有助于及时发现并处理通信中的问题。

  • 中断驱动的操作方式:支持中断驱动的操作模式,可以在不占用CPU的情况下进行数据传输,从而释放CPU资源执行其他任务。

②.UART协议的缺点

  • 数据传输速率较低:与诸如SPI或I2C等其他串行通信协议相比,UART的数据传输速率通常较低,这限制了其在高速数据传输需求场合的应用。

  • 数据丢失问题:当接收端的数据缓冲区已满而无法及时处理新数据时,可能会发生数据丢失现象,这在一些高速或高容量数据传输应用中可能成为瓶颈。

  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值