RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)

229 阅读18分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

从 UART 设备开始学会使用 RT-Thread I/O 设备模型 。

前言

通过前面的两篇文章,我们基本上完全明白了 RT-Thread I/O 设备模型的基本原理,当然我们的最终目的还是应用,所以本文开始我们就开始进行常用设备的使用学习和测试,就从 UART 设备开始。

从本文开始,就开始进行常用 I/O 设备的学习测试。

本 RT-Thread 专栏记录的开发环境:

RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX开发快速上手)

RT-Thread记录(二、RT-Thread内核启动流程 — 启动文件和源码分析)

RT-Thread 设备篇系列博文链接:

RT-Thread记录(十、全面认识 RT-Thread I/O 设备模型)

RT-Thread记录(十一、I/O 设备模型之UART设备 — 源码解析)

一、UART 设备操作

虽然在上一篇文章中,我们已经认识过 RT-Thread UART 的操作函数,但是我们并没有对其参数进行说明。 学习使用一个设备,在 RT-Thread 系统中就是一个对象, 还是得按照我们之前的流程进行简单介绍。

1.1 UART 设备控制块

在我们前面许多文章介绍其他内核对象的时候,我们首先都会介绍其对象控制块,对于 UART 设备而言,它也有自己的控制块。 但是与其他对象机制不同的是,UART 属于 I/O 设备,对于上层应用程序而言,所有的 I/O 设备都是属于 struct rt_device 类。

在我们前面文章《RT-Thread记录(十、全面认识 RT-Thread I/O 设备模型》初次介绍 I/O 设备模型的时候就已经说明了这个统一的控制块:

在这里插入图片描述

上面的控制块是对于应用程序而言,在我们的 UART 设备的设备驱动框架层,是有定义了 UART 设备自己的控制块,其继承了rt_device的内容,同时还增加了 UART 设备特有的一些配置,操作,回调函数之类的内容,如下图:

在这里插入图片描述

上面的 UART 设备控制块在我们的上一篇文章也有过分析说明。

<3 UART 设备属于 I/O 设备大类中的一个小类,对于上层应用程序而言,UART 设备控制块rt_serial_device并不透明,我们用户操作的还是 I/O 设备模型的控制块rt_device_t类型。

1.2 UART 操作函数

因为 UART 的操作函数 与 I/O 设备的操作函数基本一致,所以本小结有点类似《RT-Thread记录(十、全面认识 RT-Thread I/O 设备模型》中的 2.3 访问 I/O 设备相关 API 操作,但是针对 UART 设备,也有一些独有的参数说明。

老规矩,函数介绍部分说明看注释。

1.2.1 查找 UART 设备

需要先定义一个 I/O 设备结构体(rt_device_t类型)的指针变量,接收创建好的句柄。

/*
参数 	描述
name 	设备名称,对于UART设备而言,默认一般是 uart0,uart1,uart2,uart3 等
返回 	——
设备句柄 	查找到对应设备将返回相应的设备句柄
RT_NULL 	没有找到相应的设备对象
*/
rt_device_t rt_device_find(const char* name);

1.2.2 打开/关闭 UART 设备

先说打开 UART 设备:

/**
参数 	描述
dev 	设备句柄
oflags 	设备模式标志

oflags可选的的值如下:
#define RT_DEVICE_FLAG_STREAM       0x040     流模式     
接收模式参数
#define RT_DEVICE_FLAG_INT_RX       0x100     中断接收模式
#define RT_DEVICE_FLAG_DMA_RX       0x200     DMA 接收模式
发送模式参数
#define RT_DEVICE_FLAG_INT_TX       0x400     中断发送模式
#define RT_DEVICE_FLAG_DMA_TX       0x800     DMA 发送模式

返回值:
RT_EOK 	设备打开成功
-RT_EBUSY 	如果设备注册时指定的参数中包括 RT_DEVICE_FLAG_STANDALONE 参数,此设备将不允许重复打开
其他错误码 	设备打开失败
 */
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflag)

打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。

如果 oflags 没有指定使用中断模式或者 DMA 模式,则默认使用轮询模式。

这里有个问题,流模式是什么情况下使用的?485通讯? 暂时不知道,希望知道的朋友能够给个说明。

在官方的文档中,关于流模式有如下说明:

在这里插入图片描述

实际应用中的串口读写说明

串口RX:

在我们正常的项目使用中,一般都是 中断接收 或者 DMA 接收,基本上不会使用 轮询接收的方式(极大的浪费资源,反正我是没用过)。

所以我们打开串口设备的时候,基本上都是如下两种:

rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);

rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX);

在 RT-Thread 系统中,我们常用信号量或者消息队列 来标志是否接收到串口数据,这样的好处是当没有数据的时候,会将数据处理线程挂机,让出CPU资源。

串口TX:

对于串口 TX 来说,大部分项目中我自己一直都用的是 轮询 方式发送。

对于串口的中断发送方式,在上一篇文章我们分析 UART 源码,虽然没有详细说明,但是实际上在设备驱动层 drv_usart.c 驱动文件里,中断发送方式最终还是调用了该驱动文件里面的stm32_putc函数:

在这里插入图片描述

我感觉还是和轮询一样,将数据写入 数据寄存器DR,使用while死等发送完成(虽然时间很短)。

上面虽然只是 RT-Thread 中的UART设备驱动文件,也多少能说明一些问题,中断发送最终无非就是发送完了多一个中断通知。

对于另外一种 DMA 发送,我记得以前听老人提到过,DMA发送使用不得当,可能导致发送数据异常,简单来说就是 DMA 发送函数返回后,数据都不一定发送完成了,如果此时修改了 DMA 发送指定的buffer 区的内容,那么后面的数据就错误了。

所以,如果没有特殊需求,我们项目中的串口发送使用 轮询发送 即可(有些特殊情况的根据自己的实际需求而定)。

所以结合上面所说,我们实际应用中,使用以下两种方式打开串口设备能满足大部分场合需求:

/*轮询方式发送,中断接收*/
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
/*轮询方式发送,DMA接收*/
rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX);

有打开设备,当然也有关闭设备:

/**
参数 	描述
dev 	设备句柄
返回 	——
RT_EOK 	关闭设备成功
-RT_ERROR 	设备已经完全关闭,不能重复关闭设备
其他错误码 	关闭设备失败
 */
rt_err_t rt_device_close(rt_device_t dev)

关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。

当然在一般的应用场合,用到串口通讯的地方设备都是需要一直开启的,所以很多情况下都不需要使用 UART 设备关闭函数。

1.2.3 控制 UART 设备

rt_device_control 一般用在 rt_device_open (打开串口设备)之前,对需要使用的串口进行必要的配置。

/**
参数 	描述
dev 	设备句柄
cmd 	命令控制字,可取值:RT_DEVICE_CTRL_CONFIG
arg 	控制的参数,可取类型: 
struct serial_configure
{
    rt_uint32_t baud_rate;             波特率 
    rt_uint32_t data_bits    :4;       数据位 
    rt_uint32_t stop_bits    :2;       停止位 
    rt_uint32_t parity       :2;       奇偶校验位 
    rt_uint32_t bit_order    :1;       高位在前或者低位在前 
    rt_uint32_t invert       :1;       模式 
    rt_uint32_t bufsz        :16;      接收数据缓冲区大小 
    rt_uint32_t reserved     :4;       保留位 
};

波特率可取值:
#define BAUD_RATE_2400                  2400
#define BAUD_RATE_4800                  4800
#define BAUD_RATE_9600                  9600
#define BAUD_RATE_19200                 19200
#define BAUD_RATE_38400                 38400
#define BAUD_RATE_57600                 57600
#define BAUD_RATE_115200                115200
#define BAUD_RATE_230400                230400
#define BAUD_RATE_460800                460800
#define BAUD_RATE_921600                921600
#define BAUD_RATE_2000000               2000000
#define BAUD_RATE_3000000               3000000

数据位可取值:
#define DATA_BITS_5                     5
#define DATA_BITS_6                     6
#define DATA_BITS_7                     7
#define DATA_BITS_8                     8
#define DATA_BITS_9                     9

停止位可取值:
#define STOP_BITS_1                     0
#define STOP_BITS_2                     1
#define STOP_BITS_3                     2
#define STOP_BITS_4                     3

极性位可取值:
#define PARITY_NONE                     0
#define PARITY_ODD                      1
#define PARITY_EVEN                     2

高低位顺序可取值:
#define BIT_ORDER_LSB                   0
#define BIT_ORDER_MSB                   1

模式可取值:
#define NRZ_NORMAL                      0     
#define NRZ_INVERTED                    1 
    
接收数据缓冲区默认大小:
#define RT_SERIAL_RB_BUFSZ              64

返回 	——
RT_EOK 	函数执行成功
-RT_ENOSYS 	执行失败,dev 为空
其他错误码 	执行失败
 */
