最新RT-Thread设备和驱动总结_rtt 设备驱动模型(2),2024年最新已收藏

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

rt_err_t rt_device_close(rt_device_t dev);

参数****描述

dev设备句柄
返回——
RT_EOK关闭设备成功
-RT_ERROR设备已经完全关闭,不能重复关闭设备
其他错误码关闭设备失败

Note

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

控制设备

通过命令控制字,应用程序也可以对设备进行控制,通过如下函数完成:

rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);

参数****描述

dev设备句柄
cmd命令控制字,这个参数通常与设备驱动程序相关
arg控制的参数
返回——
RT_EOK函数执行成功
-RT_ENOSYS执行失败,dev 为空
其他错误码执行失败

参数 cmd 的通用设备命令可取如下宏定义:

#define RT\_DEVICE\_CTRL\_RESUME 0x01 /\* 恢复设备 \*/
#define RT\_DEVICE\_CTRL\_SUSPEND 0x02 /\* 挂起设备 \*/
#define RT\_DEVICE\_CTRL\_CONFIG 0x03 /\* 配置设备 \*/
#define RT\_DEVICE\_CTRL\_SET\_INT 0x10 /\* 设置中断 \*/
#define RT\_DEVICE\_CTRL\_CLR\_INT 0x11 /\* 清中断 \*/
#define RT\_DEVICE\_CTRL\_GET\_INT 0x12 /\* 获取中断状态 \*/

读写设备

应用程序从设备中读取数据可以通过如下函数完成:

rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos,void* buffer, rt_size_t size);

参数****描述

dev设备句柄
pos读取数据偏移量
buffer内存缓冲区指针,读取的数据将会被保存在缓冲区中
size读取数据的大小
返回——
读到数据的实际大小如果是字符设备,返回大小以字节为单位,如果是块设备,返回的大小以块为单位
0需要读取当前线程的 errno 来判断错误状态

调用这个函数,会从 dev 设备中读取数据,并存放在 buffer 缓冲区中,这个缓冲区的最大长度是 size,pos 根据不同的设备类别有不同的意义。

向设备中写入数据,可以通过如下函数完成:

rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos,const void* buffer, rt_size_t size);

参数****描述

dev设备句柄
pos写入数据偏移量
buffer内存缓冲区指针,放置要写入的数据
size写入数据的大小
返回——
写入数据的实际大小如果是字符设备,返回大小以字节为单位;如果是块设备,返回的大小以块为单位
0需要读取当前线程的 errno 来判断错误状态

调用这个函数,会把缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的最大长度是 size,pos 根据不同的设备类别存在不同的意义。

数据收发回调

当硬件设备收到数据时,可以通过如下函数回调另一个函数来设置数据接收指示,通知上层应用线程有数据到达:

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));

参数****描述

dev设备句柄
rx_ind回调函数指针
返回——
RT_EOK设置成功

该函数的回调函数由调用者提供。当硬件设备接收到数据时,会回调这个函数并把收到的数据长度放在 size 参数中传递给上层应用。上层应用线程应在收到指示后,立刻从设备中读取数据。

在应用程序调用 rt_device_write() 写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。可以通过如下函数设置设备发送完成指示,函数参数及返回值见:

rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));

参数****描述

dev设备句柄
tx_done回调函数指针
返回——
RT_EOK设置成功

调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。

设备访问示例

下面代码为用程序访问设备的示例,首先通过 rt_device_find() 口查找到看门狗设备,获得设备句柄,然后通过 rt_device_init() 口初始化设备,通过 rt_device_control() 口设置看门狗设备溢出时间。

#include <rtthread.h>
#include <rtdevice.h>

#define IWDG\_DEVICE\_NAME "iwg"

static rt\_device\_t wdg_dev;

static void idle\_hook(void)
{
    /\* 在空闲线程的回调函数里喂狗 \*/
    rt\_device\_control(wdg_dev, RT_DEVICE_CTRL_WDT_KEEPALIVE, NULL);
    rt\_kprintf("feed the dog!\n ");
}

int main(void)
{
    rt\_err\_t res = RT_EOK;
    rt\_uint32\_t timeout = 1000;    /\* 溢出时间 \*/

    /\* 根据设备名称查找看门狗设备,获取设备句柄 \*/
    wdg_dev = rt\_device\_find(IWDG_DEVICE_NAME);
    if (!wdg_dev)
    {
        rt\_kprintf("find %s failed!\n", IWDG_DEVICE_NAME);
        return RT_ERROR;
    }
    /\* 初始化设备 \*/
    res = rt\_device\_init(wdg_dev);
    if (res != RT_EOK)
    {
        rt\_kprintf("initialize %s failed!\n", IWDG_DEVICE_NAME);
        return res;
    }
    /\* 设置看门狗溢出时间 \*/
    res = rt\_device\_control(wdg_dev, RT_DEVICE_CTRL_WDT_SET_TIMEOUT, &timeout);
    if (res != RT_EOK)
    {
        rt\_kprintf("set %s timeout failed!\n", IWDG_DEVICE_NAME);
        return res;
    }
    /\* 设置空闲线程回调函数 \*/
    rt\_thread\_idle\_sethook(idle_hook);

    return res;
}

补充说明

I/O 设备模型框架补充图

I/O 设备模型框架补充图是对 I/O 设备模型框架图的解释和补充说明,如下图所示。

I/O 设备模型框架补充图

图中各类里的c文件是各类对应的管理接口所在,比如设备基类rt_device的管理接口在device.c中。

图中设备驱动框架层有很多 RT-Thread 写好的类,图中只列出2类,其他类用 “xxx” 来表示,这些省略的类及其管理接口可以在 RT-Thread 源码 components/drivers 目录下找寻,比如该目录下可以找到serial/i2c/spi/sensor/can 等等相关目录。

图中设备驱动层的 “xxx” ,是 RT-Thread 支持的各 BSP 平台,在源码的 src/bsp 目录下找寻,比如stm32/gd32/at32/avr32/k210 等等。各个平台各自实现各个设备类型的硬件驱动能力,比如 STM32分别实现了 stm32_uart 类/stm32_adc 类等及其对应的管理接口;同样的,其他平台也分别各自实现了诸多对应类别及管理接口。

图中设备驱动层的各类里的c文件路径和名字只是示意,具体名字和路径由各BSP开发维护者自己定的。且随着 RT-Thread 版本的变化,各 BSP 的驱动路径和名字可能会发生变化。比如图中画的 STM32 串口设备类的管理接口所在路径是 bsp/stm32/drv_usart.c ,但实际路径是在 RT-Thread 源码下的 bsp/stm32/libraries/HAL_Drivers 下 。比如有的名字叫 drv_uart.c 等等。

该图横向看是分层思想,纵向看是各类派生继承关系。从下到上不断抽象、屏蔽下层差异,体现了面向对象的抽象的思想。子类受到父类的接口约束,子类各自实现父类提供的统一接口,又体现了面向接口编程的思想。比如从驱动层到驱动框架层,由不同厂商的相同硬件模块创建了很多子类对象,然后对接到同一个父类接口上,多对一,体现面向对象的抽象的威力。以串口设备为例,不管下层是 STM32、GD32 还是别的平台的,只要都是串口设备,都对接到 RT-Thread 的串口设备类——如图所绘,多个硬件对象对接同一个父类对象接口。同理,从设备驱动框架层到IO设备管理接口层,又是多对一,又是再一次的屏蔽差异,再一次的抽象。——面向对象的思想贯穿其中。

tips: 新增 BSP 设备驱动到 I/O 设备模型框架上时,开发者只需开发驱动层即可,设备驱动框架层和 I/O 设备管理层 RT-Thread 已写好了,无需改动,除非发现BUG或增加新的类别。

我有疑问: RT-Thread 官方论坛


UART 设备

已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v1/uart

UART 简介

UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器,UART 作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。是在应用程序开发过程中使用频率最高的数据总线。

UART 串口的特点是将数据一位一位地顺序传送,只要 2 根传输线就可以实现双向通信,一根线发送数据的同时用另一根线接收数据。UART 串口通信有几个重要的参数,分别是波特率、起始位、数据位、停止位和奇偶检验位,对于两个使用 UART 串口通信的端口,这些参数必须匹配,否则通信将无法正常完成。UART 串口传输的数据格式如下图所示:

串口传输数据格式

  • 起始位:表示数据传输的开始,电平逻辑为 “0” 。
  • 数据位:可能值有 5、6、7、8、9,表示传输这几个 bit 位数据。一般取值为 8,因为一个 ASCII 字符值为 8 位。
  • 奇偶校验位:用于接收方对接收到的数据进行校验,校验 “1” 的位数为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。
  • 停止位: 表示一帧数据的结束。电平逻辑为 “1”。
  • 波特率:串口通信时的速率,它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为每秒比特数 bit/s(bps)。常见的波特率值有 4800、9600、14400、38400、115200等,数值越大数据传输的越快,波特率为 115200 表示每秒钟传输 115200 位数据。

