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函數,將當前設備指針以及待讀取的數據長度作為調用參數傳遞給用戶。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM