RT thread 设备驱动之串口设备

本文以stm32f4xx平台介绍串口驱动,主要目的是:

1、RTT中如何编写中断处理程序

2、如何编写RTT设备驱动接口代码

3、了解串行设备的常见处理机制

所涉及的主要源码文件有:usart.c,usart.h,serial.c,serial.h

一、RTT的设备驱动程序概述

编写uart的驱动程序,首先需要了解RTT的设备框架,这里以usart的驱动来具体分析RTT的IO设备管理。注:参考《RTT实时操作系统编程指南》 I/O设备管理一章。

我们可以将USART的硬件驱动分成两个部分,如下图所示

+----------------------+
  | rtt下的usart设备驱动     |
  |---------------------- |
  | usart硬件初始化代码      |
  |---------------------- |
  | usart 硬件                  |
  +----------------------+

实际上,在缺乏操作系统的平台,即裸机平台上,我们通常只需要编写USART硬件初始化代码即可。而引入了RTOS,如RTT后,RTT中自带IO设备管理层,它是为了将各种各样的硬件设备封装成具有统一的接口的逻辑设备,以方便管理及使用。

让我们从下向上看,先来看看USART硬件初始化程序,这部分代码位于usart.c和usart.h中。

二、USART硬件初始化

假如在接触RTT之前,你已经对stm32很熟悉了,那么此文件中定义的函数名一定让你倍感亲切。这里实现的函数有:

  • static void RCC_Configuration(void);
  • static void GPIO_Configuration(void);
  • static void NVIC_Configuration(void);
  • static void DMA_Configuration(void);
  • void stm32_hw_usart_init();

前四个函数,是跟ST官方固件库提供的示例代码的名字保持一致。这些函数内部也是直接调用官方库代码实现的。具体不再赘述。对STM32裸机开发不太熟悉的朋友,建议先去官方网站下载官方固件源码包,以及应用手册和示例程序,ST提供了大量的文档和示例代码,利用好这些资源可以极大地加快开发。

下面重点看一下 usart.c中stm32_hw_usart_init():

int stm32_hw_usart_init(void)
{
    struct stm32_uart *uart;
    struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;

    RCC_Configuration();
    GPIO_Configuration();

#ifdef RT_USING_UART2
    uart = &uart2;

    serial2.ops    = &stm32_uart_ops;
    serial2.config = config;

    NVIC_Configuration(&uart2);

    /* register UART2 device */
    rt_hw_serial_register(&serial2,
                          "uart2",
                          RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX, //dev->flag (not dev->open_flag)
                          uart);
#endif /* RT_USING_UART2 */

#ifdef RT_USING_UART3
...
#endif /* RT_USING_UART3 */

    return 0;
}
//INIT_BOARD_EXPORT(stm32_hw_usart_init); //it must be invoked in board.c(rt_hw_board_init for setting CONSOLE_DEVICE)

该函数调用RCC_Configuration()和GPIO_Configuration()打开串口外设时钟和IO口配置;调用NVIC_Configuration(&uart2)设置串口中断,其中参数&uart2为一个自定义结构体类型:

/* STM32 uart driver */
struct stm32_uart
{
    USART_TypeDef *uart_device;
    IRQn_Type irq;
};

接着初始化设备类对象serial2中的ops和config两个参数,并在usart.c中实现了stm32_uart_ops中的四个函数。

static const struct rt_uart_ops stm32_uart_ops =
{
    stm32_configure,
    stm32_control,
    stm32_putc,
    stm32_getc,
};
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;

最后注册串口设备serial2,注册函数位于serial.c中:

/* register UART2 device */
rt_hw_serial_register(&serial2,
                       "uart2",
                       RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX, //dev->flag (not dev->open_flag)
                       uart);