访问串口设备

应用程序通过 RT-Thread提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示:

函数****描述

rt_device_find()查找设备
rt_device_open()打开设备
rt_device_read()读取数据
rt_device_write()写入数据
rt_device_control()控制设备
rt_device_set_rx_indicate()设置接收回调函数
rt_device_set_tx_complete()设置发送完成回调函数
rt_device_close()关闭设备
查找串口设备

应用程序根据串口设备名称获取设备句柄,进而可以操作串口设备,查找设备函数如下所示,

rt_device_t rt_device_find(const char* name);

参数****描述

name设备名称
返回——
设备句柄查找到对应设备将返回相应的设备句柄
RT_NULL没有找到相应的设备对象

一般情况下,注册到系统的串口设备名称为 uart0,uart1等,使用示例如下所示:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

打开串口设备

通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。通过如下函数打开设备:

rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);

参数****描述

dev设备句柄
oflags设备模式标志
返回——
RT_EOK设备打开成功
-RT_EBUSY如果设备注册时指定的参数中包括 RT_DEVICE_FLAG_STANDALONE 参数,此设备将不允许重复打开
其他错误码设备打开失败

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 发送模式 \*/

串口数据接收和发送数据的模式分为 3 种:中断模式、轮询模式、DMA 模式。在使用的时候,这 3 种模式只能选其一,若串口的打开参数 oflags 没有指定使用中断模式或者 DMA 模式,则默认使用轮询模式。

DMA(Direct Memory Access)即直接存储器访问。 DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过 DMA 控制器为 RAM 与 I/O 设备开辟一条直接传送数据的通路,这就节省了 CPU 的资源来做其他操作。使用 DMA 传输可以连续获取或发送一段信息而不占用中断或延时,在通信频繁或有大段信息要传输时非常有用。

Note

注:* RT_DEVICE_FLAG_STREAM:流模式用于向串口终端输出字符串:当输出的字符是 "\n" (对应 16 进制值为 0x0A)时,自动在前面输出一个 "\r"(对应 16 进制值为 0x0D) 做分行。

流模式 RT_DEVICE_FLAG_STREAM 可以和接收发送模式参数使用或 “|” 运算符一起使用。

中断接收及轮询发送模式使用串口设备的示例如下所示:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

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

若串口要使用 DMA 接收模式,oflags 取值 RT_DEVICE_FLAG_DMA_RX。以DMA 接收及轮询发送模式使用串口设备的示例如下所示:

#define SAMPLE_UART_NAME       "uart2"  /* 串口设备名称 */
static rt_device_t serial;              /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

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

控制串口设备

通过控制接口,应用程序可以对串口设备进行配置,如波特率、数据位、校验位、接收缓冲区大小、停止位等参数的修改。控制函数如下所示:

rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);

参数****描述

dev设备句柄
cmd命令控制字,可取值:RT_DEVICE_CTRL_CONFIG
arg控制的参数,可取类型: struct serial_configure
返回——
RT_EOK函数执行成功
-RT_ENOSYS执行失败,dev 为空
其他错误码执行失败

控制参数结构体 struct serial_configure 原型如下:

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;      /\* 保留位 \*/
};

RT-Thread 提供的配置参数可取值为如下宏定义:

/\* 波特率可取值 \*/
#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 /\* inverted mode \*/
/\* 接收数据缓冲区默认大小 \*/
#define RT\_SERIAL\_RB\_BUFSZ 64

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

RT-Thread 提供的默认串口配置如下,即 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 \
}

Note

注:默认串口配置接收数据缓冲区大小为 RT_SERIAL_RB_BUFSZ,即 64 字节。若一次性数据接收字节数很多,没有及时读取数据,那么缓冲区的数据将会被新接收到的数据覆盖,造成数据丢失,建议调大缓冲区,即通过 control 接口修改。在修改缓冲区大小时请注意,缓冲区大小无法动态改变,只有在 open 设备之前可以配置。open 设备之后,缓冲区大小不可再进行更改。但除缓冲区之外的其他参数,在 open 设备前 / 后,均可进行更改。

若实际使用串口的配置参数与默认配置参数不符,则用户可以通过应用代码进行修改。修改串口配置参数,如波特率、数据位、校验位、缓冲区接收 buffsize、停止位等的示例程序如下:

#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);

发送数据

向串口中写入数据,可以通过如下函数完成:

rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);

参数****描述

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

调用这个函数,会把缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的大小是 size。

向串口写入数据示例程序如下所示:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
char str[] = "hello RT-Thread!\r\n";
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; /* 配置参数 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

/* 以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
/* 发送字符串 */
rt_device_write(serial, 0, str, (sizeof(str) - 1));

设置发送完成回调函数

在应用程序调用 rt_device_write() 写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。可以通过如下函数设置设备发送完成指示 :

rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));

参数****描述

dev设备句柄
tx_done回调函数指针
返回——
RT_EOK设置成功

调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由设备驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。

设置接收回调函数

可以通过如下函数来设置数据接收指示,当串口收到数据时,通知上层应用线程有数据到达 :

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));

参数****描述

dev设备句柄
rx_ind回调函数指针
dev设备句柄(回调函数参数)
size缓冲区数据大小(回调函数参数)
返回——
RT_EOK设置成功

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

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

一般情况下接收回调函数可以发送一个信号量或者事件通知串口数据处理线程有数据到达。使用示例如下所示:

#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 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_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);

参数****描述

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

读取数据偏移量 pos 针对字符设备无效,此参数主要用于块设备中。

串口使用中断接收模式并配合接收回调函数的使用示例如下所示:

static rt\_device\_t serial;                /\* 串口设备句柄 \*/
static struct rt\_semaphore rx_sem;    /\* 用于接收消息的信号量 \*/

/\* 接收数据的线程 \*/
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);
    }
}

关闭串口设备

当应用程序完成串口操作后,可以关闭串口设备,通过如下函数完成:

rt_err_t rt_device_close(rt_device_t dev);

参数****描述

dev设备句柄
返回——
RT_EOK关闭设备成功
-RT_ERROR设备已经完全关闭,不能重复关闭设备
其他错误码关闭设备失败

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

串口设备使用示例

中断接收及轮询发送

示例代码的主要步骤如下所示:

  1. 首先查找串口设备获取设备句柄。
  2. 初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。
  3. 设置串口设备的接收回调函数,之后发送字符串,并创建读取数据线程。
  • 读取数据线程会尝试读取一个字符数据,如果没有数据则会挂起并等待信号量,当串口设备接收到一个数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。
  • 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。

运行序列图如下图所示:

串口中断接收及轮询发送序列图

/\*
 \* 程序清单:这是一个 串口 设备使用例程
 \* 例程导出了 uart\_sample 命令到控制终端
 \* 命令调用格式:uart\_sample uart2
 \* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
 \* 程序功能:通过串口输出字符串"hello RT-Thread!",然后错位输出输入的字符
\*/

#include <rtthread.h>

#define SAMPLE\_UART\_NAME "uart2"

/\* 用于接收消息的信号量 \*/
static struct rt\_semaphore rx_sem;
static rt\_device\_t serial;

/\* 接收数据回调函数 \*/
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[])
{
    rt\_err\_t ret = RT_EOK;
    char uart_name[RT_NAME_MAX];
    char str[] = "hello RT-Thread!\r\n";

    if (argc == 2)
    {
        rt\_strncpy(uart_name, argv[1], RT_NAME_MAX);
    }
    else
    {
        rt\_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
    }

    /\* 查找系统中的串口设备 \*/
    serial = rt\_device\_find(uart_name);
    if (!serial)
    {
        rt\_kprintf("find %s failed!\n", uart_name);
        return RT_ERROR;
    }

    /\* 初始化信号量 \*/
    rt\_sem\_init(&rx_sem, "rx\_sem", 0, RT_IPC_FLAG_FIFO);
    /\* 以中断接收及轮询发送模式打开串口设备 \*/
    rt\_device\_open(serial, RT_DEVICE_FLAG_INT_RX);
    /\* 设置接收回调函数 \*/
    rt\_device\_set\_rx\_indicate(serial, uart_input);
    /\* 发送字符串 \*/
    rt\_device\_write(serial, 0, str, (sizeof(str) - 1));

    /\* 创建 serial 线程 \*/
    rt\_thread\_t thread = rt\_thread\_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
    /\* 创建成功则启动线程 \*/
    if (thread != RT_NULL)
    {
        rt\_thread\_startup(thread);
    }
    else
    {
        ret = RT_ERROR;
    }

    return ret;
}
/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(uart_sample, uart device sample);

DMA 接收及轮询发送

当串口接收到一批数据后会调用接收回调函数,接收回调函数会把此时缓冲区的数据大小通过消息队列发送给等待的数据处理线程。线程获取到消息后被激活,并读取数据。一般情况下 DMA 接收模式会结合 DMA 接收完成中断和串口空闲中断完成数据接收。

  • 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。

