IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> STM32串口控制收发模式总结 -> 正文阅读

[嵌入式]STM32串口控制收发模式总结

STM32串口控制收发模式总结

前言

公司的很多项目都是使用了串口通信,在我刚毕业来公司的时候,我的固件组同事都还是在使用轮询发送,串口中断接受的方式来进行串口收发,非常的低效。后面自己写项目程序,我开始查找一些串口收发的资料,实现了几种感觉比较好的控制方法,特此总结防止自己失忆。

开发环境

系统Ubuntu20.04lts;开发工具vscode + makefile + cortex-debug + openocd;固件库使用HAL库;

串口收发模式

发送

1.轮询发送

这没啥好说的,虽然方法很简单但确实很稳(我同事用这个方法很多年了),一般的操作系统在环境未准备之前似乎也是用这个方法打印日志的,目前我的固件在开机初始化的那段时间也是使用这个方法打印日志。HAL库已经实现这个函数 HAL_UART_Transmit。

2.中断发送

一般操作是定义一段缓存区,开启串口的发送完成中断,发送数据时将要发送的数据拷到缓存区,串口忙标志置位,然后发送一个字节,触发发送中断后,在中断判断是否继续发送,是的话在发送一个字节,直至发送完成清除串口忙标志。
相比轮询占用cpu更少,毕竟轮询是干等在白占资源,不过中断发送属于异步操作,实现起来稍微麻烦一点,不过HAL也已经实现了该函数 HAL_UART_Transmit_IT。

3.dma发送

dma好处不必多说,操作方法也很简单,定义一段缓存,开启串口的dma发送完成中断,发送数据时将要发送的数据拷到缓存区,串口忙标志置位,开启dma发送,触发中断后清除标志。
dma发送如果有os支持的话,配合信号量进行同步发送还是蛮香的,与中断发送相比,占用cpu更少,毕竟只要中断一下,而且还不怕被打断,是串口发送常用的方法。HAL实现了该函数 HAL_UART_Transmit_DMA。

4.dma发送+环形缓存区

在没有os支持的情况下,基本上是用dma进行异步发送,在一般场合下也足够用了,但是在间歇性发送大量数据特别是打印日志的时候效果就很差了,这时候就需要环形缓存区了(当然消息队列也可以,rtthread中这两种都有)。
环形缓存区是典型的生产者消费者模型,对于解决上述问题非常有效,下面是一个比较简单的环形缓存区代码。

#include <stdint.h>
#include <string.h>

struct ringbuf_handle
{
    uint32_t r_pos : 31;
    uint32_t r_mark : 1;
    uint32_t w_pos : 31;
    uint32_t w_mark : 1;
    uint8_t *buf;
    size_t size;
};

int ringbuf_init(struct ringbuf_handle *handle, uint8_t *buf, size_t size)
{
    if (handle == NULL || buf == NULL || size > 0x7fffffff)
        return -1;
    handle->r_pos = 0;
    handle->r_mark = 0;
    handle->w_pos = 0;
    handle->w_mark = 0;
    handle->buf = buf;
    handle->size = size;
    return 0;
}

size_t ringbuf_data_size(struct ringbuf_handle *handle)
{
    if (handle == NULL)
        return 0;
    return handle->w_mark == handle->r_mark ? handle->w_pos - handle->r_pos
                                            : handle->size + handle->w_pos - handle->r_pos;
}

size_t ringbuf_space_size(struct ringbuf_handle *handle)
{
    if (handle == NULL)
        return 0;
    return handle->w_mark == handle->r_mark ? handle->size - handle->w_pos + handle->r_pos
                                            : handle->r_pos - handle->w_pos;
}

size_t ringbuf_write(struct ringbuf_handle *handle, const uint8_t *buf, size_t size)
{
    size_t space_size;

    if (handle == NULL || buf == NULL || size == 0)
        return 0;
    space_size = ringbuf_space_size(handle);
    size = size < space_size ? size : space_size;
    if (size + handle->w_pos < handle->size)
        memcpy(&handle->buf[handle->w_pos], buf, size);
    else
    {
        memcpy(&handle->buf[handle->w_pos], buf, handle->size - handle->w_pos);
        memcpy(handle->buf, &buf[handle->size - handle->w_pos], size - handle->size + handle->w_pos);
        handle->w_mark++;
    }
    handle->w_pos = (handle->w_pos + size) % handle->size;

    return size;
}

size_t ringbuf_read(struct ringbuf_handle *handle, uint8_t *buf, size_t size)
{
    size_t data_size;

    if (handle == NULL || buf == NULL || size == 0)
        return 0;
    data_size = ringbuf_data_size(handle);
    size = size < data_size ? size : data_size;
    if (size + handle->r_pos < handle->size)
        memcpy(buf, &handle->buf[handle->r_pos], size);
    else
    {
        memcpy(&buf[0], &handle->buf[handle->r_pos], handle->size - handle->r_pos);
        memcpy(&buf[handle->size - handle->r_pos], handle->buf, size - handle->size + handle->r_pos);
        handle->r_mark++;
    }
    handle->r_pos = (handle->r_pos + size) % handle->size;

    return size;
}

有了环形缓存区以后,就可以通过dma发送中断进行异步发送,大致操作是定义一个环形环形缓存区,开启dma发送完成中断,发送数据时,先往环形缓存区写数据,再判断dma是否在发送,若是,则返回,若否,则从环形缓存区读出部分数据,然后开启dma发送该部分数据,而在dma中断就只需要判断环形缓存区是否有数据,若有,则读出部分数据再开启dma,然后在下次中断继续重复这个操作,这样通过不停的接力就将数据全部发送出去。以下是演示代码。

static struct ringbuf_handle ringbuf;
static uint8_t buf[1024];

static void _uart_dma_output(UART_HandleTypeDef *huart)
{
    static uint8_t dma_buf[128];
    size_t size;

    __disable_irq();
    size = ringbuf_read(&ringbuf, dma_buf, sizeof(dma_buf));
    __enable_irq();
    if (size)
        HAL_UART_Transmit_DMA(&huart1, dma_buf, size);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    _uart_dma_output(huart);
}

int uart_output(const char *str)
{
    size_t size = strlen(str) + 1;

    __disable_irq();
    if (size > ringbuf_space_size(&ringbuf))
    {
        __enable_irq();
        return -1;
    }
    ringbuf_write(&ringbuf, (const uint8_t *)str, size);
    __enable_irq();

    if (huart1.gState == HAL_UART_STATE_READY)
        _uart_dma_output(&huart1);

    return 0;
}

void test1(void)
{
    static const char *str_test1 = {"This is a uart test\r\n"};
    static const char *str_test2 = {"\
    123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789\r\n \
    123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789\r\n \
    123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789\r\n \
    123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789\r\n \
    123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789\r\n"};
    static uint32_t last_tick;
    uint32_t tick = HAL_GetTick();

    if (last_tick != tick)
    {
        last_tick = tick;
        if (tick % 100 == 0)
        {
            char str[30];

            sprintf(str, "Current tick:%d\r\n", (int)tick);
            uart_output(str);
        }
        if (tick % 1000 == 0)
        {
            uart_output(str_test1);
            uart_output(str_test2);
        }
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();

    ringbuf_init(&ringbuf, buf, sizeof(buf));
    while (1)
    {
        test1();
    }
}

以上测试程序为每隔100ms打印系统时间,每隔1000ms打印大量数据,实际仿真可以发现除去数据拷贝的时间,发送数据几乎不怎么占用cpu,而且使用也比较方便,即使是裸机的情况下,也能很容易的打印日志。以下为minicom接受的数据。
在这里插入图片描述

5.dma发送+环形缓存区(优化)

在上面的例程中,在每次开启dma之前,都需要重新读出数据,也就是说数据从串口发送出去需要拷贝两次,但实际上第二次可以省去,修改_uart_dma_output,直接用dma将ringbuf中的数据发送出去,程序如下。

static void _uart_dma_output(UART_HandleTypeDef *huart)
{
    static size_t last_size = 0;
    size_t data_size;
    size_t size;
    uint8_t *buf;

    __disable_irq();
    if(ringbuf.r_pos + last_size >= ringbuf.size)
        ringbuf.r_mark++;
    ringbuf.r_pos = (ringbuf.r_pos + last_size) % ringbuf.size;

    data_size = ringbuf_data_size(&ringbuf);
    buf = &ringbuf.buf[ringbuf.r_pos];
    if (data_size + ringbuf.r_pos < ringbuf.size)
        size = data_size;
    else
        size = ringbuf.size - ringbuf.r_pos;
    last_size = size;
    __enable_irq();
    if (size)
        HAL_UART_Transmit_DMA(&huart1, buf, size);
}

虽然这样略微提高了发送的效率,但是也相应的增加了模块间的耦合,还把ringbuf模块中的细节给暴露出来,所以没什么必要。

接收

1.轮询接收

使用场合较少,在裸机的情况下用的更少,HAL库已经实现这个函数 HAL_UART_Receive。

2.中断接收

以前自己做比赛进行自定义协议通信的一套惯用方法,即中断每接收一个字节,就进行一次协议解析,随后执行相应的命令,这应该是很多人用的方法,简单而且好用,但是缺点也很明显,中断讲究快进快出,如果执行一些耗时的事情不仅降低实时性,还容易丢命令,特别是对方连发命令的时候(我自己就经常这么干),HAL库已经实现这个函数HAL_UART_Receive_IT,但稍稍不一样,HAL库是接收到用户指定长度的字节数,才会中断(准确来说不是中断,是接收完成的回调),如果我们想要每接收一个字节就中断一次,需要在程序开始时执行一次HAL_UART_Receive_IT(&huart1, buf, 1);随后在中断再调用一次以发起下次的接收中断。

3.中断接收+环形缓存区

这应该是最普遍的做法了吧,完美解决了上面方法的问题。配合之前的dma发送,这里写一个简单回响程序(即将接收到的数据原模原样发回去)。

static struct ringbuf_handle tx_ringbuf;
static uint8_t tx_buf[1024];
static struct ringbuf_handle rx_ringbuf;
static uint8_t rx_buf[1024];
static uint8_t rx_data;

static void _uart_dma_output(UART_HandleTypeDef *huart)
{
    static uint8_t dma_buf[128];
    size_t size;

    __disable_irq();
    size = ringbuf_read(&tx_ringbuf, dma_buf, sizeof(dma_buf));
    __enable_irq();
    if (size)
        HAL_UART_Transmit_DMA(&huart1, dma_buf, size);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    _uart_dma_output(huart);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    __disable_irq();
    ringbuf_write(&rx_ringbuf, &rx_data, 1);
    __enable_irq();
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}

size_t uart_output(uint8_t *buf, size_t size)
{
    __disable_irq();
    ringbuf_write(&tx_ringbuf, buf, size);
    __enable_irq();

    if (huart1.gState == HAL_UART_STATE_READY)
        _uart_dma_output(&huart1);

    return 0;
}

int main(void)
{
    uint8_t buf[128];
    size_t size;

    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();

    ringbuf_init(&tx_ringbuf, tx_buf, sizeof(tx_buf));
    ringbuf_init(&rx_ringbuf, rx_buf, sizeof(rx_buf));
    HAL_UART_Receive_IT(&huart1, &rx_data, 1);
    while (1)
    {   
        __disable_irq();
        size = ringbuf_read(&rx_ringbuf, buf, sizeof(buf));
        __enable_irq();
        uart_output(buf, size);
        HAL_Delay(10);
    }
}

由于没有os,不能使用互斥锁,所以程序有很多开关全局中断的地方,主要原因还是ringbuf写的还有瑕疵,不能完美做到一对一的无锁读写,这里用minicom测试一下效果,在终端输入“This is a echo demo”,可以看到返回了一模一样的数据。
在这里插入图片描述

4.dma接收

明明用到了dma,但这种方法却用的很少,主要原因是dma只有接收到了指定长度的数据才会中断,也就是说如果接收的数据是不定长的,就用不了。对应的HAL库函数HAL_UART_Receive_DMA。

5.dma接收+idle中断

idle中断,简单来说,就是当串口接收到了数据,内部就会开始计时,若在规定时间内没有再接收到数据,就会触发中断,也就是说通过这个中断,我们可以得知对方发送的数据是否结束,是个很牛逼的功能。再搭配上面的dma,在我们开启dma接收以后,我们只需要在idle中断读取接收到的数据和dma接收的长度,然后发起下次的dma接收,我们就可以接收不定长的数据了,当然HAL也实现了这个函数HAL_UARTEx_ReceiveToIdle_DMA,对应的接收回调函数为HAL_UARTEx_RxEventCallback,这里使用idle再来编写回响程序。

static struct ringbuf_handle tx_ringbuf;
static uint8_t tx_buf[1024];
static uint8_t rx_buf[1024];

static void _uart_dma_output(UART_HandleTypeDef *huart)
{
    static uint8_t dma_buf[128];
    size_t size;

    __disable_irq();
    size = ringbuf_read(&tx_ringbuf, dma_buf, sizeof(dma_buf));
    __enable_irq();
    if (size)
        HAL_UART_Transmit_DMA(&huart1, dma_buf, size);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    _uart_dma_output(huart);
}

size_t uart_output(uint8_t *buf, size_t size)
{
    __disable_irq();
    ringbuf_write(&tx_ringbuf, buf, size);
    __enable_irq();

    if (huart1.gState == HAL_UART_STATE_READY)
        _uart_dma_output(&huart1);

    return 0;
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    uart_output(rx_buf, Size);
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, sizeof(rx_buf));
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();

    ringbuf_init(&tx_ringbuf, tx_buf, sizeof(tx_buf));
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, sizeof(rx_buf));
    while (1)
    {   
    }
}

看起来似乎这是使用DMA进行串口接收的最好方式了,然后即使这样的方式仍然存在着问题,首先我们必须知道对方发送的数据的最大长度,否则就会缓存溢出,其次当对方接连发送多包数据的时候,由于每次接收回调都需要拷贝接收数据才能发起下次dma接收,这可能会导致下一包数据的丢失,当然我们也可以定义多个接收缓存区进行乒乓操作,即每次接收回调就切换成另一个缓存区进行接收,但是由于每个缓存的大小都必须大于对方发送数据的最大长度,因此会占用大量的内存。其实还有另一种方法解决上述问题,只不过实现起来比较麻烦,我们接着看第6种接收方法。

