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

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


目录
  • 前言
  • 一、UART 设备操作
    • 1.1 UART 设备控制块
    • 1.2 UART 操作函数
      • 1.2.1 查找 UART 设备
      • 1.2.2 打开/关闭 UART 设备
        • 实际应用中的串口读写说明
      • 1.2.3 控制 UART 设备
      • 1.2.4 发送数据
      • 1.2.5 设置接收回调函数
      • 1.2.6 接收数据
  • 二、UART 设备使用步骤
    • 2.1 RT-Thread setting
    • 2.2 board.h 设置
    • 2.3 应用程序流程
  • 三、UART 示例测试
    • 3.1 与无线模块串口通讯
    • 3.2 示例说明
  • 结语

前言 通过前面的两篇文章,我们基本上完全明白了 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 设备模型的时候就已经说明了这个统一的控制块:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

上面的控制块是对于应用程序而言,在我们的 UART 设备的设备驱动框架层,是有定义了 UART 设备自己的控制块,其继承了rt_device的内容,同时还增加了 UART 设备特有的一些配置,操作,回调函数之类的内容,如下图:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之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_STREAM0x040流模式 接收模式参数 #define RT_DEVICE_FLAG_INT_RX0x100中断接收模式 #define RT_DEVICE_FLAG_DMA_RX0x200DMA 接收模式 发送模式参数 #define RT_DEVICE_FLAG_INT_TX0x400中断发送模式 #define RT_DEVICE_FLAG_DMA_TX0x800DMA 发送模式返回值: 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通讯? 暂时不知道,希望知道的朋友能够给个说明。
在官方的文档中,关于流模式有如下说明:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

实际应用中的串口读写说明 串口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函数:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

我感觉还是和轮询一样,将数据写入 数据寄存器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_24002400 #define BAUD_RATE_48004800 #define BAUD_RATE_96009600 #define BAUD_RATE_1920019200 #define BAUD_RATE_3840038400 #define BAUD_RATE_5760057600 #define BAUD_RATE_115200115200 #define BAUD_RATE_230400230400 #define BAUD_RATE_460800460800 #define BAUD_RATE_921600921600 #define BAUD_RATE_20000002000000 #define BAUD_RATE_30000003000000数据位可取值: #define DATA_BITS_55 #define DATA_BITS_66 #define DATA_BITS_77 #define DATA_BITS_88 #define DATA_BITS_99停止位可取值: #define STOP_BITS_10 #define STOP_BITS_21 #define STOP_BITS_32 #define STOP_BITS_43极性位可取值: #define PARITY_NONE0 #define PARITY_ODD1 #define PARITY_EVEN2高低位顺序可取值: #define BIT_ORDER_LSB0 #define BIT_ORDER_MSB1模式可取值: #define NRZ_NORMAL0 #define NRZ_INVERTED1 接收数据缓冲区默认大小: #define RT_SERIAL_RB_BUFSZ64返回—— RT_EOK函数执行成功 -RT_ENOSYS执行失败,dev 为空 其他错误码执行失败 */ rt_err_t rt_device_control(rt_device_t dev, int cmd, void *arg)

我们已经知道,在串口初始化的时候会有一个默认配置:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

所以在我们使用串口的时候,如果对应的配置与默认的配置不一样,就需要使用此函数修改配置。
接收缓冲区:
当串口使用中断接收模式打开时,串口驱动框架会根据 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_tpos, const void *buffer, rt_size_tsize)

写其实很好理解,除了多一个设备句柄参数,和我们裸机中使用的发送函数一样,看一下一个普通的裸机串口发送函数:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

这里说明一下,因为我们上面分析过实际应用中的串口读写,一般都使用轮询发送,所以我这里并不打断介绍 设置发送完成回调函数 。
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|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

具体详情可查看博文: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_tpos, void*buffer, rt_size_tsize)

我们与上面的设置接收回调函数一起使用官方示例作为说明:
#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 进行图形化界面的配置,如下图:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

因为Shell 工具需要使用串口,所以默认串口这里已经是勾选中的,这里说明只是为了让大家知道,在以后的 I/O 设备使用的时候,第一步就是在 RT-Thread setting 中使能设备。
2.2 board.h 设置 完成设备使能,我们还需要使用宏定义进行串口的基本设置,该设置在board.h文件中进行,如下图:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

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"
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

上图其实是根据官方示例代码,使用的 ESP8266 WIFI 模块做了一个简单的测试:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

三、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); } }

其测试结果如下:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

为了更好的做数据解析,我们需要对原始的程序进行修改,使得能够针对一帧数据一帧数据进行接收处理:
uint8_t USART_Enocean_BUF[64]; uint8_t Enocean_Data = https://www.it610.com/article/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 = https://www.it610.com/article/0; } }

测试结果如下,实现了我们所需要的的针对每一帧数据的接收(既然都已经可以区别每一帧数据了,所那么后续的处理也就简单了):
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

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

3.2 示例说明 在上面我们用了信号量作为通知的方式接收串口数据,官方的示: DMA 接收及轮询发送 采用了消息队列的方式进行处理,表面上看起来与我们上面那种方式不一样。
其实本质都是一样的,都不过是给线程一个通知,并没有“真正意义上的传递了消息”(比如串口接收到的数据):
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

如果想要使用消息队列作为缓存正常的传输串口接收的数据,不使用 I/O 设备模型的情况下更加适合,究其原因,如下图分析:
RT-Thread|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)
文章图片

如上面表格所说,使用了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|RT-Thread记录(十二、I/O 设备模型之UART设备 — 使用测试)】RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)

    推荐阅读