运行序列图如下图所示:

串口DMA接收及轮询发送序列图

/\*
 \* 程序清单:这是一个串口设备 DMA 接收使用例程
 \* 例程导出了 uart\_dma\_sample 命令到控制终端
 \* 命令调用格式:uart\_dma\_sample uart3
 \* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
 \* 程序功能:通过串口输出字符串"hello RT-Thread!",并通过串口输出接收到的数据,然后打印接收到的数据。
\*/

#include <rtthread.h>

#define SAMPLE\_UART\_NAME "uart3" /\* 串口设备名称 \*/

/\* 串口接收消息结构\*/
struct rx\_msg
{
    rt\_device\_t dev;
    rt\_size\_t size;
};
/\* 串口设备句柄 \*/
static rt\_device\_t serial;
/\* 消息队列控制块 \*/
static struct rt\_messagequeue rx_mq;

/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_input(rt\_device\_t dev, rt\_size\_t size)
{
    struct rx\_msg msg;
    rt\_err\_t result;
    msg.dev = dev;
    msg.size = size;

    result = rt\_mq\_send(&rx_mq, &msg, sizeof(msg));
    if ( result == -RT_EFULL)
    {
        /\* 消息队列满 \*/
        rt\_kprintf("message queue full!\n");
    }
    return result;
}

static void serial\_thread\_entry(void \*parameter)
{
    struct rx\_msg msg;
    rt\_err\_t result;
    rt\_uint32\_t rx_length;
    static char rx_buffer[RT_SERIAL_RB_BUFSZ + 1];

    while (1)
    {
        rt\_memset(&msg, 0, sizeof(msg));
        /\* 从消息队列中读取消息\*/
        result = rt\_mq\_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
        if (result == RT_EOK)
        {
            /\* 从串口读取数据\*/
            rx_length = rt\_device\_read(msg.dev, 0, rx_buffer, msg.size);
            rx_buffer[rx_length] = '\0';
            /\* 通过串口设备 serial 输出读取到的消息 \*/
            rt\_device\_write(serial, 0, rx_buffer, rx_length);
            /\* 打印数据 \*/
            rt\_kprintf("%s\n",rx_buffer);
        }
    }
}

static int uart\_dma\_sample(int argc, char \*argv[])
{
    rt\_err\_t ret = RT_EOK;
    char uart_name[RT_NAME_MAX];
    static char msg_pool[256];
    char str[] = "hello RT-Thread!\r\n";

    if (argc == 2)
    {
        rt\_strncpy(uart_name, argv[1], RT_NAME_MAX);
    }
    else
    {
        rt\_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
    }

    /\* 查找串口设备 \*/
    serial = rt\_device\_find(uart_name);
    if (!serial)
    {
        rt\_kprintf("find %s failed!\n", uart_name);
        return RT_ERROR;
    }

    /\* 初始化消息队列 \*/
    rt\_mq\_init(&rx_mq, "rx\_mq",
               msg_pool,                 /\* 存放消息的缓冲区 \*/
               sizeof(struct rx\_msg),    /\* 一条消息的最大长度 \*/
               sizeof(msg_pool),         /\* 存放消息的缓冲区大小 \*/
               RT_IPC_FLAG_FIFO);        /\* 如果有多个线程等待,按照先来先得到的方法分配消息 \*/

    /\* 以 DMA 接收及轮询发送方式打开串口设备 \*/
    rt\_device\_open(serial, RT_DEVICE_FLAG_DMA_RX);
    /\* 设置接收回调函数 \*/
    rt\_device\_set\_rx\_indicate(serial, uart_input);
    /\* 发送字符串 \*/
    rt\_device\_write(serial, 0, str, (sizeof(str) - 1));

    /\* 创建 serial 线程 \*/
    rt\_thread\_t thread = rt\_thread\_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
    /\* 创建成功则启动线程 \*/
    if (thread != RT_NULL)
    {
        rt\_thread\_startup(thread);
    }
    else
    {
        ret = RT_ERROR;
    }

    return ret;
}
/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(uart_dma_sample, uart device dma sample);

串口接收不定长数据

串口接收不定长数据需要用户在应用层进行处理,一般会有特定的协议,比如一帧数据可能会有起始标记位、数据长度位、数据、终止标记位等,发送数据帧时按照约定的协议进行发送,接收数据时再按照协议进行解析。

以下是一个简单的串口接收不定长数据示例代码,仅做了数据的结束标志位 DATA_CMD_END,如果遇到结束标志,则表示一帧数据结束。示例代码的主要步骤如下所示:

  1. 首先查找串口设备获取设备句柄。
  2. 初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。
  3. 设置串口设备的接收回调函数,之后发送字符串,并创建解析数据线程。
  • 解析数据线程会尝试读取一个字符数据,如果没有数据则会挂起并等待信号量,当串口设备接收到一个数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。在解析数据时,判断结束符,如果结束,则打印数据。
  • 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。
  • 当一帧数据长度超过最大长度时,这将是一帧不合格的数据,因为后面接收到的字符将覆盖最后一个字符。
/\*
 \* 程序清单:这是一个串口设备接收不定长数据的示例代码
 \* 例程导出了 uart\_dma\_sample 命令到控制终端
 \* 命令调用格式:uart\_dma\_sample uart2
 \* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
 \* 程序功能:通过串口 uart2 输出字符串"hello RT-Thread!",并通过串口 uart2 输入一串字符(不定长),再通过数据解析后,使用控制台显示有效数据。
\*/

#include <rtthread.h>

#define SAMPLE\_UART\_NAME "uart2"
#define DATA\_CMD\_END '\r' /\* 结束位设置为 \r,即回车符 \*/
#define ONE\_DATA\_MAXLEN 20 /\* 不定长数据的最大长度 \*/

/\* 用于接收消息的信号量 \*/
static struct rt\_semaphore rx_sem;
static rt\_device\_t serial;

/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_rx\_ind(rt\_device\_t dev, rt\_size\_t size)
{
    /\* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 \*/
    if (size > 0)
    {
        rt\_sem\_release(&rx_sem);
    }
    return RT_EOK;
}

static char uart\_sample\_get\_char(void)
{
    char ch;

    while (rt\_device\_read(serial, 0, &ch, 1) == 0)
    {
        rt\_sem\_control(&rx_sem, RT_IPC_CMD_RESET, RT_NULL);
        rt\_sem\_take(&rx_sem, RT_WAITING_FOREVER);
    }
    return ch;
}

/\* 数据解析线程 \*/
static void data\_parsing(void)
{
    char ch;
    char data[ONE_DATA_MAXLEN];
    static char i = 0;

    while (1)
    {
        ch = uart\_sample\_get\_char();
        rt\_device\_write(serial, 0, &ch, 1);
        if(ch == DATA_CMD_END)
        {
            data[i++] = '\0';
            rt\_kprintf("data=%s\r\n",data);
            i = 0;
            continue;
        }
        i = (i >= ONE_DATA_MAXLEN-1) ? ONE_DATA_MAXLEN-1 : i;
        data[i++] = ch;
    }
}

static int uart\_data\_sample(int argc, char \*argv[])
{
    rt\_err\_t ret = RT_EOK;
    char uart_name[RT_NAME_MAX];
    char str[] = "hello RT-Thread!\r\n";

    if (argc == 2)
    {
        rt\_strncpy(uart_name, argv[1], RT_NAME_MAX);
    }
    else
    {
        rt\_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
    }

    /\* 查找系统中的串口设备 \*/
    serial = rt\_device\_find(uart_name);
    if (!serial)
    {
        rt\_kprintf("find %s failed!\n", uart_name);
        return RT_ERROR;
    }

    /\* 初始化信号量 \*/
    rt\_sem\_init(&rx_sem, "rx\_sem", 0, RT_IPC_FLAG_FIFO);
    /\* 以中断接收及轮询发送模式打开串口设备 \*/
    rt\_device\_open(serial, RT_DEVICE_FLAG_INT_RX);
    /\* 设置接收回调函数 \*/
    rt\_device\_set\_rx\_indicate(serial, uart_rx_ind);
    /\* 发送字符串 \*/
    rt\_device\_write(serial, 0, str, (sizeof(str) - 1));

    /\* 创建 serial 线程 \*/
    rt\_thread\_t thread = rt\_thread\_create("serial", (void (\*)(void \*parameter))data_parsing, RT_NULL, 1024, 25, 10);
    /\* 创建成功则启动线程 \*/
    if (thread != RT_NULL)
    {
        rt\_thread\_startup(thread);
    }
    else
    {
        ret = RT_ERROR;
    }

    return ret;
}

/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(uart_data_sample, uart device sample);

我有疑问: RT-Thread 官方论坛


UART 设备 v2 版本

已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v2/uart

Note

注:目前只有 github 的 master 分支上的 stm32l475-pandora 的 BSP 进行了串口 V2 版本的适配。

Note

注:如果用户已经清楚了解旧版本的串口框架,那么可直接跳过该文档的前部分关于串口介绍的内容,从访问串口设备章节开始查阅即可。

UART 简介

UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器,UART 作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。是在应用程序开发过程中使用频率最高的数据总线。

UART 串口的特点是将数据一位一位地顺序传送,只要 2 根传输线就可以实现双向通信,一根线发送数据的同时用另一根线接收数据。UART 串口通信有几个重要的参数,分别是波特率、起始位、数据位、停止位和奇偶检验位,对于两个使用 UART 串口通信的端口,这些参数必须匹配,否则通信将无法正常完成。UART 串口传输的数据格式如下图所示:

串口传输数据格式

  • 起始位:表示数据传输的开始,电平逻辑为 “0” 。
  • 数据位:可能值有 5、6、7、8、9,表示传输这几个 bit 位数据。一般取值为 8,因为一个 ASCII 字符值为 8 位。
  • 奇偶校验位:用于接收方对接收到的数据进行校验,校验 “1” 的位数为偶数 (偶校验) 或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。
  • 停止位: 表示一帧数据的结束。电平逻辑为 “1”。
  • 波特率:串口通信时的速率,它用单位时间内传输的二进制代码的有效位 (bit) 数来表示,其单位为每秒比特数 bit/s(bps)。常见的波特率值有 4800、9600、14400、38400、115200 等,数值越大数据传输的越快,波特率为 115200 表示每秒钟传输 115200 位数据。

访问串口设备

应用程序通过 RT-Thread 提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示:

函数****描述

rt_device_find()查找设备
rt_device_open()打开设备
rt_device_read()读取数据
rt_device_write()写入数据
rt_device_control()控制设备
rt_device_set_rx_indicate()设置接收回调函数
rt_device_set_tx_complete()设置发送完成回调函数
rt_device_close()关闭设备
查找串口设备

应用程序根据串口设备名称获取设备句柄,进而可以操作串口设备,查找设备函数如下所示,

rt_device_t rt_device_find(const char* name);

参数****描述

name设备名称
返回——
设备句柄查找到对应设备将返回相应的设备句柄
RT_NULL没有找到相应的设备对象

一般情况下,注册到系统的串口设备名称为 uart0,uart1 等,使用示例如下所示:

#define SAMPLE_UART_NAME       "uart2"    /* 串口设备名称 */
static rt_device_t serial;                /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);

打开串口设备

通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。通过如下函数打开设备:

rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);

参数****描述

dev设备句柄
oflags设备模式标志
返回——
RT_EOK设备打开成功
-RT_EBUSY如果设备注册时指定的参数中包括 RT_DEVICE_FLAG_STANDALONE 参数,此设备将不允许重复打开
其他错误码设备打开失败

oflags 参数支持下列取值 (可以采用或的方式支持多种取值):

/\* 接收模式参数 \*/
#define RT\_DEVICE\_FLAG\_RX\_BLOCKING 0x1000 /\* 接收阻塞模式 \*/

#define RT\_DEVICE\_FLAG\_RX\_NON\_BLOCKING 0x2000 /\* 接收非阻塞模式 \*/

/\* 发送模式参数 \*/
#define RT\_DEVICE\_FLAG\_TX\_BLOCKING 0x4000 /\* 发送阻塞模式 \*/

#define RT\_DEVICE\_FLAG\_TX\_NON\_BLOCKING 0x8000 /\* 发送非阻塞模式 \*/

#define RT\_DEVICE\_FLAG\_STREAM 0x040 /\* 流模式 \*/

用户使用串口时,不再根据硬件工作模式(轮询、中断、DMA)选择,而是根据具体的操作方式去配置,一般情况下,我们会选择使用 发送阻塞模式 以及 接收非阻塞模式 来进行开发。如下例子:

rt_device_open(dev, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING); // 串口设备使用模式为 (发送阻塞 接收非阻塞) 模式

Note

注:为了避免 阻塞 / 非阻塞模式 和 轮询 / 中断 / DMA 模式 在文中描述上可能存在的误解,故本文以 应用层操作模式 指代 阻塞 / 非阻塞模式,以 硬件工作模式 指代 轮询 / 中断 / DMA 模式。

而对于流模式 RT_DEVICE_FLAG_STREAM,主要是当串口外设作为控制台时才会使用,该模式用来解决用户回车换行的问题,在正常的串口外设通信场景中,该模式一般不会使用。

Note

注:RT_DEVICE_FLAG_STREAM 流模式用于向串口终端输出字符串:当输出的字符是 "\n" (对应 16 进制值为 0x0A)时,自动在前面输出一个 "\r"(对应 16 进制值为 0x0D) 做分行。

流模式 RT_DEVICE_FLAG_STREAM 可以和接收发送模式参数使用或 “|” 运算符一起使用。

硬件工作模式选择

由于用户层使用串口时,只关心应用层操作模式,不再关心硬件工作模式,使得应用层开发变得更加便捷,也增加了应用程序的可移植性。倘若用户开发时比较关心硬件具体的工作模式,那么应该对其工作模式如何选择?

串口外设的遵循如下规则:

  1. 模式优先级为:DMA 模式 > 中断模式 > 轮询模式。即当有 DMA 配置时,默认使用 DMA 模式,以此类推。且非必要条件,不选择使用轮询模式。
  2. 串口默认配置接收和发送缓冲区
  3. 默认使用阻塞发送、非阻塞接收模式

Note

注:由于串口控制台的工作场景的独特性,其硬件工作模式为中断接收和轮询发送,用户使用时不建议参照串口控制台的模式进行配置,建议参照串口设备使用示例进行使用。

为了更加直观的表示应用层操作模式与硬件工作模式的对应关系,下面以图表和示例的方式进行说明。

发送端的模式对应关系如下表所示:

编号 配置发送缓冲区(有 / 无)说明 硬件工作模式(TX) 应用层操作模式(TX)

(1)不使用缓存区,且设置缓存区长度为0轮询阻塞
(2)不支持该模式轮询非阻塞
(3)使用缓存区中断阻塞
(4)使用缓存区中断非阻塞
(5)不使用缓存区,但需要设置缓冲区长度大于0DMA阻塞
(6)使用缓存区DMA非阻塞

对于编号 (1) 模式,如果必须使用轮询模式时,一定要将缓冲区大小配置为 0,因为如果缓冲区大小不为 0,在应用层使用发送阻塞模式时,将会使用中断模式(如果开 DMA,则使用 DMA 模式)。

对于编号 (2) 模式,当用户设置为 DMA 阻塞模式时,虽然设置了缓冲区不为 0,但是该缓冲区并不会进行初始化,而是直接进行 DMA 数据搬运。从而省去了内存搬运造成的性能下降的问题。需要注意的是,当使用 DMA 阻塞模式时,虽然不用缓冲区,但是也要将缓冲区长度设置为大于 0 的值,因为当缓冲区长度为 0 时,将会错误地使用轮询模式。

接收端的模式对应关系如下表所示:

编号 配置接收缓冲区(有 / 无)说明 硬件工作模式(RX) 应用层操作模式(RX)

(1)不使用缓存区,且设置缓存区长度为0轮询阻塞
(2)不支持该模式轮询非阻塞
(3)使用缓存区中断阻塞
(4)使用缓存区中断非阻塞
(5)使用缓存区DMA阻塞
(6)使用缓存区DMA非阻塞

对于编号 (1) 模式,如果必须使用轮询模式时,一定要将缓冲区大小配置为 0,因为如果缓冲区大小不为 0,在应用层使用接收阻塞模式时,将会使用中断模式(如果开 DMA,则使用 DMA 模式)。

下面举例说明如何配置硬件工作模式:

配置发送接收为 DMA 模式

在 menuconfig 中配置效果如下:

menuconfig

上图所示,对于 UART1 的配置为开启 DMA RX 和 DMA TX,且发送和接收缓存区大小设置为 1024 字节。

由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是 DMA 模式。

配置发送接收为中断模式

在 menuconfig 中配置效果如下:

menuconfig

上图所示,对于 UART1 的配置为关闭 DMA RX 和 DMA TX,且发送和接收缓存区大小设置为 1024 字节。

由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是中断模式。

配置发送 DMA 模式、接收中断模式

在 menuconfig 中配置效果如下:

menuconfig

上图所示,对于 UART1 的配置为关闭 DMA RX 和开启 DMA TX,且发送和接收缓存区大小设置为 1024 字节。

由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是 DMA 发送模式和中断接收模式。

在 menuconfig 中配置效果如下:

menuconfig

上图所示,对于 UART1 的配置为关闭 DMA RX 和 DMA TX,且发送和接收缓存区大小设置为 1024 字节。并且设置 UART1 TX buffer size 为 0。

由此用户在应用层对串口的接收和发送的操作模式进行配置时,发送只能使用阻塞模式,接收可以使用阻塞和非阻塞模式。串口控制台默认使用这样的配置模式,且操作模式为阻塞发送和非阻塞接收。

