
衡山派CAP模块驱动设计说明基于RT-Thread INPUT CAPTURE框架的PWM捕获实现最近在衡山派平台上做项目需要测量一个风扇的PWM调速信号正好用到了它的CAPCapture捕获模块。很多刚开始接触衡山派或者RT-Thread设备驱动的朋友可能会觉得这个驱动框架有点复杂不知道从哪里下手。今天我就结合自己的实际开发经验带大家一步步拆解衡山派CAP模块的驱动设计手把手教你如何基于RT-Thread的INPUT CAPTURE框架实现PWM信号的频率和占空比测量。这篇文章适合已经有一定RT-Thread和C语言基础的嵌入式开发者特别是那些需要在衡山派平台上进行外设驱动开发或者想学习标准设备驱动框架设计的朋友。咱们不光是看代码更要理解每一层设计的用意和背后的数据流。1. 驱动源码与整体架构首先咱们得知道代码在哪以及整个驱动是怎么分层的。这就像盖房子得先看清楚图纸和材料放在哪。1.1 源码位置说明衡山派CAP模块的驱动源码都放在RT-Thread的BSP板级支持包目录下具体路径是bsp/artinchip/。主要就三个文件分工很明确bsp/artinchip/drv/cap/drv_cap.c这是驱动层Driver Layer的实现。它负责对接RT-Thread的标准设备框架我们应用程序调用的rt_device_find、rt_device_open等接口最终都会落到这一层的函数上。bsp/artinchip/hal/pwmcs/hal_cap.c这是硬件抽象层HAL Layer的实现。它直接操作衡山派芯片的CAP控制器寄存器是最贴近硬件的一层。像设置时钟、使能中断、读取捕获值这些“粗活累活”都是它干的。bsp/artinchip/include/hal/hal_cap.h这是HAL层的头文件里面声明了所有操作硬件的函数接口比如hal_cap_init、hal_cap_get等。提示这种分层设计的好处是隔离了变化。如果以后换一个不同型号的衡山派芯片只要HAL层提供的接口不变上层的Driver层和我们的应用代码就完全不用改只需要重新实现HAL层即可。1.2 模块架构框架与裸机CAP驱动的Driver层完全遵循了RT-Thread的INPUT CAPTURE设备驱动框架。这意味着你的应用程序可以使用一套统一的、标准的API如rt_device_inputcapture_register来操作CAP设备和其他传感器、外设的用法保持一致学习成本低代码也规范。但是框架不是必须的。如果你做的项目非常精简不想用RT-Thread操作系统或者想直接操控硬件那么你可以绕过Driver层直接调用HAL层的函数。这就是所谓的“Baremetal”裸机方式。HAL层本身不依赖RT-Thread它只是对寄存器操作进行了封装给你提供了一组更友好的C函数。简单来说架构是这样的应用程序 | | (标准RT-Thread设备API) v INPUT CAPTURE 设备驱动框架 (Driver层: drv_cap.c) | | (调用HAL接口) v 硬件抽象层 (HAL层: hal_cap.c) | | (读写寄存器) v 衡山派CAP硬件控制器2. 关键流程与数据结构剖析了解了整体结构咱们深入到驱动内部看看它启动和工作的核心流程以及数据是怎么存放的。2.1 初始化流程驱动如何挂载到系统驱动的初始化入口在drv_cap_init函数。RT-Thread有一个很巧妙的功能叫“自动初始化”通过INIT_DEVICE_EXPORT(drv_cap_init)这句代码系统在启动时就会自动调用这个函数。这个初始化函数主要就干一件事向RT-Thread的INPUT CAPTURE子系统注册一个CAP设备。用的是rt_device_inputcapture_register()这个框架函数。注册的时候需要把我们自己实现的那些操作函数比如aic_cap_open打包成一个结构体传进去这样框架就知道怎么调用我们的驱动了。设备注册好了但硬件控制器还没配置。真正的硬件初始化是在aic_cap_init函数里完成的也就是框架要求我们实现的init操作。这个过程是标准化的主要分五步初始化PWMCS模块的时钟CAP模块通常是PWM/定时器系统的一部分所以先要打开整个PWMCSPWM/Capture模块的时钟门控。使能指定CAP通道的时钟衡山派的CAP可能有多个通道比如cap0, cap1这里打开你要用的那个通道的时钟。设置CAP捕捉事件告诉CAP模块你要捕获什么是上升沿、下降沿还是双边沿这决定了你测量的是脉冲高电平宽度还是周期。使能CAP的中断当捕获事件发生时产生一个中断这样CPU才能及时知道并处理数据。启动CAP计数让CAP内部的计数器开始跑起来这样它才能基于这个计数器记录下捕获时刻的时间戳。这五步做完硬件就准备就绪随时可以捕获输入的PWM信号了。2.2 核心数据结构数据如何存放驱动里有两个关键的结构体它们分别属于HAL层和Driver层承载着不同的信息。HAL层结构体struct aic_cap_data这个结构体是HAL层用来存放一次捕获计算后的结果。它很简单就三个成员struct aic_cap_data { u8 id; // 通道ID比如0,1,2... u32 freq; // 计算出的信号频率单位Hz float duty; // 计算出的信号占空比范围0.0 ~ 1.0 (或0%~100%) };当CAP硬件捕获到两个边沿比如一个上升沿和一个下降沿后HAL层的函数会根据两次捕获的计数器值计算出信号的周期和占空比然后填到这个结构体里。Driver层结构体struct aic_cap这个结构体是Driver层用来描述一个CAP设备实例的。它内嵌了RT-Thread框架要求的设备结构并关联了上面的数据体。struct aic_cap { struct rt_inputcapture_device rtdev; // RT-Thread输入捕获设备框架结构 struct aic_cap_data *data; // 指向上面那个存放捕获数据的结构体 };rtdev是框架的核心里面包含了设备名、操作函数表、用户数据等。data指针就像是一个快递柜HAL层把计算好的数据aic_cap_data放进去Driver层或者应用层再从这里取出来。3. 驱动层接口实现详解Driver层需要实现一套标准的操作函数来满足RT-Thread INPUT CAPTURE框架的要求。这套函数就像是一个“菜单”框架点哪个菜我们就上哪个菜。菜单原型定义如下struct rt_inputcapture_ops { rt_err_t (*init)(struct rt_inputcapture_device *inputcapture); rt_err_t (*open)(struct rt_inputcapture_device *inputcapture); rt_err_t (*close)(struct rt_inputcapture_device *inputcapture); rt_err_t (*get_pulsewidth)(struct rt_inputcapture_device *inputcapture, rt_uint32_t *pulsewidth_us); };下面我们看看衡山派驱动里是怎么“做菜”的。3.1 aic_cap_init硬件初始化功能初始化配置一路CAP通道。就是前面提到的五步硬件配置流程。调用时机通常在设备注册时或第一次打开设备时由框架调用。注意事项这里只做通用的、一次性的硬件配置。更具体的参数如捕获边沿可能在open或通过control函数设置。3.2 aic_cap_open / aic_cap_close打开与关闭功能open函数负责启动捕获可能包括使能中断、清空标志位等close函数则负责停止捕获禁用中断。为什么需要这样设计可以实现设备的多次打开和关闭节省功耗。不用的时候关掉用的时候再打开。3.3 aic_cap_get_pulsewidth获取脉冲宽度功能这是框架要求的标准接口用于获取相邻两次捕获事件的时间间隔单位是微秒(us)。比如如果你设置捕获上升沿它返回的就是信号的周期。参数pulsewidth_us是一个输出参数用来存放计算出的时间间隔。衡山派的扩展这里有个很重要的细节衡山派的HAL层不仅计算了时间间隔还额外计算了频率和占空比并存放在aic_cap_data结构体里。所以虽然这个标准接口只返回脉冲宽度但我们通过设备的结构体能拿到更丰富的频率和占空比信息。这在后面的Demo代码里会看到。4. HAL层接口与应用Demo解读4.1 HAL层主要接口一览HAL层的函数都在hal_cap.h里声明它们是驱动与硬件对话的“方言”。我把一些关键的列出来大家有个印象初始化与反初始化hal_cap_init,hal_cap_deinit,hal_cap_ch_init,hal_cap_ch_deinit中断控制hal_cap_int_enable,hal_cap_int_sta,hal_cap_is_pending,hal_cap_clr_flg配置与控制hal_cap_set_freq(设置计数时钟频率),hal_cap_in_config(配置输入引脚和捕获边沿),hal_cap_enable/disable计数控制hal_cap_cnt_start,hal_cap_cnt_stop数据获取hal_cap_get(触发一次捕获并计算),hal_cap_reg0/1/2(直接读原始寄存器值)状态查看hal_cap_status_show(调试用)这些函数名都很直观比如hal_cap_int_enable就是使能中断hal_cap_get就是获取一次捕获数据。在Driver层的aic_cap_init、aic_cap_open等函数里就是调用了这些HAL函数来完成具体工作的。4.2 应用Demo代码解析理论说了这么多最后来看一个实实在在的例子看看应用程序该怎么用这个驱动。下面是从test_cap.c里摘录的核心部分/* 回调函数当捕获到数据时系统会自动调用这个函数 */ static rt_err_t cap_cb(rt_device_t dev, rt_size_t size) { struct rt_inputcapture_data inputcap_data[WATER_MARK]; // 框架定义的数据结构数组 rt_device_read(dev, 0, (void *)inputcap_data, size); // 从设备读取数据 #ifdef ULOG_USING_ISR_LOG // 这里是关键通过user_data拿到我们额外的频率和占空比数据 struct aic_cap_usr *data (struct aic_cap_usr *)dev-user_data; rt_kprintf(cap%d: freq:%dHz, duty:%d.%02d%%\n, >