显然,函数 stm32_hw_usart_init(),顾名思义,是用于初始化USART硬件的函数,因此这个函数一定会在USART使用之前被调用。搜索工程发现,这个函数是在board.c中rt_hw_board_init函数中被调用,而rt_hw_board_init函数又是在startup.c里的 rtthread_startup函数中调用的。进一步在startup.c的main函数中调用的,我们将实际的路径调用过程绘制如下。

  startup.c main()
  ---> startup.c rtthread_startup()
  ---> board.c   rt_hw_board_init()
  ---> usart.c   rt_hw_usart_init()

到这里,USART硬件的初始化工作已经完成完成了99%,下一步,我们需要为USART编写代码,将其纳入到RTT的设备管理层之中,正如前面所说,这部分代码在serial.c中实现。我们来重点分析这一文件。

三、在RTT下使用USART,将USART纳入RTT的IO设备层中

相对于stm32的内核来说,USART是一种低速的串行设备,并且为了最大的发挥的MCU的性能,因此使用查询方式发送、中断方式接收(发送也可以使用DMA方式)。这些已经在usart.c中使能了。首先看一些serial.h中的重要数据结构:

/*
 * Serial FIFO mode
 */
struct rt_serial_rx_fifo
{
    /* software fifo */
    rt_uint8_t *buffer;

    rt_uint16_t put_index, get_index;
};

struct rt_serial_tx_fifo
{
    struct rt_completion completion;
};

/*
 * Serial DMA mode
 */
struct rt_serial_rx_dma
{
    rt_bool_t activated;
};

struct rt_serial_tx_dma
{
    rt_bool_t activated;
    struct rt_data_queue data_queue;
};

struct rt_serial_device
{
    struct rt_device          parent;

    const struct rt_uart_ops *ops;
    struct serial_configure   config;

    void *serial_rx;
    void *serial_tx;
};
typedef struct rt_serial_device rt_serial_t;
/**
 * uart operators
 */
struct rt_uart_ops
{
    rt_err_t (*configure)(struct rt_serial_device *serial, struct serial_configure *cfg);
    rt_err_t (*control)(struct rt_serial_device *serial, int cmd, void *arg);

    int (*putc)(struct rt_serial_device *serial, char c);
    int (*getc)(struct rt_serial_device *serial);

    rt_size_t (*dma_transmit)(struct rt_serial_device *serial, const rt_uint8_t *buf, rt_size_t size, int direction);
};

在serial.c中主要实现rtthread系统的IO设备统一接口函数,并注册串口设备:

/*
 * serial register
 */
rt_err_t rt_hw_serial_register(struct rt_serial_device *serial,
                               const char              *name,
                               rt_uint32_t              flag,
                               void                    *data)
{
    struct rt_device *device;
    RT_ASSERT(serial != RT_NULL);

    device = &(serial->parent);

    device->type        = RT_Device_Class_Char;
    device->rx_indicate = RT_NULL;
    device->tx_complete = RT_NULL;

    device->init        = rt_serial_init;
    device->open        = rt_serial_open;
    device->close       = rt_serial_close;
    device->read        = rt_serial_read;
    device->write       = rt_serial_write;
    device->control     = rt_serial_control;
    device->user_data   = data;

    /* register a character device */
    return rt_device_register(device, name, flag);
}

对于串口发送数据,默认采用查询方式。因为串口设备注册的时候,其设备标志为RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,没有RT_DEVICE_FLAG_DMA_TX或RT_DEVICE_FLAG_INT_TX标志。 数据发送流程为:rt_device_write()-->rt_serial_write()-->_serial_poll_tx()-->stm32_putc()。

考虑串口接受数据的情况,串口收到一个字节的数据,就会触发串口中断USARTx_IRQHandler,数据字节会存放于串口的硬件寄存器中。但是在RTOS中,通常存在多个线程,如果某个处理串口数据的线程在没有串口数据时阻塞,当下一串口数据到来时,如果该数据线程依然没有唤醒并启动,并读取串口字节,则上一个串口字节丢失了,因此这不是一个优良的设计,我们需要设计一种机制来解决这种潜在的问题。实际上,缓冲机制可以大大缓解这个问题。数据读取流程为:rt_device_read()-->rt_serial_read()-->rt_hw_serial_isr()-->stm32_getc()。

所谓缓冲机制,简略的来说,即开辟一个缓冲区,可以是静态数组,也可以是malloc(或mempool)申请的动态缓冲区。在串口中断中,先从串口的硬件寄存器中读取数据,并保存到缓冲区中。这种情况下,我们需要两个变量,一个用于标记当前写入的位置,另外一个用来表示已经被处理的数据的位置。这样当数据处理线程阻塞时,连续收到的数据会保存到缓冲区中而避免了丢失。当中断中已经接收到了一些串口数据后,数据处理线程终于就绪,并开始处理数据,通常来说处理数据的速度必然比接受数据要快,因此这样就能解决前面所说的问题。聪明的读者发现了,还有一个小问题,缓冲区的长度必然是有限的,终归会有用到头的时候,那该怎么办呢?别担心,缓冲区前面已经被处理过的数据所占用的空间按自然可以重复使用,即,当接收指针指向了缓冲区末尾时,只要缓冲区头的数据已经被处理过了,自然可以直接将缓冲区指针从新设置为头,对于表示已处理的指针变量同理。这样这个缓冲区也就成为了一个环形缓冲区。

下面重点看一下中断函数:

#if defined(RT_USING_UART2)
/* UART2 device driver structure */
struct stm32_uart uart2 =
{
    USART2,
    USART2_IRQn,
};
struct rt_serial_device serial2;

void USART2_IRQHandler(void)
{
    struct stm32_uart *uart;

    uart = &uart2;

    /* enter interrupt */
    rt_interrupt_enter();
    if (USART_GetITStatus(uart->uart_device, USART_IT_RXNE) != RESET)
    {
        rt_hw_serial_isr(&serial2, RT_SERIAL_EVENT_RX_IND);//USART_IT_RXNE is cleared automatically by reading USART_DR in stm32_getc()
    }
    if (USART_GetITStatus(uart->uart_device, USART_IT_TC) != RESET)
    {
        /* clear interrupt */
        USART_ClearITPendingBit(uart->uart_device, USART_IT_TC);
    }

    /* leave interrupt */
    rt_interrupt_leave();
}
#endif /* RT_USING_UART2 */

