|
|
|
@ -548,4 +548,161 @@ int main(void) {
|
|
|
|
|
1. 这个简单的格式解析是通信当中的语法和意义;但是还有些不完善,会存在什么缺陷?
|
|
|
|
|
2. 应该如何解决这些问题?
|
|
|
|
|
|
|
|
|
|
# 5. DMA方式的串口通信
|
|
|
|
|
# 5. DMA方式的串口通信
|
|
|
|
|
|
|
|
|
|
轮询方式是主动的查询串口的状态,根据寄存器状态来进行收发的操作;这种方式效率比较低。中断方式效率较高,串口当有状态变化的时候会产生中断(中断函数的优先级永远高于主函数),进行对数据进行处理。但是中断方式其实还是一个字节一个字节的收发(看看[STM32串口通信](STM32串口通信)),没发送或者是接收一个字节都会产生中断。CPU在每个字节的收发后都要进行处理,这样的方式效率还是不够高。因此有了DMA方式。简单说来,DMA(Direct Memory Access)方式是外设与内存之间建立了一个联系,当有一组数据接收或者发送完成的时候才通知CPU进行处理(中断),这样会大家提高效率,减少CPU的占用时间。
|
|
|
|
|
|
|
|
|
|
## 5.1. DMA控制器概述
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
参考手册143页,描述了芯片有2个DMA控制器。
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
1. 具备两个DMA控制器:DMA1和DMA2,每个控制器有8个数据流,每个数据流可以响应16个来自外设或存储器的 DMA 请求。
|
|
|
|
|
2. 数据流(stream)是用于连接传输源和传输目标的数据通路,每个数据流可以配置为不同的传输源和传输目标。
|
|
|
|
|
3. DMA 控制器通过总线仲裁器来协调各个 DMA 请求的优先级。
|
|
|
|
|
4. 支持三种数据传输方向:从外设到存储器、从存储器到外设以及从存储器到存储器。
|
|
|
|
|
5. 具备16字节的 FIFO。使能 FIFO功能后,源数据先送人FIFO,达到 FIFO 的触发阈值后,再传送到目标地址。
|
|
|
|
|
|
|
|
|
|
## 5.2. DMA的接口函数
|
|
|
|
|
|
|
|
|
|
### 5.2.1. 串口DMA 方式发送函数(HAL_UART_Transmit_DMA)
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
### 5.2.2. 串口DMA方式接收函数(HAL_UART_Receive_DMA)
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
### 5.2.3. DMA数据项数读取函数(__HAL_DMA_GET_COUNTER)
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
### 5.2.4. 串口DMA传输停止函数(HAL_UART_DMAStop
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
## 5.3. 利用空闲中断和 DMA 实现不定长数据的接收
|
|
|
|
|
|
|
|
|
|
什么是空闲中断?
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
串口拉低进行通信;每个字符间有一个停止位(高);当检查到停止位有两个以上的长度时表示线路Idle;或者说数据不连续。
|
|
|
|
|
|
|
|
|
|
任务描述:
|
|
|
|
|
|
|
|
|
|
使用空闲中断,接收一组连续的数据,然后把这些数据返回。因为框架并没有提供idle中断,因此需要修改一下中断处理函数。
|
|
|
|
|
|
|
|
|
|
### 5.3.1. 开发板配置
|
|
|
|
|
|
|
|
|
|
1. 设置串口,与以前的一致,需要使能串口2:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
2. 设置DMA
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
点击1位置,添加RX的DMA支持(2条目),该配置后面的(Channel,Direction,Priority)保持缺省就可以了。Channel是固定的,这里不能调整;Direction,因为是读取,肯定是外设到内存,因此也不能调整;Priority是用于DMA仲裁的,因为这里只有一个DMA,因此任意值都没有关系。
|
|
|
|
|
|
|
|
|
|
3位置用于设置 DMA 工作模式,分为Normal和 Circular 两种模式。在Normal模式下,当DMA传输结束时,即指定数量的数据已经完成传输,将不再产生 DMA操作。如果需要再次传输数据,需要在关闭DMA数据流的情况下,重新启动DMA 传输。这种方式相当于单次传输方式。
|
|
|
|
|
|
|
|
|
|
在 Circular 模式下,当前的 DMA 传输结束时,传输数据的数量将自动重载为初值,数据缓冲区重新指向首地址,只要有数据就会继续DMA传输。这种方式相当于连续传输方式。Circular模式主要用于连续、大批量的数据传递,如 A/D转换中的连续转换模式。而这里的不定长数据接收任务,无法预知一帧数据的字节数。当利用 IDLE 中断接收完一帧数据后,需要暂停 DMA 传输,获取该帧数据的字节数,然后重新启动 DMA 传输,因此选择Normal 模式。
|
|
|
|
|
|
|
|
|
|
4、5位置的Increment Address:用于设置地址递增。外设作为单一设备,地址固定,因此,地址设置为不递增。而存储器包括多个存储单元,完成一个数据传输后,地址应该自动递增,指向下一个待传输数据的地址,因此设置为自动增加。
|
|
|
|
|
|
|
|
|
|
5位置的数据宽度:用于设置数据宽度,有Byte(字节)HalfWord(2字节)和Word(4字节)三种选择。由于串口一次只能收发一字节,因此选择 Byte。
|
|
|
|
|
|
|
|
|
|
3. 设置串口2的全局中断,以便可以捕获IDLE中断。
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
### 5.3.2. 代码
|
|
|
|
|
|
|
|
|
|
在main.c的大约22行加入:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
/* USER CODE BEGIN Includes */
|
|
|
|
|
#include <string.h>
|
|
|
|
|
/* USER CODE END Includes */
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
因为等一下要用到 strlen函数,因此加入 string.h 的引用。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
在main.c的大约60行加入:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
/* USER CODE BEGIN 0 */
|
|
|
|
|
#define LENGTH 100 // 接收缓冲的长度,需要不小于最大帧(连续数据)的长度
|
|
|
|
|
uint8_t RxBuffer[LENGTH]; // 接收缓冲,也用于发送
|
|
|
|
|
uint8_t RxCount = 0; // 接收到的数据长度
|
|
|
|
|
|
|
|
|
|
volatile uint8_t RxFlag = 0; // 自定义 Idle 中断标志
|
|
|
|
|
|
|
|
|
|
const char *MSG1 = "Hello";
|
|
|
|
|
|
|
|
|
|
/* USER CODE END 0 */
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里主要是变量的定义,注意 RxFlag 这个变量设置成 volatile ,因为在中断中需要对这个变量进行写,在main.c中需要对这个变量进行读。
|
|
|
|
|
|
|
|
|
|
main.c的大约100行的main函数中:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
/* USER CODE BEGIN 2 */
|
|
|
|
|
HAL_UART_Transmit(&huart2, (uint8_t*) MSG1, strlen(MSG1), 100); // 发送提示
|
|
|
|
|
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
|
|
|
|
|
HAL_UART_Receive_DMA(&huart2, RxBuffer, LENGTH); // 开启DMA接收
|
|
|
|
|
/* USER CODE END 2 */
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
首先发送欢迎消息,然后启动串口2的IDLE中断,最后启用串口的DMA的接收。
|
|
|
|
|
|
|
|
|
|
在main函数的循环中:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
while (1) {
|
|
|
|
|
/* USER CODE END WHILE */
|
|
|
|
|
|
|
|
|
|
/* USER CODE BEGIN 3 */
|
|
|
|
|
if (RxFlag) { // 坚持到IDLE中断,表示一帧数据接收完成
|
|
|
|
|
RxFlag = 0; // 重新设置自定义中断标志
|
|
|
|
|
HAL_UART_DMAStop(&huart2); // 停止DMA 接收,因为DMA可能没有接收满
|
|
|
|
|
RxCount = LENGTH - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);// 计算收到数据的长度
|
|
|
|
|
HAL_UART_Transmit(&huart2, RxBuffer, RxCount, 100); // 发送接收到的数据
|
|
|
|
|
// 书上有把缓冲区重新填写为0的代码,其实是不需要的
|
|
|
|
|
HAL_UART_Receive_DMA(&huart2, RxBuffer, LENGTH); // 重新开启DMA接收
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
循环判断 RxFlag 标志,该标志是在串口中断服务中设置;检查到该标识表示已经接收完连续的一组数据;然后关闭DMA,发送刚刚收到的数据,最后在开始DMA读取。
|
|
|
|
|
|
|
|
|
|
以上是main.c的代码,下面看看stm32f1xx_it.c的代码。
|
|
|
|
|
|
|
|
|
|
大约在60行左右:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
/* USER CODE BEGIN EV */
|
|
|
|
|
extern volatile uint8_t RxFlag; // RxFlag 在 main.c 中定义,这里只是引用
|
|
|
|
|
/* USER CODE END EV */
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
因为FxFlag是在main.c中定义的,这里也是需要使用的,所以声明成外部变量。
|
|
|
|
|
|
|
|
|
|
在大约204行的串口中断服务中加入以下代码:
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
/* USER CODE BEGIN USART2_IRQn 1 */
|
|
|
|
|
if (__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE) != RESET) {
|
|
|
|
|
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
|
|
|
|
|
RxFlag = 1;
|
|
|
|
|
}
|
|
|
|
|
/* USER CODE END USART2_IRQn 1 */
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里判断串口的IDLE标志,如果是真,就设置FxFlag为1,以便main函数使用。
|