本章代码仓库:

git clone https://gitee.com/jumayusi/unit5-gd32-usart.git

5.1 串口UART基础知识

并行通信在同一时刻可以传输多个二进制位,我们当前使用的GD32F303使用的是16bit并行接口EXMC;STM32对应的接口一般叫做FSMC接口。

数据帧结构:

同步通信通常需要时钟信号,而异步通信通常不需要,所以uart通信我们一般归类为异步通信,IIC、SPI我们归类为同步通信。

TTL电平

RS232电平

RS485

RS232的物理接口标准

ARM32单片机串口硬件结构内是存在CK时钟线的,因此从习惯上,我们把串口通信叫做USART(跟官方API里面的命名同步synchronous)。

5.2 串口UART硬件结构

主要特性

开发板串口资源介绍:

关键寄存器与标志位:

奇偶校验

和校验\异或校验

循环冗余校验

5.3 printf打印通过串口输出

本节课的目标是利用单片机的串口通信功能重定向ARM Mirco库中的printf函数实现打印输出。

今天在使用arm_gnu时遇到了一个奇怪的问题,重定向fputc实现串口打印无效,问了AI才注意到arm_gnu里面printf依赖的是syscalls.c里面的_write函数。注意我们像使用arm_clang(keil)时候那样重写fputc是无效的!因此syscalls.c里面之前为了避免编译器报错的空实现都需要改为弱函数,然后在外部进行强实现。

5.4 如何使用串口中断接收数据

串口交互数据的几种基本场景:

1.MCU只发不收,比如单向printf打印。

2.MCU只收不发,比如传感器周期性主动发送数据。

3.MCU发送命令,接收响应,比如AT命令驱动WiFi模组。

4.MCU接受命令,发送响应,典型场景时Modbus RTU。

本节课引入如下实验:

推荐使用SSCOM:

这堂课主要犯了两个错误,代码上时三目运算符的接口函数弄反了,调试的时候用的XCOM没有校验位。

5.5 函数指针在串口中断中的应用

本节课利用函数指针实现了驱动和业务解耦。STM32的HAL里面有官方定义的__Weak中断回调函数,GD32使用的固件库整体代码风格更像STM32的标准库,因此我们需要手动编写注册函数。

5.6 为什么要使用环形队列?

如果串口发来的数据过快,那么就需要引入环形队列。

以前已经写过一篇了,唉,早岁那知世事艰,没有想到两年后工作会那么难找:

https://blog.csdn.net/qq_59757948/article/details/141031372?fromshare=blogdetail&sharetype=blogdetail&sharerId=141031372&sharerefer=PC&sharesource=qq_59757948&sharefrom=from_link

5.7 环形队列的代码实现

代码示例

queue.c:

#include "queue.h"

/**
****************************************************************
* @brief   初始化(创建)队列,每个队列须先执行该函数才能使用
* @param   queue, 队列变量指针
* @param   buffer, 队列缓存区地址
* @param   size, 队列缓存区长度
* @return 
****************************************************************
*/
void QueueInit(QueueType_t *queue, uint8_t *buffer, uint32_t size)
{
    queue->buffer = buffer;
    queue->size = size;
    queue->head = 0;
    queue->tail = 0;
}

/**
****************************************************************
* @brief   压入数据到队列中
* @param   queue, 队列变量指针
* @param   data, 待压入队列的数据
* @return  压入队列是否成功
****************************************************************
*/
QueueStatus_t QueuePush(QueueType_t *queue, uint8_t data)
{
    uint32_t index = (queue->tail + 1) % queue->size;

    if (index == queue->head)
    {
        return QUEUE_OVERLOAD;
    }
    queue->buffer[queue->tail] = data;
    queue->tail = index;
    return QUEUE_OK;
}

/**
****************************************************************
* @brief   从队列中弹出数据
* @param   queue, 队列变量指针
* @param   pdata, 待弹出队列的数据缓存地址
* @return  弹出队列是否成功
****************************************************************
*/
QueueStatus_t QueuePop(QueueType_t *queue, uint8_t *pdata)
{
    if(queue->head == queue->tail)
    {
        return QUEUE_EMPTY;
    }

    *pdata = queue->buffer[queue->head];
    queue->head = (queue->head + 1) % queue->size;
    return QUEUE_OK;
}

