STM32使用环形数组接收不定长数据

注:未经作者允许,转载请注明出处。

一、环形数组的引入

在C语言单片机编程过程中,经常使用串口接收数据,采用的方式一般有两种,第一种是轮循模式接收,这种方式往往需要消耗大量的时间检测接收的数据缓存(数据),有数据的时候就执行,没有的话就退出。此外,由于串口接收数据的时刻是无法预测的,所以即使检测到数据,有时候也会由于数据丢失造成错误。还有一种是中断,中断接收响应快,但是对于不定长数据就很难办了。那么对于以上情况,本次介绍一下环形数组的存储方式,使用串口中断接收+环形数组,能够很好的接收不定长数据,达到事半功倍的效果。

二、什么是环形数组

顾名思义,环形数组就是一个类似于圆环的数组。一般我们经常采用数组进行数据存储,试想一下,如果将数组的首尾相连组成一个环,将读数据的起始地址和写数据的起始地址都映射到该环的某一个的位置,并指明一个正方向,采用先进先出机制,每次数据存储地址都随数据存入而增长,直到数组的尾部,然后再回到数组的首部,每次读数据也类似的变化,那么,每次读地址到写地址之间的数据地址,就是最新的缓存数据地址。
环形数组示意图
例如上图,将大小为MAXN的数组首尾相连接后,以顺时针为正方向,定义tail为尾,以head为首,head是当前写入数据的首地址,tail就是当前写入数据的尾地址,同样也是读数据的首地址,数据从tail以顺时针向head方向读取,head就是读数据的末地址。那么从数组下标 7–>(MAXN-1)–> 1 的数据就是最新数据。当进行数据读取时,只需读取要以顺时针从tail所在的下标开始进行读数据,直到head下标所在的数据,就是目前最新的数据。
那么既然是一个环,那肯定会有head的地址与tail的地址相重合的时候,那么这时候怎么办?这时就存在两种情况:1、缓存区数据为空;2、缓存区数据满,首尾相连了,不能进行数据存储了。这时候如何判断这两种情况?那么就引入标志位read_flag和write_flag,采用这两个标志位记录从下标(MAXN-1)到下标0的跨界标志,每当从数组的(MAXN-1)跨界到下标0,就将read_flag或者writa_flag变换一次。那么如果存在条件:1、head和tail地址相等,且read_flag == write_flag,则数组为空;2、head和tail地址相等,且read_flag != write_flag,则数组为full。为空时,不需要进行读操作,为full时,如果不想覆盖掉之前的数据,就需要禁止对缓存区写操作,如果不介意丢数据,则可以进行写(当然一般不会这样,数据丢了还有啥意义)。

三、C语言实现环形数组

注:以下函数均来自RT-thread官网>>RT-Thread Nano>>移植控制台/FinSH>>示例代码

3.1、先建一个环,把数据存进去

struct rt_ringbuffer
{
	// rt_uint8_t是unsigned char 类型
	//rt_uint16_t 是unsigned int 类型
    rt_uint8_t *buffer_ptr;        //指向数组的指针
    rt_uint16_t read_mirror : 1;   //读 跨界标志
    rt_uint16_t read_index : 15;   //读地址下标
    rt_uint16_t write_mirror : 1;  //写 跨界标志
    rt_uint16_t write_index : 15;  //写地址下标

    rt_int16_t buffer_size;         //数组大小
};
#define UART_RX_BUF_LEN 16
rt_uint8_t uart_rx_buf[UART_RX_BUF_LEN] = {0};
struct rt_ringbuffer  uart_rxcb;         /* 定义一个 ringbuffer cb */
static UART_HandleTypeDef UartHandle;


void rt_ringbuffer_init(struct rt_ringbuffer *rb,
                        rt_uint8_t           *pool,
                        rt_int16_t            size)
{
    RT_ASSERT(rb != RT_NULL);
    RT_ASSERT(size > 0);

    /* initialize read and write index */
    rb->read_mirror = rb->read_index = 0;
    rb->write_mirror = rb->write_index = 0;

    /* set buffer pool and size */
    rb->buffer_ptr = pool;
    rb->buffer_size = RT_ALIGN_DOWN(size, RT_ALIGN_SIZE);
}