6.circle模式dma+idle中断

此方法是我上班摸鱼的时候偶然想到的,实现了一下发现还真的可以。经常用dma的应该知道,dma可以被配置为正常(normal)模式和循环(circle)模式,正常模式下dma在传输了指定长度的数据后就会停止,而循环模式下dma在传输了指定长度的数据并不会停止,其传输的地址将会复位,也就是会重新从头开始传输,并覆盖之前的数据,这个特性似乎满足环形缓存区中指针绕回的特性,那么是否可以将其作为一个ringbuf来使用呢。下面写一个程序测试一下,首先修改stm32f4xx_hal_msp.c文件,将dma模式修改成circle模式。
在这里插入图片描述
接下来仍然是修改回响程序,代码如下。

static struct ringbuf_handle tx_ringbuf;
static uint8_t tx_buf[1024];
static uint8_t rx_buf[1024];
static uint32_t read_index;

static void _uart_dma_output(UART_HandleTypeDef *huart)
{
    static uint8_t dma_buf[128];
    size_t size;

    __disable_irq();
    size = ringbuf_read(&tx_ringbuf, dma_buf, sizeof(dma_buf));
    __enable_irq();
    if (size)
        HAL_UART_Transmit_DMA(&huart1, dma_buf, size);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    _uart_dma_output(huart);
}

size_t uart_output(uint8_t *buf, size_t size)
{
    __disable_irq();
    ringbuf_write(&tx_ringbuf, buf, size);
    __enable_irq();

    if (huart1.gState == HAL_UART_STATE_READY)
        _uart_dma_output(&huart1);

    return 0;
}

size_t dma_ringbuf_data_len(void)
{
    uint32_t write_index = sizeof(rx_buf) - __HAL_DMA_GET_COUNTER(huart1.hdmarx);

    return (write_index >= read_index) ? (write_index - read_index) : (write_index + sizeof(rx_buf) - read_index);
}

size_t dma_ringbuf_read(uint8_t *buf, size_t size)
{
    size_t data_size;

    data_size = dma_ringbuf_data_len();
    size = size < data_size ? size : data_size;
    if (size + read_index < sizeof(rx_buf))
        memcpy(buf, &rx_buf[read_index], size);
    else
    {
        memcpy(buf, &rx_buf[read_index], sizeof(rx_buf) - read_index);
        memcpy(&buf[sizeof(rx_buf) - read_index], rx_buf, size - sizeof(rx_buf) + read_index);
    }
    read_index = (read_index + size) % sizeof(rx_buf);

    return size;
}

int main(void)
{
    size_t size;
    uint8_t buf[128];

    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();

    ringbuf_init(&tx_ringbuf, tx_buf, sizeof(tx_buf));
    HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
    while (1)
    {
        size = dma_ringbuf_read(buf, sizeof(buf));
        uart_output(buf, size);
    }
}

可以看到程序就开启了一次HAL_UART_Receive_DMA,并且没有用到任何接收回调,就只在循环中读取DMA的COUNTER来判断dma已经写到了缓存的哪个位置,然后我们将其写入的数据读出在发送出去。用minicom测试一下代码,测试结果仍然ok,说明该方案可行。
在这里插入图片描述
但是这么奇葩的用法肯定是有问题,他最大的问题就是无法判断dma接收到的数据是否超过了缓存区的最大长度,因此,如果长时间不读取数据,等到数据溢出以后再去读取将会得到错误的数据,并且还无法判断,唯一的解决方法,就是定时的读取,一般我们串口的波特率顶天就是115200,然后是8位比特,1位停止,无校验,那么我们数据的最大传输速率就是
115200 / (1 (开始位)+ 8 + 1) = 11520 B/s,
如果我们的缓存区定成1024,那么数据溢出的时间就是
1024 / 11520 = 0.0888s,
所以我们只要以小于这个时间的周期读取,比如50ms读取一次,就能保证数据不会溢出。
当然我们还可以开启idle中断,在中断中读取数据,由于我们不会关闭dma,因此不用担心数据丢失的问题,代码如下。

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    size_t size;
    uint8_t buf[128];

    size = dma_ringbuf_read(buf, sizeof(buf));
    uart_output(buf, size);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();

    ringbuf_init(&tx_ringbuf, tx_buf, sizeof(tx_buf));
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, sizeof(rx_buf));
    while (1)
    {
    }
}

利用dma进行串口接收,即兼顾效率和稳定的方法,我试了几种,目前这种是效果最好的,当然最稳妥的方法还是串口接收中断+ringbuf。

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2021-07-24 11:39:31  更:2021-07-24 11:41:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 18:20:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码