基于DWC2的USB驱动开发-0x0C 驱动框架设计 (qq.com)
一. 驱动架构
1.1 前言
前面我们介绍了DWC2的控制器相关的寄存器,驱动的编写本质上就是进行寄存器的配置。为了代码的健壮性,可移植性,可调试性等我们必须设计一个比较好的架构。对于驱动编程来说输入就是各个寄存器,如果使用的都是DWC2的话甚至来说寄存器都是一样的输入就只需要寄存器基地址,即如果都是使用该IP的驱动甚至只要修改寄存器基地址,然后移植中断等一些和SOC相关的内容即可复用。
1.2 架构框图
整体的设计思想如下:
借鉴面向对象的设计的依赖倒置原则:
面向接口编程,不依赖具体实现编程,应用层不直接依赖具体实现而是依赖接口,上层调用接口底层实现接口,接口是硬件隔离的桥梁.
接口即对应上图的HAL层。
硬件: 对于使用DWC2控制器的则还可以复用HW层本身的内容,只需要修改寄存器的基地址即可,因为DWC2控制器寄存器都是一样的。另外需要修改一些和SOC相关的内容,比如中断的设置。对于不同的控制器和SOC平台的则需要根据具体的平台进行封装。理论上也可以直接依赖硬件实现HAL层,而不需要HW层的封装,但是这样的话直接在HAL层操作寄存器代码的阅读性上不佳。
HW层:即对头文件中寄存器的操作进行封装,主要采用宏的方式封装,目的是为了可阅读性。同时对于一些联系紧密的操作放在一起进行封装,比如初始化等,提供比如结构体参数等,进行相应的参数配置。这一层对于使用同样的DWC2控制器的也完全可复用。
HAL层:这是硬件和上层隔离的一层,定义了一系列接口,协议栈调用该接口进行硬件操作,而HAL调用HW层实现上述的接口。对于不使用DWC2控制器的,则需要重新实现HAL层,而同使用DWC2控制器的则本层也通用。本层的设计原则是接口个数尽可能少和简单但是要足够满足协议栈层的需要。符合面向对象设计的最少知道原则。
协议栈和HAL层以及以下的层次,尽可能不进行资源分配(比如内存)等预留相应的接口,资源分配由应用层实现,这样设计的目的是在嵌入式开发环境资源往往受限,资源的分配往往需要应用层来把控,应用层来负责。底层尽可能少的依赖资源,尽可能保持确定性,避免过多动态行为。
应用层:调用协议栈实现具体的应用功能。
考虑到通用性和可移植性,我们要支持OS和无OS的实现,所以非必要不依赖OS相关的API,如果不可避免则尽可能少依赖,且实现需要依赖的API的OS和NONE-OS版本,将依赖抽离出来作为需要移植实现的部分,使得在不同OS和裸机下都能运行,这里会涉及到协议栈基于中断驱动的处理方式和基于事件驱动的处理方式两种实现,后面到协议栈设计部分再说。
1.3 调试方案
调试手段是驱动开发中需要重点考虑的一环,没有合适的调试手段,遇到问题,会两眼一抹黑完全无从下手。进行USB开发,硬件上示波器和协议分析仪是必须的。另外软件上也需要一些手段配合调试。以下是几种常见的方式。
1.3.1 仿真器
这是嵌入式开发必须的,尤其是驱动开发阶段,需要跟踪代码流,寄存器的配置,变量的值等等。仿真器调试有个问题就是需要中断正常的程序流.USB是一个有着严格时序要求,且高速的协议,程序中断会导致USB的处理过程由于超时等导致异常,所以很多时候不能通过仿真器打断点等方式进行调试。
1.3.2 IO
USB由于其严格的时序要求,且高速的处理,很多时候我们需要测试相应的处理时间,使用定时器进行计时并打印时间是一种方法,但是对于高精度计时定时器的处理代码本身需要时间会带来误差。这时使用IO翻转,来指示某种状态的变化非常有用,比如测试SOF之间的时间,可以在SOF中断中翻转IO,使用示波器或者逻辑分析仪测量波形即可。翻转IO往往一条指令即可,这使得代码带来的误差减小。
更重要的是可以通过多个IO关联多个事件,看到多个事件之间的关联关系,尤其是使用示波器和逻辑分析仪查看时。曲线会非常直观。这是使用定时器打印所不具备的。
IO翻转的调试方法是嵌入式实时分析中重要的手段。
1.3.3 串口打印
USB的处理流程对应着状态的流转,实际对于软件来说过是各种中断的进入与退出。使用仿真器跟踪会破坏USB的处理流程导致超时等,所以需要一个非中断方式的跟踪事件的方式。常见的方式是使用串口打印,这里要注意串口打印不能使用阻塞方式,否则同样的会导致USB处理流的异常。一般采用缓冲区的方式,打印接口将数据写入缓冲区,主循环或者其他线程中将缓冲区的数据通过串口发送。
当然除了串口打印还有很多其他类似的方式比如 Jlink提供的rtt等性能更高,也可以使用网口等接口,根据具体平台而定。
串口打印可是设置为宏的方式,debug版本使能,release版本可不使能,打印输出也可以设置等级和类别,这样通过等级和类别控制输出,避免一次打印过多干扰分析,也可以使用CLI动态配置等级可列别的控制。
如下是一个简单的示例,依赖printf,假设printf已经实现是一个非阻塞的版本(即写入缓冲区),其他地方再真正的发送数据。可以通过宏配置不同等级的LOG,且打印信息对应不同的颜色。
头文件中
#define USB_HAL_DEBUG 1
#define USB_HAL_LOG_LEVEL_ERROR 1
#define USB_HAL_LOG_LEVEL_WARN 1
#define USB_HAL_LOG_LEVEL_INFO 1
/** Debug levels */
typedef enum
{
USB_HAL_DEBUG_ERROR = 0,
USB_HAL_DEBUG_WARN = 1,
USB_HAL_DEBUG_INFO = 2,
}USB_HAL_DEBUG_e;
#define USB_HAL_DEBUG_COLOR_MASK_COLOR 0x0F
#define USB_HAL_DEBUG_COLOR_MASK_MODIFIER 0xF0
typedef enum {
/* Colors */
USB_HAL_DEBUG_COLOR_RESET = 0xF0,
USB_HAL_DEBUG_COLOR_BLACK = 0x01,
USB_HAL_DEBUG_COLOR_RED = 0x02,
USB_HAL_DEBUG_COLOR_GREEN = 0x03,
USB_HAL_DEBUG_COLOR_YELLOW = 0x04,
USB_HAL_DEBUG_COLOR_BLUE = 0x05,
USB_HAL_DEBUG_COLOR_MAGENTA = 0x06,
USB_HAL_DEBUG_COLOR_CYAN = 0x07,
USB_HAL_DEBUG_COLOR_WHITE = 0x08,
/* Modifiers */
USB_HAL_DEBUG_COLOR_NORMAL = 0x0F,
USB_HAL_DEBUG_COLOR_BOLD = 0x10,
USB_HAL_DEBUG_COLOR_UNDERLINE = 0x20,
USB_HAL_DEBUG_COLOR_BLINK = 0x30,
USB_HAL_DEBUG_COLOR_HIDE = 0x40,
} USB_HAL_DEBUG_COLOR_e;
void usb_hal_do_debug(USB_HAL_DEBUG_e level, const char *format, ...);
#ifdef USB_HAL_DEBUG
#define usb_hal_debug(level, format, ...) do { usb_hal_do_debug(level, format, ##__VA_ARGS__); } while(0)
#else
#define usb_hal_debug(...) do {} while (0)
#endif
#ifdef USB_HAL_LOG_LEVEL_ERROR
#define usb_hal_error(format, ...) usb_hal_debug(USB_HAL_DEBUG_ERROR, format, ##__VA_ARGS__)
#else
#define usb_hal_error(...) do {} while (0)
#endif
#ifdef USB_HAL_LOG_LEVEL_WARN
#define usb_hal_warn(format, ...) usb_hal_debug(USB_HAL_DEBUG_WARN, format, ##__VA_ARGS__)
#else
#define usb_hal_warn(...) do {} while (0)
#endif
#ifdef USB_HAL_LOG_LEVEL_INFO
#define usb_hal_info(format, ...) usb_hal_debug(USB_HAL_DEBUG_INFO, format, ##__VA_ARGS__)
#else
#define usb_hal_info(...) do {} while (0)
#endif