1.串行通信介绍

1.1TTL、RS232、RS485区别

特性 TTL串口 RS232串口 RS485串口
定义 逻辑电平标准,常用于MCU串口 串口通信的电气标准 差分信号串口通信标准
电平电压 逻辑“0”:0 ~ 0.5V;逻辑“1”:2.4 ~ 5.0V(一般3.3V或5V系统) 逻辑1:-3V ~ -15V(负电压),逻辑0:+3V ~ +15V(正电压) 差分信号,线间电压+/-1.5V以上
信号类型 单端信号 单端信号 差分信号
传输距离 短距离(通常几米以内) 短距离(通常15米以内) 远距离(可达1200米及以上)
通信模式 通常点对点,异步串行 点对点,异步串行 多点总线,半双工(全双工需特殊布线)
接口引脚 TXD、RXD、GND TXD、RXD、GND,且常带CTS、RTS等控制线 A线(DATA+)、B线(DATA-)、GND
驱动芯片 MCU直接输出 需要MAX232等电平转换芯片 需要RS485收发器芯片(如SN75176)
抗干扰性 低,易受电磁干扰 中,电平较高,抗干扰能力一般 高,差分信号强抗干扰、抗共模干扰
多机通信 不支持 不支持 支持多主多从总线通信
典型应用 MCU与模块间通信 PC串口通信、调试串口 工业控制、楼宇对讲、传感器网络
优点 简单,方便MCU直接接口 电平标准广泛,适合PC通信 远距离、抗干扰强、多机总线能力强
缺点 传输距离短,抗干扰弱 电平高需转换,线缆限制 需要额外收发器,布线复杂

抗干扰对比

  • TTL:单端信号,电压幅度小,容易受到电磁干扰,适合短距离。
  • RS232:单端信号,电压幅度大于TTL,抗干扰能力比TTL好,但仍受距离限制。
  • RS485:差分信号传输,信号通过两根线的电压差来识别,抗共模干扰能力强,适合工业环境和长距离。

1.2 常用的连接方式

下图为232和TTL电平互转(用MA3232 芯片对):
在这里插入图片描述
当然现在用的最多的还是USB转串口。

1.3 协议层时序

在这里插入图片描述
波特率:单位bit/s,比如9600,表示每秒传输1200字节,当然其中包含了起始位、停止位和校验位的数据,真正的数据是不到1200字节/秒的。

1.4 框图及寄存器

在这里插入图片描述

  • 数据寄存器USART_DR,包含了两个寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。真正的收发是需要移位寄存器配合使用的,发送时把 TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到 RDR。

  • 控制寄存器USART_CR1 各种中断的使能寄存器,比如发送完成中断使能TCIE、RXNEIE接收缓冲区非空中断使能、IDLE中断使能等等

  • 控制寄存器2USART_CR2 其他使能

  • 状态寄存器(USART_SR)【重要】

    • @arg UART_FLAG_CTS: CTS Change flag (not available for UART4 and UART5)
    • @arg UART_FLAG_LBD: LIN Break detection flag
    • @arg UART_FLAG_TXE: 发送数据寄存器空,数据从DR移走打上该标记
    • @arg UART_FLAG_TC: Transmission Complete flag
    • @arg UART_FLAG_RXNE: Receive data register not empty flag 读数据寄存器非空
    • @arg UART_FLAG_IDLE: Idle Line detection flag 监测到总线空闲
    • @arg UART_FLAG_ORE: Overrun Error flag 过载错误
    • @arg UART_FLAG_NE: Noise Error flag 噪声错误标志
    • @arg UART_FLAG_FE: Framing Error flag 帧错误
    • @arg UART_FLAG_PE: Parity Error flag 校验错误

    可通过__HAL_UART_GET_FLAG来判定这些标记!

2.工程搭建

采用野火-指南者-STM32F103板子进行测试。
MX配置如下:

2.0 SYS仿真器配置

在这里插入图片描述
在这里插入图片描述

注意,这里使用的是ST-LINK中的20引脚的JTAG连接到板子的JTAG口,这两种配置都是可以的!
【遇到的问题】启动调试后会卡主不进main函数。经过排查,网上说可能是BOOT引脚没有从FLASH启动,但最终发现可能是ST-LINK仿真器的事,重新拔插一下就好了=。=

J-Link仿真器可以对应JTAG口和SWD口两种模式,同样ST-Link仿真器也可以对应JTAG口和SWD口。

JTAG基本上带有5个引脚:

  • TDI:Test Data In,串行输入引脚
  • TDO:Test Data Out,串行输出引脚
  • TCK:Test Clock,时钟引脚
  • TMS:Test Mode Select,模式选择(控制信号)引脚
  • TRST:Test Reset,复位引脚

SWD引脚

  • SWDIO:Serial Wire Data Input Output,串行数据输入输出引脚
  • SWCLK:Serial Wire Clock,串行线时钟引脚

引脚复用
SWD的引脚在一定条件下可以和JTAG引脚复用,目前针对 JTAG 和 SWD的连接器比较多,比如20pin的接插件(有些是10pin)
在这里插入图片描述

SWD协议的优势

  • 使用引脚更少,只需SWDIO和SWCLK两个引脚
  • SWD具有特殊功能,例如通过其I / O线打印调试信息,此时PC上用专门的工具软件可查看打印信息。这样可以省掉一个串口
  • 与JTAG相比,SWD在速度方面具有更好的整体性能。

JTAG协议的优势

  • JTAG不仅限于ARM芯片,在ARM之外的芯片也受支持,比如大家熟悉的MSP430
  • JTAG具有更多多种用途,用于编程,调试和生产测试
  • JTAG是一个独立的团体,他们会随着协议的发展而发展

2.1 外部晶振

在这里插入图片描述

2.2 时钟树配置

在这里插入图片描述

2.3 串口UART

原理图,使用的是USB转串口,芯片是CH340G。使用PA10和PA9进行收发:
在这里插入图片描述
GPIO选择:
在这里插入图片描述
打上标签(USART_RX 和 USART_TX):
在这里插入图片描述
选择USART1,选择异步模式,波特率默认115200:
在这里插入图片描述

2.4 Project Manager

参考之前的连接:
https://blog.csdn.net/hao1183716597/article/details/144415565?spm=1011.2415.3001.5331

4.UART各种中断

添加中断处理函数,下面的勾上:
在这里插入图片描述
生成代码后,找到下面函数:
在这里插入图片描述
找到这个调用:
在这里插入图片描述

把这个weak函数去main中重新定义:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    	//待实现
  }
}

启用接收中断,在main函数中添加:

	char received_data;
    HAL_UART_Receive_IT(&huart1, &received_data, 1); 

HAL_UART_Receive_IT()中会启用UART_IT_ERR错误中断和UART_IT_RXNE接收中断:

HAL_StatusTypeDef UART_Start_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  //省略。。。
  /* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
  __HAL_UART_ENABLE_IT(huart, UART_IT_ERR);

  /* Enable the UART Data Register not empty Interrupt */
  __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);
  return HAL_OK;
}

4.1 串口接收中断RXNE

  • 每接收一个字节都会触发(这个在高速率通信时,根本不适用);
  • 当硬件接收寄存器RDR中的数据还未被软件读取时,新的数据又到来并写入RDR,导致上一字节数据被覆盖丢失,就会触发ORE错误中断;
  • 通过在main函数中调用HAL_UART_Receive_IT启用该RXNE中断;
  • 每次接收完一个字节的数据,必须重新调用HAL_UART_Receive_IT启用中断,因为回调HAL_UART_RxCpltCallback调用之前,会禁用相关中断:
    在这里插入图片描述

中断处理函数:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == &huart1)
  {
      // 判断是不是接收中断 - [这样判定不出,因为执行到此处,RXNE标记未知原因已经被清了!!]
      //if (__HAL_UART_GET_FLAG(huart, UART_FLAG_RXNE) != RESET)
      {
          char received_data = huart->Instance->DR; // 读取接收到的数据
          
          //数据处理
          
          //重新启动接收中断,准备接收下一个字节,如果不调用,就不会接收下一个字节了
          HAL_UART_Receive_IT(&huart1, (uint8_t*)&received_data, 1); 
      }
  }
}

这里本想通过判定UART_FLAG_RXNE标记来识别是接收的数据,结果实验发现该标记运行到此处一直是0。

4.2 串口空闲中断IDLE

触发时机:

当UART接收线上连续一段时间没有接收到任何数据位时,会触发空闲中断。
具体来说,当检测到接收线上“空闲”状态——即停止位之后没有新的数据开始传输时,且空闲时间至少超过一帧数据的时间(包括起始位、数据位、停止位)——就会触发空闲中断

什么时候使用?

主要用于接收不固定长度数据包的判断,主要配合DMA接收数据时使用!

