• RT thread 设备驱动组件之USART设备


          本文以stm32f4xx平台介绍串口驱动,主要目的是:1、RTT中如何编写中断处理程序;2、如何编写RTT设备驱动接口代码;3、了解串行设备的常见处理机制。所涉及的主要源码文件有:驱动框架文件(usart.c,usart.h),底层硬件驱动文件(serial.c,serial.h)。应用串口设备驱动时,需要在rtconfig.h中宏定义#define RT_USING_SERIAL。

    一、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函数,将当前设备指针以及待读取的数据长度作为调用参数传递给用户。
  • 相关阅读:
    MKMapVIew学习系列2 在地图上绘制出你运行的轨迹
    WPF SDK研究 Intro(6) WordGame1
    WPF SDK研究 Intro(3) QuickStart3
    WPF SDK研究 Layout(1) Grid
    WPF SDK研究 目录 前言
    WPF SDK研究 Intro(7) WordGame2
    WPF SDK研究 Layout(2) GridComplex
    对vs2005创建的WPF模板分析
    WPF SDK研究 Intro(4) QuickStart4
    《Programming WPF》翻译 第6章 资源
  • 原文地址:https://www.cnblogs.com/King-Gentleman/p/4653011.html
Copyright © 2020-2023  润新知