当前位置:首页 » 《休闲阅读》 » 正文

stm32 高效串口收发_nepqiu的博客

7 人参与  2022年02月06日 15:07  分类 : 《休闲阅读》  评论

点击全文阅读


文章目录

  • 串口通讯
    • 串口
    • USART 中断
    • 串口模式配置
    • 使用 DMA 进行连续通信
      • 使用 DMA 进行发送
      • 使用 DMA 进行接收
    • 编程
  • 接收流程
    • 主函数:
    • 中断处理函数:
    • 初始化(标准库)
  • 发送流程
    • 开启串口发送完成中断
    • 开启DMA发送完成中断
    • DMA发送函数
    • 解析
      • 程序1
      • 程序2
  • `fifo_buff`代码
    • `fifo_buff.c`
    • `fifo_buff.h`
  • 完整代码和使用示例
  • 参考

串口通讯

串口

串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口(SerialInterface)是指数据一位一位地顺序传送,其特点是通信线路简单,只要一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大降低了成本,特别适用于远距离通信,但传送速度较慢。

  • USART(universal synchronous asynchronous receiver and transmitte): 通用同步异步收发器

    • USART是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。
  • UART(universal asynchronous receiver and transmitter): 通用异步收发器

    • 异步串行通信口(UART)就是我们在嵌入式中常说的串口,它还是一种通用的数据通信议。

image-20210719135121996

区别:

USART是指单片机的一个端口模块,可以根据需要配置成同步模式(SPI,I2C),也可以将其配置为异步模式,后者就是UART。所以说UART姑且可以称之为一个与SPI,I2C对等的“协议”,而USART则不是一个协议,而是更应该理解为一个实体。

相比于同步通讯,UART不需要统一的时钟线,接线更加方便。但是,为了正常的对信号进行解码,使用UART通讯的双方必须事先约定好波特率,即单位事件内传输码元的个数。

补充:

在电子通信领域,波特(Baud)即调制速率,指的是有效数据讯号调制载波的速率,即单位时间内载波调制状态变化的次数。它是对符号传输速率的一种度量,1波特即指每秒传输1个符号,而透过不同的调制方式,可以在一个码元符号上负载多个bit位信号。[1]“波特”(Baud)本身已是速率,所以不需要写成 Baud Rate(Rate 是赘字)。单位“波特”本身就已经是代表每秒的调制数,以“波特每秒”(Baud per second)为单位是一种常见的错误,但是在一般中文口语化的沟通上还是常以“波特率”来描述“波特”(Baud)。