/* 初始化串口,中断方式 */
static int uart_init(void)
{
    /* 初始化串口接收 ringbuffer  */
    rt_ringbuffer_init(&uart_rxcb, uart_rx_buf, UART_RX_BUF_LEN);

    /* 初始化串口接收数据的信号量 */
    rt_sem_init(&(shell_rx_sem), "shell_rx", 0, 0);

    /* 初始化串口参数,如波特率、停止位等等 */
    UartHandle.Instance = USART2;
    UartHandle.Init.BaudRate   = 115200;
    UartHandle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
    UartHandle.Init.Mode       = UART_MODE_TX_RX;
    UartHandle.Init.OverSampling = UART_OVERSAMPLING_16;
    UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
    UartHandle.Init.StopBits   = UART_STOPBITS_1;
    UartHandle.Init.Parity     = UART_PARITY_NONE;

    /* 初始化串口引脚等 */
    if (HAL_UART_Init(&UartHandle) != HAL_OK)
    {
        while (1);
    }
    /* 中断配置 */
    __HAL_UART_ENABLE_IT(&UartHandle, UART_IT_RXNE);
    HAL_NVIC_EnableIRQ(USART2_IRQn);
    HAL_NVIC_SetPriority(USART2_IRQn, 3, 3);

    return 0;
}

为了采用C语言实现环形数组存储,先定义一个结构体,然后定义一个缓存数组,最后把参数进行初始化。接下来先看中断函数,如下:

/* uart 中断 */
void USART2_IRQHandler(void)
{
    int ch = -1;
    rt_base_t level;
    /* enter interrupt */
    rt_interrupt_enter();          //在中断中一定要调用这对函数,进入中断

    if ((__HAL_UART_GET_FLAG(&(UartHandle), UART_FLAG_RXNE) != RESET) &&
        (__HAL_UART_GET_IT_SOURCE(&(UartHandle), UART_IT_RXNE) != RESET))
    {
        while (1)
        {
            ch = -1;
            if (__HAL_UART_GET_FLAG(&(UartHandle), UART_FLAG_RXNE) != RESET)
            {
                ch =  UartHandle.Instance->DR & 0xff;
            }
            if (ch == -1)
            {
                break;
            }  
            /* 读取到数据,将数据存入 ringbuffer */
            rt_ringbuffer_putchar(&uart_rxcb, ch);  
        }        
        rt_sem_release(&shell_rx_sem);
    }
    /* leave interrupt */
    rt_interrupt_leave();    //在中断中一定要调用这对函数,离开中断
}

由于该中断服务函数用于RT-thread操作系统中的,所以rt_interrupt_enter();rt_interrupt_leave();是标记进入和退出临界函数标志,可以忽略。
其实中断函数中就是对接收的数据进行存储,主要的函数是rt_ringbuffer_putchar(&uart_rxcb, ch);
其函数源码如下:

/**
 * put a character into ring buffer
 */
rt_size_t rt_ringbuffer_putchar(struct rt_ringbuffer *rb, const rt_uint8_t ch)
{
    RT_ASSERT(rb != RT_NULL);

    /*检查剩余空间 */
    if (!rt_ringbuffer_space_len(rb))
        return 0;

    rb->buffer_ptr[rb->write_index] = ch;

    /* 检查是不是到达数组的最后一个下标 */
    if (rb->write_index == rb->buffer_size-1)
    {
		/*如果是最后一个下标,就转换一下标志*/
        rb->write_mirror = ~rb->write_mirror;
		/*写如数组的下标回到数组的0下标*/
        rb->write_index = 0;
    }
    else
    {
		/*对写的数组加1,用于下次存储*/
        rb->write_index++;
    }

    return 1;
}