启用空闲中断检测:

	//启用串口空闲中断
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

判定是否是空闲中断:

void USART1_IRQHandler(void)
{
      // 判断是否是串口空闲中断
      if(RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
      {
          printf("uart idle!\n");
      }  

      HAL_UART_IRQHandler(&huart1);
}

【注意】 空闲中断的判定,必须在HAL_UART_IRQHandler之前(或之后?),不能放到HAL_UART_RxCpltCallback接收中断回调中进行判定!
【推荐】如果配合DMA接收的话,还是得用定时器去配合检查DMA收到的数据并取出。

4.3 串口错误中断ORE

main函数:

	//启用串口错误中断
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_ERR);

判定:

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart == &huart1)
    {
        // 判断串口错误中断,处理溢出,比如复位接收、计数等
        if (RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_ORE))
        {
            __HAL_UART_CLEAR_OREFLAG(huart);
            printf("UART Overrun Error Occurred!\n");
        }
    }
}

**【注意】**必须使用HAL_UART_ErrorCallback中断回调来处理,也不能放到rx中断回调中处理。 使用完后需手动clear该标记,否则会一直触发。

【错误中断复现】
读取之前加上延时,这样连续发送两个字节的数据,会溢出。

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
    // 判断是否是串口空闲中断
      if(RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
      {
          printf("uart idle!\n");
      }
      
      //延时一会再读
      volatile int itick = 0; //用volatile修饰,不然该段被编译器优化掉
      while(1)
      {
          itick++ ;
          if (itick > 1000000) break;
       }
          
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

必须USART1_IRQHandler中加延时!注意千万别用hal_delay()延时,会卡死,因为systick中断优先级很低,卡在串口中断中,systick中断不会被执行,延时计数不会增加!
注意不能放到rx接收回调中加延时,因为那时候,DR寄存器起始已经被读取到内部的数据结构中了!

5.串口发送 - 阻塞和中断式

  • 阻塞发送
    发送函数会等待直到全部数据发送完成才返回,期间 CPU 被占用。一般在打印的fput.c中用。
	HAL_UART_Transmit(&huart1, data, length, timeout);
  • 中断发送
    可以发送一个数据块,异步发送,但底层靠TXE中断一个字节一个字节发送,效率极低。
    发送完成后调用 HAL_UART_TxCpltCallback()。
	HAL_UART_Transmit_IT(&huart1, data, length);
  • 查询式发送
    这里把打印printf内部调用的fputc函数重新一下:
int fputc(int ch, FILE *f)
{
    //HAL_UART_Transmit(&huart1,(uint8_t *)&ch, 1, HAL_MAX_DELAY);
    
    //查询方式发送
    //等待发送寄存器TXE为空
    while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET)
    {
    }
    //写入寄存器
    huart1.Instance->DR = ch;
    return ch;
}

就是查询UART_FLAG_TXE寄存器,被设置上表示发送完成,否则一直阻塞卡主。
当操作多个串口,但DMA不够用时,可以轮询多个串口进行查询式发送。

6. DMA介绍

DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。
无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。
STM32F1系列有一个DMA控制器,F4系列有两个DMA控制器。两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。

  • 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。
  • 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标。
  • 可编程的数据传输数目:最大为65535

在这里插入图片描述
可以看到,本次使用的USART1的收发分别对应通道5和通道4。

6.串口发送 - DMA

高波特率场景下,串口非常有必要使用DMA。

MX配置:
在这里插入图片描述
①:通过Add增加一个tx发送通道,方向是内存到外设
②:模式是标准模式,发送一次就停下,循环模式这里不适用(会循环着不停地发送)。
③:地址递增只有内存处递增,外设串口只有一个DR寄存器,不递增。数据宽度,这里受限于DR单字节,所以按字节发送。

生成代码后,串口初始化部分新增:

/* USART1 DMA Init */
    /* USART1_TX Init */
    hdma_usart1_tx.Instance = DMA1_Channel4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_tx.Init.Mode = DMA_NORMAL;
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);

比如我们定义两个buffer,第一次发送buffer1,发送完成后再发送buffer2:

// 两个发送缓冲区
uint8_t buffer1[] = "Hello, DMA Normal Mode!\r\n";
uint8_t buffer2[] = "Sending second buffer after first!\r\n";

main函数中新增:

//DMA发送 - 启动第一次传输
    HAL_UART_Transmit_DMA(&huart1, buffer1, sizeof(buffer1) - 1);