USART 中断

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkffEvL6-1632828582303)(https://raw.githubusercontent.com/Nepqiu/gallery/master/img/image-20210815155528305.png)]

USART 中断事件被连接到相同的中断向量:

  • 发送期间:发送完成、清除以发送或发送数据寄存器为空中断。
  • 接收期间:空闲线路检测、上溢错误、接收数据寄存器不为空、奇偶校验错误、LIN 断路 检测、噪声标志(仅限多缓冲区通信)和帧错误(仅限多缓冲区通信)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IbZhqtpL-1632828582305)(https://raw.githubusercontent.com/Nepqiu/gallery/master/img/image-20210811170712630.png)]

串口模式配置

image-20210811170758605

使用 DMA 进行连续通信

USART 能够使用 DMA 进行连续通信。接收缓冲区和发送缓冲区的 DMA 请求是独立的。

使用 DMA 进行发送

使用 DMA 进行发送 将 USART_CR3 寄存器中的 DMAT 位置 1 可以使能 DMA 模式进行发送。当 TXE 位置 1 时,可将数据从 SRAM 区(通过 DMA 配置,参见 DMA 部分)加载到 USART_DR 寄存器。要映射一个 DMA 通道以进行 USART 发送,请按以下步骤操作(x 表示通道编号):

  1. DMA 控制寄存器中写入 USART_DR 寄存器地址,将其配置为传输的目标地址。每次发生 TXE 事件后,数据都会从存储器移动到此地址。

  2. DMA 控制寄存器中写入存储器地址,将其配置为传输的源地址。每次发生 TXE 事件后,数据都会从这个存储区域加载到 USART_DR 寄存器中。

  3. DMA 控制寄存器中配置要传输的总字节数。

  4. DMA 寄存器中配置通道优先级。

  5. 根据应用的需求,在完成一半或全部传输后产生 DMA 中断。

  6. SR 寄存器中的 TC 位写入 0,将其清零。

  7. DMA 寄存器中激活该通道。
    当达到在 DMA 控制器中设置的数据传输量时,DMA 控制器会在 DMA 通道的中断向量上产生一个中断。
    在发送模式下,DMA 对所有要发送的数据执行了写操作(DMA_ISR 寄存器中的 TCIF 标志置 1)后,可以对 TC 标志进行监视,以确保 USART 通信已完成。在禁止 USART 或进入停止模式前必须执行此步骤,以避免损坏最后一次发送。软件必须等待直到 TC=1TC 标志在所有数据发送期间都必须保持清零状态,然后在最后一帧发送结束后由硬件置 1。

image-20210811171032071

使用 DMA 进行接收

USART_CR3 寄存器中的 DMAR 位置 1 可以使能 DMA 模式进行接收。接收数据字节时,数据会从 USART_DR 寄存器加载到 SRAM 区域中(通过 DMA 配置,参见 DMA 规范)。要映射一个 DMA 通道以进行 USART 接收,请按以下步骤操作:

  1. DMA 控制寄存器中写入 USART_DR 寄存器地址,将其配置为传输的源地址。每次发生 RXNE 事件后,数据都会从此地址移动到存储器。

  2. DMA 控制寄存器中写入存储器地址,将其配置为传输的目标地址。每次发生 RXNE 事件后,数据都会从 USART_DR 寄存器加载到此存储区。

  3. DMA 控制寄存器中配置要传输的总字节数。

  4. DMA 控制寄存器中配置通道优先级。

  5. 根据应用的需求,在完成一半或全部传输后产生中断。

  6. DMA 控制寄存器中激活该通道。
    当达到在 DMA 控制器中设置的数据传输量时,DMA 控制器会在 DMA 通道的中断向量上产生一个中断。在中断子程序中,USART_CR3 寄存器中的 DMAR 位应由软件清零。

    注意: 如果 DMA 用于接收,则不要使能 RXNEIE

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NpkQgBln-1632828582308)(https://raw.githubusercontent.com/Nepqiu/gallery/master/img/image-20210811171131932.png)]

多缓冲区通信中的错误标志和中断生成

在多缓冲区通信中,如果事务中发生任何错误,都会在当前字节后放置错误标志。如果中断使 能置 1,则会产生中断。在单字节接收过程中,与 RXNE 一同置位的帧错误、上溢错误和噪声 标志具有单独的错误标志中断使能位(USART_CR3 寄存器中的 EIE 位);如果该位置 1, 则会因其中任何一个错误而在当前字节后产生中断。

编程

接收的方式:

  1. DMA接收中断接收
  2. DMA+串口+DMA空闲中断接收
  3. DMA双缓冲区+串口+DMA空闲中断接收
  4. DMA+串口+DMA空闲中断+环形队列接收

发送的方式:

  1. DMA+串口发送
  2. 单串口发送
  3. DMA+串口发送+环形队列(双缓冲)
  4. 动态内存分配的FIFIO

下面主要用 环形队列+DMA+非动态内存分配+IDLE中断

建议先看最下面的参考文章

接收流程

USART1 + DMA + IDLE中断 +无锁队列

主函数:

int main(void)
{
    uint8_t buff_read[32];
    uint32_t length;

    usart1_init();

    while (1)
    {
        length = fifo_read_buff(pfifo_x, buff_read, 32);
        if (length)
        {
            printf("lengtt = %d", length); // 实际接收的数据长度
            //对接收的数据进行处理
        }
        else
        {
            printf("no data rx");// 没有数据
        }

        if (pfifo_x->error)
        {
            printf("fifo error %d", pfifo_x->error);// 接收错误
            pfifo_x->error = 0;
        }
    }
}

中断处理函数:

void USART1_IRQHandler(void) // 接收数据中断
{
    __IO uint8_t Len = 0;

    //发送完成中断
    /*
	 * DMA中断时,只表示需要传送的所有数据字节全部传送到串口的发送数据寄存器中了。
	 * 此时串口实际上还有2个字节并未发送完成,数据寄存器和移位寄存器中的2个字节还需要发送,并不能关闭串口发送。
	 * 同理,如果是485切换方向,必须要等到发送完成,也就是移位寄存器发送完成-TC标志置位。
	 * 
	 * TXE指的是发送缓冲器DR空,TC指的是SHIFT移位寄存器空。
	 * DMA完成只是代表把最后一个字节送到DR寄存器里面了,此时SHIFT移位寄存器有1个字节正在开始发送,
	 * DR寄存器里面有一个字节等待发送,所以就是2个字节未发送完成。
	 */
    if (USART_GetITStatus(USART1, USART_IT_TC) == SET)
    {
        USART_ClearITPendingBit(USART1, USART_IT_TC);
        USART_ITConfig(USART1, USART_IT_TC, DISABLE);
        DMA2_Stream7_working = 0;
    }
    //总线空闲中断
    if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //触发中断标志位
    {
        Len = USART1->SR;                           //清除RXNE标志
        Len = USART1->DR;                           //清USART_IT_IDLE标志
        Len = DMA_GetCurrDataCounter(DMA2_Stream5); //获取当前剩余数据量大小的函数

        if (pfifo_1 != 0)
        {
            // Len为当前接收索引
            pfifo_1->in += ((pfifo_1->last_cnt - Len) & (pfifo_1->size - 1)); //更新in
            pfifo_1->last_cnt = Len;

            if ((pfifo_1->in - pfifo_1->out) > pfifo_1->size)
            {
                pfifo_1->out = pfifo_1->in; // 清空缓存,注意赋值顺序,pfifo->in = pfifo->out 是错误的
                pfifo_1->error |= FIFO_DMA_ERROR_RX_FULL;
            }
        }
        else
        {
            pfifo_1->error |= FIFO_DMA_ERROR_RX_POINT_NULL;
        }
    }
}

初始化(标准库)

#define USART1_RX_LEN 32
#define USART1_TX_LEN 32
uint8_t Usart1_Rx[USART1_RX_LEN] = {0};
uint8_t Usart1_Tx[USART1_TX_LEN] = {0};
uint8_t Usart1_Tx_Buffer[USART1_TX_LEN] = {0};

fifo_rx_def fifo_usart_rx_1;
fifo_rx_def *pfifo_1 = &fifo_usart_rx_1;
fifo_rx_def fifo_usart_tx_1;

uint8_t DMA2_Stream7_working = 0;

void usart1_init(void)
{

    /* -------------- Enable Module Clock Source ----------------------------*/
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);  //使能GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //使能USART1时钟

    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);  //GPIOA9复用为USART1
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); //GPIOA10复用为USART1

    /* -------------- Configure GPIO ---------------------------------------*/
    {
        GPIO_InitTypeDef GPIO_InitStruct;
        NVIC_InitTypeDef NVIC_InitStructure;
        USART_InitTypeDef USART1_InitStruct;

        //USART1端口配置
        GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //GPIOA9与GPIOA10
        GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;            //复用功能
        GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;       //速度50MHz
        GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;          //推挽复用输出
        GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;            //上拉
        GPIO_Init(GPIOA, &GPIO_InitStruct);                  //初始化PA9,PA10

        //USART1 初始化设置
        USART_DeInit(USART1);
        USART1_InitStruct.USART_BaudRate = 115200;                                    //波特率设置
        USART1_InitStruct.USART_WordLength = USART_WordLength_8b;                     //字长为8位数据格式
        USART1_InitStruct.USART_StopBits = USART_StopBits_1;                          //一个停止位
        USART1_InitStruct.USART_Parity = USART_Parity_No;                             //无奇偶校验位
        USART1_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
        USART1_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                 //收发模式
        USART_Init(USART1, &USART1_InitStruct);                                       //初始化串口1

        USART_Cmd(USART1, ENABLE); //使能串口1
        USART_ClearFlag(USART1, USART_FLAG_TC);

        //Usart1 NVIC 配置
        NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;         //串口1中断通道
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
        NVIC_Init(&NVIC_InitStructure);                 //根据指定的参数初始化VIC寄存器

        USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启相关中断
        USART_ITConfig(USART1, USART_IT_TC, DISABLE);
        USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
    }

    /* -------------- Configure DMA -----------------------------------------*/
    /* 发送 */
    {
        DMA_InitTypeDef DMA_InitStruct;
        NVIC_InitTypeDef NVIC_InitStructure;

        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

        //发送数据
        DMA_Cmd(DMA2_Stream7, DISABLE); //关闭DMA
        DMA_DeInit(DMA2_Stream7);       //重置为缺省值
        DMA_InitStruct.DMA_Channel = DMA_Channel_4;
        DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t) & (USART1->DR);   //源地址
        DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)(Usart1_Tx);          //目的地址
        DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral;                 //数据传输方向为外设到内存
        DMA_InitStruct.DMA_BufferSize = USART1_TX_LEN;                       //设置数据的缓冲大小
        DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;        //外设地址不变
        DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;                 //内存缓冲区地址自加
        DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8位字节传输
        DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;         //数据宽度为8位
        DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;                           //工作在正常模式
        DMA_InitStruct.DMA_Priority = DMA_Priority_VeryHigh;                 //最高优先级
        DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
        DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
        DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
        DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
        DMA_Init(DMA2_Stream7, &DMA_InitStruct);

        NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream7_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStructure);

        DMA_ITConfig(DMA2_Stream7, DMA_IT_TC, ENABLE); //开启发送完成中断
        DMA_Cmd(DMA2_Stream7, DISABLE);
        USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); //使能串口的DMA发送

        if (fifo_init(&fifo_usart_tx_1, Usart1_Tx_Buffer, USART1_TX_LEN) == -1)
        {
            // 必须 2 的幂次方
        }
    }
    // 接收数据
    {
        DMA_InitTypeDef DMA_InitStruct;

        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

        DMA_Cmd(DMA2_Stream5, DISABLE); //关闭DMA
        while (DMA_GetCmdStatus(DMA2_Stream5) != DISABLE)
            ;
        DMA_DeInit(DMA2_Stream5); //重置为缺省值
        DMA_InitStruct.DMA_Channel = DMA_Channel_4;
        DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t) & (USART1->DR);   //源地址
        DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)(Usart1_Rx);          //目的地址
        DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;                 //数据传输方向为外设到内存
        DMA_InitStruct.DMA_BufferSize = USART1_RX_LEN;                       //设置数据的缓冲大小
        DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;        //外设地址不变
        DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;                 //内存缓冲区地址自加
        DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8位字节传输
        DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;         //数据宽度为8位
        DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;                         //工作在循环缓存模式
        DMA_InitStruct.DMA_Priority = DMA_Priority_VeryHigh;                 //最高优先级
        DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
        DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
        DMA_InitStruct.DMA_MemoryBurst = DMA_Mode_Normal; //DMA_MemoryBurst_Single;//
        DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
        DMA_Init(DMA2_Stream5, &DMA_InitStruct);

        DMA_Cmd(DMA2_Stream5, ENABLE);
        USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);

        if (fifo_init(pfifo_1, Usart1_Rx, USART1_RX_LEN) == -1)
        {
            // 必须 2 的幂次方
        }
    }
}

发送流程

在设计串口驱动过程中,要遵循的准则是:

  1. 尽量的减少程序运行时间
  2. 尽量的减少程序所占的内存

开启串口发送完成中断

void USART1_IRQHandler(void) // 接收数据中断
{
    __IO uint8_t Len = 0;

    //发送完成中断
    /*
	 * DMA中断时,只表示需要传送的所有数据字节全部传送到串口的发送数据寄存器中了。
	 * 此时串口实际上还有2个字节并未发送完成,数据寄存器和移位寄存器中的2个字节还需要发送,并不能关闭串口发送。
	 * 同理,如果是485切换方向,必须要等到发送完成,也就是移位寄存器发送完成-TC标志置位。
	 * 
	 * TXE指的是发送缓冲器DR空,TC指的是SHIFT移位寄存器空。
	 * DMA完成只是代表把最后一个字节送到DR寄存器里面了,此时SHIFT移位寄存器有1个字节正在开始发送,
	 * DR寄存器里面有一个字节等待发送,所以就是2个字节未发送完成。
	 */
    if (USART_GetITStatus(USART1, USART_IT_TC) == SET)
    {
        USART_ClearITPendingBit(USART1, USART_IT_TC);
        USART_ITConfig(USART1, USART_IT_TC, DISABLE);
        DMA2_Stream7_working = 0;
    }
}

开启DMA发送完成中断

//uint8_t DMA2_Stream7_working = 0;

/**
  * @brief          发送完成中断
  * @param[in]      void
  * @retval         void
  */
/*
 * ST官方都有APPNOTE指导的(对于UART没有RS485功能的单片机型号而言):
 * 1、启动DMA前,先关闭UART发送完成中断,并清除发送完成中断标志;
 * 2、在DMA传输完成中断函数中,开启UART发送完成中断;
 * 3、在UART发送完成中断函数中,切换RS485为接收态;
*/
void DMA2_Stream7_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA2_Stream7, DMA_IT_TCIF7) != RESET)
    {
        DMA_ClearFlag(DMA2_Stream7, DMA_IT_TCIF7);
        DMA_Cmd(DMA2_Stream7, DISABLE); //不使能传输
        //while (DMA_GetCmdStatus(DMA2_Stream7) != DISABLE);
        USART_ITConfig(USART1, USART_IT_TC, ENABLE);

        //DMA2_Stream7_working = 0;
    }
}

DMA发送函数

//uint8_t DMA2_Stream7_working = 0;

/**
  * @brief          串口一+DMA 发送
  * @param[in]      *data: 需要发送的数据
  * @param[in]      len: 数据的大小
  * @retval         void
  */
uint32_t usart1_dma_send(uint8_t *data, uint16_t len)
{
    uint32_t result = fifo_write_buff(&fifo_usart_tx_1, data, len); //将数据放入循环缓冲区

    //result != 0 说明放入数据成功 DMA2_Stream6_working == 0 说明缓冲区里面没有数据
    if (DMA2_Stream7_working == 0 && result != 0)
    {
        len = fifo_read_buff(&fifo_usart_tx_1, Usart1_Tx, USART1_TX_LEN); //从循环缓冲区获取数据

        DMA_SetCurrDataCounter(DMA2_Stream7, len); //设定传输长度
        DMA_Cmd(DMA2_Stream7, ENABLE);             //使能传输

        DMA2_Stream7_working = 1;
    }

    if (result == len)
    {
        return len;
    }
    else
    {
        return result;
    }
}

解析

https://www.amobbs.com/thread-4516795-1-1.html

stm32使用dma传输串口数据时,当dma中断发送完成

程序1

