本文只要是讲解UART设备在RT-Thread的应用层怎么使用,以及探究底层的实现方式。会从应用层和半导体厂商角度进行讲解。
一、UART简介
对于UART简介,RT-Thread官网的文档中心已经介绍,我把链接附上:UART简介。其实,网上也会有很多方面的资料,大家可以从网上搜索一些资料阅读一下。
我在这里进行一些简单的几点说明:

  • UART是一种全双工的通信方式,所谓的全双工就是在同一时刻两个UART设备能够同时进行收发数据。
  • 在工业上用的RS485、RS232总线一般都是经过UART协议转换过来的,具体如下图所示,图中MCU通过UART产生数据,经过MAX3485转换成RS485协议,或者经过MAX3232转换成RS232协议,进行相应的通信即可。
  • UART在很多地方会用到,一般的低速外设模块与MCU进行通信时都会选择UART,笔者在项目开发过程中遇到过很多;比如:GPS与MCU通信、GPRS与MCU通信等。
    好了,先啰嗦到这里,网上有很多资料写的很棒,大家自行查阅。
    二、UART设备源码剖析
    本文介绍UART设备框架的顺序为:首先从应用层角度出发,介绍应用API接口的使用方法;接着探究应用API的内部实现过程;最后从半导体厂商角度出发介绍怎样编写RT-Thread的UART设备接口代码。
    首先,作为一个应用开发人员,怎样使用UART接口函数。应用程序通过 RT-Thread提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示:
    1、rt_device_find
    a、应用层API接口
    这里是根据UART设备名称寻找一个设备,该函数会返回一个设备句柄。
    b、框架实现
    应用层通过调用如下代码,实现发现UART设备。
rt_device_find("uart0");//发现串口设备

rt_device_find代码如下:

/*** This function finds a device driver by specified name.** @param name the device driver's name** @return the registered device driver on successful, or RT_NULL on failure.*/
rt_device_t rt_device_find(const char *name)//
{struct rt_object *object;struct rt_list_node *node;struct rt_object_information *information;/* enter critical */if (rt_thread_self() != RT_NULL)rt_enter_critical();//锁定调度器,不允许任务调度/* try to find device object */information = rt_object_get_information(RT_Object_Class_Device);//寻找设备内核对象RT_ASSERT(information != RT_NULL);for (node  = information->object_list.next;node != &(information->object_list);node  = node->next){object = rt_list_entry(node, struct rt_object, list);//根据成员变量node得到内核对象struct rt_objectif (rt_strncmp(object->name, name, RT_NAME_MAX) == 0)//根据设备名称寻找设备内核对象{//寻找到设备/* leave critical */if (rt_thread_self() != RT_NULL)rt_exit_critical();return (rt_device_t)object;//强制转换成设备类型结构体,返回}}/* leave critical */if (rt_thread_self() != RT_NULL)rt_exit_critical();/* not found */return RT_NULL;
}
RTM_EXPORT(rt_device_find);

通过上面函数的内部实现可以知道,发现串口设备是通过设备名称实现的。因此,这就要求我们的产品代码中,必须为每一个外设起一个独一无二的设备名。对于串口设备,我们习惯上使用uart0、uart1、uart2等。在RT-Thread内核中,设备类型是内核对象struct rt_object *object;派生过来的。在RT-Thread内核中,通过将设备作为RT_Object_Class_Device类型的内核对象进行管理的。在RT-Thread内核中, static struct rt_object_information rt_object_container[RT_Object_Info_Unknown] 存储着所有类型的内核对象,每一种内核对象都会通过双向链表的形式组织在一起。挖坑关于这部分内容我会专门写一篇文章。
通过分析上面代码发现,函数rt_device_find返回的就是一个指针,指向的是struct rt_device类型的结构体变量。
c、底层实现(半导体或者硬件驱动需要做的事)
下面解决一个疑问:为什么应用层只需要根据串口设备的名称就能够找到串口设备,这些底层是怎么实现的呢?换句话说,在应用层调用rt_device_find发现设备之前,系统执行了哪些代码来支持该操作?我们从rt_device_find函数具体执行过程作为突破口,就能够发现。rt_device_find是通过寻找内核中是否含有name成员变量的struct rt_device类型结构体来判断系统中是否含有我们想要寻找的设备。因此,在rt_device_find函数之前一定会有struct rt_device类型结构体定义和初始化的代码。对,我们就是依靠这个思路进行底层实现的。这部分代码一般会在一些底层驱动代码的文件中,我们以文件usart.c (bsp\stm32f20x\drivers)进行分析。

#ifdef RT_USING_UART1
struct stm32_serial_int_rx uart1_int_rx;
struct stm32_serial_device uart1 =
{USART1,&uart1_int_rx,RT_NULL
};
struct rt_device uart1_device;//串口设备
#endif

上面代码是定义一个名为uart1_device的struct rt_device类型的结构体,表示一个串口设备。注意这里是一个全局变量,也就是说会一直存在,直到整个代码运行结束。
下面还是在文件usart.c (bsp\stm32f20x\drivers)中的一段代码,具体如下:

/** Init all related hardware in here* rt_hw_serial_init() will register all supported USART device*/
void rt_hw_usart_init()
{USART_InitTypeDef USART_InitStructure;RCC_Configuration();//时钟配置GPIO_Configuration();//GPIO工作模式配置NVIC_Configuration();//中断配置/* uart init */
#ifdef RT_USING_UART1USART_DeInit(USART1);USART_InitStructure.USART_BaudRate            = 115200;//设置波特率USART_InitStructure.USART_WordLength          = USART_WordLength_8b;//设置串口传输为8bitUSART_InitStructure.USART_StopBits            = USART_StopBits_1;//一位停止位USART_InitStructure.USART_Parity              = USART_Parity_No ;//USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//没有硬件流控USART_InitStructure.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;//开启串口接收和发送模式USART_Init(USART1, &USART_InitStructure);//设置底层硬件串口工作方式/* register uart1 */rt_hw_serial_register(&uart1_device, "uart1",RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX | RT_DEVICE_FLAG_STREAM,&uart1);//向RT-Thread内核注册uart1_device设备,该函数是半导体产商为适应RT-Thread的UART框架而编写的函数/* enable interrupt */USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//打开串口接收中断/* Enable USART1 */USART_Cmd(USART1, ENABLE);USART_ClearFlag(USART1,USART_FLAG_TXE);
#endif/* uart init */
#ifdef RT_USING_UART6USART_DeInit(USART6);USART_InitStructure.USART_BaudRate            = 115200;USART_InitStructure.USART_WordLength          = USART_WordLength_8b;USART_InitStructure.USART_StopBits            = USART_StopBits_1;USART_InitStructure.USART_Parity              = USART_Parity_No ;USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_InitStructure.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;USART_Init(USART6, &USART_InitStructure);/* register uart1 */rt_hw_serial_register(&uart6_device, "uart6",RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX | RT_DEVICE_FLAG_STREAM,&uart6);/* enable interrupt */USART_ITConfig(USART6, USART_IT_RXNE, ENABLE);/* Enable USART6 */USART_Cmd(USART6, ENABLE);USART_ClearFlag(USART6,USART_FLAG_TXE);
#endif
}

该函数可以分为两大部分:第一部分是调用HAL库,对ST32的片内UART进行初始化设置,这是设置的底层硬件部分。第二部分调用了函数rt_hw_serial_register,继续追踪。这里多说一句,rt_hw_serial_register是半导体产商为适配RT-Thread的UART框架(其实本质就是I/O设备模型)而编写的函数,因此这个函数对于不同半导体产商的MCU会不同。我们得到rt_hw_serial_register函数的代码如下:

/** serial register for STM32* support STM32F103VB and STM32F103ZE*/
rt_err_t rt_hw_serial_register(rt_device_t device, const char* name, rt_uint32_t flag, struct stm32_serial_device *serial)
{RT_ASSERT(device != RT_NULL);if ((flag & RT_DEVICE_FLAG_DMA_RX) ||(flag & RT_DEVICE_FLAG_INT_TX)){RT_ASSERT(0);}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   = serial;/* register a character device */return rt_device_register(device, name, RT_DEVICE_FLAG_RDWR | flag);
}

通过上面代码发现rt_hw_serial_register又调用了函数rt_device_register,继续将函数rt_device_register的代码贴上:

/*** This function registers a device driver with specified name.** @param dev the pointer of device driver structure* @param name the device driver's name* @param flags the capabilities flag of device** @return the error code, RT_EOK on initialization successfully.*/
rt_err_t rt_device_register(rt_device_t dev,const char *name,rt_uint16_t flags)
{if (dev == RT_NULL)return -RT_ERROR;if (rt_device_find(name) != RT_NULL)return -RT_ERROR;rt_object_init(&(dev->parent), RT_Object_Class_Device, name);dev->flag = flags;dev->ref_count = 0;dev->open_flag = 0;#if defined(RT_USING_POSIX)dev->fops = RT_NULL;rt_wqueue_init(&(dev->wait_queue));
#endifreturn RT_EOK;
}
RTM_EXPORT(rt_device_register);

经过上面的代码我们知道,函数rt_device_register和rt_hw_serial_register就是对uart1_device每个成员变量进行赋值,然后将该设备注册到内核的设备对象双向链表中。
总结一下代码调用关系:

rt_hw_board_init->rt_hw_usart_init->rt_hw_serial_register->rt_device_register->rt_object_init

由此,我们就知道底层对于UART设备的具体实现方式了。
2、rt_device_open
a、应用层API接口
我们要知道只有在发现设备的过程中才会使用设备名作为参数,后面的打开设备、对设备进行操作都是使用设备句柄,也就是rt_device_find的返回值,设备句柄本质就是一个指向struct rt_device类型的指针。
通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。
下面看一个使用例子,上代码:

open_result = rt_device_open(client->device, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX);//以中断接收的方式打开