/**
****************************************************************
* @brief   压入一组数据到队列中
* @param   queue, 队列变量指针
* @param   pArray, 待压入队列的数组地址
* @param   len, 待压入队列的元素个数
* @return  实际压入到队列的元素个数
****************************************************************
*/
uint32_t QueuePushArray(QueueType_t *queue, uint8_t *pArray, uint32_t len)
{
    uint32_t i;
    for (i = 0; i < len; i++)
    {
        if(QueuePush(queue, pArray[i]) == QUEUE_OVERLOAD)
        {
            break;
        }
    }
    return i;
}

/**
****************************************************************
* @brief   从队列中弹出一组数据
* @param   queue, 队列变量指针
* @param   pArray, 待弹出队列的数据缓存地址
* @param   len, 待弹出队列的数据的最大长度
* @return  实际弹出数据的数量
****************************************************************
*/
uint32_t QueuePopArray(QueueType_t *queue, uint8_t *pArray, uint32_t len)
{
    uint32_t i;
    for(i = 0; i < len; i++)
    {
        if (QueuePop(queue, &pArray[i]) == QUEUE_EMPTY)
        {
            break;
        }
    }
    return i;
}

/**
****************************************************************
* @brief   获取队列中数据的个数
* @param   queue, 队列变量指针
* @return  队列中数据的个数
****************************************************************
*/
uint32_t QueueCount(QueueType_t *queue)
{
	if (queue->head <= queue->tail)
	{
		return queue->tail - queue->head;
	}
	
	return queue->size + queue->tail - queue->head;
}

queue.h:

#ifndef __queue_H
#define __queue_H

#include <stdint.h>

typedef struct
{
    uint32_t head;
    uint32_t tail;
    uint32_t size;
    uint8_t *buffer;
} QueueType_t;

typedef enum
{
    QUEUE_OK=0,    //队列正常
    QUEUE_ERROR,
    QUEUE_OVERLOAD,
    QUEUE_EMPTY
}QueueStatus_t;

/**
****************************************************************
* @brief   初始化(创建)队列,每个队列须先执行该函数才能使用
* @param   queue, 队列变量指针
* @param   buffer, 队列缓存区地址
* @param   size, 队列缓存区长度
* @return 
****************************************************************
*/
void QueueInit(QueueType_t *queue, uint8_t *buffer, uint32_t size);

/**
****************************************************************
* @brief   压入数据到队列中
* @param   queue, 队列变量指针
* @param   data, 待压入队列的数据
* @return  压入队列是否成功
****************************************************************
*/
QueueStatus_t QueuePush(QueueType_t *queue, uint8_t data);

/**
****************************************************************
* @brief   从队列中弹出数据
* @param   queue, 队列变量指针
* @param   pdata, 待弹出队列的数据缓存地址
* @return  弹出队列是否成功
****************************************************************
*/
QueueStatus_t QueuePop(QueueType_t *queue, uint8_t *pdata);

/**
****************************************************************
* @brief   压入一组数据到队列中
* @param   queue, 队列变量指针
* @param   pArray, 待压入队列的数组地址
* @param   len, 待压入队列的元素个数
* @return  实际压入到队列的元素个数
****************************************************************
*/
uint32_t QueuePushArray(QueueType_t *queue, uint8_t *pArray, uint32_t len);

/**
****************************************************************
* @brief   从队列中弹出一组数据
* @param   queue, 队列变量指针
* @param   pArray, 待弹出队列的数据缓存地址
* @param   len, 待弹出队列的数据的最大长度
* @return  实际弹出数据的数量
****************************************************************
*/
uint32_t QueuePopArray(QueueType_t *queue, uint8_t *pArray, uint32_t len);

/**
****************************************************************
* @brief   获取队列中数据的个数
* @param   queue, 队列变量指针
* @return  队列中数据的个数
****************************************************************
*/
uint32_t QueueCount(QueueType_t *queue);

#endif

5.8 使用环形队列解决接收数据太快的问题

usb2com_app.c:

#include <stdbool.h>
#include <stdio.h>

#include "led_drv.h"
#include "usb2Com_drv.h"
#include "queue.h"

#define FRAME_HEAD_0    0x55
#define FRAME_HEAD_1    0xAA
#define MAX_BUF_SIZE    77   //最大数据帧和最小数据帧的公倍数
#define PACKET_DATA_LEN_MIN   7
#define PACKET_DATA_LEN_MAX   11