串口数据接收和发送数据的模式分为 3 种:中断模式、轮询模式、DMA 模式。在使用的时候,这 3 种模式只能选其一,若串口的打开参数 oflags 没有指定使用中断模式或者 DMA 模式,则默认使用轮询模式。

控制串口设备

通过控制接口,应用程序可以对串口设备进行配置,如波特率、数据位、校验位、接收缓冲区大小、停止位等参数的修改。控制函数如下所示:

rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);

参数****描述

dev设备句柄
cmd命令控制字,可取值:RT_DEVICE_CTRL_CONFIG
arg控制的参数,可取类型: struct serial_configure
返回——
RT_EOK函数执行成功
-RT_ENOSYS执行失败,dev 为空
其他错误码执行失败

控制参数结构体 struct serial_configure 原型如下:

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 rx_bufsz     :16;     /\* 接收数据缓冲区大小 \*/
    rt\_uint32\_t tx_bufsz     :16;     /\* 发送数据缓冲区大小 \*/
    rt\_uint32\_t reserved     :4;      /\* 保留位 \*/
};

RT-Thread 提供的配置参数可取值为如下宏定义:

/\* 波特率可取值 \*/
#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 /\* inverted mode \*/

#define RT\_SERIAL\_RX\_MINBUFSZ 64 /\* 限制接收缓冲区最小长度 \*/
#define RT\_SERIAL\_TX\_MINBUFSZ 64 /\* 限制发送缓冲区最小长度 \*/

RT-Thread 提供的默认串口配置如下,即 RT-Thread 系统中默认每个串口设备都使用如下配置:

/\* Default config for serial\_configure structure \*/
#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\_RX\_MINBUFSZ, /\* rxBuf size \*/ \
 RT\_SERIAL\_TX\_MINBUFSZ, /\* txBuf size \*/ \
 0 \
}

Note

注:虽然默认串口配置设置了 rx_bufsz 和 tx_bufsz 的大小,但是其缓冲区具体长度会在底层驱动初始化时再次配置,这里无需关心其值。

若实际使用串口的配置参数与默认配置参数不符,则用户可以通过应用代码进行修改。修改串口配置参数,如波特率、数据位、校验位、缓冲区接收 buffsize、停止位等的示例程序如下:

#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.rx_bufsz     = 128;                // 修改缓冲区 rx 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_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);

发送数据

向串口中写入数据,可以通过如下函数完成:

rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);

参数****描述

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

调用这个函数,会把缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的大小是 size。

向串口写入数据示例程序如下所示:

#define SAMPLE\_UART\_NAME "uart2" /\* 串口设备名称 \*/
static rt\_device\_t serial;                /\* 串口设备句柄 \*/
char str[] = "hello RT-Thread!\r\n";
struct serial\_configure config = RT_SERIAL_CONFIG_DEFAULT; /\* 配置参数 \*/
/\* 查找串口设备 \*/
serial = rt\_device\_find(SAMPLE_UART_NAME);

/\* 以非阻塞接收和阻塞发送模式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);
/\* 发送字符串 \*/
rt\_device\_write(serial, 0, str, (sizeof(str) - 1));

设置发送完成回调函数

在应用程序调用 rt_device_write() 写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。可以通过如下函数设置设备发送完成指示 :

rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));

参数****描述

dev设备句柄
tx_done回调函数指针
返回——
RT_EOK设置成功

调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由设备驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。

设置接收回调函数

可以通过如下函数来设置数据接收指示,当串口收到数据时,通知上层应用线程有数据到达 :

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));

参数****描述

dev设备句柄
rx_ind回调函数指针
dev设备句柄(回调函数参数)
size缓冲区数据大小(回调函数参数)
返回——
RT_EOK设置成功

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

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

一般情况下接收回调函数可以发送一个信号量或者事件通知串口数据处理线程有数据到达。使用示例如下所示:

#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 int uart\_sample(int argc, char \*argv[])
{
    serial = rt\_device\_find(SAMPLE_UART_NAME);

    /\* 以非阻塞接收和阻塞发送模式打开串口设备 \*/
    rt\_device\_open(serial, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);

    /\* 初始化信号量 \*/
    rt\_sem\_init(&rx_sem, "rx\_sem", 0, RT_IPC_FLAG_FIFO);

    /\* 设置接收回调函数 \*/
    rt\_device\_set\_rx\_indicate(serial, uart_input);
}

接收数据

可调用如下函数读取串口接收到的数据:

rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);

参数****描述

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

读取数据偏移量 pos 针对字符设备无效,此参数主要用于块设备中。

串口使用中断接收模式并配合接收回调函数的使用示例如下所示:

static rt\_device\_t serial;                /\* 串口设备句柄 \*/
static struct rt\_semaphore rx_sem;    /\* 用于接收消息的信号量 \*/

/\* 接收数据的线程 \*/
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);
    }
}

关闭串口设备

当应用程序完成串口操作后,可以关闭串口设备,通过如下函数完成:

rt_err_t rt_device_close(rt_device_t dev);

参数****描述

dev设备句柄
返回——
RT_EOK关闭设备成功
-RT_ERROR设备已经完全关闭,不能重复关闭设备
其他错误码关闭设备失败

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

新旧版本串口使用区别

  • 使用 rt_devide_open() 的入参 oflags 区别:
// 旧版本 oflags 的参数取值
RT_DEVICE_FLAG_INT_RX
RT_DEVICE_FLAG_INT_TX
RT_DEVICE_FLAG_DMA_RX
RT_DEVICE_FLAG_DMA_TX

// 新版本 oflags 的参数取值
RT_DEVICE_FLAG_RX_NON_BLOCKING
RT_DEVICE_FLAG_RX_BLOCKING
RT_DEVICE_FLAG_TX_NON_BLOCKING
RT_DEVICE_FLAG_TX_BLOCKING

为了兼容旧版本的框架,使用新版本串口框架时旧版本的应用代码可直接使用,只需注意一点,旧版本的 oflags 参数不再起作用,默认使用新版本的操作模式: 接收非阻塞发送阻塞模式。

  • 缓冲区宏定义区别

旧版本接收缓冲区统一为 RT_SERIAL_RB_BUFSZ ,旧版本没有发送缓冲区的设置。

新版本缓冲区进行了分离接收和发送,并且也可以对各个串口进行单独设置,例如:

// 设置 串口 2 的发送缓冲区为 256 字节,接收缓冲区为 1024 字节,见 rtconfig.h
#define BSP_UART2_RX_BUFSIZE 256
#define BSP_UART2_TX_BUFSIZE 1024

当从新版本往旧版本进行迁移时,如果使用了RT_SERIAL_RB_BUFSZ,那么需要将本参数更改为对应的串口的具体的宏定义

  • 串口配置 serial_configure 成员变量 bufsz 的区别:

旧版本的 bufsz 指代串口接收缓冲区的大小,新版本由于需要分别设置发送和接收缓冲区,因此成员变量调整为 rx_bufsztx_bufsz

// 旧版本
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                :6;
};

// 新版本
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 rx_bufsz                :16;
    rt\_uint32\_t tx_bufsz                :16;
    rt\_uint32\_t reserved                :6;
};

串口设备使用示例

非阻塞接收和阻塞发送模式

当串口接收到一批数据后会调用接收回调函数,接收回调函数会把此时缓冲区的数据大小通过消息队列发送给等待的数据处理线程。线程获取到消息后被激活,并读取数据。

此例程以开启了 DMA 发送和接收模式为例,一般情况下 DMA 接收模式会结合 DMA 接收半完成中断、完成中断和串口空闲中断完成数据接收。

  • 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。

运行序列图如下图所示:

串口 DMA 接收及轮询发送序列图

/\*
 \* 程序清单:这是一个串口设备 开启 DMA 模式后使用例程
 \* 例程导出了 uart\_dma\_sample 命令到控制终端
 \* 命令调用格式:uart\_dma\_sample uart1
 \* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
 \* 程序功能:通过串口输出字符串 "hello RT-Thread!",并通过串口输出接收到的数据,然后打印接收到的数据。
\*/

#include <rtthread.h>
#include <rtdevice.h>

#define SAMPLE\_UART\_NAME "uart1" /\* 串口设备名称 \*/

/\* 串口接收消息结构 \*/
struct rx\_msg
{
    rt\_device\_t dev;
    rt\_size\_t size;
};
/\* 串口设备句柄 \*/
static rt\_device\_t serial;
/\* 消息队列控制块 \*/
static struct rt\_messagequeue rx_mq;

/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_input(rt\_device\_t dev, rt\_size\_t size)
{
    struct rx\_msg msg;
    rt\_err\_t result;
    msg.dev = dev;
    msg.size = size;

    result = rt\_mq\_send(&rx_mq, &msg, sizeof(msg));
    if (result == -RT_EFULL)
    {
        /\* 消息队列满 \*/
        rt\_kprintf("message queue full!\n");
    }
    return result;
}

static void serial\_thread\_entry(void \*parameter)
{
    struct rx\_msg msg;
    rt\_err\_t result;
    rt\_uint32\_t rx_length;
    static char rx_buffer[BSP_UART1_RX_BUFSIZE + 1];

    while (1)
    {
        rt\_memset(&msg, 0, sizeof(msg));
        /\* 从消息队列中读取消息 \*/
        result = rt\_mq\_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
        if (result == RT_EOK)
        {
            /\* 从串口读取数据 \*/
            rx_length = rt\_device\_read(msg.dev, 0, rx_buffer, msg.size);
            rx_buffer[rx_length] = '\0';
            /\* 通过串口设备 serial 输出读取到的消息 \*/
            rt\_device\_write(serial, 0, rx_buffer, rx_length);
            /\* 打印数据 \*/
            rt\_kprintf("%s\n",rx_buffer);
        }
    }
}

static int uart\_dma\_sample(int argc, char \*argv[])
{
    rt\_err\_t ret = RT_EOK;
    char uart_name[RT_NAME_MAX];
    static char msg_pool[256];
    char str[] = "hello RT-Thread!\r\n";

    if (argc == 2)
    {
        rt\_strncpy(uart_name, argv[1], RT_NAME_MAX);
    }
    else
    {
        rt\_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
    }

    /\* 查找串口设备 \*/
    serial = rt\_device\_find(uart_name);
    if (!serial)
    {
        rt\_kprintf("find %s failed!\n", uart_name);
        return RT_ERROR;
    }

    /\* 初始化消息队列 \*/
    rt\_mq\_init(&rx_mq, "rx\_mq",
               msg_pool,                 /\* 存放消息的缓冲区 \*/
               sizeof(struct rx\_msg),    /\* 一条消息的最大长度 \*/
               sizeof(msg_pool),         /\* 存放消息的缓冲区大小 \*/
               RT_IPC_FLAG_FIFO);        /\* 如果有多个线程等待,按照先来先得到的方法分配消息 \*/

    /\* 以 DMA 接收及轮询发送方式打开串口设备 \*/
    rt\_device\_open(serial, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);
    /\* 设置接收回调函数 \*/
    rt\_device\_set\_rx\_indicate(serial, uart_input);
    /\* 发送字符串 \*/
    rt\_device\_write(serial, 0, str, (sizeof(str) - 1));

    /\* 创建 serial 线程 \*/
    rt\_thread\_t thread = rt\_thread\_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
    /\* 创建成功则启动线程 \*/
    if (thread != RT_NULL)
    {
        rt\_thread\_startup(thread);
    }
    else
    {
        ret = RT_ERROR;
    }

    return ret;
}
/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(uart_dma_sample, uart device dma sample);

我有疑问: RT-Thread 官方论坛


PIN 设备

已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/pin/pin

引脚简介

芯片上的引脚一般分为 4 类:电源、时钟、控制与 I/O,I/O 口在使用模式上又分为 General Purpose Input Output(通用输入 / 输出),简称 GPIO,与功能复用 I/O(如 SPI/I2C/UART 等)。

大多数 MCU 的引脚都不止一个功能。不同引脚内部结构不一样,拥有的功能也不一样。可以通过不同的配置,切换引脚的实际功能。通用 I/O 口主要特性如下:

  • 可编程控制中断:中断触发模式可配置,一般有下图所示 5 种中断触发模式:

5 种中断触发模式

  • 输入输出模式可控制。
    • 输出模式一般包括:推挽、开漏、上拉、下拉。引脚为输出模式时,可以通过配置引脚输出的电平状态为高电平或低电平来控制连接的外围设备。
    • 输入模式一般包括:浮空、上拉、下拉、模拟。引脚为输入模式时,可以读取引脚的电平状态,即高电平或低电平。

访问 PIN 设备

应用程序通过 RT-Thread 提供的 PIN 设备管理接口来访问 GPIO,相关接口如下所示:

函数****描述

rt_pin_get()获取引脚编号
rt_pin_mode()设置引脚模式
rt_pin_write()设置引脚电平
rt_pin_read()读取引脚电平
rt_pin_attach_irq()绑定引脚中断回调函数
rt_pin_irq_enable()使能引脚中断
rt_pin_detach_irq()脱离引脚中断回调函数
获取引脚编号

RT-Thread 提供的引脚编号需要和芯片的引脚号区分开来,它们并不是同一个概念,引脚编号由 PIN 设备驱动程序定义,和具体的芯片相关。有3种方式可以获取引脚编号: API 接口获取、使用宏定义或者查看PIN 驱动文件。

使用 API

使用 rt_pin_get() 获取引脚编号,如下获取 PF9 的引脚编号:

pin_number = rt_pin_get("PF.9");

使用宏定义

如果使用 rt-thread/bsp/stm32 目录下的 BSP 则可以使用下面的宏获取引脚编号:

GET_PIN(port, pin)

获取引脚号为 PF9 的 LED0 对应的引脚编号的示例代码如下所示:

#define LED0_PIN        GET_PIN(F,  9)

查看驱动文件

如果使用其他 BSP 则需要查看 PIN 驱动代码 drv_gpio.c 文件确认引脚编号。此文件里有一个数组存放了每个 PIN 脚对应的编号信息,如下所示:

static const rt\_uint16\_t pins[] =
{
    __STM32_PIN_DEFAULT,
    __STM32_PIN_DEFAULT,
    \_\_STM32\_PIN(2, A, 15),
    \_\_STM32\_PIN(3, B, 5),
    \_\_STM32\_PIN(4, B, 8),
    __STM32_PIN_DEFAULT,
    __STM32_PIN_DEFAULT,
    __STM32_PIN_DEFAULT,
    \_\_STM32\_PIN(8, A, 14),
    \_\_STM32\_PIN(9, B, 6),
    ... ...
}

__STM32_PIN(2, A, 15)为例,2 为 RT-Thread 使用的引脚编号,A 为端口号,15 为引脚号,所以 PA15 对应的引脚编号为 2。

设置引脚模式

引脚在使用前需要先设置好输入或者输出模式,通过如下函数完成:

void rt_pin_mode(rt_base_t pin, rt_base_t mode);

参数****描述

pin引脚编号
mode引脚工作模式

目前 RT-Thread 支持的引脚工作模式可取如所示的 5 种宏定义值之一,每种模式对应的芯片实际支持的模式需参考 PIN 设备驱动程序的具体实现:

#define PIN_MODE_OUTPUT 0x00            /* 输出 */
#define PIN_MODE_INPUT 0x01             /* 输入 */
#define PIN_MODE_INPUT_PULLUP 0x02      /* 上拉输入 */
#define PIN_MODE_INPUT_PULLDOWN 0x03    /* 下拉输入 */
#define PIN_MODE_OUTPUT_OD 0x04         /* 开漏输出 */

使用示例如下所示:

#define BEEP_PIN_NUM            35  /* PB0 */

/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);

设置引脚电平

设置引脚输出电平的函数如下所示:

void rt_pin_write(rt_base_t pin, rt_base_t value);

参数****描述

pin引脚编号
value电平逻辑值,可取 2 种宏定义值之一:PIN_LOW 低电平,PIN_HIGH 高电平

使用示例如下所示:

#define BEEP_PIN_NUM            35  /* PB0 */

/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
/* 设置低电平 */
rt_pin_write(BEEP_PIN_NUM, PIN_LOW);

读取引脚电平

读取引脚电平的函数如下所示:

int rt_pin_read(rt_base_t pin);

参数****描述

pin引脚编号
返回——
PIN_LOW低电平
PIN_HIGH高电平

使用示例如下所示:

#define BEEP_PIN_NUM            35  /* PB0 */
int status;

/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
/* 设置低电平 */
rt_pin_write(BEEP_PIN_NUM, PIN_LOW);

status = rt_pin_read(BEEP_PIN_NUM);

绑定引脚中断回调函数

若要使用到引脚的中断功能,可以使用如下函数将某个引脚配置为某种中断触发模式并绑定一个中断回调函数到对应引脚,当引脚中断发生时,就会执行回调函数:

rt_err_t rt_pin_attach_irq(rt_int32_t pin, rt_uint32_t mode,
                           void (*hdr)(void *args), void *args);

参数****描述

pin引脚编号
mode中断触发模式
hdr中断回调函数,用户需要自行定义这个函数
args中断回调函数的参数,不需要时设置为 RT_NULL
返回——
RT_EOK绑定成功
错误码绑定失败

中断触发模式 mode 可取如下 5 种宏定义值之一:

#define PIN_IRQ_MODE_RISING 0x00         /* 上升沿触发 */
#define PIN_IRQ_MODE_FALLING 0x01        /* 下降沿触发 */
#define PIN_IRQ_MODE_RISING_FALLING 0x02 /* 边沿触发(上升沿和下降沿都触发)*/
#define PIN_IRQ_MODE_HIGH_LEVEL 0x03     /* 高电平触发 */
#define PIN_IRQ_MODE_LOW_LEVEL 0x04      /* 低电平触发 */