对于第一个参数就是一个设备类型的指针,其实client是包含设备句柄的,具体结构如下:

struct at_client
{rt_device_t device;at_status_t status;char end_sign;/* the current received one line data buffer */char *recv_line_buf;/* The length of the currently received one line data */rt_size_t recv_line_len;/* The maximum supported receive data length */rt_size_t recv_bufsz;rt_sem_t rx_notice;rt_mutex_t lock;at_response_t resp;rt_sem_t resp_notice;at_resp_status_t resp_status;struct at_urc_table *urc_table;rt_size_t urc_table_size;rt_thread_t parser;
};

第二个参数为RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX。
b、框架实现
rt_device_open函数的具体实现如下:

/*** This function will open a device** @param dev the pointer of device driver structure* @param oflag the flags for device open** @return the result*/
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflag)
{rt_err_t result = RT_EOK;RT_ASSERT(dev != RT_NULL);RT_ASSERT(rt_object_get_type(&dev->parent) == RT_Object_Class_Device);/* if device is not initialized, initialize it. */if (!(dev->flag & RT_DEVICE_FLAG_ACTIVATED))//设备没有处于活跃状态,也就是没有初始化{if (device_init != RT_NULL)//设备初始化函数指针不为NULL,也就是该设备含有初始化函数{result = device_init(dev);//对设备进行初始化if (result != RT_EOK){//初始化失败,直接返回rt_kprintf("To initialize device:%s failed. The error code is %d\n",dev->parent.name, result);return result;}}dev->flag |= RT_DEVICE_FLAG_ACTIVATED;//初始化成功,将设备的标志域设置RT_DEVICE_FLAG_ACTIVATED}/* device is a stand alone device and opened */if ((dev->flag & RT_DEVICE_FLAG_STANDALONE) &&(dev->open_flag & RT_DEVICE_OFLAG_OPEN))//此设备只允许打开一次,且已经打开过{return -RT_EBUSY;//返回函数的执行结果}/* call device_open interface */if (device_open != RT_NULL)//设备的打开函数不为空,换言之,在rt_hw_serial_register函数中对device->open      = rt_serial_open;进行了赋值{result = device_open(dev, oflag);//执行device->open   函数,在本例中就是执行rt_serial_open函数}else{//对于有些设备,在应用层在代用设备打开时,不需要专门代码去执行相关的打开//工作,也就是说,有些设备可能在设备初始化完成之后,就默认打开了。那么这//样就不需要专门的打开函数。此时,只对dev->open_flag设置打开设备的属//性,也就是rt_device_open函数的第二个参数。/* set open flag */dev->open_flag = (oflag & RT_DEVICE_OFLAG_MASK);//}/* set open flag */if (result == RT_EOK || result == -RT_ENOSYS){//设备打开成功dev->open_flag |= RT_DEVICE_OFLAG_OPEN;//标识设备打开成功dev->ref_count++;//引用计数++,标识该设备正在被使用,该计数变量作为执行设备关闭的标志。只有该计数变量变为0时,才执行真正的关闭函数。/* don't let bad things happen silently. If you are bitten by this assert,* please set the ref_count to a bigger type. */RT_ASSERT(dev->ref_count != 0);}return result;
}
RTM_EXPORT(rt_device_open);

通过该函数的分析,我们知道,rt_device_open主要就是做两件事:第一是设置rt_device的open_flag和ref_count成员变量;第二就是调用底层函数设备打开函数。
c、底层实现(半导体或者硬件驱动需要做的事)
通过上面分析rt_device_open函数,我们发现该函数会通过result = device_open(dev, oflag);调用底层函数(本例中就是调用rt_serial_open)。那我们接着分析:(上代码)

static rt_err_t rt_serial_open(rt_device_t dev, rt_uint16_t oflag)
{return RT_EOK;
}

上面是STM32相关的底层代码实现,是不是很惊讶,没有执行底层的操作。原因就是,串口设备在我们上面分析的rt_hw_usart_init函数已经执行了相应的硬件操作了。那为什么还要写这么一个函数呢?我猜可能是考虑以后代码升级用。
这部分代码是半导体厂商为适配RT-Thread的UART设备框架而编写的,那么我们找一个其他半导体厂商的代码看看人家在里面做了什么。就拿恩智浦的看一下吧,具体代码在文件:Serial.c (bsp\lpc2478\drivers)

static rt_err_t rt_serial_open(rt_device_t dev, rt_uint16_t oflag)
{struct rt_lpcserial* lpc_serial;lpc_serial = (struct rt_lpcserial*) dev;RT_ASSERT(lpc_serial != RT_NULL);if (dev->flag & RT_DEVICE_FLAG_INT_RX){/* init UART rx interrupt */UART_IER(lpc_serial->hw_base) = 0x01;/* install ISR */rt_hw_interrupt_install(lpc_serial->irqno,rt_hw_uart_isr, lpc_serial, RT_NULL);rt_hw_interrupt_umask(lpc_serial->irqno);}return RT_EOK;
}

接着贴代码

