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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 深入理解 RT-Thread I/O 设备模型 — 分析 UART设备源码。
深入理解 RT-Thread I/O 设备模型 — 分析 UART设备源码。

前言

上文我们认识了解了 RT-Thread I/O 设备模型,本来计划是从最简单的设备 GPIO 口开始讲解 RT-Thread 的设备模型,但是实际上 PIN 设备模型有点特殊,并不是完美符合上一篇博文中 《2.3 访问 I/O 设备相关》小结介绍的函数,所以这个我们放在后面文章说明。

而 UART 设备模型的操作完美贴合上一篇博文的介绍,所以我把 UART 设备先说明了,这样更加加深一下对 RT-Thread I/O 设备模型的认识。

本文从 UART 设备驱动层 和 设备驱动框架层 分析 RT-Thread 中 UART 设备的实现。目的在于通过官方一个成熟的设备驱动的实例,让我们确实的理解体会 RT-Thread I/O 设备模型。

本 RT-Thread 专栏记录的开发环境:
RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX开发快速上手)
RT-Thread记录(二、RT-Thread内核启动流程 — 启动文件和源码分析
RT-Thread 内核篇系列博文链接:
RT-Thread记录(三、RT-Thread 线程操作函数及线程管理与FreeRTOS的比较)
RT-Thread记录(四、RT-Thread 时钟节拍和软件定时器)
RT-Thread记录(五、RT-Thread 临界区保护)
RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
RT-Thread记录(七、IPC机制之邮箱、消息队列)
RT-Thread记录(八、理解 RT-Thread 内存管理)
RT-Thread记录(九、RT-Thread 中断处理与阶段小结)
RT-Thread 设备篇系列博文链接:
RT-Thread记录(十、全面认识 RT-Thread I/O 设备模型)

一、初识 UART 操作函数(应用程序)

首先我们来看一下在 RT-Thread 中 UART 操作函数,这是模型框架中最上层的应用层所需要调用的函数,如下面的表格:

函数 描述
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() 关闭设备

可以看到,对 UART 的操作和上一篇文章 《RT-Thread记录(十、全面认识 RT-Thread I/O 设备模型)》 几乎一模一样,这也是前言中我说的为什么 UART 设备模型 是复习理解 RT-Thread I/O 设备模型的完美设备。

对于这些操作函数,是给最上层的应用程序使用的,我们要使用一个 UART 设备,应用程序最开始肯定是需要使用rt_device_find()查找设备,在上一篇文章说过,大部分常用的设备 RT-Thread 已经帮我们写好了驱动,我们直接在应用层调用操作接口即可,UART的驱动也是 RT-Thread 已经写好的。

那么我们该查找什么名字呢?RT-Thread 底层是如何实现的呢? 带着这些问题,我们从最开始来分析说明一下 RT-Thread 的 UART 设备。

<3 先列出 RT-Thread 的 UART 操作函数,让我们对 UART 应用层的函数有个了解,然后带着一些好奇让我们从底层源码来分析一下 RT-Thread 的 UART 设备。

二、UART 的初始化

首先,UART 设备作为一个外设,肯定需要初始化,我们在系列博文第二篇《RT-Thread记录(二、RT-Thread内核启动流程 — 启动文件和源码分析)》分析过 RT-Thread 初始化。

2.1 UART 设备初始化位置

在文中章节 “2.2.1 板级硬件初始化 — rt_hw_board_init” 讲到了硬件初始化相关,如下图:

在这里插入图片描述

rt_hw_board_init() 函数中有一个 hw_board_init,使用到的 UART 设备的初始化就在这个函数里面,如图:

在这里插入图片描述

说明一下,这个hw_board_init里面初始化的哪些设备是和 RT-Thread 配置一一对应的。

注意到他们都是条件编译,在 env 工具中配置了使用的外设之后,都会在这里进行初始化,对于我们使用 RT-Thread Studio 来说,就是如下图所示:

在这里插入图片描述

2.2 UART 设备初始化函数分析

通过上文介绍,我们找到了 UART 设备的初始化函数 rt_hw_usart_init

int rt_hw_usart_init(void)
{
    rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);
    struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
    rt_err_t result = 0;

    stm32_uart_get_dma_config();

    for (int i = 0; i < obj_num; i++)
    {
        uart_obj[i].config = &uart_config[i];
        uart_obj[i].serial.ops    = &stm32_uart_ops;
        uart_obj[i].serial.config = config;
        /* register UART device */
        result = rt_hw_serial_register(&uart_obj[i].serial, uart_obj[i].config->name,
                                       RT_DEVICE_FLAG_RDWR
                                       | RT_DEVICE_FLAG_INT_RX
                                       | RT_DEVICE_FLAG_INT_TX
                                       | uart_obj[i].uart_dma_flag
                                       , NULL);
        RT_ASSERT(result == RT_EOK);
    }

    return result;
}

这个初始化函数直接看上去,只有一个函数我们比较熟悉rt_hw_serial_register,顾名思义,串口设备注册函数,不同于简单的 I/O 设备注册函数 rt_device_register,说明它 UART 设备还有设备驱动框架层,这个rt_hw_serial_register就是 UART 设备驱动框架层定义的函数。

这个设备驱动层 和 设备驱动框架层我们待会再来说明,我们先从头简单分析一下这个 UART 设备驱动程序。

第一句,这个语句是为了确认一下有几个串口设备需要进行初始化:

rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);

其中 uart_obj 有如下定义:

static struct stm32_uart uart_obj[sizeof(uart_config) / sizeof(uart_config[0])] = {0};

uart_objstm32_uart 类型的结构体数组,其数组长度为sizeof(uart_config)/sizeof(uart_config[0])

stm32_uart 结构体

在 RT-Thread 操作系统中,对 UART设备的初始化,可以理解为就是对 stm32_uart 结构体对象 的初始化 。

我画了一张结构图如下:
在这里插入图片描述

stm32_uart 结构体这里我们先不分析里面具体的含义,在后文对应的地方都会有响应的说明,我们先回到初始化的问题上来。

我们接着上面分析,数组变量 uart_obj 的长度是多少呢?看一下 uart_config 是什么,如下图:
在这里插入图片描述

uart_configstm32_uart_config 类型的结构体数组,其数组长度是根据 RT-Thread 配置使用哪些串口决定的。

比如我们使用了 串口1 和 串口3,那么uart_config 就等于:

static struct stm32_uart_config uart_config[2] =
{
    UART1_CONFIG,
    UART3_CONFIG,
};

UARTX_CONFIG

这里讲到 UART1_CONFIG 就顺带提一下,UART1_CONFIGstm32_uart_config 类型的结构体,在RT-Thread 中是通过 宏定义来定义的:
在这里插入图片描述

引出这么多,我们回到最初的rt_hw_usart_init函数第一句的代码:

rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);

以上面为例,只使用了 UART1 和 UART3 ,uart_obj数组长度为2,也就表明有2个stm32_uart 结构体的成员需要进行初始化,也就是需要初始化 2个 UART 设备。 上面句子中 obj_num = 2;

接下来的语句:

struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;

串口配置结构体,初始化等于默认配置,这里具体也好理解,看下图便知:
在这里插入图片描述

再往下看,获取串口 DMA 配置:

stm32_uart_get_dma_config();

函数如下,如果没有使用DMA ,那么只会有一条语句,就是 uart_dma_flag = 0; 表示没有使用DMA。

在上面我们介绍stm32_uart 结构体的时候,uart_dma_flag 就是这个结构体的一个成员变量。
在这里插入图片描述

stm32_uart 结构体初始化