使用示例如下所示:

#define KEY0\_PIN\_NUM 55 /\* PD8 \*/
/\* 中断回调函数 \*/
void beep\_on(void \*args)
{
    rt\_kprintf("turn on beep!\n");

    rt\_pin\_write(BEEP_PIN_NUM, PIN_HIGH);
}
static void pin\_beep\_sample(void)
{
    /\* 按键0引脚为输入模式 \*/
    rt\_pin\_mode(KEY0_PIN_NUM, PIN_MODE_INPUT_PULLUP);
    /\* 绑定中断,下降沿模式,回调函数名为beep\_on \*/
    rt\_pin\_attach\_irq(KEY0_PIN_NUM, PIN_IRQ_MODE_FALLING, beep_on, RT_NULL);
}

使能引脚中断

绑定好引脚中断回调函数后使用下面的函数使能引脚中断:

rt_err_t rt_pin_irq_enable(rt_base_t pin, rt_uint32_t enabled);

参数****描述

pin引脚编号
enabled状态,可取 2 种值之一:PIN_IRQ_ENABLE(开启),PIN_IRQ_DISABLE(关闭)
返回——
RT_EOK使能成功
错误码使能失败

使用示例如下所示:

#define KEY0\_PIN\_NUM 55 /\* PD8 \*/
/\* 中断回调函数 \*/
void beep\_on(void \*args)
{
    rt\_kprintf("turn on beep!\n");

    rt\_pin\_write(BEEP_PIN_NUM, PIN_HIGH);
}
static void pin\_beep\_sample(void)
{
    /\* 按键0引脚为输入模式 \*/
    rt\_pin\_mode(KEY0_PIN_NUM, PIN_MODE_INPUT_PULLUP);
    /\* 绑定中断,下降沿模式,回调函数名为beep\_on \*/
    rt\_pin\_attach\_irq(KEY0_PIN_NUM, PIN_IRQ_MODE_FALLING, beep_on, RT_NULL);
    /\* 使能中断 \*/
    rt\_pin\_irq\_enable(KEY0_PIN_NUM, PIN_IRQ_ENABLE);
}

脱离引脚中断回调函数

可以使用如下函数脱离引脚中断回调函数:

rt_err_t rt_pin_detach_irq(rt_int32_t pin);

参数****描述

pin引脚编号
返回——
RT_EOK脱离成功
错误码脱离失败

引脚脱离了中断回调函数以后,中断并没有关闭,还可以调用绑定中断回调函数再次绑定其他回调函数。

#define KEY0\_PIN\_NUM 55 /\* PD8 \*/
/\* 中断回调函数 \*/
void beep\_on(void \*args)
{
    rt\_kprintf("turn on beep!\n");

    rt\_pin\_write(BEEP_PIN_NUM, PIN_HIGH);
}
static void pin\_beep\_sample(void)
{
    /\* 按键0引脚为输入模式 \*/
    rt\_pin\_mode(KEY0_PIN_NUM, PIN_MODE_INPUT_PULLUP);
    /\* 绑定中断,下降沿模式,回调函数名为beep\_on \*/
    rt\_pin\_attach\_irq(KEY0_PIN_NUM, PIN_IRQ_MODE_FALLING, beep_on, RT_NULL);
    /\* 使能中断 \*/
    rt\_pin\_irq\_enable(KEY0_PIN_NUM, PIN_IRQ_ENABLE);
    /\* 脱离中断回调函数 \*/
    rt\_pin\_detach\_irq(KEY0_PIN_NUM);
}

PIN 设备使用示例

PIN 设备的具体使用方式可以参考如下示例代码,示例代码的主要步骤如下:

  1. 设置蜂鸣器对应引脚为输出模式,并给一个默认的低电平状态。
  2. 设置按键 0 和 按键1 对应引脚为输入模式,然后绑定中断回调函数并使能中断。
  3. 按下按键 0 蜂鸣器开始响,按下按键 1 蜂鸣器停止响。
/\*
 \* 程序清单:这是一个 PIN 设备使用例程
 \* 例程导出了 pin\_beep\_sample 命令到控制终端
 \* 命令调用格式:pin\_beep\_sample
 \* 程序功能:通过按键控制蜂鸣器对应引脚的电平状态控制蜂鸣器
\*/

#include <rtthread.h>
#include <rtdevice.h>

/\* 引脚编号,通过查看设备驱动文件drv\_gpio.c确定 \*/
#ifndef BEEP\_PIN\_NUM
    #define BEEP\_PIN\_NUM 35 /\* PB0 \*/
#endif
#ifndef KEY0\_PIN\_NUM
    #define KEY0\_PIN\_NUM 55 /\* PD8 \*/
#endif
#ifndef KEY1\_PIN\_NUM
    #define KEY1\_PIN\_NUM 56 /\* PD9 \*/
#endif

void beep\_on(void \*args)
{
    rt\_kprintf("turn on beep!\n");

    rt\_pin\_write(BEEP_PIN_NUM, PIN_HIGH);
}

void beep\_off(void \*args)
{
    rt\_kprintf("turn off beep!\n");

    rt\_pin\_write(BEEP_PIN_NUM, PIN_LOW);
}

static void pin\_beep\_sample(void)
{
    /\* 蜂鸣器引脚为输出模式 \*/
    rt\_pin\_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
    /\* 默认低电平 \*/
    rt\_pin\_write(BEEP_PIN_NUM, PIN_LOW);

    /\* 按键0引脚为输入模式 \*/
    rt\_pin\_mode(KEY0_PIN_NUM, PIN_MODE_INPUT_PULLUP);
    /\* 绑定中断,下降沿模式,回调函数名为beep\_on \*/
    rt\_pin\_attach\_irq(KEY0_PIN_NUM, PIN_IRQ_MODE_FALLING, beep_on, RT_NULL);
    /\* 使能中断 \*/
    rt\_pin\_irq\_enable(KEY0_PIN_NUM, PIN_IRQ_ENABLE);

    /\* 按键1引脚为输入模式 \*/
    rt\_pin\_mode(KEY1_PIN_NUM, PIN_MODE_INPUT_PULLUP);
    /\* 绑定中断,下降沿模式,回调函数名为beep\_off \*/
    rt\_pin\_attach\_irq(KEY1_PIN_NUM, PIN_IRQ_MODE_FALLING, beep_off, RT_NULL);
    /\* 使能中断 \*/
    rt\_pin\_irq\_enable(KEY1_PIN_NUM, PIN_IRQ_ENABLE);
}
/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(pin_beep_sample, pin beep sample);

我有疑问: RT-Thread 官方论坛


ADC 设备

已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/adc/adc

ADC 简介

ADC(Analog-to-Digital Converter) 指模数转换器。是指将连续变化的模拟信号转换为离散的数字信号的器件。真实世界的模拟信号,例如温度、压力、声音或者图像等,需要转换成更容易储存、处理和发射的数字形式。模数转换器可以实现这个功能,在各种不同的产品中都可以找到它的身影。与之相对应的 DAC(Digital-to-Analog Converter),它是 ADC 模数转换的逆向过程。ADC 最早用于对无线信号向数字信号转换。如电视信号,长短播电台发射接收等。

转换过程

如下图所示模数转换一般要经过采样、保持和量化、编码这几个步骤。在实际电路中,有些过程是合并进行的,如采样和保持,量化和编码在转换过程中是同时实现的。

ADC 转换过程

采样是将时间上连续变化的模拟信号转换为时间上离散的模拟信号。采样取得的模拟信号转换为数字信号都需要一定时间,为了给后续的量化编码过程提供一个稳定的值,在采样电路后要求将所采样的模拟信号保持一段时间。

将数值连续的模拟量转换为数字量的过程称为量化。数字信号在数值上是离散的。采样保持电路的输出电压还需要按照某种近似方式归化到与之相应的离散电平上,任何数字量只能是某个最小数量单位的整数倍。量化后的数值最后还需要编码过程,也就是 A/D 转换器输出的数字量。

分辨率

分辨率以二进制(或十进制)数的位数来表示,一般有8位、10位、12位、16位等,它说明模数转换器对输入信号的分辨能力,位数越多,表示分辨率越高,恢复模拟信号时会更精确。

精度

精度表示 ADC 器件在所有的数值点上对应的模拟值和真实值之间的最大误差值,也就是输出数值偏离线性最大的距离。

Note

注:精度与分辨率是两个不一样的概念,请注意区分。

转换速率

转换速率是指 A/D 转换器完成一次从模拟到数字的 AD 转换所需时间的倒数。例如,某 A/D 转换器的转换速率为 1MHz,则表示完成一次 AD 转换时间为 1 微秒。

访问 ADC 设备

应用程序通过 RT-Thread 提供的 ADC 设备管理接口来访问 ADC 硬件,相关接口如下所示:

函数****描述