rt_err_t rt_device_control(rt_device_t dev, int cmd, void *arg)

我们已经知道,在串口初始化的时候会有一个默认配置:

在这里插入图片描述

所以在我们使用串口的时候,如果对应的配置与默认的配置不一样,就需要使用此函数修改配置。

接收缓冲区:

当串口使用中断接收模式打开时,串口驱动框架会根据 RT_SERIAL_RB_BUFSZ 大小开辟一块缓冲区用于保存接收到的数据,底层驱动接收到一个数据,都会在中断服务程序里面将数据放入缓冲区。

在修改缓冲区大小时请注意,缓冲区大小无法动态改变,只有在 open 设备之前可以配置。open 设备之后,缓冲区大小不可再进行更改。但除缓冲区之外的其他参数,在 open 设备前 / 后,均可进行更改。

串口控制修改使用官方修改示例说明一下:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;  /* 初始化配置参数 */

/* step1:查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

/* step2:修改串口配置参数 */
config.baud_rate = BAUD_RATE_9600;        //修改波特率为 9600
config.data_bits = DATA_BITS_8;           //数据位 8
config.stop_bits = STOP_BITS_1;           //停止位 1
config.bufsz     = 128;                   //修改缓冲区 buff size 为 128
config.parity    = PARITY_NONE;           //无奇偶校验位

/* step3:控制串口设备。通过控制接口传入命令控制字,与控制参数 */
rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &config);

/* step4:打开串口设备。以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);

1.2.4 发送数据

/**
参数 	描述
dev 	设备句柄
pos 	写入数据偏移量,此参数串口设备未使用
buffer 	内存缓冲区指针,放置要写入的数据
size 	写入数据的大小
返回 	——
写入数据的实际大小 	如果是字符设备,返回大小以字节为单位;
0 	需要读取当前线程的 errno 来判断错误状态
 */
rt_size_t rt_device_write(rt_device_t dev,
                          rt_off_t    pos,
                          const void *buffer,
                          rt_size_t   size)

写其实很好理解,除了多一个设备句柄参数,和我们裸机中使用的发送函数一样,看一下一个普通的裸机串口发送函数:

在这里插入图片描述

这里说明一下,因为我们上面分析过实际应用中的串口读写,一般都使用轮询发送,所以我这里并不打断介绍 设置发送完成回调函数 。

1.2.5 设置接收回调函数

/**
参数 	描述
dev 	设备句柄
rx_ind 	回调函数指针

回调函数参数 描述
dev 		设备句柄
size 		缓冲区数据大小
返回 	——
RT_EOK 	设置成功
 */
rt_err_t
rt_device_set_rx_indicate(rt_device_t dev,
                          rt_err_t (*rx_ind)(rt_device_t dev, rt_size_t size))

若串口以中断接收模式打开: 当串口接收到一个数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在 size 参数里,把串口设备句柄放在 dev 参数里供调用者获取。

若串口以 DMA 接收模式打开: 当 DMA 完成一批数据的接收后会调用此回调函数。

在使用 RT-Thread 时候,一般会用一个信号量通知串口数据处理线程有数据到达。

在使用 RT-Thread Nano 的时候,其实我也是使用信号量来处理数据的接收:

在这里插入图片描述

具体详情可查看博文:RT-Thread 应用篇 — 在STM32L051上使用 RT-Thread (四、无线温湿度传感器 之 串口通讯)

回调函数处理的示例我们使用官方示例说明,与下面的接收数据函数一起展示。

1.2.6 接收数据

数据接收处理函数,在接收回调函数运行之后运行。

/**
参数 	描述
dev 	设备句柄
pos 	读取数据偏移量,此参数串口设备未使用
buffer 	缓冲区指针,读取的数据将会被保存在缓冲区中
size 	读取数据的大小
返回 	——
读到数据的实际大小 	如果是字符设备,返回大小以字节为单位
0 					需要读取当前线程的 errno 来判断错误状态
 */
rt_size_t rt_device_read(rt_device_t dev,
                         rt_off_t    pos,
                         void       *buffer,
                         rt_size_t   size)

我们与上面的设置接收回调函数一起使用官方示例作为说明:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
static struct rt_semaphore rx_sem;    /* 用于接收消息的信号量 */

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
    rt_sem_release(&rx_sem);

    return RT_EOK;
}

/* 接收数据的线程 */
static void serial_thread_entry(void *parameter)
{
    char ch;

    while (1)
    {
        /* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */
        while (rt_device_read(serial, -1, &ch, 1) != 1)
        {
            /* 阻塞等待接收信号量,等到信号量后再次读取数据 */
            rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
        }
        /* 读取到的数据通过串口错位输出 */
        ch = ch + 1;
        rt_device_write(serial, 0, &ch, 1);
    }
}

static int uart_sample(int argc, char *argv[])
{
    serial = rt_device_find(SAMPLE_UART_NAME);

    /* 以中断接收及轮询发送模式打开串口设备 */
    rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);

    /* 初始化信号量 */
    rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);

    /* 设置接收回调函数 */
    rt_device_set_rx_indicate(serial, uart_input);
}

示例中使用的是信号量,收到一个数据,便会唤醒接收数据的线程,所以其实是一个字节一个字节(一个字符等于一个字节)的读取, 示例处理方式只能使用 RT_DEVICE_FLAG_INT_RX 方式接收。

接收数据rt_device_read 函数的返回值需要注意一下,返回值为读到的数据实际大小,就是接收到的数据长度。

二、UART 设备使用步骤

简单介绍一下在 RT-Thread Studio 开发环境下 UART的使用步骤。

2.1 RT-Thread setting

如果需要使用某个设备,是需要在 ENV 工具中配置的,现在有了 RT-Thread Studio ,所以可以直接通过工程目录下的 RT-Thread setting 进行图形化界面的配置,如下图:

在这里插入图片描述

因为Shell 工具需要使用串口,所以默认串口这里已经是勾选中的,这里说明只是为了让大家知道,在以后的 I/O 设备使用的时候,第一步就是在 RT-Thread setting 中使能设备。

2.2 board.h 设置

完成设备使能,我们还需要使用宏定义进行串口的基本设置,该设置在board.h文件中进行,如下图:

在这里插入图片描述

board.h 中包括了很多外设的使用说明,除了 UART,还有I2C、SPI、ADC等设备,我们在后面学习这些设备使用的时候,需要经常用到这个头文件,一些基本的使能配置都是在这个文件中用宏定义使能。

2.3 应用程序流程

完成上面 2 步的基本配置以后,我们就可以在应用程序通过上文介绍的 UART 设备操作函数进行串口的使用,具体的步骤概括如下:

UART 设备使用步骤 :

#include "rtdevice.h"

1、使用rt_device_find查找串口设备;

2、根据需求使用rt_device_control设置串口;

3、初始化回调函数中使用的信号量(在接收回调函数中 发送信号量 唤醒数据处理线程),如果使用消息队列接收初始化消息队列;

4、使用rt_device_open打开串口设备(根据自己的情况判断使用什么方式接收,发送前面分析过了,一本应用使用轮询发送即可);

5、使用rt_device_set_rx_indicate设置串口设备的接收回调函数

6、创建数据读取的线程。

按照上面的步骤,我进行了如下的示例测试,不要忘记 #include "rtdevice.h"

在这里插入图片描述

上图其实是根据官方示例代码,使用的 ESP8266 WIFI 模块做了一个简单的测试:

在这里插入图片描述

三、UART 示例测试

在上面介绍应用程序流程的时候,其实已经做了一个简单的示例测试。

同时在官方已经也提供了3种典型的示例程序:

中断接收及轮询发送、DMA 接收及轮询发送、串口接收不定长数据

作为以应用为目的系列博文,我自己还是根据自己的工作需求进行串口通讯的测试,使用的是 Enocean 无线通讯模块,当时在 RT-Thread 的应用篇,RT-Thread Nano 使用记录的时候就使用的这个无线模块。

要说明的是,用什么模块做通讯并不是重点,重点在于使用过程中对串口数据的处理方式。

3.1 与无线模块串口通讯