该中断函数在usart.c中,在RTT下的每一个中断服务子程序的入口都调用了rt_interrupt_enter(),在中断函数的子程序的出口则调用了rt_interrupt_leave()。
/* ISR for serial interrupt */
void rt_hw_serial_isr(struct rt_serial_device *serial, int event)
{
    switch (event & 0xff)
    {
        case RT_SERIAL_EVENT_RX_IND:
        {
            int ch = -1;
            rt_base_t level;
            struct rt_serial_rx_fifo* rx_fifo;

            rx_fifo = (struct rt_serial_rx_fifo*)serial->serial_rx;
            RT_ASSERT(rx_fifo != RT_NULL);

            /* interrupt mode receive */
            RT_ASSERT(serial->parent.open_flag & RT_DEVICE_FLAG_INT_RX);

            while (1)
            {
                ch = serial->ops->getc(serial);
                if (ch == -1) break;

                /* disable interrupt */
                level = rt_hw_interrupt_disable();

                rx_fifo->buffer[rx_fifo->put_index] = ch;
                rx_fifo->put_index += 1;
                if (rx_fifo->put_index >= serial->config.bufsz) rx_fifo->put_index = 0;

                /* if the next position is read index, discard this ‘read char‘ */
                if (rx_fifo->put_index == rx_fifo->get_index)
                {
                    rx_fifo->get_index += 1;
                    if (rx_fifo->get_index >= serial->config.bufsz) rx_fifo->get_index = 0;
                }

                /* enable interrupt */
                rt_hw_interrupt_enable(level);
            }

            /* invoke callback */
            if (serial->parent.rx_indicate != RT_NULL)
            {
                rt_size_t rx_length;

                /* get rx length */
                level = rt_hw_interrupt_disable();
                rx_length = (rx_fifo->put_index >= rx_fifo->get_index)? (rx_fifo->put_index - rx_fifo->get_index):
                    (serial->config.bufsz - (rx_fifo->get_index - rx_fifo->put_index));
                rt_hw_interrupt_enable(level);

                serial->parent.rx_indicate(&serial->parent, rx_length);
            }
            break;
        }
        case RT_SERIAL_EVENT_TX_DONE:
        {
            struct rt_serial_tx_fifo* tx_fifo;

            tx_fifo = (struct rt_serial_tx_fifo*)serial->serial_tx;
            rt_completion_done(&(tx_fifo->completion));
            break;
        }
        case RT_SERIAL_EVENT_TX_DMADONE:
        {
            const void *data_ptr;
            rt_size_t data_size;
            const void *last_data_ptr;
            struct rt_serial_tx_dma* tx_dma;

            tx_dma = (struct rt_serial_tx_dma*) serial->serial_tx;

            rt_data_queue_pop(&(tx_dma->data_queue), &last_data_ptr, &data_size, 0);
            if (rt_data_queue_peak(&(tx_dma->data_queue), &data_ptr, &data_size) == RT_EOK)
            {
                /* transmit next data node */
                tx_dma->activated = RT_TRUE;
                serial->ops->dma_transmit(serial, data_ptr, data_size, RT_SERIAL_DMA_TX);
            }
            else
            {
                tx_dma->activated = RT_FALSE;
            }

            /* invoke callback */
            if (serial->parent.tx_complete != RT_NULL)
            {
                serial->parent.tx_complete(&serial->parent, (void*)last_data_ptr);
            }
            break;
        }
        case RT_SERIAL_EVENT_RX_DMADONE:
        {
            int length;
            struct rt_serial_rx_dma* rx_dma;

            rx_dma = (struct rt_serial_rx_dma*)serial->serial_rx;
            /* get DMA rx length */
            length = (event & (~0xff)) >> 8;
            serial->parent.rx_indicate(&(serial->parent), length);
            rx_dma->activated = RT_FALSE;
            break;
        }
    }
}

该函数位于serial.c中,默认情况下usart的rt_device结构体中rx_indicate域被置空,因此不会运行这一段代码。如果使用rt_device_set_rx_indicate(rt_device_t dev, rt_err_t(* rx_ind)(rt_device_t dev, rt_size_t size))函数为一个串口设备注册了接收事件回调函数,在该串口接收到数据后,就会调用之前注册的rx_ind函数,将当前设备指针以及待读取的数据长度作为调用参数传递给用户。
时间: 2024-10-11 12:52:20

RT thread 设备驱动之串口设备的相关文章

Linux设备驱动框架设计

引子 Linux操作系统的一大优势就是支持数以万计的芯片设备,大大小小的芯片厂商工程师都在积极地向Linux kernel提交设备驱动代码.能让这个目标得以实现,这背后隐藏着一个看不见的技术优势:Linux内核提供了一套易于扩展和维护的设备驱动框架.Linux内核本身提供一套设备驱动模型,此模型提供了Linux内核对设备的一般性抽象描述,包括设备的电源管理.对象生命周期管理.用户空间呈现等等.在设备模型的帮助下,设备驱动开发工程师从设备的一般性抽象中解脱出来.但是每个设备的具体功能实现还需要大量

【转】linux设备驱动之MMC SD卡——核心层简单分析

原文网址:http://blog.chinaunix.net/uid-28685940-id-3889878.html /*************************************************************************************************************************************//* bus.c */ /* *  linux/drivers/mmc/core/bus.c * *  Cop

Linux设备驱动开发基础