/*指针是指向ptr,需要发送count个数据*/
void USART1WriteDataToBuffer(*ptr,u8 count)
{
    /*判断数据是否发送完毕*/
    while(count--)
    {
        /*发送数据*/
        USART1SendByte(*ptr++);
        /*等待这个数据发送完毕,然后进入下一个数据的发送过程*/
        while(USART_GetFlagStatus(USART1, USART_FLAG_TC);
    }
    /*数据发送完毕,返回*/
}

这段程序会在实际应用中产生灾难性的后果。首先,但发送数据送到寄存器启动后,CPU就一直在等待这个数据发送完成,这样,直到所有要发送的数据完成,CPU才能做其他事情。相对于CPU内核运行的速度而言,串口外设的运行速度是非常快的,让一个非常快的设备去等待相对很慢的设备,程序的效率是非常底下的。

所以尽量采取中断的方式去发送数据

程序2

/*将数据写入发送缓冲区*/
void USART1WriteDataToBuffer(*ptr,u8 count)
{
    while (count != '\0')
    {
        USART1SendTCB[Index++] = *ptr++;
        Count = count;
    }
    /***判断溢出等其他代码省略***/
}
/***发送中断的ISR***/
void USART1SendUpdate(void)
{
    /***判断发送缓冲区中的数据是否发送完毕***/
    /***将发送缓冲区的数据发送出去***/
    USART1SendByte( *ptr++ ); /***发送指针加一,待发送的字节数减一等代码***/
}

这样,当调用USART1WriteDataToBuffer这个函数的时候,我们将要发送的数据写入发送缓冲区,CPU就可以执行其他任务了,待一个数据发送完成以后,中断ISR就会触发,在中断服务程序中将下一个数据写入发送寄存器,启动下一次发送,直到完全发送完毕。

但是在实际工程应用中,经常会遇到这种类似的情况,串口显示屏需要显示1000个点,通过串口发送这1000个点的颜色的RGB年度数值。将这1000个数据写入发送寄存器以后,启动发送。在115200的波特率,一位起始位,一位停止位,在无校验位的情况下,至少需要(10*1000*2)/115200=0.1736秒,在这段期以内,时钟更新了,需要再发送一串时间更新数据,这个数据大约有100个,这样这串数据需要写入到发送缓冲区的发送字节后面。

同样的道理,在这个时候如果有显示任务更新的话,将会有其他的数据写入到缓冲区。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JxeVlfbL-1632828582309)(https://raw.githubusercontent.com/Nepqiu/gallery/master/img/ourdev_611415V21OP9.jpg)]

从图上可以看出,程序2虽然满足了时间上的要求,却没有满空间上的要求,它的数据缓冲区的单向的,这样,当发送缓冲区撑满了以后才能将发送发送缓冲区的数据清空,以便下次的缓冲数据。这样内存较小的嵌入式系统来说是不能容忍的。

因此,也可以将发送缓冲区建立一个环形缓冲区,在这个缓冲区内,通过头指针(HostIndex)和尾指针(HostIndex)来定位空白区和数据区。

  • 头指针:指向有数据区的顶部,每次写入数据,都更新头指针,如果到了缓冲区的末端,就自动返回到缓冲区的起始处(StartIndex),直到写入到尾指针为止,这时缓冲区已经被装满,不能再装入数据。
  • 尾指针:指向有数据区的尾部,当数据发送完毕后,更新尾指针的位置,如果到了缓冲区的末端(EndIndex),就自动返回到缓冲区的起始处(StartIndex),直到遇到头指针为止,这是证明所有的数据已经发送完毕。

img

这样就实现了发送缓冲区的动态调整空白区和数据区,刚刚发送完毕的数据,马上就被开辟出来用于存放下一个数据,最大可能的节省了宝贵的发送缓冲区的空间,提高了效率。

fifo_buff代码

fifo_buff.c

/**
  ******************************************************************************
  * @file       fifo_buff.c/h
  * @brief      无锁队列
  * @note       
  * @history    2021.07.08
  *
  @verbatim   
  ==============================================================================
  利用 串口+DMA+IDLE中断+无锁队列,提高串口接收效率  (参考《鱼鹰谈单片机公众号》)
  1、串口初始化函数一旦执行完成,串口就开始使用 DMA接收数据,空闲中断产生时,
    用户才能在后续得到 DMA缓存接收的数据。
  2、因为 DMA数据的更新由串口空闲中断决定,所以一旦一帧数据很长
    (在这里为大于 128,或者一帧数据大于剩余缓存空间),那么程序会发现这个错误,
    并设置标志位(有些错误可能无法发现),所以这里的缓存大小设置比较关键,
    必须大于一帧缓存,最好两帧以上,并且是 2 的幂次方。
  3、如果内存有限制,无法开更大缓存,那么可以开启 DMA的半传输中断,
    这样也可以及时取走 DMA缓存的数据(或者使用定时更新的方式)。
  4、用户缓存 buff_read 可以随意设置,没有限制,但为了节省内存,
    一般小于等于 DMA 的接收缓存 usart_buff_rx。另外在该例子中,
    buff_read 并没有清除数据,可以按需清除。
  5、fifo_read_buff 返回值为实际接收到的数据长度,如果等于 0,代表没有接收到任何数据,
    并且读取完之后,会自动清除 DMA缓存的数据,
    用户不需要清除它(实际上,缓存的数据还在,只是用户读取不了,并最终会被后面接收的数据所覆盖)
  6、串口中断一般可以设置为最低优先级,因为是 DMA后台自动接收的,
    所以中断优先级最低并不会丢失数据(前提是缓存足够大)。
  7、如果使用串口不为空(USART_IT_RXNE)中断,一般接收会出现 ORE错误,
    此时如果不清除该错误会导致死机现象,但一般 DMA总是能及时接收数据,
    应该不会产生该错误,但为了发现这种情况,也设置了错误标志。
  总结一下,串口接收的关键就是 DMA循环接收,和接收索引的更新。
  ==============================================================================
  @endverbatim
  ******************************************************************************
  */
#include "fifo_buff.h"
#include <limits.h>

#define IS_POWER_OF_2(x) ((x) != 0 && (((x) & ((x)-1)) == 0))

// 不建议使用宏,除非确定没有使用隐患
// 建议用内联函数,除非你对应用min_fifo的场景陷阱非常熟悉,否则还是用内联函数比较稳妥,这也是effective c这本书所推荐的做法。
static inline uint32_t min_fifo(uint32_t X, uint32_t Y)
{
    return ((X) > (Y) ? (Y) : (X));
}

/**
  * @brief      计算能四舍五入到下一个2次幂的最大数
  * @param[in]  Num: 要更改的数字
  * @retval     返回更改后的数字
  * @attention
  */
uint32_t roundup_pow_of_two(uint32_t Num)
{
    uint32_t result = 1;

    if (IS_POWER_OF_2(Num) || Num == 0)
        return Num;
    else if (Num > LONG_MAX)
        return (LONG_MAX ^ ULONG_MAX); // WARN: 如果Num大于(LONG_MAX+1),那么result将等于(LONG_MAX+1)

    while (Num)
    {
        Num >>= 1;
        result <<= 1;
    }

    return result;
}

/**
  * @brief          计算能四舍五入到下一个2次幂的最小数
  * @param[in]      Num: 要更改的数字
  * @retval         返回更改后的数字
  * @attention
  */
uint32_t rounddown_pow_of_two(uint32_t Num)
{
    uint32_t result = 1;

    if (IS_POWER_OF_2(Num) || Num == 0)
        return Num;
    else if (Num > LONG_MAX)
        return (LONG_MAX ^ ULONG_MAX); // WARN: 如果Num大于(LONG_MAX+1),那么result将等于(LONG_MAX+1)

    while (Num)
    {
        Num >>= 1;
        result <<= 1;
    }

    return result >> 1;
}

/**
  * @brief          环形缓冲区的初始化
  * @param[in]      pfifo: 将循环缓冲区初始化
  * @param[in]      buff: 用于存储数据的环形缓冲区
  * @param[in]      size: 缓冲区的大小
  * @retval         成功返回0
  * @attention
  */
int32_t fifo_init(fifo_rx_def *pfifo, uint8_t *buff, uint32_t size)
{
    assert_param(pfifo != NULL || buff != NULL);

    if (!IS_POWER_OF_2(size)) // 必须 2 的幂次方
    {
        return -1;
    }

    pfifo->in = 0;
    pfifo->out = 0;
    pfifo->buffer = buff;
    pfifo->size = size; // 必须最后设置大小
    pfifo->last_cnt = size;

    return 0;
}

/**
  * @brief          从环形缓冲区获取数据
  * @param[in]      pfifo: 存储数据的环形缓冲区
  * @param[in]      buffer: 将存储来自循环缓冲区的数据的目标缓冲区
  * @param[in]      len: 要从循环缓冲区中获取的长度
  * @retval         从循环缓冲区获得的实际长度
  * @attention
  */
uint32_t fifo_read_buff(fifo_rx_def *pfifo, uint8_t *buffer, uint32_t len)
{
    uint32_t length;

    assert_param(pfifo != NULL || pfifo->buffer != NULL || buffer != NULL);

    len = min_fifo(len, pfifo->in - pfifo->out); //获取队列out开始到数组结束的大小

    /* first get the data from pfifo->out until the end of the buffer */
    length = min_fifo(len, pfifo->size - (pfifo->out & (pfifo->size - 1))); //获取队列out开始到数组结束的大小
    memcpy(buffer, pfifo->buffer + (pfifo->out & (pfifo->size - 1)), length);

    /* then get the rest (if any) from the beginning of the buffer */
    memcpy(buffer + length, pfifo->buffer, len - length);

    pfifo->out += len;

    return len;
}

/**
  * @brief          将数据放入环形缓冲区
  * @param[in]      pfifo: 存储数据的环形缓冲区
  * @param[in]      buffer: 要存储到环形缓冲区中的数据
  * @param[in]      len: 要存储到环形缓冲区中的数据长度
  * @retval         存储在环形缓冲区中的实际大小
  * @attention
  */
uint32_t fifo_write_buff(fifo_rx_def *pfifo, uint8_t *buffer, uint32_t len)
{
    uint32_t length;

    len = min_fifo(len, (pfifo->size - (pfifo->in - pfifo->out)));

    length = min_fifo(len, pfifo->size - (pfifo->in & (pfifo->size - 1)));

    memcpy(pfifo->buffer + (pfifo->in & pfifo->size - 1), buffer, length);
    memcpy(pfifo->buffer, buffer + length, len - length);

    pfifo->in += len;

    return len;
}

/**
  * @brief          获取环形缓冲区的可用内存大小
  * @param[in]      pfifo: 存储数据的环形缓冲区
  * @retval         环形缓冲区的可用内存大小
  * @attention
  */
unsigned int fifo_get_free(fifo_rx_def *pfifo)
{
    return ((pfifo->size > 0) ? (pfifo->size - (pfifo->in - pfifo->out)) : 0);
}

/**
  * @brief          获取环形缓冲区已使用的内存大小
  * @param[in]      pfifo: 存储数据的环形缓冲区
  * @retval         环形缓冲区已使用的内存大小
  * @attention
  */
unsigned int fifo_get_full(fifo_rx_def *pfifo)
{
    return (pfifo->in - pfifo->out);
}

/**
  * @brief          检查环形缓冲区是否为空
  * @param[in]      pfifo: 存储数据的环形缓冲区
  * @retval         如果没有数据就返回1
  * @attention
  */
unsigned int fifo_is_empty(fifo_rx_def *pfifo)
{
    return ((pfifo->size > 0) && (pfifo->in == pfifo->out));
}

/**
  * @brief          检查环形缓冲区是否已满
  * @param[in]      pfifo: 存储数据的环形缓冲区
  * @retval         如果满了就返回1
  * @attention
  */
unsigned int fifo_is_full(fifo_rx_def *pfifo)
{
    return ((pfifo->size == 0) || (pfifo->size == (pfifo->in - pfifo->out)));
}

fifo_buff.h

/**
  ******************************************************************************
  * @file       nepqiu_fifo_buff.c/h
  * @brief      无锁队列
  * @note       2021.07.08
  *
  @verbatim   
  ==============================================================================
  利用 串口+DMA+IDLE中断+无锁队列,提高串口接收效率  (参考《鱼鹰谈单片机公众号》)
  1、串口初始化函数一旦执行完成,串口就开始使用 DMA接收数据,空闲中断产生时,
    用户才能在后续得到 DMA缓存接收的数据。
  2、因为 DMA数据的更新由串口空闲中断决定,所以一旦一帧数据很长
    (在这里为大于 128,或者一帧数据大于剩余缓存空间),那么程序会发现这个错误,
    并设置标志位(有些错误可能无法发现),所以这里的缓存大小设置比较关键,
    必须大于一帧缓存,最好两帧以上,并且是 2 的幂次方。
  3、如果内存有限制,无法开更大缓存,那么可以开启 DMA的半传输中断,
    这样也可以及时取走 DMA缓存的数据(或者使用定时更新的方式)。
  4、用户缓存 buff_read 可以随意设置,没有限制,但为了节省内存,
    一般小于等于 DMA 的接收缓存 usart_buff_rx。另外在该例子中,
    buff_read 并没有清除数据,可以按需清除。
  5、fifo_read_buff 返回值为实际接收到的数据长度,如果等于 0,代表没有接收到任何数据,
    并且读取完之后,会自动清除 DMA缓存的数据,
    用户不需要清除它(实际上,缓存的数据还在,只是用户读取不了,并最终会被后面接收的数据所覆盖)
  6、串口中断一般可以设置为最低优先级,因为是 DMA后台自动接收的,
    所以中断优先级最低并不会丢失数据(前提是缓存足够大)。
  7、如果使用串口不为空(USART_IT_RXNE)中断,一般接收会出现 ORE错误,
    此时如果不清除该错误会导致死机现象,但一般 DMA总是能及时接收数据,
    应该不会产生该错误,但为了发现这种情况,也设置了错误标志。
  总结一下,串口接收的关键就是 DMA循环接收,和接收索引的更新。
  ==============================================================================
  @endverbatim
  ******************************************************************************
  */

#ifndef __NEPQIU_FIFO_BUFF_H__
#define __NEPQIU_FIFO_BUFF_H__
#include <string.h>
#include <stdint.h>
#include "struct_typedef.h"

#define FIFO_DMA_ERROR_RX_NOT_IDLE (0x1 << 0)   // 非空闲中断
#define FIFO_DMA_ERROR_RX_POINT_NULL (0x1 << 1) // 指针为空
#define FIFO_DMA_ERROR_RX_FULL (0x1 << 2)       // 非空闲中断

typedef struct
{
    uint8_t *buffer;
    uint32_t in;
    uint32_t out;
    uint16_t size;
    
    uint16_t error; // 接收错误
    uint16_t last_cnt;
} fifo_rx_def;



int32_t fifo_init(fifo_rx_def *pfifo, uint8_t *buff, uint32_t size);
uint32_t fifo_read_buff(fifo_rx_def *pfifo, uint8_t *buffer, uint32_t len);
uint32_t fifo_write_buff(fifo_rx_def *pfifo, uint8_t *buffer, uint32_t len);
unsigned int fifo_get_free(fifo_rx_def *pfifo);
unsigned int fifo_get_full(fifo_rx_def *pfifo);
unsigned int fifo_is_empty(fifo_rx_def *pfifo);
unsigned int fifo_is_full(fifo_rx_def *pfifo);

#endif /* __NEPQIU_FIFO_BUFF_H */

完整代码和使用示例

#include "usart1.h"
#include "fifo_buff.h"

#define USART1_IMU_RX_LEN 32
#define USART1_IMU_TX_LEN 32
uint8_t Usart1_IMU_Rx[USART1_IMU_RX_LEN] = {0};
uint8_t Usart1_IMU_Tx[USART1_IMU_TX_LEN] = {0};
uint8_t Usart1_IMU_Tx_Buffer[USART1_IMU_TX_LEN] = {0};

fifo_rx_def fifo_usart_rx_1;
fifo_rx_def *pfifo_1 = &fifo_usart_rx_1;
fifo_rx_def fifo_usart_tx_1;

uint8_t DMA2_Stream7_working = 0;


void usart1_init(void)
{

    /* -------------- Enable Module Clock Source ----------------------------*/
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);  //使能GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //使能USART1时钟

    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);  //GPIOA9复用为USART1
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); //GPIOA10复用为USART1

    /* -------------- Configure GPIO ---------------------------------------*/
    {
        GPIO_InitTypeDef GPIO_InitStruct;
        NVIC_InitTypeDef NVIC_InitStructure;
        USART_InitTypeDef USART1_InitStruct;

        //USART1端口配置
        GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //GPIOA9与GPIOA10
        GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;            //复用功能
        GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;       //速度50MHz
        GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;          //推挽复用输出
        GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;            //上拉
        GPIO_Init(GPIOA, &GPIO_InitStruct);                  //初始化PA9,PA10

        //USART1 初始化设置
        USART_DeInit(USART1);
        USART1_InitStruct.USART_BaudRate = 115200;                                    //波特率设置
        USART1_InitStruct.USART_WordLength = USART_WordLength_8b;                     //字长为8位数据格式
        USART1_InitStruct.USART_StopBits = USART_StopBits_1;                          //一个停止位
        USART1_InitStruct.USART_Parity = USART_Parity_No;                             //无奇偶校验位
        USART1_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
        USART1_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                 //收发模式
        USART_Init(USART1, &USART1_InitStruct);                                       //初始化串口1

        USART_Cmd(USART1, ENABLE); //使能串口1
        USART_ClearFlag(USART1, USART_FLAG_TC);

        //Usart1 NVIC 配置
        NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;         //串口1中断通道
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
        NVIC_Init(&NVIC_InitStructure);                 //根据指定的参数初始化VIC寄存器

        USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启相关中断
        USART_ITConfig(USART1, USART_IT_TC, DISABLE);
        USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
    }

    /* -------------- Configure DMA -----------------------------------------*/
    /* 发送 */
    {
        DMA_InitTypeDef DMA_InitStruct;
        NVIC_InitTypeDef NVIC_InitStructure;

        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

        //发送数据
        DMA_Cmd(DMA2_Stream7, DISABLE); //关闭DMA
        DMA_DeInit(DMA2_Stream7);       //重置为缺省值
        DMA_InitStruct.DMA_Channel = DMA_Channel_4;
        DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t) & (USART1->DR);   //源地址
        DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)(Usart1_IMU_Tx);      //目的地址
        DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral;                 //数据传输方向为外设到内存
        DMA_InitStruct.DMA_BufferSize = USART1_IMU_TX_LEN;                   //设置数据的缓冲大小
        DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;        //外设地址不变
        DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;                 //内存缓冲区地址自加
        DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8位字节传输
        DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;         //数据宽度为8位
        DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;                           //工作在正常模式
        DMA_InitStruct.DMA_Priority = DMA_Priority_VeryHigh;                 //最高优先级
        DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
        DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
        DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
        DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
        DMA_Init(DMA2_Stream7, &DMA_InitStruct);

        NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream7_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStructure);

        DMA_ITConfig(DMA2_Stream7, DMA_IT_TC, ENABLE); //开启发送完成中断
        DMA_Cmd(DMA2_Stream7, DISABLE);
        USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); //使能串口的DMA发送

        if (fifo_init(&fifo_usart_tx_1, Usart1_IMU_Tx_Buffer, USART1_IMU_TX_LEN) == -1)
        {
            // 必须 2 的幂次方
        }
    }
    // 接收数据
    {
        DMA_InitTypeDef DMA_InitStruct;

        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

        DMA_Cmd(DMA2_Stream5, DISABLE); //关闭DMA
        while (DMA_GetCmdStatus(DMA2_Stream5) != DISABLE)
            ;
        DMA_DeInit(DMA2_Stream5); //重置为缺省值
        DMA_InitStruct.DMA_Channel = DMA_Channel_4;
        DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t) & (USART1->DR);   //源地址
        DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)(Usart1_IMU_Rx);      //目的地址
        DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;                 //数据传输方向为外设到内存
        DMA_InitStruct.DMA_BufferSize = USART1_IMU_RX_LEN;                   //设置数据的缓冲大小
        DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;        //外设地址不变
        DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;                 //内存缓冲区地址自加
        DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8位字节传输
        DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;         //数据宽度为8位
        DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;                         //工作在循环缓存模式
        DMA_InitStruct.DMA_Priority = DMA_Priority_VeryHigh;                 //最高优先级
        DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
        DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
        DMA_InitStruct.DMA_MemoryBurst = DMA_Mode_Normal; //DMA_MemoryBurst_Single;//
        DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
        DMA_Init(DMA2_Stream5, &DMA_InitStruct);

        DMA_Cmd(DMA2_Stream5, ENABLE);
        USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);

        if (fifo_init(pfifo_1, Usart1_IMU_Rx, USART1_IMU_RX_LEN) == -1)
        {
            // 必须 2 的幂次方
        }
    }
}

