写点什么

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

作者:矜辰所致
  • 2022 年 8 月 23 日
    江苏
  • 本文字数:9028 字

    阅读完需:约 30 分钟

RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
从 UART 设备开始学会使用 RT-Thread I/O 设备模型 。
复制代码


前言

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


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


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

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

https://xie.infoq.cn/article/44be1057caace7a6a2c4c4b59

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

https://xie.infoq.cn/article/44be1057caace7a6a2c4c4b59

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

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

https://xie.infoq.cn/article/40536d29988d683c78b4ba5ff

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

https://xie.infoq.cn/article/38bb4bf15cb81f1fdb060b29e

一、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 设备控制块在我们的上一篇文章也有过分析说明。


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_CONFIGarg   控制的参数,可取类型: 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; //修改波特率为 9600config.data_bits = DATA_BITS_8; //数据位 8config.stop_bits = STOP_BITS_1; //停止位 1config.bufsz = 128; //修改缓冲区 buff size 为 128config.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_trt_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 (四、无线温湿度传感器 之 串口通讯)https://xie.infoq.cn/article/550465aa9a61bf7f4bf3258b2


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

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);}
复制代码


示例中使用的是信号量,收到一个数据,便会唤醒接收数据的线程,所以其实是一个字节一个字节(一个字符等于一个字节)的读取,<font color=#0033FF> 示例处理方式只能使用 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 (四、无线温湿度传感器 之 串口通讯)

https://xie.infoq.cn/article/550465aa9a61bf7f4bf3258b2


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

https://xie.infoq.cn/article/1f49bfd6c69377deb9eee838f

发布于: 21 小时前阅读数: 36
用户头像

矜辰所致

关注

不浮夸,不将就,认真对待学知识的我们! 2022.08.02 加入

不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开! 为了活下去的嵌入式工程师,画画板子,敲敲代码,玩玩RTOS,搞搞Linux ...

评论

发布
暂无评论
RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)_RT-Thread_矜辰所致_InfoQ写作社区