第3章
CHAPTER 3


串 口 开 发







作为MCU的重要外部接口,串口同时也是软件开发重要的调试手段。通用同步异步收发器(USART)能够灵活地与外部设备进行全双工数据交换,满足外部设备对工业标准 NRZ 异步串行数据格式的要求。USART 通过小数波特率发生器提供了多种波特率,通过配置多个缓冲区使用DMA可实现高速数据通信。

串口主要有轮询、中断、DMA三种通信方式,本章逐一介绍其相关原理并通过实例帮助读者掌握串口开发能力。



视频7

3.1串口通信: 轮询
学习目标
了解串口的工作原理、ARM CortexM系列芯片的串口的分类,通过配置STM32F407芯片的串口外设来实现数据的收发。

3.1.1开发原理

任何 USART 双向通信均需要至少两个引脚: 接收数据输入引脚(RX)和发送数据输出引脚(TX)。在同步模式下,需要使用时钟输出引脚(CK),在硬件流控制模式下,需要使用清除发送引脚(CTS)和发送请求引脚(RTS)。

1. STM32中串口通信数据包

串口通信的数据包由发送设备通过自身的TXD接口传输到接收设备的RXD接口,通信双方的数据包格式要一致才能正常收发数据,STM32中串口通信数据包的内容有起始位、数据位、奇偶校验位、停止位。

1) 起始位和停止位

串口通信的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一个逻辑0的数据位表示,而数据包的停止信号可由 0.5、1、1.5或 2个逻辑 1的数据位表示,只要双方约定一致即可。

2) 数据位

在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为8或9位长。

3) 奇偶校验位

在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。有无奇偶校验位数据帧格式如表31所示。


表31奇偶校验位



数 据 长 度奇 偶 校 验数据帧格式


8无起始位+8位数据+停止位
8有起始位+7位数据+校验位+停止位
9无起始位+9位数据+停止位
9有起始位+8位数据+校验位+停止位


奇校验是指有效数据和校验位中1的个数为奇数,比如一个8位字长的有效数据为00110101,此数据总共有4个1,为达到奇校验效果,校验位为1,最后传输的数据将是8位的有效数据加上1位的校验位总共9位。

偶校验与奇校验要求刚好相反,要求帧数据和校验位中1的个数为偶数,比如00110101,此数据帧1的个数为4个,所以偶校验位为0。

4) 波特率

本节主要讲解的是串口异步通信,异步通信中由于没有时钟信号,所以两个通信设备之间约定好波特率,即每个码元的长度,以便对信号进行解码,常见的波特率为4800bps、9600bps、57600bps、115200bps等。

USART功能框图如图31所示。



图31USART功能框图






5) 功能引脚

 TX: 发送数据输出引脚。

 RX: 接收数据输入引脚。

 SW_RX: 数据接收引脚,只用于单线和智能卡模式,属于内部引脚没有具体外部引脚。

 nRTS: 发送数据请求引脚,用于指示UART已经准备好接收数据。低电平有效,该引脚只适用于硬件流控制。

 nCTS: 清除发送引脚,用于在当前传输结束时阻止数据发送。低电平有效,该引脚只适用于硬件流控制。

STM32F407ZGT6芯片的USART引脚分布如图32所示。

STM32F407ZGT6有4个USART和2个UART,其中USART1和USART6的时钟来源于APB2总线时钟,其最大频率为84MHz,其他4个的时钟来源于APB1总线时钟,其最大频率为42MHz。

UART只是异步传输功能,所以没有SCLK、nCTS和nRTS 功能引脚。

6) 数据寄存器

USART_DR包含了已发送的数据或者接收到的数据。USART_DR 实际上包含了两个寄存器: 一个专门用于发送的可写 TDR,另一个专门用于接收的可读 RDR。当进行发送操作时,向USART_DR写入的数据会自动存储在TDR内; 当进行读取操作时,从USART_DR读取数据时会自动提取RDR数据。

TDR和RDR都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把TDR的内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位数据保存在接收移位寄存器内,然后才转移到RDR。

7) 控制器

USART有专门控制发送的发送器、控制接收的接收器,还有唤醒单元、中断控制等。使用USART之前需要将USART_CR1寄存器的UE位置1使能USART。发送或者接收数据






APB2(最高84MHz)APB1(最高42MHz)
USART1USART6USART2USART3USART4USART5