void USART1_IRQHandler(void) // 接收数据中断
{
    __IO uint8_t Len = 0;

    //发送完成中断
    /*
	 * DMA中断时,只表示需要传送的所有数据字节全部传送到串口的发送数据寄存器中了。
	 * 此时串口实际上还有2个字节并未发送完成,数据寄存器和移位寄存器中的2个字节还需要发送,并不能关闭串口发送。
	 * 同理,如果是485切换方向,必须要等到发送完成,也就是移位寄存器发送完成-TC标志置位。
	*/
    if (USART_GetITStatus(USART1, USART_IT_TC) == SET)
    {
        USART_ClearITPendingBit(USART1, USART_IT_TC);
        USART_ITConfig(USART1, USART_IT_TC, DISABLE);
        DMA2_Stream7_working = 0;
    }
    //总线空闲中断
    if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //触发中断标志位
    {
        Len = USART1->SR;                           //清除RXNE标志
        Len = USART1->DR;                           //清USART_IT_IDLE标志
        Len = DMA_GetCurrDataCounter(DMA2_Stream5); //获取当前剩余数据量大小的函数

        if (pfifo_1 != 0)
        {
            // Len为当前接收索引
            pfifo_1->in += ((pfifo_1->last_cnt - Len) & (pfifo_1->size - 1)); //更新in
            pfifo_1->last_cnt = Len;

            if ((pfifo_1->in - pfifo_1->out) > pfifo_1->size)
            {
                pfifo_1->out = pfifo_1->in; // 清空缓存,注意赋值顺序,pfifo->in = pfifo->out 是错误的
                pfifo_1->error |= FIFO_DMA_ERROR_RX_FULL;
            }
        }
        else
        {
            pfifo_1->error |= FIFO_DMA_ERROR_RX_POINT_NULL;
        }
    }
}

