通过前面对RT-Thread设备模型框架,以及UART、IIC、SPI 等设备驱动实现过程的介绍,我们应该对驱动分层思想并不陌生了。操作系统为什么对设备驱动采用分层管理呢?驱动分层有什么好处呢?
在介绍驱动分层的好处前,我们先看看著名的TCP/IP协议栈分层模型:
TCP/IP协议栈每层都有自己独特的作用:
类比TCP/IP协议的分层思想,不难得知操作系统驱动分层的好处,再参照RT-Thread I/O 设备模型框架:
RT-Thread I/O设备模型框架位于硬件和应用程序之间,共分成三层,每一层也都有自己的作用:
协议和驱动虽然分层管理,每一层专注完成自己的事情,但层与层之间想要协同工作,还需要留出各自的接口用来交互资源信息等,最常见的接口就是设备对象的注册或初始化,当然也包括设备访问接口的注册(包含在设备对象的注册过程中)。RT-Thread创建设备对象、向上面的 I/O 设备管理器注册设备对象和访问接口、应用程序通过设备对象和接口访问设备的一般过程如下所示:
创建设备实际上就是在设备驱动层创建一个设备对象,并将该硬件设备的属性信息(比如基地址、中断号、时钟、DMA等)与访问接口保存到该设备对象结构体或类中,将初始化后的设备对象向上注册到 I/O 设备管理器中。
既然可以将设备对象向上注册,又可以从上层访问该设备对象,这就要求不同层级描述设备的结构体或类相互兼容,存在自上而下的继承关系。上层保存同类设备的通用属性和访问接口,下层增加硬件设备的专有属性并完成硬件访问接口的实现,就像下面这样:
下面分别用前面介绍过的串口设备、I2C设备、SPI设备为例,回顾下具体的设备驱动是如何将驱动分层思想实现在代码中的。
每一层都有自己的设备描述结构和接口函数集合,每一层的接口函数集合由低一层实现并注册。在当前层直接调用,用来实现更上层的接口函数集合。
串口设备的驱动框架层提供的设备描述结构和接口函数集合如下:
// .\rt-thread-4.0.1\components\drivers\include\drivers\serial.h
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;
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);
};
串口设备框架层使用rt_uart_ops接口函数实现 I / O 设备管理层的接口函数集合rt_device_ops,并将实现的接口函数集合serial_ops注册到上层的过程如下:
// .\rt-thread-4.0.1\components\drivers\serial\serial.c
#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops serial_ops =
{
rt_serial_init,
rt_serial_open,
rt_serial_close,
rt_serial_read,
rt_serial_write,
rt_serial_control
};
#endif
/*
* serial register
*/
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
......
#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;
}
接口函数集合serial_ops中的函数实现最终都是通过调用rt_uart_ops接口函数实现的,rt_uart_ops接口函数则由更底层的UART设备驱动层来实现并注册。
串口设备驱动层提供的设备描述结构如下:
// .\libraries\HAL_Drivers\drv_usart.h
/* stm32 uart dirver class */
struct stm32_uart
{
UART_HandleTypeDef handle;
struct stm32_uart_config *config;
#ifdef RT_SERIAL_USING_DMA
struct
{
DMA_HandleTypeDef handle;
rt_size_t last_index;
} dma;
#endif
rt_uint8_t uart_dma_flag;
struct rt_serial_device serial;
};
// .\libraries\HAL_Drivers\drv_usart.c
static struct stm32_uart uart_obj[sizeof(uart_config) / sizeof(uart_config[0])] = {0};
static struct stm32_uart_config uart_config[] =
{
#ifdef BSP_USING_UART1
{
.name = "uart1",
.Instance = USART1,
.irq_type = USART1_IRQn,
}
#endif
......
};
stm32_uart结构中包含的UART_HandleTypeDef由芯片厂商ST提供的标准库HAL提供,UART驱动层用以实现上层rt_uart_ops接口函数的基础函数也由HAL库提供,可能用到的HAL库函数如下:
// .\libraries\STM32L4xx_HAL\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_uart.h
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DeInit(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);
HAL_UART_StateTypeDef HAL_UART_GetState(UART_HandleTypeDef *huart);
uint32_t HAL_UART_GetError(UART_HandleTypeDef *huart);
串口设备驱动层使用HAL库函数实现串口设备框架层的接口函数集合rt_uart_ops,并将实现的接口函数集合stm32_uart_ops注册到上层的过程如下:
// .\libraries\HAL_Drivers\drv_usart.c
static const struct rt_uart_ops stm32_uart_ops =
{
.configure = stm32_configure,
.control = stm32_control,
.putc = stm32_putc,
.getc = stm32_getc,
};
int rt_hw_usart_init(void)
{
rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
rt_err_t result = 0;
stm32_uart_get_dma_config();
for (int i = 0; i < obj_num; i++)
{
uart_obj[i].config = &uart_config[i];
uart_obj[i].serial.ops = &stm32_uart_ops;
uart_obj[i].serial.config = config;
#if defined(RT_SERIAL_USING_DMA)
if(uart_obj[i].uart_dma_flag)
{
/* register UART device */
result = rt_hw_serial_register(&uart_obj[i].serial,uart_obj[i].config->name,
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX| RT_DEVICE_FLAG_DMA_RX
,&uart_obj[i]);
}
else
#endif
{
/* register UART device */
result = rt_hw_serial_register(&uart_obj[i].serial,uart_obj[i].config->name,
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX
,&uart_obj[i]);
}
RT_ASSERT(result == RT_EOK);
}
return result;
}
函数rt_hw_usart_init内部有一个for循环,可以将所有启用的串口设备全部初始化并注册到 I/O 设备管理层。启用串口设备也比较简单,先使用CubeMX配置好要启用的串口设备引脚,再在Kconfig和menuconfig中配置并使能相关的宏定义就可以了。rt_hw_usart_init函数已经在board.c文件中的rt_hw_board_init()函数内部被调用(需要定义宏RT_USING_SERIAL),在博客系统启动与初始化过程中有介绍。
到这里就可以通过 I/O 设备管理接口rt_device_ops来访问uart串口设备了,还记得rt_device设备描述结构中有两个中断回调函数,I/O设备管理层也有两个函数接口分别用来设置这两个可由用户自定义的中断回调函数吗?
// .\rt-thread-4.0.1\include\rtdef.h
/* Device structure */
struct rt_device
{
......
/* device call back */
rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);
#ifdef RT_USING_DEVICE_OPS
const struct rt_device_ops *ops;
......
};
// .\rt-thread-4.0.1\include\rtthread.h
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_err_t
rt_device_set_tx_complete(rt_device_t dev,
rt_err_t (*tx_done)(rt_device_t dev, void *buffer));
当我们调用这两个接口函数设置自定义的中断回调函数后,驱动层是如何实现中断调用的呢?我们在驱动框架层源文件“serial.c”中查找关键字“rx_indicate”,发现其只在函数rt_hw_serial_isr()中被调用,部分代码如下:
// .\rt-thread-4.0.1\components\drivers\serial\serial.c
/* 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:
{
......
while (1)
{
......
/* invoke callback */
if (serial->parent.rx_indicate != RT_NULL)
{
......
if (rx_length)
{
serial->parent.rx_indicate(&serial->parent, rx_length);
}
}
break;
}
case RT_SERIAL_EVENT_TX_DONE:
......
#ifdef RT_SERIAL_USING_DMA
case RT_SERIAL_EVENT_TX_DMADONE:
{
......
/* 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:
{
......
if (serial->config.bufsz == 0)
{
......
serial->parent.rx_indicate(&(serial->parent), length);
rx_dma->activated = RT_FALSE;
}
else
{
......
/* invoke callback */
if (serial->parent.rx_indicate != RT_NULL)
{
serial->parent.rx_indicate(&(serial->parent), length);
}
}
break;
}
#endif /* RT_SERIAL_USING_DMA */
}
}
要想实现中断回调功能,函数rt_hw_serial_isr()也应该在底层被调用,我们在串口驱动层源文件“drv_usart.c”中查找关键词“rt_hw_serial_isr”,发现其在两个函数uart_isr()和HAL_UART_RxCpltCallback()中被调用,后面这个是HAL库函数,在完成UART接收后会自动被调用,我们看前面这个函数uart_isr()被谁调用呢?
// .\libraries\HAL_Drivers\drv_usart.c
static void uart_isr(struct rt_serial_device *serial)
{
......
/* UART in mode Receiver -------------------------------------------------*/
if ((__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_RXNE) != RESET))
{
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_IND);
}
#ifdef RT_SERIAL_USING_DMA
else if ((uart->uart_dma_flag) && (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_IDLE) != RESET) &&
(__HAL_UART_GET_IT_SOURCE(&(uart->handle), UART_IT_IDLE) != RESET))
{
......
if (recv_len)
{
rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE | (recv_len << 8));
}
__HAL_UART_CLEAR_IDLEFLAG(&uart->handle);
}
#endif
......
}
#if defined(BSP_USING_UART1)
void USART1_IRQHandler(void)
{
/* enter interrupt */
rt_interrupt_enter();
uart_isr(&(uart_obj[UART1_INDEX].serial));
/* leave interrupt */
rt_interrupt_leave();
}
......
由上面的代码可以看出,函数uart_isr()最终被UART中断请求函数USARTx_IRQHandler()调用(每个启用的UART中断请求函数都会调用),函数USARTx_IRQHandler()也是HAL库函数提供的,在UART设备触发中断后会被自动调用。
在串口设备中设置接收回调函数rx_indicate()非常常见,比如在finsh shell组件中,设置finsh shell的交互设备为串口设备,就需要设置接收回调函数。当用户输入数据后会触发UART中断,自动调用用户设置的接收回调函数,及时读取用户输入的数据,而不用进行低效的轮询检查。finsh shell组件设置交互设备和接收回调函数的过程代码如下:
// .\rt-thread-4.0.1\components\finsh\shell.c
void finsh_set_device(const char *device_name)
{
rt_device_t dev = RT_NULL;
RT_ASSERT(shell != RT_NULL);
dev = rt_device_find(device_name);
if (dev == RT_NULL)
{
rt_kprintf("finsh: can not find device: %s\n", device_name);
return;
}
/* check whether it's a same device */
if (dev == shell->device) return;
/* open this device and set the new device in finsh shell */
if (rt_device_open(dev, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX | \
RT_DEVICE_FLAG_STREAM) == RT_EOK)
{
if (shell->device != RT_NULL)
{
/* close old finsh device */
rt_device_close(shell->device);
rt_device_set_rx_indicate(shell->device, RT_NULL);
}
/* clear line buffer before switch to new device */
memset(shell->line, 0, sizeof(shell->line));
shell->line_curpos = shell->line_position = 0;
shell->device = dev;
rt_device_set_rx_indicate(dev, finsh_rx_ind);
}
}
static rt_err_t finsh_rx_ind(rt_device_t dev, rt_size_t size)
{
RT_ASSERT(shell != RT_NULL);
/* release semaphore to let finsh thread rx data */
rt_sem_release(&shell->rx_sem);
return RT_EOK;
}
static int finsh_getchar(void)
{
#ifdef RT_USING_POSIX
return getchar();
#else
char ch = 0;
RT_ASSERT(shell != RT_NULL);
while (rt_device_read(shell->device, -1, &ch, 1) != 1)
rt_sem_take(&shell->rx_sem, RT_WAITING_FOREVER);
return (int)ch;
#endif
}
UART是一种比较简单的双工对等总线,没有主从之分,所以在串口设备描述结构中并没有专门区分串口总线与串口设备。由于UART在收发数据时不需要同步时钟线,是一种异步传输协议,双方进行通信时需要约定一致的波特率,常见的波特率比如115200bps,通信速率并不高,由于协议简单,常用于调试接口输出log信息。
STM32平台由于软件模拟IIC比较常用,且方便移植,RT-Thread的IIC设备驱动也是使用的软件模拟方式实现的。使用软件模拟I2C协议,虽然可以根据需要灵活配置模拟IIC通信的GPIO引脚,但协议层需要自己实现,而且软件模拟IIC工作效率比硬件IIC低不少,一般常用400kbps传输速率。
I2C设备是一种单工非对等总线,所以在I2C设备描述结构中对I2C总线与I2C设备分别描述,I2C设备驱动框架只对I2C总线进行了描述,I2C设备在具体的设备描述结构中出现,比如AHT10温湿度传感器的设备描述结构“aht10_device”就是一个I2C设备,aht10_device结构体继承自rt_i2c_bus_device。
I2C协议的设备驱动框架层比UART协议更复杂,我们可以将其再细分为三层,分别为bus_dev layer、core layer、bit-ops layer三层,这三层的顺序从高到低:
I2C总线设备的描述结构与接口函数集合如下:
// .\rt-thread-4.0.1\components\drivers\include\drivers\i2c.h
/*for i2c bus driver*/
struct rt_i2c_bus_device
{
struct rt_device parent;
const struct rt_i2c_bus_device_ops *ops;
rt_uint16_t flags;
rt_uint16_t addr;
struct rt_mutex lock;
rt_uint32_t timeout;
rt_uint32_t retries;
void *priv;
};
struct rt_i2c_bus_device_ops
{
rt_size_t (*master_xfer)(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_size_t (*slave_xfer)(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_err_t (*i2c_bus_control)(struct rt_i2c_bus_device *bus,
rt_uint32_t,
rt_uint32_t);
};
由于I2C总线支持多主多从(同一时刻只允许有一个主设备)方式工作,通信时涉及到从设备寻址与读写标记等,需要一个专门的数据结构来描述I2C通信的消息帧,从上面的I2C总线接口函数参数类型可以看出,I2C消息帧结构名为rt_i2c_msg,描述代码如下:
// .\rt-thread-4.0.1\components\drivers\include\drivers\i2c.h
struct rt_i2c_msg
{
rt_uint16_t addr;
rt_uint16_t flags;
rt_uint16_t len;
rt_uint8_t *buf;
};
// flags type
#define RT_I2C_WR 0x0000
#define RT_I2C_RD (1u << 0)
#define RT_I2C_ADDR_10BIT (1u << 2) /* this is a ten bit chip address */
#define RT_I2C_NO_START (1u << 4)
#define RT_I2C_IGNORE_NACK (1u << 5)
#define RT_I2C_NO_READ_ACK (1u << 6) /* when I2C reading, we do not ACK */
从这里可以看出,I2C设备访问接口用来传递读写数据的参数结构一般为rt_i2c_msg或者将消息帧结构体中的各个成员都作为参数传入,I2C core layer实现的I2C设备访问接口如下:
// .\rt-thread-4.0.1\components\drivers\include\drivers\i2c.h
rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_size_t rt_i2c_master_send(struct rt_i2c_bus_device *bus,
rt_uint16_t addr,
rt_uint16_t flags,
const rt_uint8_t *buf,
rt_uint32_t count);
rt_size_t rt_i2c_master_recv(struct rt_i2c_bus_device *bus,
rt_uint16_t addr,
rt_uint16_t flags,
rt_uint8_t *buf,
rt_uint32_t count);
当底层驱动都实现后,用户可以直接调用上面的接口函数来访问I2C设备,但该接口函数的参数与 I/O 设备管理接口并不兼容,因此RT-Thread增加了bus_dev layer用来实现两种接口之间的适配。
bus_dev layer跟前面介绍的串口驱动框架层类似,也是实现并向上层注册 I/O管理接口,只不过用以实现 I/O 管理接口的函数并非是总线访问接口函数rt_i2c_bus_device_ops,而是下面的I2C core layer利用rt_i2c_bus_device_ops实现的接口函数(接口函数原型见上面的代码)。
该层实现并向上层注册 I/O管理接口的过程如下:
// .\rt-thread-4.0.1\components\drivers\i2c\i2c_dev.c
#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops i2c_ops =
{
RT_NULL,
RT_NULL,
RT_NULL,
i2c_bus_device_read,
i2c_bus_device_write,
i2c_bus_device_control
};
#endif
rt_err_t rt_i2c_bus_device_device_init(struct rt_i2c_bus_device *bus,
const char *name)
{
struct rt_device *device;
RT_ASSERT(bus != RT_NULL);
device = &bus->parent;
device->user_data = bus;
/* set device type */
device->type = RT_Device_Class_I2CBUS;
/* initialize device interface */
#ifdef RT_USING_DEVICE_OPS
device->ops = &i2c_ops;
#else
......
#endif
/* register to device manager */
rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);
return RT_EOK;
}
static rt_size_t i2c_bus_device_read(rt_device_t dev,
rt_off_t pos,
void *buffer,
rt_size_t count)
{
......
addr = pos & 0xffff;
flags = (pos >> 16) & 0xffff;
return rt_i2c_master_recv(bus, addr, flags, buffer, count);
}
static rt_size_t i2c_bus_device_write(rt_device_t dev,
rt_off_t pos,
const void *buffer,
rt_size_t count)
{
......
addr = pos & 0xffff;
flags = (pos >> 16) & 0xffff;
return rt_i2c_master_send(bus, addr, flags, buffer, count);
}
从上面的代码可以看出,当使用 I/O 设备管理接口访问I2C设备时,参数pos就要特别设置了,32位的pos应包含 i2c设备的addr与flags信息(uart设备没有使用pos参数)。
I2C core layer理解起来比较简单,就是利用底层提供的接口函数rt_i2c_bus_device_ops,实现bus_dev layer需要的接口函数,当然I2C core layer实现的接口函数也可以被用户直接调用。该层接口函数实现并向上层注册的过程如下:
// .\rt-thread-4.0.1\components\drivers\i2c\i2c_core.c
rt_err_t rt_i2c_bus_device_register(struct rt_i2c_bus_device *bus,
const char *bus_name)
{
......
res = rt_i2c_bus_device_device_init(bus, bus_name);
return res;
}
rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num)
{
rt_size_t ret;
if (bus->ops->master_xfer)
{
for (ret = 0; ret < num; ret++)
{
rt_mutex_take(&bus->lock, RT_WAITING_FOREVER);
ret = bus->ops->master_xfer(bus, msgs, num);
rt_mutex_release(&bus->lock);
return ret;
}
else
return 0;
}
rt_size_t rt_i2c_master_send(struct rt_i2c_bus_device *bus,
rt_uint16_t addr,
rt_uint16_t flags,
const rt_uint8_t *buf,
rt_uint32_t count)
{
......
ret = rt_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}
rt_size_t rt_i2c_master_recv(struct rt_i2c_bus_device *bus,
rt_uint16_t addr,
rt_uint16_t flags,
rt_uint8_t *buf,
rt_uint32_t count)
{
......
ret = rt_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}
bit-ops layer主要实现I2C协议的软件模拟,使用 I2C 设备驱动层提供的接口函数rt_i2c_bit_ops实现 I2C 总线设备访问接口rt_i2c_bus_device_ops,I2C总线协议通讯时序图可以参考文件I2C总线规范,这里主要介绍驱动分层思想,就不赘述协议实现了。bit-ops layer向core layer注册接口函数rt_i2c_bus_device_ops的过程如下:
// .\rt-thread-4.0.1\components\drivers\i2c\i2c-bit-ops.c
static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
i2c_bit_xfer,
RT_NULL,
RT_NULL
};
rt_err_t rt_i2c_bit_add_bus(struct rt_i2c_bus_device *bus,
const char *bus_name)
{
bus->ops = &i2c_bit_bus_ops;
return rt_i2c_bus_device_register(bus, bus_name);
}
// .\rt-thread-4.0.1\components\drivers\include\drivers\i2c-bit-ops.h
struct rt_i2c_bit_ops
{
void *data; /* private data for lowlevel routines */
void (*set_sda)(void *data, rt_int32_t state);
void (*set_scl)(void *data, rt_int32_t state);
rt_int32_t (*get_sda)(void *data);
rt_int32_t (*get_scl)(void *data);
void (*udelay)(rt_uint32_t us);
rt_uint32_t delay_us; /* scl and sda line delay */
rt_uint32_t timeout; /* in tick */
};
bit-ops layer使用的接口函数rt_i2c_bit_ops就要靠底层的I2C驱动层提供了,rt_i2c_bit_ops接口函数主要是对SDA、SCL引脚状态的获取与设置(也即对GPIO引脚电平的读取与设置),加上I2C协议时许图中引脚状态转换需要的延时函数等。
I2C驱动层的设备描述结构如下:
// .\libraries\HAL_Drivers\drv_soft_i2c.h
/* stm32 i2c dirver class */
struct stm32_i2c
{
struct rt_i2c_bit_ops ops;
struct rt_i2c_bus_device i2c2_bus;
};
// .\libraries\HAL_Drivers\drv_soft_i2c.c
static struct stm32_i2c i2c_obj[sizeof(soft_i2c_config) / sizeof(soft_i2c_config[0])];
static const struct stm32_soft_i2c_config soft_i2c_config[] =
{
#ifdef BSP_USING_I2C1
{
.scl = BSP_I2C1_SCL_PIN,
.sda = BSP_I2C1_SDA_PIN,
.bus_name = "i2c1",
}
#endif
......
};
......
既然使用了I2C软件模拟协议,可灵活配置I2C通讯引脚SCL_PIN与SDA_PIN,我们在使用I2C协议前就需要配置这两个引脚的GPIO编号。
I2C设备驱动层是配置软件模拟I2C设备的GPIO引脚,对SDA与SCL引脚的管理是使用RT-Thread提供的pin设备驱动接口,相当于I2C软件模拟协议是在pin设备上层的,pin设备的驱动分层可以参考博客PIN设备对象管理。
I2C设备驱动层实现并向驱动框架层注册接口函数rt_i2c_bit_ops的过程如下:
// .\libraries\HAL_Drivers\drv_soft_i2c.c
static const struct rt_i2c_bit_ops stm32_bit_ops_default =
{
.data = RT_NULL,
.set_sda = stm32_set_sda,
.set_scl = stm32_set_scl,
.get_sda = stm32_get_sda,
.get_scl = stm32_get_scl,
.udelay = stm32_udelay,
.delay_us = 1,
.timeout = 100
};
/* I2C initialization function */
int rt_hw_i2c_init(void)
{
rt_size_t obj_num = sizeof(i2c_obj) / sizeof(struct stm32_i2c);
rt_err_t result;
for (int i = 0; i < obj_num; i++)
{
i2c_obj[i].ops = stm32_bit_ops_default;
i2c_obj[i].ops.data = (void*)&soft_i2c_config[i];
i2c_obj[i].i2c2_bus.priv = &i2c_obj[i].ops;
stm32_i2c_gpio_init(&i2c_obj[i]);
result = rt_i2c_bit_add_bus(&i2c_obj[i].i2c2_bus, soft_i2c_config[i].bus_name);
RT_ASSERT(result == RT_EOK);
stm32_i2c_bus_unlock(&soft_i2c_config[i]);
}
return RT_EOK;
}
INIT_BOARD_EXPORT(rt_hw_i2c_init);
从上面的代码可以看出,I2C设备支持自动初始化,用户在Kconfig和menuconfig中配置好I2C软件模拟设备的GPIO引脚与相应的宏定义后,就可以使用I2C总线设备访问接口或 I/O 设备管理层统一接口来访问I2C设备了。
RT-Thread为I2C设备提供的驱动并不支持中断响应,因此也不支持设置中断回调函数,只能由I2C总线设备主动读写指定I2C从设备。当然也可以在应用中设置一个缓冲区,为I2C设备轮询访问创建一个线程,将轮询读取的I2C设备数据保存到缓冲区。
从前面 I/O 设备管理器分层模型可以看出,I2C驱动与SPI驱动都分为总线类型和设备类型,UART驱动则没有作此区分,这是为何呢?
前面已经介绍过,UART串口属于双工对等总线,没有主从之分,所以在串口设备描述结构中并没有专门区分串口总线与串口设备。I2C与SPI都属于非对等总线,支持一主多从方式工作,也即多个从设备可以共用一组I2C或SPI总线进行通信,共用同一组总线进行通信的多个从设备访问接口一样,也有很多共有的属性配置信息。因此抽取出总线类型作为主机控制器来管理多个从设备的通信,可以带来跟驱动分层类似的好处,这就是主机控制器与外设的分离思想。
什么是总线呢?总线实际上是处理器和一个或多个设备之间进行数据传输的通道,所有的设备都通过总线与CPU进行通信。假设有一个叫 GITCHAT 的网卡,它需要连接到 CPU 的内部总线上,需要地址总线、数据总线和控制总线,以及中断 pin 脚等。外接的 GITCHAT 网卡硬件称为外设或从设备,连接到SOC上的一组 I/O 引脚,这组 I/O 引脚实际就是SOC引出的一组总线(比如SPI总线或SDIO总线),程序设计中可以把这组总线封装为一个主机控制器,作为一个设备对象来管理连接在其上面的多个外设。SOC内CPU与GITCHAT主机控制器直接的总线连接示意图如下(图片取自博客:Linux 总线、设备、驱动模型的探究):
由此可见,总线类型或主机控制器是跟SOC或CPU芯片平台相关的,外设或从设备是跟具体的硬件设备相关的。我们在项目开发过程中,不仅外接的硬件设备(比如各种sensor或flash chip)型号会经常更换,我们使用的CPU或SOC主芯片型号也常会更换,如果将主机控制器驱动与外设驱动分开描述管理,能在更换CPU/SOC和硬件外设时,更方便的进行驱动移植,极大减少驱动新增开发量。反之,如果将主机控制器驱动与外设驱动耦合到一块开发,外设驱动将和SOC/CPU强相关,当更换CPU/SOC时外设驱动需要全部更改,对驱动开发管理很不友好。
随着芯片与外设总类的增加,系统的拓扑结构也越来越复杂,对热插拔、跨平台移植性的要求也越来越高,为适应这种形势的需要,从Linux 2.6内核开始提供了全新的设备模型,其优势在于采用了总线的模型对设备与驱动进行了管理,提高了程序的可移植性。RT-Thread在设计时,更多的借鉴了Linux的设计思想,当然也吸收了Linux的总线设备驱动模型,Linux的总线设备模型示意图如下(图片取自博客:驱动程序分层分离概念-总线设备驱动模型):
Linux连接的外设通常比较复杂,有些外设并不像一般的sensor或flash chip那样,只需要单一的总线协议(比如I2C或SPI总线)完成数据访问即可。比如WiFi网卡外设,虽然与CPU/SOC之间通过一种总线协议(比如SDIO总线)进行通信,但在SDIO总线协议之上还需要专门的WiFi驱动才能使该无线网卡正常工作,该WiFi驱动也是跟外设硬件相关的,但常常一个WiFi驱动可以兼容同一系列多个型号的WiFi网卡,因此Linux把驱动与外设也分开管理,总线、驱动、外设共同构成了Linux的总线设备驱动模型,总线则负责外设与驱动之间的匹配。
RT-Thread比较复杂的协议(比如SDIO驱动、USB驱动)都使用Linux的总线设备驱动模型,比较简单的协议(比如I2C驱动、SPI驱动)则对上面的总线设备驱动模型进行简化,只保留了总线类型与设备类型,并没有将驱动专门分离出来进行管理。
总线、设备、驱动三者都是跟硬件相关的,我们在进行驱动开发时,三者都需要完成对象创建、设备注册等工作,下面先以简单的SPI驱动为例,回顾下具体的设备驱动是如何将驱动分层与主从分离思想实现在代码中的。总线设备驱动模型相对复杂,后面在介绍SDIO驱动WiFi无线网卡时再详细介绍总线设备驱动模型的实现。
SPI设备是一种双工非对等总线协议,支持一主多从方式工作,因此跟I2C协议类似,对SPI总线设备与SPI从设备分别进行描述。I2C设备通过消息帧包含的地址信息寻找目标从设备,SPI设备则通过片选信号线NCS的电平状态来选择目标从设备。
SPI设备驱动框架层跟I2C设备类似,也可以将其细分为两层,也即bus_dev layer和core layer,SPI设备并不需要软件模拟协议实现,所以不需要I2C设备中的bit-ops layer,这两层的功能跟I2C设备类似:
SPI总线设备描述结构与总线接口函数集合如下:
// .\rt-thread-4.0.1\components\drivers\include\drivers\spi.h
struct rt_spi_bus
{
struct rt_device parent;
rt_uint8_t mode;
const struct rt_spi_ops *ops;
struct rt_mutex lock;
struct rt_spi_device *owner;
};
/**
* SPI operators
*/
struct rt_spi_ops
{
rt_err_t (*configure)(struct rt_spi_device *device, struct rt_spi_configuration *configuration);
rt_uint32_t (*xfer)(struct rt_spi_device *device, struct rt_spi_message *message);
};
SPI从设备或外设的描述结构如下:
// .\rt-thread-4.0.1\components\drivers\include\drivers\spi.h
/**
* SPI Virtual BUS, one device must connected to a virtual BUS
*/
struct rt_spi_device
{
struct rt_device parent;
struct rt_spi_bus *bus;
struct rt_spi_configuration config;
void *user_data;
};
SPI设备通过片选信号线选择从设备,其支持全双工,也即可以同时进行发送、接收数据操作,因此访问SPI设备的接口参数也以封装的消息帧格式传递,SPI设备消息帧数据结构如下:
// .\rt-thread-4.0.1\components\drivers\include\drivers\spi.h
/**
* SPI message structure
*/
struct rt_spi_message
{
const void *send_buf;
void *recv_buf;
rt_size_t length;
struct rt_spi_message *next;
unsigned cs_take : 1;
unsigned cs_release : 1;
};
SPI总线设备接口rt_spi_ops.xfer就使用了rt_spi_message作为参数传入,如果嫌每次构造rt_spi_message结构比较麻烦,SPI core layer也为我们实现了几个不需要传入rt_spi_message结构体,只需要传入数据缓冲区首地址与长度的接口函数原型如下:
// .\rt-thread-4.0.1\components\drivers\include\drivers\spi.h
/**
* This function transfers a message list to the SPI device.
*/
struct rt_spi_message *rt_spi_transfer_message(struct rt_spi_device *device,
struct rt_spi_message *message);
/**
* This function transmits data to SPI device.
*/
rt_size_t rt_spi_transfer(struct rt_spi_device *device,
const void *send_buf,
void *recv_buf,
rt_size_t length);
/* set configuration on SPI device */
rt_err_t rt_spi_configure(struct rt_spi_device *device,
struct rt_spi_configuration *cfg);
/* send data then receive data from SPI device */
rt_err_t rt_spi_send_then_recv(struct rt_spi_device *device,
const void *send_buf,
rt_size_t send_length,
void *recv_buf,
rt_size_t recv_length);
rt_err_t rt_spi_send_then_send(struct rt_spi_device *device,
const void *send_buf1,
rt_size_t send_length1,
const void *send_buf2,
rt_size_t send_length2);
当底层驱动都实现后,用户可以直接调用上面的接口函数来访问SPI设备,但该接口函数的参数与 I/O 设备管理接口并不兼容,因此RT-Thread增加了bus_dev layer用来实现两种接口之间的适配。
bus_dev layer跟前面介绍的 I2C 驱动框架层类似,也是使用SPI core layer利用rt_spi_ops实现的接口函数(特别是rt_spi_transfer()),向 I/O 设备管理层注册统一的接口。
既然SPI总线与设备分开描述,二者都是与硬件相关的(SPI总线与CPU/SOC相关,SPI设备与外设硬件相关),二者都需要向上注册设备对象。因此,bus_dev layer包含SPI bus的注册和SPI device的注册两部分,这两种设备向上层注册 I/O管理接口的过程如下:
// .\rt-thread-4.0.1\components\drivers\spi\spi_dev.c
#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops spi_bus_ops =
{
RT_NULL,
RT_NULL,
RT_NULL,
_spi_bus_device_read,
_spi_bus_device_write,
_spi_bus_device_control
};
#endif
rt_err_t rt_spi_bus_device_init(struct rt_spi_bus *bus, const char *name)
{
struct rt_device *device;
RT_ASSERT(bus != RT_NULL);
device = &bus->parent;
/* set device type */
device->type = RT_Device_Class_SPIBUS;
/* initialize device interface */
#ifdef RT_USING_DEVICE_OPS
device->ops = &spi_bus_ops;
#else
......
#endif
/* register to device manager */
return rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);
}
// .\rt-thread-4.0.1\components\drivers\spi\spi_dev.c
#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops spi_device_ops =
{
RT_NULL,
RT_NULL,
RT_NULL,
_spidev_device_read,
_spidev_device_write,
_spidev_device_control
};
#endif
rt_err_t rt_spidev_device_init(struct rt_spi_device *dev, const char *name)
{
struct rt_device *device;
RT_ASSERT(dev != RT_NULL);
device = &(dev->parent);
/* set device type */
device->type = RT_Device_Class_SPIDevice;
#ifdef RT_USING_DEVICE_OPS
device->ops = &spi_device_ops;
#else
......
#endif
/* register to device manager */
return rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);
}
接口函数spi_bus_ops与spi_device_ops的实现主要通过调用SPI core layer提供的rt_spi_transfer()函数实现的,且向上注册的接口函数rt_device_ops中的参数pos也没有使用。
SPI core layer跟I2C也类似,也是利用设备驱动层提供的接口函数rt_spi_ops,实现bus_dev layer需要的接口函数,当然SPI core layer实现的接口函数也可以被用户直接调用。该层实现的接口函数向上层注册的过程也分为SPI bus和SPI device两部分,这两种设备向上层注册的过程如下:
// .\rt-thread-4.0.1\components\drivers\spi\spi_core.c
rt_err_t rt_spi_bus_register(struct rt_spi_bus *bus,
const char *name,
const struct rt_spi_ops *ops)
{
rt_err_t result;
result = rt_spi_bus_device_init(bus, name);
if (result != RT_EOK)
return result;
/* initialize mutex lock */
rt_mutex_init(&(bus->lock), name, RT_IPC_FLAG_FIFO);
/* set ops */
bus->ops = ops;
/* initialize owner */
bus->owner = RT_NULL;
/* set bus mode */
bus->mode = RT_SPI_BUS_MODE_SPI;
return RT_EOK;
}
// .\rt-thread-4.0.1\components\drivers\spi\spi_core.c
rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device,
const char *name,
const char *bus_name,
void *user_data)
{
rt_err_t result;
rt_device_t bus;
/* get physical spi bus */
bus = rt_device_find(bus_name);
if (bus != RT_NULL && bus->type == RT_Device_Class_SPIBUS)
{
device->bus = (struct rt_spi_bus *)bus;
/* initialize spidev device */
result = rt_spidev_device_init(device, name);
if (result != RT_EOK)
return result;
rt_memset(&device->config, 0, sizeof(device->config));
device->parent.user_data = user_data;
return RT_EOK;
}
/* not found the host bus */
return -RT_ERROR;
}
该层实现的接口函数原型在前面已经给出来了(比如rt_spi_transfer()),这些接口函数最终是通过调用SPI总线设备接口rt_spi_ops实现的,而rt_spi_ops则由SPI设备驱动层实现并向本层注册。
SPI设备驱动层由于每个SPI从设备的片选引脚不一样,因此需要在注册/初始化SPI从设备时将片选引脚一起传递给上层,以便用户据此访问特定的SPI从设备。
SPI驱动层的设备描述结构和片选引脚CS描述结构如下:
// .\libraries\HAL_Drivers\drv_spi.h
/* stm32 spi dirver class */
struct stm32_spi
{
SPI_HandleTypeDef handle;
struct stm32_spi_config *config;
struct rt_spi_configuration *cfg;
struct
{
DMA_HandleTypeDef handle_rx;
DMA_HandleTypeDef handle_tx;
} dma;
rt_uint8_t spi_dma_flag;
struct rt_spi_bus spi_bus;
};
struct stm32_hw_spi_cs
{
GPIO_TypeDef* GPIOx;
uint16_t GPIO_Pin;
};
// .\libraries\HAL_Drivers\drv_spi.c
static struct stm32_spi spi_bus_obj[sizeof(spi_config) / sizeof(spi_config[0])] = {0};
static struct stm32_spi_config spi_config[] =
{
#ifdef BSP_USING_SPI1
{
.Instance = SPI1,
.bus_name = "spi1",
}
#endif
......
};
stm32_spi结构中包含的SPI_HandleTypeDef、DMA_HandleTypeDef等都由ST厂商的标准库HAL提供(与UART设备驱动层类似),在SPI驱动层实现rt_spi_ops最终也是要调用HAL库函数的,可能调用到的HAL库函数如下:
// .\libraries\STM32L4xx_HAL\STM32L4xx_HAL_Driver\Inc\stm32l4xx_hal_spi.h
HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_DeInit(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi);
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi);
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi);
HAL_SPI_StateTypeDef HAL_SPI_GetState(SPI_HandleTypeDef *hspi);
uint32_t HAL_SPI_GetError(SPI_HandleTypeDef *hspi);
SPI设备驱动层使用HAL库函数实现SPI设备框架层的接口函数集合rt_spi_ops,并将实现的接口函数集合stm_spi_ops伴随stm_spi_bus设备对象注册到上层的过程如下:
// .\libraries\HAL_Drivers\drv_spi.c
static const struct rt_spi_ops stm_spi_ops =
{
.configure = spi_configure,
.xfer = spixfer,
};
static int rt_hw_spi_bus_init(void)
{
rt_err_t result;
for (int i = 0; i < sizeof(spi_config) / sizeof(spi_config[0]); i++)
{
spi_bus_obj[i].config = &spi_config[i];
spi_bus_obj[i].spi_bus.parent.user_data = &spi_config[i];
spi_bus_obj[i].handle.Instance = spi_config[i].Instance;
if (spi_bus_obj[i].spi_dma_flag & SPI_USING_RX_DMA_FLAG)
{
/* Configure the DMA handler for Transmission process */
......
}
if (spi_bus_obj[i].spi_dma_flag & SPI_USING_TX_DMA_FLAG)
{
/* Configure the DMA handler for Transmission process */
......
}
result = rt_spi_bus_register(&spi_bus_obj[i].spi_bus, spi_config[i].bus_name, &stm_spi_ops);
RT_ASSERT(result == RT_EOK);
}
return result;
}
int rt_hw_spi_init(void)
{
stm32_get_dma_info();
return rt_hw_spi_bus_init();
}
INIT_BOARD_EXPORT(rt_hw_spi_init);
SPI总线设备使用了自动初始化机制,当我们需要使用某个或某几个SPI总线设备时,只需要使用CubeMX配置好想要的SPI外设,并在Kconfig和menuconfig中配置并定义相应的条件宏就可以了。
SPI从设备又如何向上层注册呢?又如何将SPI从设备的片选引脚向上传递呢?该过程的实现代码如下:
// .\libraries\HAL_Drivers\drv_spi.c
/**
* Attach the spi device to SPI bus, this function must be used after initialization.
*/
rt_err_t rt_hw_spi_device_attach(const char *bus_name, const char *device_name, GPIO_TypeDef *cs_gpiox, uint16_t cs_gpio_pin)
{
RT_ASSERT(bus_name != RT_NULL);
RT_ASSERT(device_name != RT_NULL);
rt_err_t result;
struct rt_spi_device *spi_device;
struct stm32_hw_spi_cs *cs_pin;
/* initialize the cs pin && select the slave*/
......
HAL_GPIO_Init(cs_gpiox, &GPIO_Initure);
HAL_GPIO_WritePin(cs_gpiox, cs_gpio_pin, GPIO_PIN_SET);
/* attach the device to spi bus*/
spi_device = (struct rt_spi_device *)rt_malloc(sizeof(struct rt_spi_device));
RT_ASSERT(spi_device != RT_NULL);
cs_pin = (struct stm32_hw_spi_cs *)rt_malloc(sizeof(struct stm32_hw_spi_cs));
RT_ASSERT(cs_pin != RT_NULL);
cs_pin->GPIOx = cs_gpiox;
cs_pin->GPIO_Pin = cs_gpio_pin;
result = rt_spi_bus_attach_device(spi_device, device_name, bus_name, (void *)cs_pin);
if (result != RT_EOK)
{
LOG_E("%s attach to %s faild, %d\n", device_name, bus_name, result);
}
RT_ASSERT(result == RT_EOK);
LOG_D("%s attach to %s done", device_name, bus_name);
return result;
}
SPI从设备的注册或总线绑定接口并没有使用自动初始化机制,因此当用户想要将某个SPI从设备绑定到指定的SPI总线上时,需要自己主动调用接口rt_hw_spi_device_attach(),除了传入SPI总线名和SPI设备名外,还需要传入该SPI从设备使用的片选引脚,在配置片选引脚时别忘了使能对应的RCC时钟。
以使用SPI接口的ENC28J60网卡设备的使用为例(详见博客LwIP协议栈移植),我们来看下是如何向特定SPI总线注册SPI外设的:
// https://github.com/StreamAI/LwIP_Projects/blob/master/stm32l475-pandora-lwip/applications/enc28j60_port.c
int enc28j60_init(void)
{
__HAL_RCC_GPIOD_CLK_ENABLE();
rt_hw_spi_device_attach("spi2", "spi21", GPIOD, GPIO_PIN_5);
/* attach enc28j60 to spi. spi21 cs - PD6 */
enc28j60_attach("spi21");
......
return 0;
}
INIT_COMPONENT_EXPORT(enc28j60_init);
配置好相应的SPI引脚和相应宏定义后,spi bus就会被自动初始化,如果要使用SPI外设,再配置好相应的CS片选引脚(注意使能该引脚对应的RCC时钟),调用rt_hw_spi_device_attach()接口函数即可将SPI设备注册/绑定到相应的SPI总线,然后通过SPI上层接口访问该SPI外设。
SPI 驱动的主从分离并没有体现出Linux的总线设备驱动模型结构,SDIO 驱动则体现了总线设备模型驱动的结构框架,限于篇幅,在后面的博客:SDIO设备对象管理 + AP6181(BCM43362) WiFi模块中专门介绍SDIO驱动是如何体现Linux的总线设备驱动模型思想的。