发送完成中断,当DMA发送完成后,会调用HAL_UART_TxCpltCallback弱引用函数,注意,该函数在DMA发送完成时或者HAL_UART_Transmit_IT()中断发送后进入,普通发送不会进入:

//发送完成中断
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart == &huart1)
    {
        //第二次发送
        static int is_cnt = 0; //保证只发一次,因为第二次发送还会进该回调
        
        if (is_cnt == 0)
        {
            HAL_UART_Transmit_DMA(&huart1, buffer2, sizeof(buffer2) - 1);
        }

        is_cnt++;
        
    }
    
}

7.串口接收 - DMA + 定时器方式

高波特率场景下,串口非常有必要使用DMA。
MX配置:
在这里插入图片描述
这里选择循环接收,使用循环模式,DMA 会不断从DR寄存器接收数据,无需软件频繁重启 DMA,避免数据丢失。
代码自动生成后新增:

 /* USART1_RX Init */
    hdma_usart1_rx.Instance = DMA1_Channel5;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);

启用DMA接收:

//串口DMA接收缓冲区
#define UART_RX_BUFFER_SIZE  8
uint8_t uart_rx_buffer[UART_RX_BUFFER_SIZE];
#define DMA_RECV 1 //是否DMA接收

main()
{
	//省略。。。
	//DMA接收
    HAL_UART_Receive_DMA(&huart1, uart_rx_buffer, UART_RX_BUFFER_SIZE);
}

定义全满和半满中断回调:

//串口接收中断或者DMA全满接收中断
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == &huart1)
  {
    #if(DMA_RECV==0)
    //中断接收
    
    #else
        //DMA接收,此时缓冲区满了
        int a = 1;//打断点查看
    #endif
      
  }
}

//DMA半满接收中断
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    // 这里是缓冲区接收一半时的回调
    if(huart == &huart1)
    {
        // 处理缓冲区前半部分数据
        #if(DMA_RECV==1)
        int a = 1; //打断点查看,缓冲区前半部分数据
        #endif
    }
}

测试的时候,因为定义的接收缓冲区为8字节,可以先发4字节(“abcd”),再发4字节(“1234”),再发4字节(“efgh”),分别打断点查看。

那么当收到的不满足半满也不满足全满条件时,怎么取出数据呢?
网络对此大多采用空闲中断,由于空闲中断的触发并没有那么理想,这里建议采用守护定时器的方式进行定时监控。

【关键点1】 如何判断DMA缓冲区中已经接收了多少数据?

//返回当前 DMA 还剩多少字节未接收
__HAL_DMA_GET_COUNTER(huart->hdmarx);
//下面就是已接收的数量
int irecved_len = UART_RX_BUFFER_SIZE -__HAL_DMA_GET_COUNTER(huart->hdmarx);  

【关键点2】 取出数据后如何“重置”DMA?

其实无论DMA中断到了还是定时器超时到了,只要新增一个变量:【记录的上次DMA缓冲区收到的数据下标】,在两个情况下数据被取出后,更新该变量即可。无需重置DMA。

【关键点3】 重置定时器时机
定时器的超时时间可随意设置,注意在半满或者全满后,重置定时器超时时间

最终的目的就是半满触发或者全满触发后,只收到一小部分数据,但后面收不到了,导致触发不了半满或者全满中断,此时定时器超时检查会将这部分数据给读出来!

8.实验 - 串口收发自环

经测试,当用中断方式收到一个字节,接着发送该字节时,第二次发送可能报忙失败,原因是收发波特率不是完全对齐的!。对接收来说,它是一直在接收,但发送因为进了接收中断,会有一点点延迟,如下图所示:

在这里插入图片描述
这里第二个或第三个接收中断跳进去,底层可能还在发送!

那么如何解决呢?
【方法一】收到后,同步等待TXE置位再写数据!问题是可能后续数据会丢

		// 收数据
		uint8_t received = (uint8_t)(huartx.Instance->DR & 0xFF);
		// 等待发送缓冲区空
        while (!__HAL_UART_GET_FLAG(&huartx, UART_FLAG_TXE))
        // 发送回复
        huartx.Instance->DR = response_byte; 

【方法二】收到后存储环形缓冲区,通过发送完成中断发送。问题是可能回复会滞后。
【方法三】通过DMA接收+DMA发送,在半满或全满或定时器超时中断后,取出DMA缓冲区数据,扔给发送缓冲区进行发送!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