STM32F103 직렬 포트 DMA + 가변 길이 데이터 송수신을위한 유휴 인터럽트

    직렬 포트 DMA 시리즈에는 두 가지 기사가 있습니다. 하나는 DMA + 유휴 인터럽트를 사용하여 가변 길이의 데이터를 송수신하는 것이고 다른 하나는 DMA 인터럽트를 사용하여 고정 길이 데이터를 송수신하는 것입니다. 기사 링크는 다음과 같습니다.
    01 STM32F103 직렬 DMA + 유휴 인터럽트를 사용하여 가변 길이를 실현합니다. 데이터 트랜시버
    02 STM32F103 직렬 포트 + DMA 인터럽트를 통해 데이터 트랜시버 달성

    DMA (Direct Memory Access)는 모든 최신 컴퓨터의 중요한 기능으로, CPU의 막대한 인터럽트로드에 의존하지 않고 서로 다른 속도의 하드웨어 장치가 통신 할 수 있도록합니다. 그렇지 않으면 CPU는 각 세그먼트의 데이터를 소스에서 스크래치 패드로 복사 한 다음 다시 새 위치에 써야합니다. 이 시간 동안 CPU는 다른 작업에 사용할 수 없습니다.
    DMA에 대한 기본 지식은 https://blog.csdn.net/gdjason/article/details/51019219 기사를 참조하세요 . 개인적으로 블로거 기사의 마지막 코드 부분이 약간 지저분하다는 느낌이 들어서 다시 기록을 정리하겠습니다. Serial DMA는 두 가지 인터럽트 트리거 방식을 가질 수 있는데, 하나는 STM32의 IDLE Idle 인터럽트를 사용하여 가변 길이의 데이터 수신을 용이하게하는 것입니다.이 방법은 자주 사용되며, 두 번째는 DMA 자체 전송 완료 인터럽트를 사용하는 것입니다. 이 방법은 전송이 완료된 후 전송 완료 인터럽트를 생성 할 수 있는데, 유휴 인터럽트는 가변 길이의 데이터 수신에 편리하고 DMA 전송 완료 인터럽트는 정의 된 길이의 데이터를 수신 한 후에 만 ​​수신 인터럽트를 생성한다는 점이 다릅니다.
 

1. 유휴 인터럽트

    이 텍스트는 485 버스를 사용하여 직렬 포트의 DMA 유휴 인터럽트를 실험하고 데이터 송수신 테스트를 실현합니다. 485의 차이점은 추가적인 Enable 핀이 있다는 것인데,이 핀은 송신을 가능하게하기 위해서는 High Level이고 수신을 가능하게하기 위해서는 Low-Level이므로 반이중 통신이며 나머지는 직렬 포트와 일치합니다. 이 예에서는 STM32F103 직렬 포트 1, TX 핀은 PA9, RX 핀은 PA10, 485 활성화 핀은 PD1을 사용합니다. 보드에서 시리얼 포트 기능 테스트를 사용할 때 485 인 에이블 핀을 고려할 필요가 없습니다. 본 기사에서 485에 대한 부분은 / **** RS485 **** /로 표시되어 있으며, 다음은 코드 부분입니다.

1.1 uart_dma.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include "stm32f10x_dma.h"
#include "misc.h"

#include "systick.h"	// 利用嘀嗒计时器实现了ms级的死等延时,用于切换485收发功能使用,实际项目中不能用死等延时
#include "uart_dma.h"

uint8_t uart1RecvData[32] = {
    
    0};    // 接收数据缓冲区
uint8_t uart1RecvFlag = 0;          // 接收完成标志位
uint8_t uart1RecvLen = 0;           // 接收的数据长度

uint8_t uart1SendData[32] = {
    
    0};    // 发送数据缓冲区
uint8_t uart1SendFlag = 0;          // 发送完成标志位

/* 串口1 GPIO引脚初始化 */
void Uart1GpioInit(void)
{
    
    
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   // 使能GPIOA时钟

	/************ ↓ RS485 相关 ↓ ************/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);   // 使能GPIOD时钟
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;    // 输入输出使能引脚 推挽输出
    GPIO_InitStruct.GPIO_Pin = UART1_EN_PIN;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(UART1_EN_PORT, &GPIO_InitStruct);     // PD1
    Uart1RxEnable();    // 初始化接收模式
    /************ ↑ RS485 相关 ↑ ************/
    
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;    // TX 推挽输出
    GPIO_InitStruct.GPIO_Pin = UART1_TX_PIN;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(UART1_TX_PORT, &GPIO_InitStruct);     // PA9
    
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;      // RX上拉输入
    GPIO_InitStruct.GPIO_Pin = UART1_RX_PIN;
    GPIO_Init(UART1_RX_PORT, &GPIO_InitStruct);     // PA10
}

/************ ↓ RS485 相关 ↓ ************/
/* 使能485发送 */
void Uart1TxEnable(void)
{
    
    
    GPIO_WriteBit(UART1_EN_PORT, UART1_EN_PIN, Bit_SET);    // 485的使能引脚,高电平为使能发送
    Delay_ms(5);
}

/* 使能485接收 */
void Uart1RxEnable(void)
{
    
    
    GPIO_WriteBit(UART1_EN_PORT, UART1_EN_PIN, Bit_RESET);  // 485的使能引脚,低电平为使能发送
    Delay_ms(5);
}
/************ ↑ RS485 相关 ↑ ************/
 
/* 串口1配置 9600 8n1 */
void Uart1Config(void)
{
    
    
    USART_InitTypeDef USART_InitStruct;		// 串口配置
    NVIC_InitTypeDef NVIC_InitStructure;	// 中断配置
    DMA_InitTypeDef DMA_InitStruct;			// DMA 配置
    
    USART_DeInit(USART1);   // 寄存器恢复默认值
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);  // 使能串口时钟
    
    /* 串口参数配置 */
    USART_InitStruct.USART_BaudRate = BAUD_RATE;            // 波特率:9600
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;    // 无流控
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;    // 收发
    USART_InitStruct.USART_Parity = USART_Parity_No;                // 无校验位 
    USART_InitStruct.USART_StopBits = USART_StopBits_1;             // 1个停止位
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;        // 8个数据位
    USART_Init(USART1, &USART_InitStruct);
    USART_Cmd(USART1, ENABLE);  // 使能串口
    
    /* 串口中断配置 */
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;             // 使能
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;   // 抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;          // 子优先级
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;           // 串口1中断
    NVIC_Init(&NVIC_InitStructure);     // 嵌套向量中断控制器初始化

    USART_ITConfig(USART1, USART_IT_TC,   ENABLE);  // 使能串口发送中断,发送完成产生 USART_IT_TC 中断
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);  // 使能串口空闲中断,接收一帧数据产生 USART_IT_IDLE 空闲中断
    
    /* 串口DMA配置 */
    DMA_DeInit(DMA1_Channel4);  // DMA1 通道4,寄存器复位
    DMA_DeInit(DMA1_Channel5);  // DMA1 通道5,寄存器复位
    
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);  // 使能 DMA1 时钟
    
    // RX DMA1 通道5
    DMA_InitStruct.DMA_BufferSize = sizeof(uart1RecvData);      // 定义了接收的最大长度
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;             // 串口接收,方向是外设->内存
    DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;                   // 本次是外设到内存,所以关闭内存到内存
    DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)uart1RecvData;// 内存的基地址,要存储在哪里
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;// 内存数据宽度,按照字节存储
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;        // 内存递增,每次串口收到数据存在内存中,下次收到自动存储在内存的下一个位置
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;                  // 正常模式
    DMA_InitStruct.DMA_PeripheralBaseAddr = USART1_BASE + 0x04; // 外设的基地址,串口的数据寄存器
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;    // 外设的数据宽度,按照字节存储,与内存的数据宽度一致
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   // 接收只有一个数据寄存器 RDR,所以外设地址不递增
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;            // 优先级
    DMA_Init(DMA1_Channel5, &DMA_InitStruct);
    
    // TX DMA1 通道4  
    DMA_InitStruct.DMA_BufferSize = 0;                          // 发送缓冲区的大小,初始化为0不发送
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;             // 发送是方向是外设到内存,外设作为目的地
    DMA_InitStruct.DMA_MemoryBaseAddr =(uint32_t)uart1SendData; // 发送内存地址,从哪里发送
    DMA_Init(DMA1_Channel4, &DMA_InitStruct);
     
    USART_DMACmd(USART1, USART_DMAReq_Tx | USART_DMAReq_Rx, ENABLE);// 使能DMA串口发送和接受请求
    DMA_Cmd(DMA1_Channel5, ENABLE);     // 使能接收
    DMA_Cmd(DMA1_Channel4, DISABLE);    // 禁止发送
}

