RT-Thread uart串口设备驱动代码结构剖析

硬件测试平台:正点原子潘多拉STM32L4开发板
OS内核版本:4.0.0

注意:下面的示例代码是从原子提供的例程中摘录,因此可能与最新的RT-Thread源码有出入(因为RT-Thread源码在不断的开发维护中)
下面摘录的例程中,关键位置我给出了注释

下面开始正文:

RT-Thread的Finsh串口控制台有个标志性的开头打印信息如下:

 \ | /
- RT -     Thread Operating System
 / | \     4.0.0 build Dec 18 2018
 2006 - 2018 Copyright by rt-thread team

在components.c中找到rtthread_startup()函数

int rtthread_startup(void)
{
    rt_hw_interrupt_disable();

    /* board level initialization
     * NOTE: please initialize heap inside board initialization.
     */
    rt_hw_board_init();

    /* show RT-Thread version */
    rt_show_version();

rt_show_version();就是打印这段开头信息的。

/**
 * This function will show the version of rt-thread rtos
 */
void rt_show_version(void)
{
    rt_kprintf("\n \\ | /\n");
    rt_kprintf("- RT -     Thread Operating System\n");
    rt_kprintf(" / | \\     %d.%d.%d build %s\n",
               RT_VERSION, RT_SUBVERSION, RT_REVISION, __DATE__);
    rt_kprintf(" 2006 - 2018 Copyright by rt-thread team\n");
}
RTM_EXPORT(rt_show_version);

显然,既然能串口打印了,那么必然在这之前已经初始化了该串口,那么rt_show_version()之前的rt_hw_board_init()函数里一定初始化了串口。

void rt_hw_board_init()
{
//...略

    /* Second initialize the serial port, so we can use rt_kprintf right away */
#ifdef RT_USING_SERIAL
    stm32_hw_usart_init();
#endif

#ifdef RT_USING_CONSOLE
    rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
//...略
}
int stm32_hw_usart_init(void)
{
    struct stm32_uart* uarts[] =
    {
#ifdef BSP_USING_UART1
        &uart1,
#endif

#ifdef BSP_USING_UART2
        &uart2,
#endif

#ifdef BSP_USING_UART3
        &uart3,
#endif

#ifdef BSP_USING_LPUART1
        &lpuart1,
#endif
    };
    int i;

    for (i = 0; i < sizeof(uarts) / sizeof(uarts[0]); i++)
    {
        struct stm32_uart *uart = uarts[i];
        rt_err_t result;

        /* register UART device */
        result = rt_hw_serial_register(&uart->serial,
                              uart->uart_name,
#ifdef BSP_UART_USING_DMA_RX
                              RT_DEVICE_FLAG_DMA_RX |
#endif
                              RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
                              uart);
        RT_ASSERT(result == RT_EOK);
        (void)result;
    }

    return 0;
}

stm32_hw_usart_init()这个函数注册了使用到的串口设备到设备框架层,注册完就可以直接使用内核通用接口函数操作设备了,比如开启设备,读设备,写设备,通过BSP_USING_UART1这类宏控来控制将要使用哪几个串口。注意,这里只是注册,并没完成真正的底层硬件初始化。真正的完成底层硬件初始化在rt_console_set_device(RT_CONSOLE_DEVICE_NAME);这里面。即调用了打开设备才是完成了串口硬件初始化串口才能正常使用。


/* UART1 device driver structure */
static struct stm32_uart uart1 =
{
    {USART1},               // UART_HandleTypeDef UartHandle;
    USART1_IRQn,            // IRQn_Type irq;

#ifdef BSP_UART_USING_DMA_RX
    USART1_RX_DMA_IRQN,     // IRQn_Type dma_irq;
    0,                      // rt_size_t last_index;
    // DMA_HandleTypeDef hdma_rx;
    {USART1_RX_DMA_CHANNEL, {USART1_RX_DMA_REUQEST}},
#endif

    "uart1",                // char * uart_name;
    // struct rt_serial_device serial;
    {{0}, &stm32_uart_ops, RT_SERIAL_CONFIG_DEFAULT}
};

stm32_uart类型如下:

/* STM32 uart driver */
struct stm32_uart
{
    UART_HandleTypeDef UartHandle;
    IRQn_Type irq;

#ifdef BSP_UART_USING_DMA_RX
    IRQn_Type dma_irq;
    rt_size_t last_index;
    DMA_HandleTypeDef hdma_rx;
#endif

    char * uart_name;
    struct rt_serial_device serial;
};

struct rt_serial_device 类型如下:

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;

&stm32_uart_ops 对应 const struct rt_uart_ops *ops;
RT_SERIAL_CONFIG_DEFAULT 对应 struct serial_configure config;

stm32_uart_ops这个结构体实现接口层和底层的连接。
RT_SERIAL_CONFIG_DEFAULT 这个是串口默认配置。

/* 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_RB_BUFSZ, /* Buffer size */  \
    0                                      \
}
//一一对应关系如下
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;      /* 保留位 */
};

再看stm32_uart_ops

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

这里的stm32_configure函数就实现了STM32串口的底层初始化流程,包含HAL_UART_Init函数调用。
stm32_control函数是开启和清楚接收中断的,以及串口DMA配置。
限于篇幅这几个函数不放在这里了。