再接下来就是uart_obj[i]的初始化了,有几个串口就初始化几遍:

 for (int i = 0; i < obj_num; i++)
    {
        uart_obj[i].config = &uart_config[i];
        uart_obj[i].serial.ops    = &stm32_uart_ops;
        uart_obj[i].serial.config = config;
        /* register UART device */
        result = rt_hw_serial_register(&uart_obj[i].serial, uart_obj[i].config->name,
                                       RT_DEVICE_FLAG_RDWR
                                       | RT_DEVICE_FLAG_INT_RX
                                       | RT_DEVICE_FLAG_INT_TX
                                       | uart_obj[i].uart_dma_flag
                                       , NULL);
        RT_ASSERT(result == RT_EOK);
    }

首先里面第一句:

uart_obj[i].config = &uart_config[i];

其中 uart_config[i] 就是我们上文说的 UARTX_CONFIG,通过宏定义定义的 stm32_uart_config 类型的结构体。

第二句:

 uart_obj[i].serial.ops    = &stm32_uart_ops;

上文分析过stm32_uart 结构体,但是并没有深入分析其中的成员serial,它是 RT-Thread 的 UART 设备对象控制块,其中ops为结构体类型的指针:
在这里插入图片描述
stm32_uart_ops为 RT-Thread 设备驱动层定义好的,其作用是指定 UART 设备的操作函数:
在这里插入图片描述

第三句:

uart_obj[i].serial.config = config;

上文讲过的,默认都是RT_SERIAL_CONFIG_DEFAULT,如果我们需要修改,可以通过rt_device_control修改。

第四句:

result = rt_hw_serial_register(&uart_obj[i].serial, uart_obj[i].config->name,
                                       RT_DEVICE_FLAG_RDWR
                                       | RT_DEVICE_FLAG_INT_RX
                                       | RT_DEVICE_FLAG_INT_TX
                                       | uart_obj[i].uart_dma_flag
                                       , NULL);

这个函数就是我们讲过的 I/O 设备模型中的设备注册函数,如图:
在这里插入图片描述

在上面初始化中:
uart_obj[i].serial 为 rt_serial_device 类型,就是 UART 设备的控制块,它付给注册函数第一个参数;
uart_obj[i].config->name 中的name名字,就是设备注册后 使用rt_device_find() 寻找的名字。

其中rt_hw_serial_register函数属于(设备驱动框架层的函数),他会调用通用的 rt_device_register(I/O设备管理层的函数)对 UART 设备进行注册。

2.3 UART 设备初始化结果图

经过上面的一系列分析,最终一个 UART设备初始化以后的结果如下图所示:

在这里插入图片描述

<3 UART 的初始化,最主要的是要了解 stm32_uart 结构体(以STM32驱动为例),通过对结构体的认识,初始化步骤的分析,让我们认识到了RT-Thread 对于 UART 设备驱动层的设计,也让我们接下来对认识 不同层之间如何联系打下了一定的基础。

三、UART 设备驱动框架层

我们回头来看本文开头说的 UART 那些操作函数,再结合上文所提到的初始,再结合上一篇文章《RT-Thread记录(十、全面认识 RT-Thread I/O 设备模型)》的基础,我们可以确定,上层应用所用到的UART 操作函数就是在使用rt_hw_serial_register 时候关联到驱动框架层的:
在这里插入图片描述

而且再复习一下, 设备驱动框架层是 RT-Thread 系统的东西,官方已经写好的,UART 设备驱动框架层的代码为 serial.c,其位置如下图:

在这里插入图片描述

在其对应的 serial.h 头文件中包含了许多 UART 设备通用的宏定义,大家可以自行查看。

设备驱动框架层如何与设备驱动层关联

☆在这里我们主要需要关注的就是,设备驱动框架层是如何 和 设备驱动层关联起来的。☆

首先我们先看一下其中的几个串口操作函数:

设备驱动框架层操作函数 对应
rt_serial_init() device->init = rt_serial_init
rt_serial_open() device->open = rt_serial_open
rt_serial_close() device->close = rt_serial_close
rt_serial_read() device->read = rt_serial_read
rt_serial_write() device->write = rt_serial_write
rt_serial_control() device->control = rt_serial_control

我们随意查看其中一个函数查看,如下图:
在这里插入图片描述

可以看到上图有一句关键的代码:

if (serial->ops->configure)
        result = serial->ops->configure(serial, &serial->config);

上面我们在将初始化的时候有过代码:

/*
static const struct rt_uart_ops stm32_uart_ops =
{
    .configure = stm32_configure,
    .control = stm32_control,
    .putc = stm32_putc,
    .getc = stm32_getc,
    .dma_transmit = stm32_dma_transmit
};
*/
uart_obj[i].serial.ops    = &stm32_uart_ops;

所以上面的表格可进一步的改为如下对应表格:

设备驱动层 设备驱动框架层操作函数 对应
stm32_configure() rt_serial_init() device->init = rt_serial_init
stm32_control() rt_serial_open() device->open = rt_serial_open
stm32_control() srt_serial_close() device->close = rt_serial_close
stm32_getc()轮询接收,实际一般用中断 rt_serial_read() device->read = rt_serial_read
stm32_putc() srt_serial_write() device->write = rt_serial_write
stm32_configure() rt_serial_control() device->control = rt_serial_control

通过上面的分析,基本上有点拨云见日的感觉!

<3 UART 设备驱动框架层是 RT-Thread 系统通用的,他上连接 I/O 设备管理层,下连接 设备驱动层。 通过分析,我们已经知道他们之间如何关联。

四、UART 设备驱动层

其实在上面的文章分析的时候已经说清楚了 UART 设备驱动是如何与 设备驱动层关联起来的。

在 RT-Thread 中,我们的 UART 设备驱动文件为:drv_usart.c ,其位置位于 drivers 文件夹下面:
在这里插入图片描述

这一层就是与我们使用的硬件设备直接关联的一层,我们在上面介绍的 UART 设备初始化函数也在这个驱动文件中。

再次复习一下,设备驱动层是与使用的硬件直接关联的,因为使用的是STM32 ,其很多地方都调用了 ST官方 HAL 库的定义,是在 HAL 库的基础之上实现的驱动代码。

我们只选几个部分做示例说明,在驱动中下面几个函数肯定是有的:
在这里插入图片描述

配置函数

我们看一下驱动层的配置函数stm32_configure,不难发现这个函数其实和裸机中的差不多,其中还调用了 HAL 库中的 HAL_UART_Init函数(函数还是比较简单的,我们这里说明一下举个例子即可):