/* 清除DMA的传输数量寄存器 */
void uart1DmaClear(void)
{
    
    
    DMA_Cmd(DMA1_Channel5, DISABLE);    // 关闭 DMA1_Channel5 通道
    DMA_SetCurrDataCounter(DMA1_Channel5, sizeof(uart1RecvData));   // 重新写入要传输的数据数量
    DMA_Cmd(DMA1_Channel5, ENABLE);     // 使能 DMA1_Channel5 通道
}

/* 串口1发送数组 */
void uart1SendArray(uint8_t *arr, uint8_t len)
{
    
    
    if(len == 0)	// 判断长度是否有效
      return;
	
	uint8_t sendLen = len>sizeof(uart1SendData) ? sizeof(uart1SendData) : len;	// 防止越界

    /************ ↓ RS485 相关 ↓ ************/ 
    Uart1TxEnable();    // 使能发送
    /************ ↑ RS485 相关 ↑ ************/
    
    while (DMA_GetCurrDataCounter(DMA1_Channel4));  // 检查DMA发送通道内是否还有数据
    if(arr) 
      memcpy(uart1SendData, arr, sendLen);
    
    // DMA发送数据-要先关 设置发送长度 开启DMA
    DMA_Cmd(DMA1_Channel4, DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel4, sendLen);   // 重新写入要传输的数据数量
    DMA_Cmd(DMA1_Channel4, ENABLE);     // 启动DMA发送  
}

 

1.2 uart_dma.h

#ifndef _UART_DAM_H_
#define _UART_DMA_H_

#include <stdint.h>

#define UART1_TX_PORT   GPIOA
#define UART1_TX_PIN    GPIO_Pin_9
#define UART1_RX_PORT   GPIOA
#define UART1_RX_PIN    GPIO_Pin_10
#define UART1_EN_PORT   GPIOD
#define UART1_EN_PIN    GPIO_Pin_1
#define BAUD_RATE       (9600)

extern uint8_t uart1RecvData[32];
extern uint8_t uart1RecvFlag;
extern uint8_t uart1RecvLen;
extern uint8_t uart1SendFlag;

void Uart1GpioInit(void);
void Uart1Config(void);
void uart1DmaClear(void);
void uart1SendArray(uint8_t *arr, uint8_t len);

/************ ↓ RS485 相关 ↓ ************/ 
void Uart1RxEnable(void);
void Uart1TxEnable(void);
/************ ↑ RS485 相关 ↑ ************/

#endif  /* uart_dma.h */

 

1.3 main.c

#include "uart_dma.h"
#include "misc.h"

int main()
{
    
     
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  // 设置中断优先级分组
    
    /************ ↓ RS485 相关 ↓ ************/ 
    SysTickInit();          // 嘀嗒计时器初始化,没用485可以省去
    /************ ↑ RS485 相关 ↑ ************/
    
    Uart1GpioInit();	// 串口GPIO初始化
    Uart1Config();		// 串口和DMA配置

    while(1)
    {
    
         
        if(uart1RecvFlag == 1)	// 接收到数据
        {
    
    
            uart1RecvFlag = 0;  // 接收标志清空
            uart1DmaClear();    // 清空DMA接收通道
            uart1SendArray(uart1RecvData, uart1RecvLen);        // 使用DMA发送数据
            memset(uart1RecvData, '\0', sizeof(uart1RecvData)); // 清空接收缓冲区
        }
        
        if(uart1SendFlag == 1)
        {
    
    
            uart1SendFlag = 0;  // 清空发送标志   
            Uart1RxEnable();    // 发送完成打开接收
        }
    }
}

 

1.4 stm32f10x_it.c

#include "stm32f10x_it.h"
#include "stm32f10x_usart.h"
#include "stm32f10x_dma.h"

#include "uart_dma.h"