TXPA9/PB6PC6/PG14PA2/PD5PB10/PD8/PC10PA0/PC10PC12
RXPA10/PB7PC7/PG9PA3/PD6PB11/PD9/PC11PA1/PC11PD2
SCLKPA8PG7/PC8PA4/PD7PB12/PD10/PC12——
nCTSPA11PG13/PG15PA0/PD3PB13/PD11——
nRTSPA12PG8/PG12PA1/PD4PB14/PD12——



图32USART引脚分布


字长可选8位或9位,由USART_CR1的M位控制。

8) 波特率生成

波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示。比特率指单位时间内传输的比特数。对于USART波特率与比特率相等,波特率越大,传输速率越快。波特率的常用值有2400bps、9600bps、19200bps、115200bps。

串口通信有3种方式用来处理数据,包括轮询方式、中断方式、DMA方式。本节只介绍轮询方式。轮询方式是指程序中循环查询串口寄存器,如果寄存器接收到数据,则进行相应的数据处理。

2. 寄存器介绍

(1) 状态寄存器(USART_SR),如图33所示。



图33状态寄存器


 位7: 传输寄存器为空。当TDR寄存器内容被传输到移位寄存器时,硬件将设置此位。

 位6: 传输完成。当包含数据帧传输完成,且设置了TXE,硬件将设置此位。

 位5: 读取数据寄存器非空。当RDR移位寄存器的内容转移到USART_DR寄存器时,硬件将设置此位。

(2) 数据寄存器(USART_DR)。

位8:0: 包含接收或传输的数据字符。

(3) 波特率寄存器(USART_BRR)。

位15:0: 用来配置波特率。

(4) 控制寄存器1(USART_CR1),如图34所示。



图34控制寄存器1


 位13: 串口使能位。

 位12: 用来配置数据长度。

 位10: 使能奇偶校验。

 位9: 配置奇偶校验。

 位3: 发送使能。

 位2: 接收使能。

(5) 控制寄存器2(USART_CR2),如图35所示。



图35控制寄存器2


 位13:12: 用来配置停止位。

 位3:0: 用来配置串口地址。

3. 串口通信配置步骤

(1) 初始化串口和GPIO时钟。

(2) 初始化串口引脚GPIO。

(3) 初始化串口地址、数据长度、停止位、波特率等参数。

(4) 使能串口。

(5) 调用数据收发函数。

3.1.2开发步骤

(1) 定义结构体变量UART1_Handler,用来传入串口参数,以便初始化串口。

(2) 定义UART_Init()函数。

在UART_Init()函数中配置结构体变量UART1_Handler中的串口地址、波特率等参数,最后将结构体变量UART1_Handler传入HAL_UART_Init,以便初始化串口。

//初始化串口函数

void UART_Init(void)

{

UART1_Handler.Instance = USART1;//串口1

UART1_Handler.Init.BaudRate =115200;//波特率

UART1_Handler.Init.HwFlowCtl = UART_HWCONTROL_NONE;//无硬件流控

UART1_Handler.Init.Mode = UART_MODE_TX_RX;//收发模式

UART1_Handler.Init.Parity = UART_PARITY_NONE;//无奇偶校验

UART1_Handler.Init.StopBits = UART_STOPBITS_1;//一个停止位

UART1_Handler.Init.WordLength = UART_WORDLENGTH_8B;//字长为8位格式



HAL_UART_Init(&UART1_Handler); //初始化串口

}

(3) 重定义HAL_UART_MspInit()函数初始化,初始化串口硬件参数。

当调用HAL_UART_Init()串口初始化函数时,该函数内部会调用HAL_UART_MspInit()函数,用来初始化串口使用到的引脚。这实际是HAL库的一个优点,它通过开放一个回调函数HAL_UART_MspInit(),让用户自己去编写与串口有关的硬件初始化,而与串口相关的参数配置则放在HAL_UART_Init()函数中,这样在将程序移植到其他STM32平台时,只需要修改HAL_UART_MspInit()函数中的配置即可。

//初始化硬件

void HAL_UART_MspInit(UART_HandleTypeDef *huart)

