目录
UART串口设备
串口概述
本章主要介绍串口设备在RT-Thread操作系统中应用层如何使用。关于串口设备的使用,我们只需要在应用层找到串口设备,然后去初始化,初始化完成以后就可以读写串口设备了。
操作的方法在应用层,驱动是不需要我们去完成的,驱动已经在操作系统中提供了。
关于串口的详细介绍和STM32的串口使用可查看我的如下博客:
访问串口设备接口
- 应用程序通过RT-Thread提供的IO设备管理接口来访问串口硬件:
- 查找串口设备(例:"uart2"),使用rt_device_find函数
- 打开串口设备(串口收发数据模式:中断、轮询、DMA),使用rt_device_open函数
以上具体查看IO设备章节
- 控制串口设备
cpp
rt_err_trt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);
cmd 命令控制字,可取值:RT_DEVICE_CTRL_CONFIG
arg 控制的参数,可取类型:struct serial_conigure
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 /* normal mode*/
#define NRZ_INVERTED 1 /* invertedmode */
/* 接收数据缓冲区默认大小*/
#define RT_SERIAL_RB_BUFSZ 64
RT-Thread 提供的默认串口配置
#define RT_SERIAL_CONFIG_DEFAULT \
{ \
BAUD_RATE_115200, /* 115200 bits/s */ \
DATA_BITS_8, /* 8 databits */ \
STOP_BITS_1, /* 1 stopbit */ \
PARITY_NONE, /* No parity */ \
BIT_ORDER_LSB, /* LSB first sent */ \
NRZ_NORMAL, /* Normal mode */ \
RT_SERIAL_RB_BUFSZ, /* Buffer size */ \
0 \
}
注:缓冲区通过 control 接口修改,缓冲区大小无法动态改变,只有在 open 设备之前可以配置。open 设备之后,缓冲区大小不可再进行更改。但除过缓冲区之外的其他参数,在 open 设备前 / 后,均可进行更改。
- 发送数据
- 设置发送完成回调函数
在应用程序调用rt_device_write()写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用
- 设置接收回调函数
若串口以中断接收模式打开,当串口接收到一个数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在size参数里,把串口设备句柄放在dev参数里供调用者获取。
若串口以 DMA 接收模式打开,当 DMA 完成一批数据的接收后会调用此回调函数。
使用:一般情况下接收回调函数可以发送一个信号量或者事件通知串口数据处理线程有数据到达。(也就是说接收数据需要我们另外开启一个线程来接收,通过回调函数来通知线程是否有数据到达。当没有数据的时候,接收线程应该处于挂起状态,可以采用信号量或事件通知的方式)
比如在线程接收数据的时候,先获取一下信号量,如果有资源,就执行,如果没有就让接收线程挂起。我们需要在设置接收回调函数里面发送信号量。当接收回调函数被调用的时候,说明接收到了数据,一旦接收到了数据就设置信号量来唤醒挂起状态中的接收线程。
- 接收数据
- 关闭串口设备
cpp
rt_err_t rt_device_close(rt_device_t dev);
数据发送方法
一般来说,对于串口发送没有太多的要求,我们在想要发送的时候只需要调用write函数写入数据即可,只要发送的频率不是那么快(如在while循环里一直发送,可能会导致发送的数据出问题),一般都会间隔一段时间再发,如用延时函数或者定时器一定时间后发送采集到的传感器数据。
但如果发送的频率过快,有可能硬件的上一个数据还没发送出去,下一条发送指令就已经执行到了。这种情况就需要用到发送中断或者DMA的发送方式。
数据接收方法
对于接收来讲,有一个问题。接收并不是由主控设备自己来决定的,因为接收数据的前提条件是对方由数据发过来之后,接收方才能接收。因此接收这块我们必须想方法,看采用什么样的方式才能够更好地接收到数据。
一种方式就是写一个while死循环,然后在循环里面一直读,什么时候对方发送数据,什么时候就接收到数据。但这样在RTT中需要我们单独开启一个线程一直去读,线程一直在while循环里面去读,而且不加延时,这样就与RTT线程的创建和线程的要求相矛盾。所以我们在RT-Thread不采用这种方式。
因此我们可以采用中断或者DMA的方式来接收数据。
中断读取:当硬件设备接收到数据以后,触发接收中断,在中断处理函数里面我们将数据接收到。
DMA读取:开启DMA以后,通过DMA的方式直接从串口数据寄存器中接收数据,接收到数据以后先放到缓冲区。接收一段时间以后,通过触发一个中断,然后我们将数据从缓冲区读走。
串口设备使用流程
其中我们只需要编写应用程序代码,ISR和串口外设相关的硬件驱动代码在操作系统中是有驱动支持的,不需要人为关心。
在硬件产生一个中断以后是如何通知应用层的呢?我们是通过设置回调函数的方式来接收中断信息的。
先来看我们的应用程序端:
应用程序端,我们在编写的时候,首先要初始化信号量,要通过信号量来进行数据的接收;然后设置接收回调函数;接着创建数据处理线程,用来专门处理接收到的数据;最后我们在创建好线程以后,先开启线程,然后让线程阻塞等待信号量,信号量的作用就是实现数据接收的同步。
再解释一下,我们在线程处理函数中写一个while循环,一直去接收数据。但在接收数据之前要先获取信号量,如果信号量能够获取成功,就往下读取数据。如果信号量获取失败,则线程进入休眠态。
接收线程的接收数据还是休眠(挂起),由信号量来决定。也就是说信号量标志着是否有数据发送过来了,而数据是否发过来是由接受回调函数决定的,一旦回调函数被执行。我们就在回调函数中发送信号量,从而唤醒数据处理线程去接收数据。
再来看一下硬件驱动:
当用户从电脑通过串口输入一个字符发送给STM32时,单片机接收到数据之后就会触发一个接收中断,从而调用我们再应用层设置的接收回调函数。在中断服务程序(ISR)中,会将接收的数据放入缓冲区,并在接收回调函数中发送信号量激活线程,也就是说我们设置的回调函数中一定要记得发送信号量。
串口中断接受实例
串口配置及串口发送
1.首先我们先将串口配置好,比如我们在创建工程的时候,选择了串口2。那么创建好工程后,系统会自动配置串口2,并在board.h中可以查看
2.如果我们想再添加一个UART1,我们可以参考自带的步骤说明
(1)首先我们要定义要使用串口的宏
(2)说明所要使用串口的发送和接收引脚
(3)和(4)是使用DMA来配置的,在后面介绍
cpp
/** After configuring corresponding UART or UART DMA, you can use it.
*
* STEP 1, define macro define related to the serial port opening based on the serial port number
* such as #define BSP_USING_UART1
*
* STEP 2, according to the corresponding pin of serial port, define the related serial port information macro
* such as #define BSP_UART1_TX_PIN "PA9"
* #define BSP_UART1_RX_PIN "PA10"
*
* STEP 3, if you want using SERIAL DMA, you must open it in the RT-Thread Settings.
* RT-Thread Setting -> Components -> Device Drivers -> Serial Device Drivers -> Enable Serial DMA Mode
*
* STEP 4, according to serial port number to define serial port tx/rx DMA function in the board.h file
* such as #define BSP_UART1_RX_USING_DMA
*
*/
3.我们将UART1添加进去
4.添加完之后,我们在main.c中完成对UART1的应用层配置
(1)首先查找串口1设备句柄
(2)成功找到之后,打开串口设备(注意要以读写和接收中断的方式打开)
(3)打开设备之后,我们还需要对串口设备进行协议相关的配置,我们使用rt_device_control函数
首先我们要创建一个串口信息结构体,这里我们使用默认的串口配置
默认配置具体内容如下:
然后我们将结构体取地址传入设备控制函数中,并将cmd设置为设备配置模式
5.配置完成以后,如果我们想要发送数据,需要调用rt_device_write函数,其中第一个参数传入发送的设备,第二个参数为相对于buffer起始地址的偏移量(起始位置),第三个参数为buff(要发送的内容),第四个参数为buff的大小
编译一下,发现有错误
这是因为没有包含串口相关的头文件,我们将serial.h包含进来
编译发送,仍有错:找不到头文件
我们通过光标定位找到头文件位置
我们将头文件路径手动添加到工程中:
(1)打开构建配置
(2)打开C/C++构建中的设置
(3)右边出现汇编工具和C编译工具,我们选择C Compiler的Includes
(4)我们添加目录路径,通过文件系统的方式来添加
(5)重新编译,发现新的错误,serial.h中有些类型无法找到
我们通过定位将相关头文件包含进serial.h中
(6)再次编译发现还是有错误,我们需要将上一级ipc的路径也手动添加到工程中
最终编译发现没有任何错误
6.我们验证下程序是否正确
首先下载程序,然后打开串口
然后选择相应串口,并点击确定
复位STM32开发板,成功发送信息
串口中断接收
在之前已经成功验证了串口1可以正常使用,下面就来写一下中断接收的方式
1.首先设置接收回调函数
第二个参数为我们需要调用的回调函数的函数指针
(1)我们拿到函数原型
(2)然后到main.c中定义
我们起名叫做rx_callback,并返回值设置为RT_EOK,即返回0
(3)最后将设置回调函数相关参数填入
2.回调函数设置好以后,也就是说当硬件接收到数据以后,触发中断。触发中断就会回调我们设置的rx_callback函数。下面我们还需要加上信号量和线程。
3.创建信号量
我们之前使用的是动态创建信号量,所以这里再以静态创建信号量为例
(1)我们首先要定义一个信号量结构体变量,待会儿取地址传入参数
(2)初始化信号量
第一个参数传入刚刚创建的结构体地址
第二个参数为信号量名字,我们表示为接收信号量
第三个参数:信号量的资源值设置为0,因为我们一开始是没有数据的。只有当对方发送数据之后,调用接收回调函数,在回调函数中发送信号量,这样信号量的value值就会加1,从而唤醒接收线程。
第四个参数模式设置为先进先出
这里返回值就不做判断了。
4.创建线程
我们使用动态创建的方式
(1)首先创建线程结构体指针和线程处理函数
(2)然后动态创建线程(由于时间问题,返回值就不做判断了)
5.启动线程
6.完善线程处理函数
(1)我们使用while循环在线程处理函数中一直读取,这里单次读取一个字符,如果读取成功就进入一次中断
(2)如果成功接收到数据,就将数据通过调试串口打印出去(调试串口为创建工程时选择的串口)。
7.完善接收回调函数,一旦回调函数被执行就释放信号量
8.实验现象:
我们将程序下载到开发板,打开串口2的终端,可以发现暂时没有任何数据输出
由于我们的接收数据是通过串口1接收的,因此我们需要打开一个串口调试助手,通过上位机向串口1发送数据,然后串口2将串口1接收到的数据打印出来。
在本例中我们是一个字符一个字符来接收的,每一次接收新的字符都会将buffer中的数据给覆盖掉。在实际的应用开发中,我们应该定义某些数据类型将每次接收的数据保存起来,如可以定义数组或者使用复杂点的数据结构(如队列),将数据循环的保存到一块缓存区。需要处理的时候,再将数据从数组或队列中读走再处理。
DMA接收
使用DMA接收,首先我们要再设置里面使能串口DMA模式,最后要再board.h中加上宏声明
1.使能DMA模式
添加宏定义
2.打开设备我们修改为以DMA的方式接收
3.然后对于之前的中断接收工程,我们其它的代码都不需要动,我们只需要动接收回调函数和线程处理函数。
再之前使用中断接收的时候是每次接收到一个字符触发一次中断,我们现在使用DMA接收的时候每次接收的是一块缓冲区,也就是说接收的是一段数据。接收到的数据长度会通过回调函数的第二个参数返回。
因此返回多少个长度,我们在线程接收函数中就应该接收到多少
4.因此我们定义一个全局变量rx_len,在回调函数和线程处理函数中使用
5.然后我们再定义一个局部变量len来接收实际读取的数据长度,再将接收到的数据通过串口2打印出来。
6.此外还要加上信号量的释放和获取
7.编译并下载程序到开发板,复位发现出现一个错误:线程在挂起的时候报错了
这是因为创建线程的时候,设置线程栈的大小是1024,而在线程处理函数中的buffer里面我们创建的也是1024,这样就会导致线程栈溢出。
因此我们将线程处理函数中的buffer大小改小一点,如512
再重新下载程序,发现运行正常,使用上位机向STM32发送数据也可以正常被接收到
至此,RT-Thread的串口设备介绍和应用到此结束。