void USART1_IRQHandler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    uart_isr(&uart1.serial);

    /* leave interrupt */
    rt_interrupt_leave();
}

void USART1_RX_DMA_IRQHandler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    HAL_DMA_IRQHandler(uart1.UartHandle.hdmarx);

    /* leave interrupt */
    rt_interrupt_leave();
}

串口中断和串口接收DMA中断也在drv_usart.c文件里
uart_isr(&uart1.serial);主要是清除中断标志位

用过HAL库的都知道,外设的GPIO初始化,外设时钟使能,NVIC配置 都在HAL_UART_MspInit函数中完成,HAL_UART_MspInit函数也在drv_usart.c文件里

void HAL_UART_MspInit(UART_HandleTypeDef *huart)

同时将由于不同的GPIO可能导致的不同的端口配置和复用通道,DMA通道都用宏封装

#ifdef BSP_USING_UART1
    #define USART1_RX_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOA_CLK_ENABLE()
    #define USART1_TX_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOA_CLK_ENABLE()

    /* Definition for USART1 Pins */
    #define USART1_TX_PIN                    GPIO_PIN_9
    #define USART1_TX_GPIO_PORT              GPIOA
    #define USART1_TX_AF                     GPIO_AF7_USART1
    #define USART1_RX_PIN                    GPIO_PIN_10
    #define USART1_RX_GPIO_PORT              GPIOA
    #define USART1_RX_AF                     GPIO_AF7_USART1

    #define USART1_RX_DMA_CHANNEL            DMA1_Channel5
    #define USART1_RX_DMA_REUQEST            DMA_REQUEST_2
    #define USART1_RX_DMA_IRQN               DMA1_Channel5_IRQn
    #define USART1_RX_DMA_IRQHandler         DMA1_Channel5_IRQHandler
#endif

上面的串口1使用的是默认配置RT_SERIAL_CONFIG_DEFAULT,
如果要开其他串口或是要改配置比如改波特率,可以仿照RT_SERIAL_CONFIG_DEFAULT这个宏再封装一个出来改对应的项,赋值到对应串口的设备结构体中去,如static struct stm32_uart uart1

上面提到,实际打开串口完成硬件初始化配置是在rt_console_set_device函数中

#ifdef RT_USING_CONSOLE
    rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif

rt_device_t rt_console_set_device(const char *name)
{
    rt_device_t new, old;

    /* save old device */
    old = _console_device;

    /* find new console device */
    new = rt_device_find(name);
    if (new != RT_NULL)
    {
        if (_console_device != RT_NULL)
        {
            /* close old console device */
            rt_device_close(_console_device);
        }

        /* set new console device */
        rt_device_open(new, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM);
        _console_device = new;
    }

    return old;
}

#define RT_CONSOLE_DEVICE_NAME “uart1”
这个宏定义在rtconfig.h中,定义了使用哪个串口作为控制台的端口。这里使用的是串口1,可以根据需要进行修改

在rt_console_set_device函数中,完成了打开串口设备的动作 rt_device_open(new, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM);
rt_device_open函数中调用了device_init和device_open,
#define device_init (dev->init)
#define device_open (dev->open)
实际上是 (dev->init)和(dev->open)

在rt_hw_serial_register函数中device->init = rt_serial_init;
在rt_serial_init函数中看到了serial->ops->configure(serial, &serial->config); 前面说了,configure这个函数指针指向了stm32_configure,这个里面调用了HAL_UART_Init函数完成串口初始化配置。

rt_err_t rt_hw_serial_register(struct rt_serial_device *serial,
                               const char              *name,
                               rt_uint32_t              flag,
                               void                    *data)
{
    rt_err_t ret;
    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;

#ifdef RT_USING_DEVICE_OPS
    device->ops         = &serial_ops;
#else
    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;
#endif
    device->user_data   = data;

    /* register a character device */
    ret = rt_device_register(device, name, flag);

#if defined(RT_USING_POSIX)
    /* set fops */
    device->fops        = &_serial_fops;
#endif

    return ret;
}
static rt_err_t rt_serial_init(struct rt_device *dev)
{
    rt_err_t result = RT_EOK;
    struct rt_serial_device *serial;

    RT_ASSERT(dev != RT_NULL);
    serial = (struct rt_serial_device *)dev;

    /* initialize rx/tx */
    serial->serial_rx = RT_NULL;
    serial->serial_tx = RT_NULL;

    /* apply configuration */
    if (serial->ops->configure)
        result = serial->ops->configure(serial, &serial->config);

    return result;
}

本文通篇讲了用于控制台的串口1如何注册到系统中并完成了初始化,其余串口也是同理。
主要关注两个问题,1是如何修改某个串口的波特率,2是如果增加串口。
如果底层驱动做好的情况下,实际上直接改宏控就能完成增加串口,env工具可以快速完成配置,也可以手动改rtconfig.h文件。

RT-Thread 官方串口配置文档

上一篇:STM32F103C8T6-CubeMx串口收发程序详细设计与测试(2)——程序规划、代码编写及测试


下一篇:UART接口算法移植加密芯片的调试技巧——算法调试