从 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 设备模型的时候就已经说明了这个统一的控制块:
上面的控制块是对于应用程序而言,在我们的 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_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 设备模型,理解到位才能用起来游刃有余,也能够在以后出问题的时候更容易的发现问题,解决问题。
❤️ 学会了使用一个东西当然是一件庆幸的事情,但是能够理解它才是更加重要的事情! ❤️