rt_device_find()根据 ADC 设备名称查找设备获取设备句柄
rt_adc_enable()使能 ADC 设备
rt_adc_read()读取 ADC 设备数据
rt_adc_disable()关闭 ADC 设备
查找 ADC 设备

应用程序根据 ADC 设备名称获取设备句柄,进而可以操作 ADC 设备,查找设备函数如下所示:

rt_device_t rt_device_find(const char* name);

参数****描述

nameADC 设备名称
返回——
设备句柄查找到对应设备将返回相应的设备句柄
RT_NULL没有找到设备

一般情况下,注册到系统的 ADC 设备名称为 adc0,adc1等,使用示例如下所示:

#define ADC_DEV_NAME        "adc1"  /* ADC 设备名称 */
rt_adc_device_t adc_dev;            /* ADC 设备句柄 */
/* 查找设备 */
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);

使能 ADC 通道

在读取 ADC 设备数据前需要先使能设备,通过如下函数使能设备:

rt_err_t rt_adc_enable(rt_adc_device_t dev, rt_uint32_t channel);

参数****描述

devADC 设备句柄
channelADC 通道
返回——
RT_EOK成功
-RT_ENOSYS失败,设备操作方法为空
其他错误码失败

使用示例如下所示:

#define ADC_DEV_NAME        "adc1"  /* ADC 设备名称 */
#define ADC_DEV_CHANNEL     5       /* ADC 通道 */
rt_adc_device_t adc_dev;            /* ADC 设备句柄 */
/* 查找设备 */
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);
/* 使能设备 */
rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);

读取 ADC 通道采样值

读取 ADC 通道采样值可通过如下函数完成:

rt_uint32_t rt_adc_read(rt_adc_device_t dev, rt_uint32_t channel);

参数****描述

devADC 设备句柄
channelADC 通道
返回——
读取的数值

使用 ADC 采样电压值的使用示例如下所示:

#define ADC\_DEV\_NAME "adc1" /\* ADC 设备名称 \*/
#define ADC\_DEV\_CHANNEL 5 /\* ADC 通道 \*/
#define REFER\_VOLTAGE 330 /\* 参考电压 3.3V,数据精度乘以100保留2位小数\*/
#define CONVERT\_BITS (1 << 12) /\* 转换位数为12位 \*/

rt\_adc\_device\_t adc_dev;            /\* ADC 设备句柄 \*/
rt\_uint32\_t value;
/\* 查找设备 \*/
adc_dev = (rt\_adc\_device\_t)rt\_device\_find(ADC_DEV_NAME);
/\* 使能设备 \*/
rt\_adc\_enable(adc_dev, ADC_DEV_CHANNEL);
/\* 读取采样值 \*/
value = rt\_adc\_read(adc_dev, ADC_DEV_CHANNEL);
/\* 转换为对应电压值 \*/
vol = value \* REFER_VOLTAGE / CONVERT_BITS;
rt\_kprintf("the voltage is :%d.%02d \n", vol / 100, vol % 100);

实际电压值的计算公式为:采样值 * 参考电压 / (1 << 分辨率位数),上面示例代码乘以 100 将数据放大,最后通过 vol / 100 获得电压的整数位值,通过 vol % 100 获得电压的小数位值。

关闭 ADC 通道

关闭 ADC 通道可通过如下函数完成:

rt_err_t rt_adc_disable(rt_adc_device_t dev, rt_uint32_t channel);

参数****描述

devADC 设备句柄
channelADC 通道
返回——
RT_EOK成功
-RT_ENOSYS失败,设备操作方法为空
其他错误码失败

使用示例如下所示:

#define ADC\_DEV\_NAME "adc1" /\* ADC 设备名称 \*/
#define ADC\_DEV\_CHANNEL 5 /\* ADC 通道 \*/
rt\_adc\_device\_t adc_dev;            /\* ADC 设备句柄 \*/
rt\_uint32\_t value;
/\* 查找设备 \*/
adc_dev = (rt\_adc\_device\_t)rt\_device\_find(ADC_DEV_NAME);
/\* 使能设备 \*/
rt\_adc\_enable(adc_dev, ADC_DEV_CHANNEL);
/\* 读取采样值 \*/
value = rt\_adc\_read(adc_dev, ADC_DEV_CHANNEL);
/\* 转换为对应电压值 \*/
vol = value \* REFER_VOLTAGE / CONVERT_BITS;
rt\_kprintf("the voltage is :%d.%02d \n", vol / 100, vol % 100);
/\* 关闭通道 \*/
rt\_adc\_disable(adc_dev, ADC_DEV_CHANNEL);

FinSH 命令

在使用设备前,需要先查找设备是否存在,可以使用命令 adc probe 后面跟注册的 ADC 设备的名称。如下所示:

msh >adc probe adc1
probe adc1 success

使能设备的某个通道可以使用命令 adc enable 后面跟通道号。



![img](https://img-blog.csdnimg.cn/img_convert/77ff5d1b61f64f26f201ef63c05cfc03.png)
![img](https://img-blog.csdnimg.cn/img_convert/418149ca11f35b982dc869346e0f3b20.png)
![img](https://img-blog.csdnimg.cn/img_convert/3a2b2eb4f227f2d9a3ba4cedf581360e.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

 | --- |
| dev | ADC 设备句柄 |
| channel | ADC 通道 |
| **返回** | —— |
| RT\_EOK | 成功 |
| -RT\_ENOSYS | 失败,设备操作方法为空 |
| 其他错误码 | 失败 |


使用示例如下所示:



#define ADC_DEV_NAME “adc1” /* ADC 设备名称 /
#define ADC_DEV_CHANNEL 5 /
ADC 通道 /
rt_adc_device_t adc_dev; /
ADC 设备句柄 /
/
查找设备 /
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);
/
使能设备 */
rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);


#### [读取 ADC 通道采样值]( )


读取 ADC 通道采样值可通过如下函数完成:



rt_uint32_t rt_adc_read(rt_adc_device_t dev, rt_uint32_t channel);


**参数\*\*\*\*描述**




|  |  |
| --- | --- |
| dev | ADC 设备句柄 |
| channel | ADC 通道 |
| **返回** | —— |
| 读取的数值 |  |


使用 ADC 采样电压值的使用示例如下所示:



#define ADC_DEV_NAME “adc1” /* ADC 设备名称 */
#define ADC_DEV_CHANNEL 5 /* ADC 通道 */
#define REFER_VOLTAGE 330 /* 参考电压 3.3V,数据精度乘以100保留2位小数*/
#define CONVERT_BITS (1 << 12) /* 转换位数为12位 */

rt_adc_device_t adc_dev; /* ADC 设备句柄 */
rt_uint32_t value;
/* 查找设备 */
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);
/* 使能设备 */
rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);
/* 读取采样值 */
value = rt_adc_read(adc_dev, ADC_DEV_CHANNEL);
/* 转换为对应电压值 */
vol = value * REFER_VOLTAGE / CONVERT_BITS;
rt_kprintf(“the voltage is :%d.%02d \n”, vol / 100, vol % 100);


实际电压值的计算公式为:采样值 \* 参考电压 / (1 << 分辨率位数),上面示例代码乘以 100 将数据放大,最后通过 vol / 100 获得电压的整数位值,通过 vol % 100 获得电压的小数位值。


#### [关闭 ADC 通道]( )


关闭 ADC 通道可通过如下函数完成:



rt_err_t rt_adc_disable(rt_adc_device_t dev, rt_uint32_t channel);


**参数\*\*\*\*描述**




|  |  |
| --- | --- |
| dev | ADC 设备句柄 |
| channel | ADC 通道 |
| **返回** | —— |
| RT\_EOK | 成功 |
| -RT\_ENOSYS | 失败,设备操作方法为空 |
| 其他错误码 | 失败 |


使用示例如下所示:



#define ADC_DEV_NAME “adc1” /* ADC 设备名称 */
#define ADC_DEV_CHANNEL 5 /* ADC 通道 */
rt_adc_device_t adc_dev; /* ADC 设备句柄 */
rt_uint32_t value;
/* 查找设备 */
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);
/* 使能设备 */
rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);
/* 读取采样值 */
value = rt_adc_read(adc_dev, ADC_DEV_CHANNEL);
/* 转换为对应电压值 */
vol = value * REFER_VOLTAGE / CONVERT_BITS;
rt_kprintf(“the voltage is :%d.%02d \n”, vol / 100, vol % 100);
/* 关闭通道 */
rt_adc_disable(adc_dev, ADC_DEV_CHANNEL);


#### [FinSH 命令]( )


在使用设备前,需要先查找设备是否存在,可以使用命令 `adc probe` 后面跟注册的 ADC 设备的名称。如下所示:



msh >adc probe adc1
probe adc1 success


使能设备的某个通道可以使用命令 `adc enable` 后面跟通道号。



[外链图片转存中…(img-RjCjODVM-1715573730514)]
[外链图片转存中…(img-1ORAXa8S-1715573730514)]
[外链图片转存中…(img-t9z7rW3b-1715573730514)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 20
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值