对于这段源码, rt_ringbuffer_space_len()函数是检查是否还有存储空间,暂时先不管。然后就是把数据对数组进行赋值,其中
rb->write_index在每次存储一个字节数据时,都会增加,毕竟在一个环上循环,在这里就是一个写入到数组位置的标记。不过在增加之前,还检查了写入的是不是数组的尾部,在数组最后一个字节空间写入数据后,下次存储就是下一个环,所以应该跳转到数组的首部。
通过以上函数,环形数组数据存储就实现了,再回首看一下rt_ringbuffer_space_len()函数如何检查剩余空间的,源码如下:

#define rt_ringbuffer_space_len(rb) ((rb)->buffer_size - rt_ringbuffer_data_len(rb))


rt_inline enum rt_ringbuffer_state rt_ringbuffer_status(struct rt_ringbuffer *rb)
{
    if (rb->read_index == rb->write_index)
    {
        if (rb->read_mirror == rb->write_mirror)
            return RT_RINGBUFFER_EMPTY;
        else
            return RT_RINGBUFFER_FULL;
    }
    return RT_RINGBUFFER_HALFFULL;
}

/** 
 * get the size of data in rb 
 */
rt_size_t rt_ringbuffer_data_len(struct rt_ringbuffer *rb)
{
    switch (rt_ringbuffer_status(rb))
    {
    case RT_RINGBUFFER_EMPTY:
        return 0;
    case RT_RINGBUFFER_FULL:
        return rb->buffer_size;
    case RT_RINGBUFFER_HALFFULL:
    default:
        if (rb->write_index > rb->read_index)
            return rb->write_index - rb->read_index;
        else
            return rb->buffer_size - (rb->read_index - rb->write_index);
    };
}

从源码可以看出,rt_ringbuffer_space_len()函数就是一个宏,该宏调用了rt_ringbuffer_data_len()函数,该函数在内部调用rt_ringbuffer_status()函数对数组做了计算,从源码中可以看到,通过检查rb->read_index 和 rb->write_index两个参数(其中rb->read_index 会随着读函数变化,稍后会讲),检查数据缓存是full还是null,或者两者都不是,最终返回剩余空间大小。
通过以上函数的调用,最终把数据以一个环形的方式放入存储空间。

3.2、还是那个环,把数据读出来

既然数据已经存入到环形数组里面了,那么如何将数据从环形数组中读出?调用读数据的函数是应用函数,该应用函数如下:

char rt_hw_console_getchar(void)
{
    char ch = 0;

    /* 从 ringbuffer 中拿出数据 */
    while (rt_ringbuffer_getchar(&uart_rxcb, (rt_uint8_t *)&ch) != 1)
    {
        rt_sem_take(&shell_rx_sem, RT_WAITING_FOREVER);
    } 
    return ch;   
}

这个应用函数有什么用暂时不管,本次就看是如何把数据读出来的。由函数可知,内部调用了rt_ringbuffer_getchar函数来读数据,进入该函数源码:

/**
 * get a character from a ringbuffer
 */
rt_size_t rt_ringbuffer_getchar(struct rt_ringbuffer *rb, rt_uint8_t *ch)
{
    RT_ASSERT(rb != RT_NULL);

    /* ringbuffer is empty */
    if (!rt_ringbuffer_data_len(rb))
        return 0;
    /* put character */
    *ch = rb->buffer_ptr[rb->read_index];
    if (rb->read_index == rb->buffer_size-1)
    {
        rb->read_mirror = ~rb->read_mirror;
        rb->read_index = 0;
    }
    else
    {
        rb->read_index++;
    }
    return 1;
}

在该函数中,也是首先检查缓存是否是空的,空的就返回,如果不是空的,就返回当前读到的数据,顺便检查一下数据缓存空间的状态,对读的地址加1,用于下次读数据使用。
总结以上可以知道,环形数组就是一边存数据,一边读数据,一旦数据读出后,那么下次读的地址就变化了,其实也就是释放数组空间,用于下次存储。
以上就是对环形存储缓存的代码实现介绍,具体源码请看RT-thread源码

四、扩展