虽然换了一个通讯设备,但是官方给的例程:中断接收及轮询发送 还是适用的,我们先来看一看直接使用官方的例程做的测试:

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
    /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
    rt_sem_release(&rx_sem);
    return RT_EOK;
}
static void test_thread_entry(void *par){
    uint8_t ch;
    while (1)
    {
        /* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */
        while (rt_device_read(testuart, -1, &ch, 1) != 1)
        {
            /* 阻塞等待接收信号量,等到信号量后再次读取数据 */
            rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
        }
        rt_kprintf("%x ",ch);
    }
}

其测试结果如下:

在这里插入图片描述

为了更好的做数据解析,我们需要对原始的程序进行修改,使得能够针对一帧数据一帧数据进行接收处理:

uint8_t USART_Enocean_BUF[64];
uint8_t Enocean_Data = 0;

/* 接收数据回调函数 */
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{

    Enocean_Data = size;
    /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
    rt_sem_release(&rx_sem);
    return RT_EOK;
}
static void test_thread_entry(void *par){
    uint8_t i = 0;
    while (1)
    {
        if(rt_sem_take(&rx_sem, RT_WAITING_FOREVER) == RT_EOK){
              while(!rt_sem_take(&rx_sem, 7));
              rt_device_read(testuart, -1, USART_Enocean_BUF, Enocean_Data);

        }

        for (i = 0; i < Enocean_Data; ++i) {
            rt_kprintf("%x ",USART_Enocean_BUF[i]);
        }
        rt_kprintf("\r\n");

        rt_memset(&USART_Enocean_BUF, 0, sizeof(USART_Enocean_BUF));
        Enocean_Data = 0;
    }
}

测试结果如下,实现了我们所需要的的针对每一帧数据的接收(既然都已经可以区别每一帧数据了,所那么后续的处理也就简单了):

在这里插入图片描述

对于数据的接收处理信号量只是其中一种。

即便是信号量,也有多种可实现行的方式,而上面我测试使用的方式也只不过其中的一种:收到第一个数据的时候等待一定的时间,然后认为是一帧数据接收完成。 这也只是判断一帧数据接收完成的方法中的一种 = =!

3.2 示例说明

在上面我们用了信号量作为通知的方式接收串口数据,官方的示: DMA 接收及轮询发送 采用了消息队列的方式进行处理,表面上看起来与我们上面那种方式不一样。

其实本质都是一样的,都不过是给线程一个通知,并没有“真正意义上的传递了消息”(比如串口接收到的数据):

在这里插入图片描述

如果想要使用消息队列作为缓存正常的传输串口接收的数据,不使用 I/O 设备模型的情况下更加适合,究其原因,如下图分析:

在这里插入图片描述

如上面表格所说,使用了I/O 设备模型之后,我们底层串口初始化的时候已经有了一段数据接收的buffer了,所以我们直接使用 rt_device_read 函数从驱动层的 buffer 读取数据,用临时 buffer 来处理就可以了(不过如果需要对处理程序,单独设计函数,也可以用一个全局 buffer 来处理),也不过是2个buffer 的内存占用。

所以在官方的示例中,虽然给的是信号量,和消息队列的不同的处理方式,但是究其根本还是一样的。只是给了一个通知,这个其他的 IPC机制 比如 事件集一样可以做到,即便不用 IPC 机制,普通简单的应用,全局变量也未尝不可。(对于消息队列传递串口接收数据的应用,以后我还是会单独的说明的,本文在于说明 UART 基于 I/O 设备模型的使用,所以就不做测试了 = =!)

使用了 UART 设备模型,最终还是需要使用rt_device_read函数,从内部缓存读取串口数据,IPC只不过是给线程一个通知。

结语

一个 UART 设备画了两篇文章,还算是比较值得的,通过上一篇文章加深对 RT-Thread I/O 设备模型的理解,通过本文实际体验了一把 UART 设备。

体验上来说,还是感觉特别方便简单的。但是这个前提条件时,能够真正的理解 RT-Thread I/O 设备模型,理解到位才能用起来游刃有余,也能够在以后出问题的时候更容易的发现问题,解决问题。

学会了使用一个东西当然是一件庆幸的事情,但是能够理解它才是更加重要的事情!

推荐阅读:

RT-Thread 应用篇 — 在STM32L051上使用 RT-Thread (四、无线温湿度传感器 之 串口通讯)

RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)