/*** This function will install a interrupt service routine to a interrupt.* @param vector the interrupt number* @param handler the interrupt service routine to be installed* @param param the parameter for interrupt service routine* @name unused.** @return the old handler*/
rt_isr_handler_t rt_hw_interrupt_install(int vector, rt_isr_handler_t handler, void *param, const char *name)
{rt_isr_handler_t old_handler = RT_NULL;if(vector >= 0 && vector < MAX_HANDLERS){old_handler = irq_desc[vector].handler;if (handler != RT_NULL){irq_desc[vector].handler = handler;irq_desc[vector].param = param;}}return old_handler;
}

在lpc2478 MCU中,该函数就是串口中断服务函数和相应的参数。
3、rt_device_control
a、应用层API接口
通过控制接口,应用程序可以对串口设备进行配置,如波特率、数据位、校验位、接收缓冲区大小、停止位等参数的修改。
其中对于UART设备来说,参数arg指向struct serial_configure结构体的指针。

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;//保留位
};

b、框架实现
rt_device_control函数的具体实现如下(代码在文件Device.c (src) 12311中):

/*** This function will perform a variety of control functions on devices.** @param dev the pointer of device driver structure* @param cmd the command sent to device* @param arg the argument of command** @return the result*/
rt_err_t rt_device_control(rt_device_t dev, int cmd, void *arg)
{RT_ASSERT(dev != RT_NULL);RT_ASSERT(rt_object_get_type(&dev->parent) == RT_Object_Class_Device);/* call device_write interface */if (device_control != RT_NULL){return device_control(dev, cmd, arg);//调用设备相关的函数}return -RT_ENOSYS;
}
RTM_EXPORT(rt_device_control);

通过该函数的分析,我们知道,rt_device_control主要就是做一件事。如果设备控制函数不为NULL,就会调用该函数。否则直接返回-RT_ENOSYS。对于UART设备来说,该函数一般是rt_serial_control。
c、底层实现(半导体或者硬件驱动需要做的事)
通过上面分析rt_device_control函数,我们发现该函数会通过return device_control(dev, cmd, arg);调用底层函数(本例中就是调用rt_serial_control)。那我们接着分析:(上代码)

static rt_err_t rt_serial_control (rt_device_t dev, int cmd, void *args)
{struct stm32_serial_device* uart;RT_ASSERT(dev != RT_NULL);uart = (struct stm32_serial_device*)dev->user_data;switch (cmd){case RT_DEVICE_CTRL_SUSPEND://挂起UART设备/* suspend device */dev->flag |= RT_DEVICE_FLAG_SUSPENDED;USART_Cmd(uart->uart_device, DISABLE);//调用底层STM32的HAL库函数,关闭串口中断break;case RT_DEVICE_CTRL_RESUME://打开UART设备/* resume device */dev->flag &= ~RT_DEVICE_FLAG_SUSPENDED;USART_Cmd(uart->uart_device, ENABLE);//调用底层STM32的HAL库函数,打开串口中断break;}return RT_EOK;
}

上面是STM32相关的底层代码实现。在STM32中该函数主要是接收两个设备控制命令,分别是RT_DEVICE_CTRL_SUSPEND和RT_DEVICE_CTRL_RESUME。对于传入其他命令,该函数不做任何处理。该函数主要的任务就是两个:第一,设置rt_device设备结构体的flag标志位;第二是调用STM32的HAL库函数禁止或者使能UART外设。
这里还是要强调一下,函数rt_serial_control是半导体厂商需要实现的,每个半导体厂商会根据自己MCU的特性实现的功能不相同。
4、rt_device_write
a、应用层API接口
向串口中写入数据,可以通过如下函数完成:
将buffer中的size字节的数据通过串口发送出去。要注意pos参数在UART设备并没有用到,由于rt_device_write函数不仅仅用于串口发送当我们向其他设备写入数据的时候也是调用该函数。因此,参数pos一般是用在像flash这样的存储设备。大家可以理解为linux驱动程序中的块设备和字符设备,对于块设备来说,可以在任何位置开始读写,那么这个pos就会有意义;对于字符设备来说,数据以流的方式进行传输,因此pos没有意义。
b、框架实现
rt_device_write函数的具体实现如下(代码在文件Device.c (src) 12311中):

/*** This function will write some data to a device.** @param dev the pointer of device driver structure* @param pos the position of written* @param buffer the data buffer to be written to device* @param size the size of buffer** @return the actually written size on successful, otherwise negative returned.** @note since 0.4.0, the unit of size/pos is a block for block device.*/
rt_size_t rt_device_write(rt_device_t dev,rt_off_t    pos,const void *buffer,rt_size_t   size)
{RT_ASSERT(dev != RT_NULL);RT_ASSERT(rt_object_get_type(&dev->parent) == RT_Object_Class_Device);if (dev->ref_count == 0){rt_set_errno(-RT_ERROR);return 0;}/* call device_write interface */if (device_write != RT_NULL){return device_write(dev, pos, buffer, size);//调用底层数据发送函数}/* set error code */rt_set_errno(-RT_ENOSYS);return 0;
}
RTM_EXPORT(rt_device_write);

对于UART设备来说,该函数的主要作用就是调用底层的串口发送函数。真正调用的函数为rt_serial_write。
c、底层实现(半导体或者硬件驱动需要做的事)
经过上面分析,我们知道 给UART设备传输数据时,rt_device_write会调用底层的rt_serial_write函数实现数据发送。而函数rt_serial_write是半导体厂商自己实现的,我们就以STM32为例进行讲解:上代码

static rt_size_t rt_serial_write (rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size)
{rt_uint8_t* ptr;rt_err_t err_code;struct stm32_serial_device* uart;err_code = RT_EOK;ptr = (rt_uint8_t*)buffer;uart = (struct stm32_serial_device*)dev->user_data;if (dev->flag & RT_DEVICE_FLAG_INT_TX)//判断UART设备发送方式设置{//STM32中断方式发送不支持/* interrupt mode Tx, does not support */RT_ASSERT(0);}else if (dev->flag & RT_DEVICE_FLAG_DMA_TX){//采用DAM方式发送/* DMA mode Tx *//* allocate a data node */struct stm32_serial_data_node* data_node = (struct stm32_serial_data_node*)rt_mp_alloc (&(uart->dma_tx->data_node_mp), RT_WAITING_FOREVER);//申请struct stm32_serial_data_node大小的空间if (data_node == RT_NULL){//内存申请失败/* set error code */err_code = -RT_ENOMEM;}else{//内存申请成功rt_uint32_t level;/* fill data node */data_node->data_ptr    = ptr;//指向发送数据位置data_node->data_size    = size;//发送的字节数/* insert to data link */data_node->next = RT_NULL;//将申请的struct stm32_serial_data_node变量插入链表/* disable interrupt */level = rt_hw_interrupt_disable();//关闭中断data_node->prev = uart->dma_tx->list_tail;if (uart->dma_tx->list_tail != RT_NULL)uart->dma_tx->list_tail->next = data_node;uart->dma_tx->list_tail = data_node;//将上面申请的data_node 插入双向链表中if (uart->dma_tx->list_head == RT_NULL){/* start DMA to transmit data */uart->dma_tx->list_head = data_node;/* Enable DMA Channel */rt_serial_enable_dma(uart->dma_tx->dma_channel,(rt_uint32_t)uart->dma_tx->list_head->data_ptr,uart->dma_tx->list_head->data_size);//开启DMA传输数据}/* enable interrupt */rt_hw_interrupt_enable(level);//打开中断}}else{//采用普通方式传输数据/* polling mode */if (dev->flag & RT_DEVICE_FLAG_STREAM){//采用流模式传输/* stream mode */while (size){if (*ptr == '\n'){while (!(uart->uart_device->SR & USART_FLAG_TXE));//等待一字节数据发送完成uart->uart_device->DR = '\r';//发送下1字节数据}while (!(uart->uart_device->SR & USART_FLAG_TXE));uart->uart_device->DR = (*ptr & 0x1FF);++ptr; --size;}}else{//直接传输/* write data directly */while (size){while (!(uart->uart_device->SR & USART_FLAG_TXE));//等待1字节数据发送完成uart->uart_device->DR = (*ptr & 0x1FF);//发送下一字节数据++ptr; --size;}}}/* set error code */rt_set_errno(err_code);return (rt_uint32_t)ptr - (rt_uint32_t)buffer;
}

对于STM32来说,该函数的主要任务如下:

  • 首先判断采用哪种方式发送,如果是采用中断方式发送,则直接返回,表示不支持中断模式发送。
  • 如果采用DMA方式发送,就会申请一个stm32_serial_data_node类型的节点,并将其加入到说向量链表中。然后关闭中断,打开DMA传输。说明关于DMA传输STM32采用了一个双向链表来存储需要发送的数据,在每次调用DMA发送数据的时候,会判断前一包数据是否传送完成,如果完成才会取出一包数据进行发送。
  • 如果是采用普通传输方式,那么就会判断是否使用流模式还是直接传输。最后执行串口发送。
    还是要说一下,函数rt_serial_write 对于不同的半导体厂商的实现方式是不同的,大家可以查看其它MCU的代码。
    5、rt_device_set_tx_complete
    a、应用层API接口
    在应用程序调用 rt_device_write() 写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由设备驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。
    b、框架实现
    下面看一下rt_device_set_tx_complete代码是怎么实现的。
/*** This function will set the indication callback function when device has* written data to physical hardware.** @param dev the pointer of device driver structure* @param tx_done the indication callback function** @return RT_EOK*/
rt_err_t
rt_device_set_tx_complete(rt_device_t dev,rt_err_t (*tx_done)(rt_device_t dev, void *buffer))
{RT_ASSERT(dev != RT_NULL);RT_ASSERT(rt_object_get_type(&dev->parent) == RT_Object_Class_Device);dev->tx_complete = tx_done;//就是设置发送完成回调函数return RT_EOK;
}
RTM_EXPORT(rt_device_set_tx_complete);

c、底层实现(半导体或者硬件驱动需要做的事)
对于rt_device_set_tx_complete函数的作用就是设置一个回调函数。大家可以理解为,应用层人员高速MCU你如果在发送完成数据之后,要执行一段代码(这段代码就是tx_done),来明确高速我发送完成。我们只看一下tx_done在哪里显示调用就可以。
以STM32代码为例,查看一下:

/** ISR for DMA mode Tx*/
void rt_hw_serial_dma_tx_isr(rt_device_t device)
{rt_uint32_t level;struct stm32_serial_data_node* data_node;struct stm32_serial_device* uart = (struct stm32_serial_device*) device->user_data;/* DMA mode receive */RT_ASSERT(device->flag & RT_DEVICE_FLAG_DMA_TX);/* get the first data node */data_node = uart->dma_tx->list_head;RT_ASSERT(data_node != RT_NULL);/* invoke call to notify tx complete */if (device->tx_complete != RT_NULL)device->tx_complete(device, data_node->data_ptr);/* disable interrupt */level = rt_hw_interrupt_disable();/* remove list head */uart->dma_tx->list_head = data_node->next;if (uart->dma_tx->list_head == RT_NULL) /* data link empty */uart->dma_tx->list_tail = RT_NULL;/* enable interrupt */rt_hw_interrupt_enable(level);/* release data node memory */rt_mp_free(data_node);if (uart->dma_tx->list_head != RT_NULL){/* transmit next data node */rt_serial_enable_dma(uart->dma_tx->dma_channel,(rt_uint32_t)uart->dma_tx->list_head->data_ptr,uart->dma_tx->list_head->data_size);}else{/* no data to be transmitted, disable DMA */DMA_Cmd(uart->dma_tx->dma_channel, DISABLE);}
}

上面这个函数调用tx_done的位置是:

/* invoke call to notify tx complete */if (device->tx_complete != RT_NULL)device->tx_complete(device, data_node->data_ptr);

但是很遗憾,在STM32中UART的rt_device的tx_complete成员变量设置为NULL,即device->tx_complete=NULL(可以通过函数rt_hw_serial_register看到,具体代码的位置位Serial.c (bsp\stm32f20x\drivers) 9670)。
6、rt_device_set_rx_indicate
a、应用层API接口
可以通过如下函数来设置数据接收指示,当串口收到数据时,通知上层应用线程有数据到达。该函数的回调函数由调用者提供。若串口以中断接收模式打开,当串口接收到一个数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在 size 参数里,把串口设备句柄放在 dev 参数里供调用者获取。若串口以 DMA 接收模式打开,当 DMA 完成一批数据的接收后会调用此回调函数。
b、框架实现
rt_device_set_rx_indicate函数的实现代码如下:(Device.c (src) 12311)

/*** This function will set the reception indication callback function. This callback function* is invoked when this device receives data.** @param dev the pointer of device driver structure* @param rx_ind the indication callback function** @return RT_EOK*/
rt_err_t
rt_device_set_rx_indicate(rt_device_t dev,rt_err_t (*rx_ind)(rt_device_t dev, rt_size_t size))
{RT_ASSERT(dev != RT_NULL);RT_ASSERT(rt_object_get_type(&dev->parent) == RT_Object_Class_Device);dev->rx_indicate = rx_ind;return RT_EOK;
}
RTM_EXPORT(rt_device_set_rx_indicate);

该函数同rt_device_set_tx_complete函数类似。应用开发人员通过这个接口告诉MCU,当接收到数据之后,需要执行一段代码。这段代码就是rx_ind指向的函数。
c、底层实现(半导体或者硬件驱动需要做的事)
关于底层实现不再进行解析,大多数都没有用到。
7、rt_device_read
读取数据偏移量 pos 针对字符设备无效,此参数主要用于块设备中。
b、框架实现
rt_device_read函数的实现代码如下:(Device.c (src) 12311)

/*** This function will read some data from a device.** @param dev the pointer of device driver structure* @param pos the position of reading* @param buffer the data buffer to save read data* @param size the size of buffer** @return the actually read size on successful, otherwise negative returned.** @note since 0.4.0, the unit of size/pos is a block for block device.*/
rt_size_t rt_device_read(rt_device_t dev,rt_off_t    pos,void       *buffer,rt_size_t   size)
{RT_ASSERT(dev != RT_NULL);RT_ASSERT(rt_object_get_type(&dev->parent) == RT_Object_Class_Device);if (dev->ref_count == 0){rt_set_errno(-RT_ERROR);return 0;}/* call device_read interface */if (device_read != RT_NULL){return device_read(dev, pos, buffer, size);}/* set error code */rt_set_errno(-RT_ENOSYS);return 0;
}
RTM_EXPORT(rt_device_read);

该函数其实就是最终执行return device_read(dev, pos, buffer, size);,对于UART设备来说就是调用rt_serial_read。
c、底层实现(半导体或者硬件驱动需要做的事)
我们以STM32为例讲解rt_serial_read的实现,直接上代码(Serial.c (bsp\stm32f20x\drivers) 9670)

static rt_size_t rt_serial_read (rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size)
{rt_uint8_t* ptr;rt_err_t err_code;struct stm32_serial_device* uart;ptr = buffer;err_code = RT_EOK;uart = (struct stm32_serial_device*)dev->user_data;//串口设备接收到的数据会放到dev->user_data中if (dev->flag & RT_DEVICE_FLAG_INT_RX){//中断模式接收,注意如果是中断模式接收的话,数据已经放到内存中去了,也就是放到了dev->user_data中,这里相当于是进行搬运/* interrupt mode Rx */while (size){rt_base_t level;/* disable interrupt */level = rt_hw_interrupt_disable();//关闭中断if (uart->int_rx->read_index != uart->int_rx->save_index)//说明已经接收到新数据{/* read a character */*ptr++ = uart->int_rx->rx_buffer[uart->int_rx->read_index];//将数据传到应用层指定的buffer中size--;/* move to next position */uart->int_rx->read_index ++;//指向下一个需要传送的字节if (uart->int_rx->read_index >= UART_RX_BUFFER_SIZE)//大于串口接收最大字节数uart->int_rx->read_index = 0;}else{//没有接收到新的数据/* set error code */err_code = -RT_EEMPTY;/* enable interrupt */rt_hw_interrupt_enable(level);//打开串口break;}/* enable interrupt */rt_hw_interrupt_enable(level);//打开串口}}else{//polling方式,理解为是一种轮询的方式,这时候不是从内存中取数据到应用层//而是直接从串口相关的接收寄存器中取数据/* polling mode */while ((rt_uint32_t)ptr - (rt_uint32_t)buffer < size){while (uart->uart_device->SR & USART_FLAG_RXNE)//判断是否有数据产生{*ptr = uart->uart_device->DR & 0xff;//有数据产生,直接从串口接收寄存器复制到用户提供的内存中ptr ++;}}}/* set error code */rt_set_errno(err_code);return (rt_uint32_t)ptr - (rt_uint32_t)buffer;
}

对于STM32实现的底层接收函数,如果是采用的中断方式读取UART设备中的数据,其实本质上只是将数据从一个内存区域赋值到另一个内存区域。当串口接收到数据之后,会将数据复制到dev->user_data中等待应用层读取。当应用层调用rt_device_read读取UART设备中的数据之后,会将dev->user_data中的数据传送给用户提供的buffer中。如果采用polling方式,那是直接从MCU内部串口接收寄存器中读取数据,由于数据是按字节接收的。如果我们应用层一次接收多个数据,代码会一直等待。知道数据 接收完成。
8、rt_device_close
a、应用层API接口
当应用程序完成串口操作后,可以关闭串口设备,通过如下函数完成:
关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。
b、框架实现
rt_device_read函数的实现代码如下:(Device.c (src) 12311)

/*** This function will close a device*  * @param dev the pointer of device driver structure*  * @return the result*/
rt_err_t rt_device_close(rt_device_t dev)
{rt_err_t result = RT_EOK;RT_ASSERT(dev != RT_NULL);RT_ASSERT(rt_object_get_type(&dev->parent) == RT_Object_Class_Device);if (dev->ref_count == 0)return -RT_ERROR;dev->ref_count--;if (dev->ref_count != 0)return RT_EOK;/* call device_close interface */if (device_close != RT_NULL){result = device_close(dev);}/* set open flag */if (result == RT_EOK || result == -RT_ENOSYS)dev->open_flag = RT_DEVICE_OFLAG_CLOSE;return result;
}
RTM_EXPORT(rt_device_close);

该函数主要执行以下几个操作:

  • 对ref_count减1操作,对于ref_count我们前面说过,每执行一次设备打开操作,该变量就会+1;(也就是说打开多少次设备就会对应执行多少次设备关闭,才能够最终实现设备关闭)
  • 若ref_count变为0,就会执行关闭,具体会调用底层函数;
  • 设置open_flag标志为 RT_DEVICE_OFLAG_CLOSE,表示设备关闭。
    c、底层实现(半导体或者硬件驱动需要做的事)
    同样对于UART设备来说,关闭设备之后最终调用的是rt_serial_close函数。该函数在不同MCU实现方式会有差别。这里看一下STM32的底层实现:
static rt_err_t rt_serial_close(rt_device_t dev)
{return RT_EOK;
}

又是什么都没有实现。就这样吧,不必再探究了。
三、总结
其实对于UART设备的底层实现,主要是分为两步:第一步将RT-Thread的底层接口调用根据MCU的特点实现,具体点就是如何实现rt_serial_close、rt_serial_write 等;第二步就是定义一个全局变量rt_device类型的变量,并将上面实现的函数赋值给相应的数据域就可以了。
这篇文章写得又臭又长,希望对看到这篇文章的朋友有所帮助。还有一些东西没有写出来,最主要原因是不知道怎么组织语言。在应用层的一些简单的API接口调用,看似很简单,但是底层实现上还是很巧妙的,挺复杂的。正所谓“每一个简单现象的背后并不简单”,我们在感叹应用层在操作UART设备是如此方便的同时,也应该知道这些方便的背后总有人为我们负重前行。完了,有点煽情,千万别喷我。

RT-Thread源码解读-------UART设备相关推荐

  1. boost thread 判断是否正在运行_java高端基础:Thread源码解读

    阅读本篇文章之前建议先了解线程的生命周期以及状态之间的可能的转换 Java高端基础:线程的生命周期 wait() 使当前线程等待,直到其他线程调用该对象的notify()或者notifyAll()方法 ...

  2. Linux内核网络协议栈:udp数据包发送(源码解读)

    <监视和调整Linux网络协议栈:接收数据> <监控和调整Linux网络协议栈的图解指南:接收数据> <Linux网络 - 数据包的接收过程> <Linux网 ...

  3. PyTorch 源码解读之 cpp_extension:讲解 C++/CUDA 算子实现和调用全流程

    "Python 用户友好却运行效率低","C++ 运行效率较高,但实现一个功能代码量会远大于 Python".平常学习工作中你是否常听到类似的说法?在 Pyth ...

  4. [并发编程] - Executor框架#ThreadPoolExecutor源码解读03

    文章目录 Pre execute源码分析 addWorker()解读 Worker解读 Pre [并发编程] - Executor框架#ThreadPoolExecutor源码解读02 说了一堆结论性 ...

  5. Android 开源框架之 Android-async-http 源码解读

    开源项目链接 Android-async-http仓库:https://github.com/loopj/android-async-http android-async-http主页:http:// ...

  6. KClient——kafka消息中间件源码解读

    目录 kclient消息中间件 kclient-processor top.ninwoo.kclient.app.KClientApplication top.ninwoo.kclient.app.K ...

  7. aqs java 简书,Java AQS源码解读

    1.先聊点别的 说实话,关于AQS的设计理念.实现.使用,我有打算写过一篇技术文章,但是在写完初稿后,发现掌握的还是模模糊糊的,模棱两可. 痛定思痛,脚踏实地重新再来一遍.这次以 Java 8源码为基 ...

  8. ExecutorService源码解读

    ExecutorService源码解读 〇.[源码版本] jdk 1.8 一.ExecutorService接口详解 1.ExecutorService关闭方法概述 [举例1]代码示例 2.Execu ...

  9. Executor源码解读

    Executor源码解读 〇.[源码版本] jdk 1.8 一.不再显式创建线程 [举例1]代码示例 二.不严格要求执行是异步的 [举例1]代码示例 三.任务在调用者线程之外的某个线程中执行 [举例1 ...

最新文章

  1. 智能车竞赛B车模车轮毂断裂原因所在
  2. 第十六届全国大学生智能车竞赛-航天智慧物流创意组-技术培训
  3. html 空格_HTML标签
  4. 阮一峰es6电子书_ES6理解进阶【大前端高薪训练营】
  5. Redis之java操作篇(数据对象的存取)
  6. [LeetCode]--20. Valid Parentheses
  7. link引入html5,CSS引入方式 | link和@import的区别 — 生僻的前端考点
  8. sorl6.0+jetty+mysql搭建solr服务
  9. 【报告分享】新基建专题报告:5g和数据中心的投资机会分析.pdf(附下载链接)...
  10. Laravel.com 中国镜像、中文站点
  11. mysql配置文件改密码_mysql8.0 安装教程(自定义配置文件,密码方式已修改)
  12. 海量数据挖掘MMDS week2: Association Rules关联规则与频繁项集挖掘
  13. spark入门Intellj环境配置scalark入门Intellj环境配置scala
  14. idea打包SpringBoot项目打包成jar包和war
  15. 《从零开始学Swift》学习笔记(Day 12)——说几个特殊运算符
  16. jmeter安全证书_使用Jmeter进行https接口测试时,如何导入证书
  17. 2021年11月视频行业用户洞察
  18. 如何制作win7 U盘安装盘
  19. [FirefoxOS_开发环境]Linux和Ubuntu环境下B2G(Firefox OS)安装、编译、测试教程集合
  20. Springboot Vue个人简历网站系统java项目源码

热门文章

  1. Revit API:View 视图概述
  2. 1.1.4 分支, if, if else, if elseif else, switch,循环,for,break,continue,双重for,while, do while
  3. 美国最佳本科计算机科学,美国本科计算机科学专业排名
  4. .bat 文件打开软件
  5. 软件管理的“七个女妖”-不要相信她们
  6. mysql workbench crows foot_一步一步设计你的数据库(三)
  7. 报错Unknown custom element: <组件名> - did you register the component correctly?的原因及解决办法
  8. Jsp+Ssm+Mysql实现的零食商城系统
  9. 变压器的阻抗匹配作用
  10. python中除号怎么写_除号怎么写