1.驱动概述和开发环境搭建 1.1驱动设备的作用 对设备驱动最通俗的解释就是"驱动硬件设备行动".驱动与底层硬件直接打交道,按照硬件设备的具体工作方式,读写设备的寄存器,完成设备的轮训.中断处理.DMA通信,进行物理内存向虚拟内存的映射等,最终让通信设备能收发数据,让显示设备能显示文字和画面,让存储设备能记录文件和数据. 由此可见,设备驱动充当了硬件和应用软件之间的纽带,他使得应用软件只需要调用系统软件的应用编程接口(API)就可让硬件去完成要求的工作.在系统中没有操作系统的情况下,工

linux设备驱动第一篇:基础知识点

首先,我们知道驱动是内核的一部分,那么驱动在内核中到底扮演了什么角色呢? 设备驱动程序在内核中的角色:他们是一个个独立的"黑盒子",使某个特定的硬件响应一个定义良好的内部编程接口,这些接口完全隐藏了设备的工作细节.(说白了,驱动程序除了对外提供特定的接口外,任何实现细节对应用程序都是不可见的.)用户的操作通过一组标准化的调用执行,而这些调用独立于特定的驱动程序.驱动程序的任务是把这些标准化调用映射到实际硬件的设备特有操作上. 在编写驱动程序时,程序员应该特别注意下面这个概念:编写访问硬

编写linux设备驱动的注意事项之一

编写linux设备驱动的注意事项之一 当然写驱动,你得清楚上层调用是否导致存在函数重入.什么执行context下(1. 中断.2. 软中断.3. tasklet(软中断的一种). 4. timer(基于软中断实现).5. hrtimer (软中断或硬中断环境下执行) .6. workqueue(也是kernel thread).7. kernel thread.8. 普通thread的内核态下)会访问此函数和此资源,需要什么样的"同步机制"保护. 你利用子系统(例如input子系统)的

RT-thread 设备驱动组件之SPI设备

本文主要介绍RT-thread中的SPI设备驱动,涉及到的文件主要有:spi_dev.c,spi_core.c,spi.h,spi_hard.c,spi_hard.h. 一.SPI设备框架 先来看spi.h中的一些数据结构: ** * SPI message structure */ struct rt_spi_message { const void *send_buf; void *recv_buf; rt_size_t length; struct rt_spi_message *next

linux设备驱动第五篇:驱动中的并发与竟态

综述 在上一篇介绍了linux驱动的调试方法,这一篇介绍一下在驱动编程中会遇到的并发和竟态以及如何处理并发和竞争. 首先什么是并发与竟态呢?并发(concurrency)指的是多个执行单元同时.并行被执行.而并发的执行单元对共享资源(硬件资源和软件上的全局.静态变量)的访问则容易导致竞态(race conditions).可能导致并发和竟态的情况有: SMP(Symmetric Multi-Processing),对称多处理结构.SMP是一种紧耦合.共享存储的系统模型,它的特点是多个CPU使用共

【深入Linux块设备驱动层次之一】整体层次

最近在做文件系统和linux块设备相关的工作,需要对文件系统和底层块设备之间的关系和交互有比较深入的了解.笔者参考的书籍有敖青云所著的<存储技术原理分析-基于Linux2.6内核原代码>,同时参考的还有Jonathan Corbet.Alessandro Rubini和Greg Kroah-Hartman 一起著作的linux设备经典书籍<Linux设备驱动Edition 3 >.陈学松写的<深入Linux设备驱动内核机制>.对比了一下,还是敖青云写的很存储结合更紧密一

Linux 设备驱动IO操作

每个外设都是通过读写其寄存器来控制的.外设寄存器也称为I/O端口,通常包括:控制寄存器.状态寄存器和数据寄存器三大类. 根据CPU体系结构的不同,CPU对IO端口的编址方式有两种: (1)I/O映射方式(I/O-mapped) 典型地,如X86处理器将外设的寄存器看成一个独立的地址空间(称为"I/O地址空间"或者"I/O端口空间"),所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令. (2)内存映射方式(Memor