{

if(huart->Instance == USART1)

{

GPIO_InitTypeDef GPIO_Initure;



__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO引脚

__HAL_RCC_USART1_CLK_ENABLE();//使能串口



GPIO_Initure.Mode = GPIO_MODE_AF_PP;//复用推挽模式

GPIO_Initure.Pin = GPIO_PIN_9|GPIO_PIN_10;//PA9、PA10

GPIO_Initure.Pull = GPIO_PULLUP;//上拉

GPIO_Initure.Speed = GPIO_SPEED_FAST;//高速

GPIO_Initure.Alternate = GPIO_AF7_USART1;//复用为 USART1

HAL_GPIO_Init(GPIOA, &GPIO_Initure);//初始化 PA9、PA10

}

}

(4) 定义发送数据函数。函数传入参数为发送数据缓冲区地址和发送数据量的大小。

//发送数据函数

void UART_Transmit(uint8_t *pData, uint16_t Size)

{

HAL_UART_Transmit(&UART1_Handler, pData, Size, HAL_MAX_DELAY);

}

(5) 定义接收数据函数。函数传入参数为接收数据缓冲区地址和接收数据量的大小。

//接收数据函数

void UART_Receive(uint8_t *pData, uint16_t Size)

{

HAL_UART_Receive(&UART1_Handler, pData, Size, HAL_MAX_DELAY);

}

(6) 添加printf()的重定向函数,在工程中调用printf()函数,将通过串口发送数据。

//标准库需要的支持函数

struct __FILE

{

int handle;

}; 

FILE __stdout;

//重定义fputc函数 

int fputc(int ch, FILE *f)

{

while((USART1->SR&0X40)==0);//循环发送,直到发送完毕

USART1->DR = (uint8_t) ch;

return ch;

}

(7) main.c主函数代码如下: 

第一步,定义存储数据变量。

第二步,初始化系统时钟和串口。

第三步,调用数据收发函数进行数据通信。

uint8_t buffer;



int main(void)

{

CLOCLK_Init();//配置系统时钟为168MHz

UART_Init();//初始化



while(1)

{

UART_Receive(&buffer, 1);

UART_Transmit(&buffer, 1);

}

}

(8) USB转TTL模块连接方法: 

 模块TX连接开发板U1_Rx引脚; 

 模块RX连接开发板U1_Tx引脚; 

 模块GND连接开发板GND。

3.1.3运行结果

将程序下载到开发板中,找到USART1引脚通过USB转串口模块连接到计算机上,打开串口助手,配置好波特率、数据位、校验位、停止位等,单击打开串口。如果此刻发送一个1,则会看到串口助手会打印1,如图36所示。



图36输出结果


练习

(1) 简述STM32中串口通信数据包的内容。

(2) 简述串口通信配置步骤。

(3) 将波特率更改为9600bps,进行串口数据收发。



视频8

3.2串口通信: 中断
学习目标
了解串口的工作原理、ARM CortexM系列芯片串口的分类以及串口中断响应,通过配置STM32F407芯片的串口外设和中断,来实现数据的收发。

3.2.1开发原理

3.1节介绍了串口的轮询方式通信,轮询方式主要特点是让CPU以一定的周期查询串口外设,如果有数据输入则进行数据处理,否则进行循环判断。轮询方式缺点就是当程序执行内容较多时实时性不高,当数据输入比较频繁的时候,串口不能及时地进行数据处理。针对这个问题,我们引入串口中断通信。

串口中断请求如表32所示。



表32串口中断请求



中 断 事 件事 件 标 志使能控制位


发送寄存器为空TXETXEIE
清除发送标志CTSCTSIE
发送完成TCTCIE


接收寄存器为空RXNE
溢出错误检测ORERXNEIE

空闲总线检测IDLEIDLEIE
校验错误PEPEIE
中断标志LBDLBDIE
多缓冲区通信中的噪声标记、溢出错误和帧错误
NF或ORE或FEEIE


串口中断事件连接在相同的中断向量,如果设置了相应的使能位,那么这些事件将生成一个中断,如图37所示。



图37串口中断事件


3.2.2开发步骤

(1) 定义结构体变量UART1_Handler和接收数据变量buffer。

UART_HandleTypeDef UART1_Handler;//UART句柄

uint8_t buffer[50] = {0};//串口接收数据

(2) 定义UART_Init()函数,在UART_Init()函数中配置结构体变量UART1_Handler中的串口地址、波特率等参数,然后将结构体变量UART1_Handler传入到HAL_UART_Init,以便初始化串口。