/**
  * @brief          发送完成中断
  * @param[in]      void
  * @retval         void
  */
/*
 * ST官方都有APPNOTE指导的(对于UART没有RS485功能的单片机型号而言):
 * 1、启动DMA前,先关闭UART发送完成中断,并清除发送完成中断标志;
 * 2、在DMA传输完成中断函数中,开启UART发送完成中断;
 * 3、在UART发送完成中断函数中,切换RS485为接收态;
*/
void DMA2_Stream7_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA2_Stream7, DMA_IT_TCIF7) != RESET)
    {
        DMA_ClearFlag(DMA2_Stream7, DMA_IT_TCIF7);
        DMA_Cmd(DMA2_Stream7, DISABLE); //不使能传输
        //while (DMA_GetCmdStatus(DMA2_Stream7) != DISABLE);
        USART_ITConfig(USART1, USART_IT_TC, ENABLE);

        //DMA2_Stream7_working = 0;
    }
}

/**
  * @brief          串口一+DMA 发送
  * @param[in]      *data
  * @param[in]      len
  * @retval         void
  */
uint32_t usart1_dma_send(uint8_t *data, uint16_t len)
{
    uint32_t result = fifo_write_buff(&fifo_usart_tx_1, data, len); //将数据放入循环缓冲区

    //result != 0 说明放入数据成功 DMA2_Stream6_working == 0 说明缓冲区里面没有数据
    if (DMA2_Stream7_working == 0 && result != 0)
    {
        len = fifo_read_buff(&fifo_usart_tx_1, Usart1_IMU_Tx, USART1_IMU_TX_LEN); //从循环缓冲区获取数据

        DMA_SetCurrDataCounter(DMA2_Stream7, len); //设定传输长度
        DMA_Cmd(DMA2_Stream7, ENABLE);             //使能传输

        DMA2_Stream7_working = 1;
    }

    if (result == len)
    {
        return len;
    }
    else
    {
        return result;
    }
}

