最近一直研究阅读RT-Thread的设备模型,把一些感悟做一个笔记,也算是一个学习总结。
对于设备模型涉及到的思想、软件分层 继承 多态 等(可以去RT-Thread的官网文档中心查看,解释的很详细,教程实例很丰富),这里只是个人总结。
少废话,先看怎么用。下面是发送数据简单演示代码。
/* 定义设备 */
rt_device_t uart1;
/* 定义数据 */
rt_uint8_t buff[] = "Hello RT-Thread\n";
/* 查找设备,并做返回值判断 */
uart1 = rt_device_find("uart1");
if(uart1 == RT_NULL)
{
while(1);
}
/* 打开找到的设备,一般的能找到设备说明在系统中存在 该设备,但是相关的硬件并没有初始化,
open操作就是实现了硬件初始,能先找到说明支持这个设备,再打开说明要用这个设备 */
rt_device_open(uart1, RT_DEVICE_FLAG_INT_RX);
/* 数据发送 */
rt_device_write(uart1, -1, &buff, sizeof(buff));
上电复位,就可以看到串口助手收到的数据了!
一开始感觉很神奇,这样写就能实现了数据的发送了,那么具体是怎么实现的呢?为啥通过这几个函数就能往串口里写数据呢?
原来RT-Thread在软件上将数据收发的过程进行了分层。
应用层 | 用户调用 | rt_device_find等接口 |
---|---|---|
设备驱动层 | 由RTT提供 | 用户不需要更改 |
硬件层 | 驱动人员编写 | 涉及具体硬件,如stm32 |
进行裸机编程的时候 ,我们对数据的收发通常是直接操作芯片的寄存器或者通过库函数调用最终操作寄存器,实际上是和硬件层打交道。应用操作系统之后,操作系统通过封装呈现给用户的是通用的调用接口。通过这样分层之后,就把应用编写和驱动编写的人员进行分割,各司其职,加快研发效率。
从简单应用中我们直接就通过rt_device_find
函数找到了设备。能够找到的原因是在上电初始化的阶段对该设备进行了注册,通俗的说就是告诉操作系统我有这样一个设备啦,你可以用啦!这样就可以通过上层的通用函数调用,看串口驱动的注册过程。
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;
/* 获取设备指针 */
device = &(serial->parent);
/* 设备类型为字符设备 */
device->type = RT_Device_Class_Char;
device->rx_indicate = RT_NULL;
device->tx_complete = RT_NULL;
/* 这里将串口特有的操作挂接到设备的方法上,这样上层调用open函数时就会调
用串口特有的操作方法这个在高级语言上叫做多态 */
#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;
/* 调用公用的设备注册接口 */
ret = rt_device_register(device, name, flag);
#if defined(RT_USING_POSIX)
/* set fops */
device->fops = &_serial_fops;
#endif
return ret;
}
这样注册之后,用户调用通用接口open
read
write
(在RT-Thread中叫做rt_device_open
rt_device_read
rt_device_write
)时,实际上就是调用的rt_serial_open
rt_serial_read
rt_serial_write
可以看出这里就实现了软件上的分层,将内核程序与用户程序分离。
那么Hello RT-Thread\n
这个字符串是如何发送的呢?
rt_size_t rt_device_write(rt_device_t dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
{
/* 调用对应的接口,在这里就是调用rt_serial_write函数 */
if (device_write != RT_NULL)
{
/* device_write 是个宏,见下文,调用注册的接口 */
return device_write(dev, pos, buffer, size);
}
return 0;
}
#ifdef RT_USING_DEVICE_OPS
#define device_init (dev->ops->init)
#define device_open (dev->ops->open)
#define device_close (dev->ops->close)
#define device_read (dev->ops->read)
#define device_write (dev->ops->write)
#define device_control (dev->ops->control)
#else
#define device_init (dev->init)
#define device_open (dev->open)
#define device_close (dev->close)
#define device_read (dev->read)
#define device_write (dev->write)
#define device_control (dev->control)
#endif
所以数据最终是由rt_serial_write
发送出去。
static rt_size_t rt_serial_write(struct rt_device *dev,
rt_off_t pos,
const void *buffer,
rt_size_t size)
{
struct rt_serial_device *serial;
serial = (struct rt_serial_device *)dev;
/* 根据打开的方式不同选择不同的发送函数 */
if (dev->open_flag & RT_DEVICE_FLAG_INT_TX)
{
/* 中断方式发送 */
return _serial_int_tx(serial, (const rt_uint8_t *)buffer, size);
}
#ifdef RT_SERIAL_USING_DMA
else if (dev->open_flag & RT_DEVICE_FLAG_DMA_TX)
{
/* DMA方式发送 */
return _serial_dma_tx(serial, (const rt_uint8_t *)buffer, size);
}
#endif /* RT_SERIAL_USING_DMA */
else
{
/* 轮询方式发送 */
return _serial_poll_tx(serial, (const rt_uint8_t *)buffer, size);
}
}
由于rt_device_open(uart1, RT_DEVICE_FLAG_INT_RX)
时候,打开方式只有中断接收所以默认是以轮询方式发送出去的。轮询方式发送比较简单,就是调用最底层的字符发送函数发送数据。
rt_inline int _serial_poll_tx(struct rt_serial_device *serial,
const rt_uint8_t *data, int length)
{
int size;
RT_ASSERT(serial != RT_NULL);
size = length;
while (length)
{
/* 流控方式打开的话,遇到换行自动添加一个回车字符,使光标回到行首 */
if (*data == '\n' && (serial->parent.open_flag & RT_DEVICE_FLAG_STREAM))
{
serial->ops->putc(serial, '\r');
}
/* 调用下层的单个字符发送函数 */
serial->ops->putc(serial, *data);
++ data;
-- length;
}
/* 返回实际写的字节数 */
return size - length;
}
这样字符串就一个个的被发送了出去了。那么putc
这个方法谁提供呢?很明显这是由具体的硬件决定的。
那么与串口相关的这些接口又做了一些什么事情呢?以rt_serial_read
为例进行说明,从下面代码中可以看出该函数,以dev->open_flag
的不同分为不同的方式,有中断接收,DMA接收和轮询接收。
static rt_size_t rt_serial_read(struct rt_device *dev,
rt_off_t pos,
void *buffer,
rt_size_t size)
{
struct rt_serial_device *serial;
RT_ASSERT(dev != RT_NULL);
if (size == 0) return 0;
/* 获取串口设备指针,这里之所以能够强制转换,是由于rt_serial_device
继承于rt_device ,且rt_device 是 rt_serial_device 的第一个成员 */
serial = (struct rt_serial_device *)dev;
/* 判断设备的打开方式,中断方式打开 */
if (dev->open_flag & RT_DEVICE_FLAG_INT_RX)
{
return _serial_int_rx(serial, (rt_uint8_t *)buffer, size);
}
#ifdef RT_SERIAL_USING_DMA
/* 判断设备的打开方式,DMA方式打开 */
else if (dev->open_flag & RT_DEVICE_FLAG_DMA_RX)
{
return _serial_dma_rx(serial, (rt_uint8_t *)buffer, size);
}
#endif /* RT_SERIAL_USING_DMA */
/* 判断设备的打开方式,轮询方式打开 */
return _serial_poll_rx(serial, (rt_uint8_t *)buffer, size);
}
中断方式的接收函数(这是一个内联函数),在一个系统中一般都会运行好多个任务,多任务运行可能会导致去读收到的字节时,已经被下一个收到的字节覆盖,这就会出现错误。用一个缓冲区是一个有效的解决办法,在RT-Thread中定义了rt_serial_rx_fifo
,从中断接收到的字符先被存在该缓冲区中,用户读这些数据时,实际上是从缓冲区中读数据。
rt_inline int _serial_int_rx(struct rt_serial_device *serial, rt_uint8_t *data, int length)
{
int size;
struct rt_serial_rx_fifo* rx_fifo;
/* 备份要读取的数据长度 */
size = length;
/* 获得rt_serial_rx_fifo指针 */
rx_fifo = (struct rt_serial_rx_fifo*) serial->serial_rx;
/* 从FIFO中读取数据,存放在data中,读取的数据长度为length */
while (length)
{
int ch;
rt_base_t level;
/* disable interrupt */
level = rt_hw_interrupt_disable();
/* FIFO中没有数据,直接跳出循环 */
if ((rx_fifo->get_index == rx_fifo->put_index) &&
(rx_fifo->is_full == RT_FALSE))
{
rt_hw_interrupt_enable(level);
break;
}
/* 从FIFO中获取一字节数据 */
ch = rx_fifo->buffer[rx_fifo->get_index];
rx_fifo->get_index += 1;
if (rx_fifo->get_index >= serial->config.bufsz) rx_fifo->get_index = 0;
if (rx_fifo->is_full == RT_TRUE)
{
rx_fifo->is_full = RT_FALSE;
}
rt_hw_interrupt_enable(level);
/* 获取的数据赋值给data,传递给上层 */
*data = ch & 0xff;
data ++; length --;
}
/* 返回实际读到的长度 */
return size - length;
}
轮询方式接收(也是一个内联函数),顾名思义,这种方式就是不停的去读。实际的过程也比较直接,就是从串口中读取数据,调用的是rt_uart_ops
中的getc
方法。这样就需要不同的硬件平台提供不同的对应方法,这里也实现了分层。这样呢,串口相关的分了三层,从上到下以此是 设备层
串口设备层
硬件层
。最终调用到底层硬件。
rt_inline int _serial_poll_rx(struct rt_serial_device *serial,
rt_uint8_t *data,
int length)
{
int ch;
int size;
size = length;
while (length)
{
/* 从串口中读取一个字符 */
ch = serial->ops->getc(serial);
/* 读取的是-1的话,跳出 */
if (ch == -1) break;
/* 将读到的数据,返回给上层 */
*data = ch;
data ++; length --;
/* 如果是换行,跳出 */
if (ch == '\n') break;
}
/* 返回实际读的字节数 */
return size - length;
}
相关的结构体定义在 serial.h文件中,具体如下:
/* 波特率定义 */
#define BAUD_RATE_2400 2400
#define BAUD_RATE_4800 4800
#define BAUD_RATE_9600 9600
#define BAUD_RATE_19200 19200
#define BAUD_RATE_38400 38400
#define BAUD_RATE_57600 57600
#define BAUD_RATE_115200 115200
#define BAUD_RATE_230400 230400
#define BAUD_RATE_460800 460800
#define BAUD_RATE_921600 921600
#define BAUD_RATE_2000000 2000000
#define BAUD_RATE_3000000 3000000
/* 数据位长度 */
#define DATA_BITS_5 5
#define DATA_BITS_6 6
#define DATA_BITS_7 7
#define DATA_BITS_8 8
#define DATA_BITS_9 9
/* 停止位 */
#define STOP_BITS_1 0
#define STOP_BITS_2 1
#define STOP_BITS_3 2
#define STOP_BITS_4 3
/* 校验位 */
#define PARITY_NONE 0
#define PARITY_ODD 1
#define PARITY_EVEN 2
/* 数据位方向 */
#define BIT_ORDER_LSB 0
#define BIT_ORDER_MSB 1
#define NRZ_NORMAL 0
#define NRZ_INVERTED 1
/* 默认缓冲区大小 */
#ifndef RT_SERIAL_RB_BUFSZ
#define RT_SERIAL_RB_BUFSZ 64
#endif
/* 自定义事件标志 */
#define RT_SERIAL_EVENT_RX_IND 0x01 /* Rx indication */
#define RT_SERIAL_EVENT_TX_DONE 0x02 /* Tx complete */
#define RT_SERIAL_EVENT_RX_DMADONE 0x03 /* Rx DMA transfer done */
#define RT_SERIAL_EVENT_TX_DMADONE 0x04 /* Tx DMA transfer done */
#define RT_SERIAL_EVENT_RX_TIMEOUT 0x05 /* Rx timeout */
/* DMA相关 */
#define RT_SERIAL_DMA_RX 0x01
#define RT_SERIAL_DMA_TX 0x02
/* 中断相关 */
#define RT_SERIAL_RX_INT 0x01
#define RT_SERIAL_TX_INT 0x02
/* 错误标志 */
#define RT_SERIAL_ERR_OVERRUN 0x01
#define RT_SERIAL_ERR_FRAMING 0x02
#define RT_SERIAL_ERR_PARITY 0x03
#define RT_SERIAL_TX_DATAQUEUE_SIZE 2048
#define RT_SERIAL_TX_DATAQUEUE_LWM 30
/* 默认配置宏 */
#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;
};
/* 软件接收FIFO结构体 */
struct rt_serial_rx_fifo
{
/* 指向数据存储区 */
rt_uint8_t *buffer;
/* 存取标志位 */
rt_uint16_t put_index, get_index;
/* 满标志位 */
rt_bool_t is_full;
};
/* 发送FIFO结构体 */
struct rt_serial_tx_fifo
{
struct rt_completion completion;
};
/* DMA接收 */
struct rt_serial_rx_dma
{
rt_bool_t activated;
};
/* DMA发送 */
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;
/* 串口操作集,包括配置、控制、发送一个字符、接收一个字符、DAM传输,由具体的硬件实现 */
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, rt_uint8_t *buf, rt_size_t size, int direction);
};
/* 中断相关的 */
void rt_hw_serial_isr(struct rt_serial_device *serial, int event);
/* 串行设备注册 */
rt_err_t rt_hw_serial_register(struct rt_serial_device *serial,
const char *name,
rt_uint32_t flag,
void *data);