//初始化串口函数

void UART_Init(void)

{

UART1_Handler.Instance = USART1;//串口1

UART1_Handler.Init.BaudRate =115200;//波特率

UART1_Handler.Init.HwFlowCtl = UART_HWCONTROL_NONE;//无硬件流控

UART1_Handler.Init.Mode = UART_MODE_TX_RX;//收发模式

UART1_Handler.Init.Parity = UART_PARITY_NONE;//无奇偶校验

UART1_Handler.Init.StopBits = UART_STOPBITS_1;//一个停止位

UART1_Handler.Init.WordLength = UART_WORDLENGTH_8B;//字长为8位格式

HAL_UART_Init(&UART1_Handler);//初始化串口

}

(3) 重定义HAL_UART_MspInit()函数,初始化串口使用的GPIO参数,并使能串口中断。

//初始化串口GPIO

void HAL_UART_MspInit(UART_HandleTypeDef *huart)

{

if(huart->Instance == USART1)

{

GPIO_InitTypeDef GPIO_Initure;



__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO引脚

__HAL_RCC_USART1_CLK_ENABLE();//使能串口



GPIO_Initure.Mode = GPIO_MODE_AF_PP;//复用推挽模式

GPIO_Initure.Pin = GPIO_PIN_9|GPIO_PIN_10;//PA9、PA10

GPIO_Initure.Pull = GPIO_PULLUP;//上拉

GPIO_Initure.Speed = GPIO_SPEED_FAST;//高速

GPIO_Initure.Alternate = GPIO_AF7_USART1;//复用为 USART1

HAL_GPIO_Init(GPIOA, &GPIO_Initure);//初始化 PA9、PA10



__HAL_UART_ENABLE_IT(&UART1_Handler, UART_IT_IDLE);//使能空闲中断

__HAL_UART_ENABLE_IT(&UART1_Handler, UART_IT_RXNE);//使能接收完成中断



HAL_NVIC_SetPriority(USART1_IRQn, 2, 1);//设置串口中断优先级

HAL_NVIC_EnableIRQ(USART1_IRQn);//使能串口中断

}

}

(4) 定义串口中断服务函数。函数中分别获取接收寄存器非空、空闲中断的标志位,当检测接收寄存器非空标志时,将串口接收到的字节数据存储到接收缓冲区buffer中; 当检测到空闲中断标志时,说明整个字符串已经接收完成,这时通过调用UART_Transmit()发送函数,根据计算的数据长度变量size,将整个接收的字符串发送出去。

uint16_t size; //接收到的数据长度

//串口中断服务函数

void USART1_IRQHandler(void)

{

//获取接收寄存器非空标志位

if(__HAL_UART_GET_FLAG(&UART1_Handler, UART_FLAG_RXNE) != RESET)

{

buffer[size++] = (uint8_t)UART1_Handler.Instance->DR;//获取一个字节数据

__HAL_UART_CLEAR_FLAG(&UART1_Handler, UART_FLAG_RXNE);//清除标志位

}



//获取空闲中断标志位

if(__HAL_UART_GET_FLAG(&UART1_Handler, UART_FLAG_IDLE) != RESET)

{

UART_Transmit(buffer, size);//将接收到的数据发送出去

size = 0;//下次接收重新开始计数

}

}

(5) 定义发送数据函数。函数传入参数为发送数据缓冲区地址和发送数据量的大小。

//发送数据函数

void UART_Transmit(uint8_t *pData, uint16_t Size)

{

HAL_UART_Transmit(&UART1_Handler, pData, Size, HAL_MAX_DELAY);

}

(6) 定义接收数据函数。函数传入参数为接收数据缓冲区地址和接收数据量的大小。

//接收数据函数

void UART_Receive(uint8_t *pData, uint16_t Size)

{

HAL_UART_Receive(&UART1_Handler, pData, Size, HAL_MAX_DELAY);

}

(7) 在main.c文件的主函数中,只需要初始化系统时钟和串口即可。

int main(void)

{

CLOCK_Init();//配置系统时钟为168MHz

UART_Init();//串口初始化



while(1)

{

}

}

3.2.3运行结果

将程序下载到开发板中,打开串口。如果此刻发送一个字符串“123456”,将会看到串口助手会返回字符串“123456”,如图38所示。



图38运行结果


练习

(1) 简述串口中断与串口轮询的异同。

(2) 通过本节的学习实现串口发送完成中断。



视频9

3.3串口通信: DMA
学习目标
了解DMA 的工作原理,通过配置 STM32F407芯片的DMA,实现串口+DMA数据收发。

3.3.1开发原理

基于USART的数据通信中采用中断方式可以在接收信息或发送数据时产生中断,在中断服务程序中完成数据的接收与发送,但是中断方式的CPU使用率更高。在简单的系统中,使用中断方式确实是一种好方法。但是在复杂的系统中,处理器需要处理串口通信,多个传感器数据的采集及处理,这涉及多个中断的优先级分配问题。为了保证数据发送与接收的可靠性,需要把USART的中断优先级设计得较高,但是系统可能还有其他的需要更高优先级的中断,必须保证其定时的准确,这样就有可能造成串行通信的中断不能及时响应,从而造成数据丢失。为了保证串行通信的数据及时可靠地接收,同时兼顾其他任务不受影响,采用了基于DMA和中断方式相结合的USART串行通信方式。

DMA的全称为Direct Memory Access,即直接存储器访问。DMA传输将数据从一个地址空间复制到另一个地址空间。DMA传输方式无须CPU直接控制传输,也没有中断处理方式那样的保留现场和恢复现场过程,通过硬件为RAM和I/O设备开辟一条直接传输数据的通道,使得CPU的效率大大提高。DMA最主要的作用是为CPU减小负担。

STM32F4xx系列的DMA功能齐全,工作模式众多,适合不同的编程环境要求。STM32F4xx系列的DMA支持外设到存储器传输、存储器到外设传输和存储器到存储器传输3种传输模式。这里的外设一般指外设的数据寄存器,比如 ADC、SPI、I2C、DCMI等外设的数据寄存器,存储器一般是指片内SRAM、外部存储器、片内Flash 等。

外设到存储器传输就是把外设数据寄存器的内容转移到指定的内存空间。比如进行ADC采集时可以利用DMA传输将AD转换数据转移到我们定义的存储区中,这对于多通道采集、采样频率高、连续输出数据的 AD采集是非常高效的处理方法。

存储区到外设传输就是将特定存储区内容转移至外设的数据寄存器中,这种多用于外设的发送通信。

存储器到存储器传输就是将一个指定的存储区内容复制到另一个存储区空间。功能类似于C语言内存复制函数memcpy,利用 DMA传输可以达到更高的传输效率,特别是DMA传输是不占用CPU的,可以节省很多CPU资源。

STM32F4xx系列的DMA可以实现外设与寄存器、存储器之间或者存储器与存储器之间传输3种模式,这主要得益于DMA控制器是采用AHB主总线,可以控制AHB总线矩阵来启动AHB事务DMA控制框图如图39所示。



图39DMA控制框图


1. 外设通道选择

STM32F4xx系列资源丰富,具有两个DMA控制器,同时外设繁多,为实现正常传输,DMA需要通道选择控制。每个 DMA控制器具有 8个数据流,每个数据流对应 8个外设请求。在实现DMA传输之前,DMA控制器会通过DMA数据流x配置寄存器DMA_SxCR(x为0~7,对应 8 个 DMA 数据流)的 CHSEL[2:0]位选择对应的通道作为该数据流的目标外设。

外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。

DMA1请求映射如图310所示。





外设请求数据流0数据流1数据流2数据流3数据流4数据流5数据流6数据流7


通道0SPI3_PXSPI3_RXSPI2_RXSPI2_TXSPI3_TXSPI3_TX
通道1I2C1_RXTIM7_UPTIM7_UPI2C1_RXI2C1_TXI2C1_TX
通道2TIM4_CH1I2S3_EXT_
RXTIM4_CH2I2S2_EXT_
TXI2S3_
EXT_TXTIM4_UPTIM4_CH3
通道3I2S3_EXT_
RXTIM2_UP
TIM2_CH3I2C3_RXI2S2_EXT_
RXI2C3_TXTIM2_CH1TIM2_CH2
TIM2_CH4TIM2_UP
TIM2_CH4
通道4UART5_
RXUSART3_
RXUART4_
RXUSART3_
TXUART4_TXUSART2_
RXUSART2_
TXUART5_
TX
通道5UART8_
TX(1)UART7_
TX(1)TIM3_CH4
TIM3_UPUART7_
RX(1)TIM3_CH1
TIM3_TRIGTIM3_CH2UARTB_
RX(1)TIM3_CH3
通道6TIM5_CH3
TIM5_UPTIM5_CH4
TIM5_TRIGTIM5_CH1TIM5_CH4
TIM5_TRIGTIM5_CH2TIM5_UP
通道7TIM6_UPI2C2_RXI2C2_RXUSART3_
TXDAC1DAC2I2C2_TX