#define FUNC_DATA_IDX   3
#define LED_CTRL_CODE   0x06

static uint8_t g_rcvDataBuf[MAX_BUF_SIZE];
static QueueType_t g_rcvQueue;

typedef struct
{
   uint8_t ledNo;
   uint8_t ledState;
}LedCtrlInfo_t;

static void ProcUartData(uint8_t data)
{
   QueuePush(&g_rcvQueue,data);
}

/**
***********************************************************
* @brief USB转串口应用初始化函数
* @param
* @return 
**
*********************************************************
*/
void Usb2COMAppInit(void)
{
    regUsb2ComCallback(ProcUartData);
    QueueInit(&g_rcvQueue,g_rcvDataBuf,MAX_BUF_SIZE);
}

/**
***********************************************************
* @brief 计算数据的异或校验和
* @param pData 指向待计算数据的指针
* @param len 数据长度(字节数)
* @return 计算得到的异或校验和
***********************************************************
**/
static uint8_t CalXorSum(const uint8_t *pData,uint32_t len)
{
    uint8_t xorSum=0;
    for(uint32_t i=0;i<len;i++)
    {
        xorSum^=pData[i];
    }
    return xorSum;
}

/**
***********************************************************
* @brief  控制LED
* @param  ctrlData 指向LED控制参数的指针
* @return 
***********************************************************
**/
static void CtrlLed(LedCtrlInfo_t *ctrlData)
{
   (ctrlData->ledState)!=0 ?TurnOnLed(ctrlData->ledNo):TurnOffLed(ctrlData->ledNo);
}

/**
***********************************************************
* @brief USB转串口任务处理函数
* @param
* @return 
**
*********************************************************
*/
void Usb2ComTask(void)
{
    uint8_t readBuf[PACKET_DATA_LEN_MAX]={0};

    while(QueuePop(&g_rcvQueue,&readBuf[0])==QUEUE_OK)
    {
        if(readBuf[0]!=FRAME_HEAD_0)
        {
            continue;   //直到找到帧头为止
        }
        if((QueuePop(&g_rcvQueue,&readBuf[1])==QUEUE_EMPTY) || (readBuf[1]!=FRAME_HEAD_1))
        {
            continue;   
        }
        if((QueuePop(&g_rcvQueue,&readBuf[2])==QUEUE_EMPTY) || (readBuf[2]>=PACKET_DATA_LEN_MAX))
        {
            continue;
        }
         if(QueuePopArray(&g_rcvQueue,&readBuf[3],readBuf[2]+1)!=readBuf[2]+1)     
        {
            continue;
        }
        if(CalXorSum(readBuf,readBuf[2]+3)!=readBuf[readBuf[2]+3])
        {
            continue;
        }

        if(g_rcvDataBuf[FUNC_DATA_IDX]==LED_CTRL_CODE)
        {
         CtrlLed((LedCtrlInfo_t *)(&readBuf[FUNC_DATA_IDX+1]));
        }

        //打印接收到的数据包
        for(uint32_t i=0;i<readBuf[2]+4;i++)
        {
            printf("%02X ",readBuf[i]);
        }
        printf("\r\n");
    }
}

5.9 为什么要使用空闲中断和DMA

之前利用环形缓冲区和非空中断接收串口数据的代码,虽然解决了发送过快易覆盖的问题,但是因为中断频繁打断CPU进程(每个字节被串口接收都会导致进入中断),影响了整个系统的实时性。

串口的中断事件

当检测到RX引脚空闲(高电平)时间超过传输一个字符帧所需的时间时,产生空闲标志IDLEF。同一包数据内,空闲位占用的时间是很短的。

DMA介绍

DMA工作原理

DMA0的七个通道可以连接到哪些外设:

DMA1更少:

5.10 空闲中断配合DMA,接收数据更高效

其实把这些作为prompt喂给AI,它自己会帮你写好的:

 注意,IDLE空闲中断清除标志位和RBNE清除标志位是不同的。

这节课的代码调试过程中,主要犯了两个错误;

1.想要在栈内实现结构体指针,但写成了野指针,只声明了一个指针变量。

belike:

dma_parameter_struct *dmaStruct_t;

正确写法:

2.中断服务函数里面参数写错了,都是千问帮我看出来的,唉以后我这种人就是AI的焊工苦力罢了((((。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