环形数组的存在,使我们能够采用串口中断方式接收不定长的数据,但是还是有弊端的,比如什么时候接收完?正在读取时又有新数据到来怎么办?接收大数据时,内存不够怎么办?
然而,即便在难的事情也难不住我们伟大的程序员,在操作系统中,经常使用环形对列操作,有兴趣的话可以学习研究一下。开源共享、共同进步。

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在STM32F103系列微控制器中,可以使用DMA(直接内存访问)来实现串口接收不定数据。下面是实现的大致步骤: 1. 首先,需要配置串口进行接收: - 初始化串口,并设置波特率、数据位、停止位等参数。 - 打开串口接收中断,以便在接收数据时触发中断。 - 开启DMA的UART接收通道。 2. 配置DMA: - 设置DMA通道的源地址为串口数据寄存器地址。 - 设置DMA通道的目的地址为接收数据的存储位置,可以是单个变量或数组。 - 设置DMA数据传输的度为最大接收数据度。 - 配置DMA通道为循环模式,以便在接收数据时自动重新启动DMA传输。 3. 在串口接收中断的处理函数中,可以在每次接收数据时检查DMA是否已经接收到足够的数据。可以根据接收到的数据情况进行进一步处理,例如打印数据或进行其他操作。 需要注意的是,在使用DMA接收不定数据时,需要确保DMA传输的度足够,以便接收到的数据不会超出DMA缓冲区的范围。此外,为了避免数据丢失或覆盖,建议在处理接收到的数据之前判断DMA是否已经完成传输。 总的来说,通过配置USART接收中断和DMA通道,可以实现STM32F103系列微控制器的串口DMA接收不定数据的功能。这种方法可以在保证低功耗的同时,提高系统的效率和响应速度。 ### 回答2: 在STM32F103系列微控制器中,使用DMA(直接存储器访问)来接收不定数据是一种高效和可靠的方法。 首先,我们需要配置串口通信的DMA接收功能。通过设置串口的DMA接收使能位(RXDMAEN),可以使用DMA来接收数据。然后,通过配置DMA控制器的通道、缓冲区和传输度等参数,将数据从串口接收到DMA缓冲区中。 在不定数据接收的情况下,可以通过设置DMA传输完成中断(TC)来判断数据是否接收完整。每当DMA接收到指定度的数据时,将触发一个DMA传输完成中断,在中断服务程序中可以处理接收到的数据以及进行后续操作。 另外,为了区分每一帧数据的开始和结束,可以通过给数据添加开始标志和结束标志的方式进行帧同步。当接收到一个完整的帧后,可以通过软件逻辑进行数据处理和分析。 需要注意的是,不定数据接收可能存在干扰和错误。为了提高接收数据的可靠性,可以通过一些策略来进行数据完整性检查和错误处理,如校验和检验、超时机制等。 综上所述,使用DMA接收不定数据需要配置串口的DMA接收使能位,并设置DMA控制器的相关参数。通过中断服务程序和逻辑判断,可以实现对不定数据接收和处理。 ### 回答3: STM32F103是一种具有DMA(直接内存访问)功能的微控制器,它可以用于实现串口接收不定数据。在进行串口DMA接收不定数据之前,需要先配置串口和DMA的相关寄存器。 首先,需要使能串口的DMA接收模式。可以通过设置串口控制寄存器CR3的位DMA-RX使能。然后,将DMA的通道选择为串口的接收通道。 接下来,需要配置DMA的相关寄存器,包括源和目的地址、传输度和数据流向等。对于串口DMA接收,源地址通常是串口的数据寄存器,目的地址是存储接收数据的缓冲区。传输度可设置为一个较大的值,以确保能够接收不定数据数据流向应设置为从外设到内存。 然后,需要配置DMA的传输模式,包括循环模式和自动请求使能。循环模式可以确保DMA在接收完指定度的数据后,自动重新开始传输。自动请求使能则用于自动触发DMA传输。 最后,可以通过使能DMA的接收完成中断来对接收到的数据进行处理。当DMA接收完成后,会触发DMA的中断,可以在中断函数中处理接收到的数据,比如打印输出或进行其他操作。 综上所述,通过配置串口和DMA的相关寄存器,并处理DMA的接收完成中断,就可以实现STM32F103串口DMA接收不定数据

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值