(1) 这些请求仅在STM32F42xxx和STM32F43xxx上可用。





图310DMA1请求映射


DMA2请求映射如图311所示。





外设请求数据流0数据流1数据流2数据流3数据流4数据流5数据流6数据流7


通道0ADC1TIM8_CH1
TIM8_CH2
TIM8_CH3ADC1TIM1_CH1
TIM1_CH2
TIM1_CH3
通道1DCMIADC2ADC2SPI6_TX(1)SPI6_RX(1)DCMI
通道2ADC3ADC3SPI5_RX(1)SPI5_TX(1)CRYP_OUTCRYP_INHASH_IN
通道3SPI1_RXSPI1_RXSPI1_TXSPI1_TX
通道4SPI4_
RX(1)SPI4_
TX(1)USART1_
RXSDIOUSART1_
RXSDIOUSART1_
TX
通道5USART6_
RXUSART6_
RXSPI4_RX(1)SPI4_TX(1)USART6_
TXUSART6_
TX
通道6TIM1_
TRIGTIM1_CH1TIM1_
CH2TIM1_CH1TIM1_CH4
TIM1_TRIG
TIM1_COMTIM1_UPTIM1_
CH3
通道7TIM8_UPTIM8_
CH1TIM8_CH2TIM8_CH3SPI5_RX(1)SPI5_
TX(1)TIM8_CH4
TIM8_TRIG
TIM8_COM

(1) 这些请求在STM32F42xxx和STM32F43xxx上可用。





图311DMA2请求映射


2. 仲裁器

一个DMA控制器对应8个数据流,数据流包含要传输数据的源地址、目标地址、数据等信息。如果需要同时使用同一个DMA控制器(DMA1或DMA2)处理多个外设请求,那么必然需要同时使用多个数据流,究竟哪一个数据流具有优先传输的权利呢?这就需要仲裁器来管理判断了。

仲裁器管理数据流方法分为两个阶段。第一阶段属于软件阶段,在配置数据流时可以通过寄存器设定它的优先级别,具体配置DMA_SxCR寄存器PL[1:0]位,可以设置为非常高、高、中和低4个级别。第二阶段属于硬件阶段,如果两个或以上数据流软件设置优先级一样,则其优先级取决于数据流编号,编号越低越具有优先权,比如数据流2优先级高于数据流3。

3. FIFO

每个数据流都独立拥有4级32位FIFO(先进先出存储器缓冲区)。DMA传输具有FIFO模式和直接模式。

直接模式就是每个外设请求都立即启动对存储器传输。在直接模式下,如果DMA配置为存储器和外设之间传输,那么DMA会将一个数据存放在FIFO内。如果外设启动DMA传输请求,则可以马上将数据传输过去。

在FIFO模式下,FIFO用于在源数据传输到目标地址之前临时存放这些数据。可以通过DMA数据流x FIFO控制寄存器DMA_SxFCR的FTH[1:0]位来控制FIFO的阈值,分别为1/4、1/2、3/4和1。如果数据存储量达到阈值级别时,FIFO内容将传输到目标中。

FIFO对于要求源地址和目标地址数据宽度不同时非常有用,比如源数据是源源不断的字节数据,而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来4个8位字节的数据拼凑成一个32位数据。此时使用FIFO功能先把数据缓存起来,然后根据需要输出数据。

4. 存储器端口、外设端口

DMA控制器实现双AHB主接口,更好利用总线矩阵和并行传输。DMA控制器通过存储器端口和外设端口与存储器和外设进行数据传输,关系如图312所示。DMA控制器的功能是快速转移内存数据,需要一个连接至源数据地址的端口和一个连接至目标地址的端口。



图312DMA控制器和外设端口与存储器和外设进行数据传输