无论在用于接收什么数据都可以用一样的配置,需要更改的只有 USART1_IMU_RX_LENUSART1_IMU_TX_LEN 这两个宏

只用在需要的地方循环调用

extern fifo_rx_def fifo_usart_rx_1;
fifo_rx_def *pfifo_x = &fifo_usart_rx_1;

void usart_data_receive(void)
{
	uint8_t buff_read[32];

	uint32_t length = fifo_read_buff(pfifo_x, buff_read, 32);
    if (length)
    {
        //printf("lengtt = %d", length); // 实际接收的数据长度
	}
	else
	{
		//没有数据
	}
	
	if (pfifo_x->error)
    {
		// 接收错误
        pfifo_x->error = 0; 
    }
}

要更换串口也非常简单,只需要将上面的

extern fifo_rx_def fifo_usart_rx_1;
fifo_rx_def *pfifo_x = &fifo_usart_rx_1;

改为对应的串口,比如串口二的

extern fifo_rx_def fifo_usart_rx_2;
fifo_rx_def *pfifo_x = &fifo_usart_rx_2;

剩下的配置完全一致

参考

  • DMA+串口传输的问题
  • 串口通讯协议
  • 一个严谨的STM32串口DMA发送&接收
  • STM32串口中断、DMA接收的几点注意地方
  • STM32 DMA详解
  • 环形队列+内存动态分配+DMA的串口驱动

点击全文阅读


本文链接:http://zhangshiyu.com/post/34335.html

发送  数据  中断  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1