void USART1_IRQHandler(void)    // 串口1 的中断处理函数
{
    
    
    uint8_t clear;

    if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)   // 空闲中断
    {
    
    
        clear = USART1->SR; // 清除空闲中断
        clear = USART1->DR; // 清除空闲中断
        
        uart1RecvFlag = 1;  // 置接收标志位
        uart1RecvLen = sizeof(uart1RecvData) - DMA_GetCurrDataCounter(DMA1_Channel5);// 总的buf长度减去剩余buf长度,得到接收到数据的长度
    }   
    
    if(USART_GetITStatus(USART1, USART_IT_TC) != RESET)     // 发送完成
    {
    
    
        USART_ClearITPendingBit(USART1, USART_IT_TC);       // 清除完成标记
        DMA_Cmd(DMA1_Channel4, DISABLE);                    // 关闭DMA
        uart1SendFlag = 1;                                  // 设置发送完成标志位
    }
}

 

1.5 효과 데모

    이 기사에서는 직렬 포트를 사용하여 DMA 유휴 인터럽트를 사용하여 가변 길이의 데이터를 수신합니다. 데이터를 수신 한 후 수신 된 데이터는 DMA를 사용하여 전송되고 직렬 포트 디버깅 도우미가 디버깅에 사용됩니다. 효과는 다음과 같습니다. 데이터가 정의 된 최대 데이터를 초과 할 때 알 수 있습니다. 예를 들어 32 바이트 이후) 수신자는 32 개의 데이터 만 수신 할 수 있으며 다른 데이터는 폐기됩니다.
여기에 사진 설명 삽입
 

1.6 지식 보충

1.6.1 주변 기본 주소

    이 기사에 정의 된 주변 장치 대 기본 주소는 USART1_BASE + 0x04입니다. 그 이유는 무엇입니까? STM32 참조 설명서를 확인하고 직렬 포트 장을 찾은 다음 아래 그림과 같이 USART 레지스터 주소 맵을 조회합니다. 데이터 레지스터가 직렬 포트 기본 주소 레지스터 오프셋 0x04 이후의 위치임을 알 수 있습니다. 직렬 포트 1 기본 주소의 매크로 정의는 아래 그림과 같이 stm32f10x.h에서 찾을 수 있습니다.
여기에 사진 설명 삽입
직렬 포트 1 기본 주소 매크로 정의
 

1.6.2 유휴 인터럽트 해제

    유휴 인터럽트가 생성 된 후 먼저 SR을 읽고 DR을 읽어 유휴 인터럽트 플래그 비트를 삭제합니다 (매뉴얼의 직렬 포트 25.6.1 장 상태 레지스터 (USART_SR)).
여기에 사진 설명 삽입
 

1.6.3 DMA 전송 횟수

    설명서를 확인하십시오. DMA 전송량 레지스터의 값은 전송 될 남은 바이트 수를 나타내므로 여기에 정의 된 총 수 c- 레지스터의 값 = 수신 된 수를 나타냅니다. 이 장소는 특별한주의가 필요합니다. 그렇지 않으면 수신 된 바이트 수가 잘못 계산됩니다.
여기에 사진 설명 삽입
    또 다른 요점은 각 수신이 완료된 후 아래 첨자 0에서 다음 수신을 메모리에 저장하거나 저장하려면 전송할 데이터 양을 다시 써야합니다. 그렇지 않으면 다음에 마지막 전송 위치에서 직접 수신 및 저장을 시작할 수 있습니다. 전송할 때도 마찬가지입니다. 코드 uart_dma.c의 127 ~ 129 행과 149 ~ 151 행을 참조하십시오.
    128 행을 주석 처리하고 매번 전송량 레지스터를 다시 쓰지 않으면 데모 결과는 다음과 같습니다. 결과에서 128 행을 주석 처리하면 DMA가 데이터를 수신 버퍼로 전송할 때 아래 첨자가 마지막 끝 위치에서 시작하는 것을 볼 수 있습니다. 시작 위치 [이때 전송량 레지스터의 값이 0이 아니므로 32-7입니다], 각 수신 후 수신 버퍼가 클리어되기 때문에 전면은 '/ 0'입니다. 따라서 데이터 프레임을 수신 한 후 전송 수량 레지스터를 0으로 지워야합니다.
여기에 사진 설명 삽입

    DMA 학습 과정에서 질문이 있으면 의사 소통을 환영합니다.

추천

출처blog.csdn.net/qq_36310253/article/details/109641759