DMA2(DMA控制器2)的存储器端口和外设端口都是连接到AHB总线矩阵,可以使用AHB总线矩阵功能。DMA2存储器和外设端口可以访问相关的内存地址,包括内部Flash、内部SRAM、AHB1外设、AHB2外设、APB2外设和外部存储器空间。

DMA1的存储器端口相比DMA2的减少了AHB2外设的访问权,同时DMA1外设端口是没有连接至总线矩阵的,只连接到APB1外设,所以DMA1不能实现存储器到存储器的数据传输。

5. 编程端口

AHB从器件编程端口是连接至AHB2外设的。AHB2外设在使用DMA传输时需要相关控制信号。

DMA寄存器包括DMA低中断状态寄存器(DMA_LISR)、DMA高中断状态寄存器(DMA_HISR)、DMA低中断标志清除寄存器(DMA_LIFCR)、DMA高中断标志清除寄存器(DMA_HIFCR)、DMA流配置寄存器(DMA_SxCR)、DMA流数据个数寄存器(DMA_SxNDTR)、DMA流外设地址寄存器(DMA_SxPAR)、DMA流内存0地址寄存器(DMA_SxM0AR)、DMA流内存1地址寄存器(DMA_SxM1AR)、DMA流FIFO控制寄存器(DMA_SxM1AR)。

3.3.2开发步骤

(1) 定义串口DMA发送和接收的结构体初始化变量。

DMA_HandleTypeDef UART1RxDMA_Handler;//串口接收DMA句柄

DMA_HandleTypeDef UART1TxDMA_Handler;//串口发送DMA句柄

(2) 定义UART_DMA_Init()函数,在函数中实现串口DMA的初始化。

第一步,使能DMA2时钟。

第二步,分别配置UART1RxDMA_Handler和UART1TxDMA_Handler结构体变量中的参数,然后将两个结构体变量传入到HAL_DMA_Init()函数中,以便初始化串口DMA。

第三步,使能DMA2数据流7(串口DMA发送)中断。

//串口DMA初始化

void UART_DMA_Init(void)

{

__HAL_RCC_DMA2_CLK_ENABLE();//使能DMA2时钟



//接收DMA配置

UART1RxDMA_Handler.Instance = DMA2_Stream5;//数据流选择

UART1RxDMA_Handler.Init.Channel = DMA_CHANNEL_4;//通道选择

UART1RxDMA_Handler.Init.Direction = DMA_PERIPH_TO_MEMORY;//外设到存储器

UART1RxDMA_Handler.Init.FIFOMode = DMA_FIFOMODE_DISABLE;//FIFO不使能

UART1RxDMA_Handler.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;//FIFO阈值

UART1RxDMA_Handler.Init.MemBurst = DMA_MBURST_SINGLE;//内存突发传输配置

UART1RxDMA_Handler.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;//存储器数据长度

UART1RxDMA_Handler.Init.MemInc = DMA_MINC_ENABLE;//内存寄存器地址是否自增

UART1RxDMA_Handler.Init.Mode = DMA_CIRCULAR;//循环模式

UART1RxDMA_Handler.Init.PeriphBurst = DMA_PBURST_SINGLE;//外设突发传输配置

UART1RxDMA_Handler.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;//外设数据长度

UART1RxDMA_Handler.Init.PeriphInc = DMA_PINC_DISABLE;//外设地址非自增

UART1RxDMA_Handler.Init.Priority = DMA_PRIORITY_MEDIUM;//中等优先级

HAL_DMA_Init(&UART1RxDMA_Handler);



//发送DMA配置

UART1TxDMA_Handler.Instance = DMA2_Stream7;//数据流选择

UART1TxDMA_Handler.Init.Channel = DMA_CHANNEL_4;//通道选择

UART1TxDMA_Handler.Init.Direction = DMA_MEMORY_TO_PERIPH;//外设到存储器

UART1TxDMA_Handler.Init.FIFOMode = DMA_FIFOMODE_DISABLE;//FIFO不使能

UART1TxDMA_Handler.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;//FIFO阈值

UART1TxDMA_Handler.Init.MemBurst = DMA_MBURST_SINGLE;//内存突发传输配置

UART1TxDMA_Handler.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;//存储器数据长度

UART1TxDMA_Handler.Init.MemInc = DMA_MINC_ENABLE;//内存寄存器地址是否自增

UART1TxDMA_Handler.Init.Mode = DMA_NORMAL;//正常模式

UART1TxDMA_Handler.Init.PeriphBurst = DMA_PBURST_SINGLE;//外设突发传输配置

UART1TxDMA_Handler.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;//外设数据长度

UART1TxDMA_Handler.Init.PeriphInc = DMA_PINC_DISABLE;//外设地址非自增

UART1TxDMA_Handler.Init.Priority = DMA_PRIORITY_MEDIUM;//中等优先级

HAL_DMA_Init(&UART1TxDMA_Handler);



HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 0, 0);

HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);

}

(3) 定义DMA2数据流7(串口DMA发送)中断服务函数,在函数中调用HAL_DMA_IRQHandler()处理DMA中断请求。

//缓冲区 -> Tx 中断服务函数

void DMA2_Stream7_IRQHandler(void)

{

HAL_DMA_IRQHandler(&UART1TxDMA_Handler);

}

(4) 定义串口DMA接收数据函数。在函数中调用HAL_UART_Receive_DMA()实现使用DMA从串口接收数据。

//串口DMA接收数据

void UART_DMA_Receive(uint8_t *pData, uint16_t Size)

{

HAL_UART_Receive_DMA(&UART1_Handler, pData, Size);

}

(5) 定义串口DMA发送数据函数。在函数中首先调用HAL_DMA_Start_IT()开启串口DMA发送完成中断,并设置发送数据缓冲区地址、外设寄存器地址以及发送数据的个数,然后调用SET_BIT配置串口DMA发送标志位(DMAT)。

extern uint8_t Rx_buffer[RXBUFFERSIZE]; //发送数据缓冲区



//串口DMA发送数据

void UART_DMA_Transmit(UART_HandleTypeDef *huart)

{

HAL_DMA_Start_IT(UART1_Handler.hdmatx, (uint32_t)Rx_buffer, (uint32_t)&UART1_Handler.Instance->DR, RXBUFFERSIZE);

SET_BIT(UART1_Handler.Instance->CR3, USART_CR3_DMAT);

}

(6) 修改串口初始化函数,将串口DMA发送和接收初始化句柄传入串口初始化结构体变量中,具体实现如下: 

//初始化串口函数

void UART_Init(void)

{

UART1_Handler.Instance = USART1;//串口1

UART1_Handler.Init.BaudRate =115200;//波特率

UART1_Handler.Init.HwFlowCtl = UART_HWCONTROL_NONE;//无硬件流控制

UART1_Handler.Init.Mode = UART_MODE_TX_RX;//收发模式

UART1_Handler.Init.Parity = UART_PARITY_NONE;//无奇偶校验

UART1_Handler.Init.StopBits = UART_STOPBITS_1;//一个停止位

UART1_Handler.Init.WordLength = UART_WORDLENGTH_8B;//字长为8位格式

UART1_Handler.hdmarx = &UART1RxDMA_Handler;//传入接收DMA句柄

UART1_Handler.hdmatx = &UART1TxDMA_Handler;//传入发送DMA句柄

HAL_UART_Init(&UART1_Handler);//初始化串口

}

(7) 在main.c文件中调用函数实现串口DMA数据收发。

第一步,声明串口初始化句柄方便下面函数调用,并定义接收数据缓冲区。

第二步,初始化系统时钟、DMA及串口。

第三步,在循环中调用UART_DMA_Transmit和HAL_Delay()函数,实现每隔1s发送一组收到的数据。

extern UART_HandleTypeDef UART1_Handler;//UART句柄

uint8_t Rx_buffer[RXBUFFERSIZE];//串口接收数据缓冲区



int main(void)

{

CLOCLK_Init();//配置系统时钟为168MHz

UART_DMA_Init();//串口DMA初始化

UART_Init();//串口初始化



UART_DMA_Receive(Rx_buffer, RXBUFFERSIZE);//使用DMA接收数据



while(1)

{

HAL_Delay(1000);//延时1s

UART_DMA_Transmit(&UART1_Handler);//发送数据

}

}

3.3.3运行结果

将程序下载到开发板中,打开串口。将发送的数据添加到发送缓冲区,单击自动发送按钮,可以看到接收缓冲区每隔1s会接收到返回的数据,如图313所示。



图313运行结果


练习

(1) 什么是DMA?

(2) 以DMA的方式配置其他串口,实现数据接收与发送。