static rt_err_t stm32_configure(struct rt_serial_device *serial, struct serial_configure *cfg)
{
    struct stm32_uart *uart;
    RT_ASSERT(serial != RT_NULL);
    RT_ASSERT(cfg != RT_NULL);

    uart = rt_container_of(serial, struct stm32_uart, serial);

    /* uart clock enable */
    stm32_uart_clk_enable(uart->config);
    /* uart gpio clock enable and gpio pin init */
    stm32_gpio_configure(uart->config);

    uart->handle.Instance          = uart->config->Instance;
    uart->handle.Init.BaudRate     = cfg->baud_rate;
    uart->handle.Init.HwFlowCtl    = UART_HWCONTROL_NONE;
    uart->handle.Init.Mode         = UART_MODE_TX_RX;
    uart->handle.Init.OverSampling = UART_OVERSAMPLING_16;
    switch (cfg->data_bits)
    {
    case DATA_BITS_8:
        uart->handle.Init.WordLength = UART_WORDLENGTH_8B;
        break;
    case DATA_BITS_9:
        uart->handle.Init.WordLength = UART_WORDLENGTH_9B;
        break;
    default:
        uart->handle.Init.WordLength = UART_WORDLENGTH_8B;
        break;
    }
    switch (cfg->stop_bits)
    {
    case STOP_BITS_1:
        uart->handle.Init.StopBits   = UART_STOPBITS_1;
        break;
    case STOP_BITS_2:
        uart->handle.Init.StopBits   = UART_STOPBITS_2;
        break;
    default:
        uart->handle.Init.StopBits   = UART_STOPBITS_1;
        break;
    }
    switch (cfg->parity)
    {
    case PARITY_NONE:
        uart->handle.Init.Parity     = UART_PARITY_NONE;
        break;
    case PARITY_ODD:
        uart->handle.Init.Parity     = UART_PARITY_ODD;
        break;
    case PARITY_EVEN:
        uart->handle.Init.Parity     = UART_PARITY_EVEN;
        break;
    default:
        uart->handle.Init.Parity     = UART_PARITY_NONE;
        break;
    }

    if (HAL_UART_Init(&uart->handle) != HAL_OK)
    {
        return -RT_ERROR;
    }

    return RT_EOK;

发送函数:
在这里插入图片描述

关于中断:

中断入口函数还是我们熟悉的USART1_IRQHandler,其流程如下图所示:

在这里插入图片描述

UART 设备驱动层直接与 UART 硬件相关,其中函数都可以直接对硬件进行操作,其实上层应用可以直接调用 驱动层的函数使用,很多函数的实现基于官方的HAL 库。

结语

本文通过对 UART设备初始化分析,对 UART 设备模型各层次的源码关联进行对应说明,通过现成的UART 设备模型,我们更加的理解了 RT-Thread 的I/O 设备模型,最后总结如图所示:
在这里插入图片描述
其实从应用来说,知道不知道底层的这些实现都没有太大的关系,所以即便一下子看不懂也没有关系,多看看源码,静下心来好看还是不难理解的。

<3 如果上一篇博文还没能理解 RT-Thread I/O 设备模型,那么加上这篇文章,你一定行 (* ̄︶ ̄) <3

为了加深对 RT-Thread 的I/O 设备模型的说明,本文花了不少时间,在接下来的设备使用测试中,如果不是特除情况,应该就不会再进行这样的分析了,我们就要正式进入 RT-Thread 设备的使用学习过程。

下一篇文章我们就要从 UART 设备使用开始学习 RT-Thread 设备的使用。

相关文章
|
1天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
1天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
1天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
22天前
|
机器学习/深度学习 人工智能 PyTorch
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
本文探讨了Transformer模型中变长输入序列的优化策略,旨在解决深度学习中常见的计算效率问题。文章首先介绍了批处理变长输入的技术挑战,特别是填充方法导致的资源浪费。随后,提出了多种优化技术,包括动态填充、PyTorch NestedTensors、FlashAttention2和XFormers的memory_efficient_attention。这些技术通过减少冗余计算、优化内存管理和改进计算模式,显著提升了模型的性能。实验结果显示,使用FlashAttention2和无填充策略的组合可以将步骤时间减少至323毫秒,相比未优化版本提升了约2.5倍。
42 3
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
|
2天前
|
网络协议 安全 网络安全
探索网络模型与协议:从OSI到HTTPs的原理解析
OSI七层网络模型和TCP/IP四层模型是理解和设计计算机网络的框架。OSI模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,而TCP/IP模型则简化为链路层、网络层、传输层和 HTTPS协议基于HTTP并通过TLS/SSL加密数据,确保安全传输。其连接过程涉及TCP三次握手、SSL证书验证、对称密钥交换等步骤,以保障通信的安全性和完整性。数字信封技术使用非对称加密和数字证书确保数据的机密性和身份认证。 浏览器通过Https访问网站的过程包括输入网址、DNS解析、建立TCP连接、发送HTTPS请求、接收响应、验证证书和解析网页内容等步骤,确保用户与服务器之间的安全通信。
17 1
|
25天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
52 12
|
20天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
2天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
78 0

推荐镜像

更多