做RT-Thread设备驱动开发,最怕的不是看不懂芯片手册,而是找不到一个能把"框架理解"和"硬件操作"串起来的完整例子。今天我就用《RT-Thread设备驱动开发指南》基础篇的思路,以先楫BSP里的hwtimer设备为例,把从设备模型、ops实现、中断处理到Kconfig接入的整个流程讲清楚。这篇内容适合两类朋友:一是准备给新板子移植RT-Thread、需要从零写驱动的开发者;二是已经在用先楫HPM系列芯片,想快速跑通硬件定时器,但不想从底层寄存器开始硬啃的人。我会尽量还原我实际开发时的取舍过程,而不是只扔一份写好的代码。
1. 项目背景与整体思路拆解
1.1 为什么拿先楫BSP的hwtimer当教学案例
选hwtimer练手,纯粹因为它"小但完整"。
一个小驱动要串起几件事:设备结构体定义、ops函数表、寄存器操作、中断服务、构建系统接入、应用层验证。hwtimer的逻辑量比UART、SPI少很多,UART要考虑DMA和流控,SPI要考虑时钟极性和片选管理,hwtimer只需要把"设置频率、启动、停止、读计数、超时回调"这五件事做好。但反过来,它已经把设备驱动开发的骨架完整包含了。
先楫BSP在这里又是个很合适的载体。先楫的HPM6xxx系列MCU上,TMR外设通道多、模式多,SDK封装也比较规整,不会出现"读数据手册读了三天还不知道该配置哪个位"的尴尬。加上RT-Thread官方BSP仓库里已经有先楫的移植,时钟、中断、GPIO这类基础设施全都现成,我们可以把注意力全部放在驱动实现上。
我在实际准备阶段也考虑过拿STM32的BSP来讲,后来放弃了。STM32的TIM虽然经典,但功能太多太复杂,做基础篇反而容易淹没重点。先楫TMR的通道模型更贴近"一个定时器就是一个计数器加比较器"的原始形态,对理解hwtimer框架更友好。
1.2 hwtimer驱动要解决的核心问题
在动手写代码之前,你要先回答四个问题。
第一,上层怎么表达"定时"这件事?答案是频率和计数个数。hwtimer框架里,频率决定计数时钟周期,计数个数决定超时时间。第二,上层怎么知道"时间到了"?答案是回调通知,应用层通过rt_device_set_rx_indicate注册一个超时回调。第三,上层怎么开关定时器?答案是start和stop控制命令。第四,用户怎么读当前计数值、怎么设置工作模式?答案都落到统一的control和read接口上。
所以hwtimer驱动的本质,就是把先楫TMR的能力翻译成这五组操作。驱动开发者要做的不是设计一套新API,而是老老实实填好struct rt_hwtimer_ops里的函数,再通过rt_hwtimer_register挂到设备框架里。
这个思路是《RT-Thread设备驱动开发指南》基础篇最强调的。你一旦理解"接口是标准的、实现是自由的"这件事,后面写任何外设驱动都会快很多。先楫TMR的寄存器一时间没看明白不要紧,先搞懂你需要在ops里完成哪些行为,再回头对着寄存器找,思路会清晰得多。
2. 先楫BSP与RT-Thread驱动框架:几个必须搞懂的关键机制
2.1 RT-Thread的"设备模型"到底在模型什么
RT-Thread的设备框架不是一堆空壳,它把硬件设备抽象成rt_device节点,对应用层暴露统一的init/open/close/read/write/control接口。这样写应用的人只需要认设备名,不需要关心底层是STM32还是先楫HPM。
hwtimer在标准rt_device之上又包了一层struct rt_hwtimer_device,增加了几样东西:info描述定时器硬件能力,ops挂接驱动实现函数,以及prescaler、frequency、timeout_callback这些运行时状态。最终注册到系统里的设备节点,对上层来说就是一个叫"timer0"或"timer1"的普通字符设备。
我在首次接触这套结构时有个误区,以为要先写一堆设备的read/write回调。其实不用。hwtimer框架内部已经把rt_device的通用回调翻译到rt_hwtimer_ops上了。你只要填好rt_hwtimer_ops,框架会帮你处理好设备层接口。所以读懂hwtimer框架源码里的hwtimer.c,比急着翻芯片手册更重要。
2.2 hwtimer设备信息与配置参数
struct rt_hwtimer_info是驱动里最先被上层查询的数据,它通常长这样:
struct rt_hwtimer_info { rt_uint32_t maxfreq; /* 最大计数频率 */ rt_uint32_t minfreq; /* 最小计数频率 */ rt_uint32_t maxcnt; /* 最大计数值 */ rt_hwtimer_mode_t cntmode; /* 计数模式 */ };这四个字段不是随便填的,它们会直接影响上层行为。
比如用户调用HWTIMER_CTRL_FREQ_SET把频率设置成10kHz,框架先检查10kHz是否落在minfreq到maxfreq范围内。如果你把maxfreq填成100MHz,但硬件实际只能分频到50MHz,上层设置60MHz时驱动又没有做边界保护,后面算出来的分频系数就会错误。反过来,maxcnt对应定时器计数寄存器的位宽。32位定时器最大计数值是0xFFFFFFFF,你填成65535,那上层想定时超过这个范围时会直接失败。
cntmode我习惯理解成"往大了数还是往小了数"。RT-Thread框架一般默认递增模式,先楫TMR的通道支持递增和递减两种,驱动适配时把硬件配置成递增模式最省心。如果你非要映射成递减模式,就得在start时手动计算初始值,纯属给自己找麻烦。
2.3 先楫TMR硬件如何映射到hwtimer框架
先楫HPM系列的TMR模块,一个外设里集成多个通道,每个通道基本就是"计数器 + 比较寄存器 + 中断标志"的组合。通道计数器在时钟驱动下递增或递减,当计数值和比较值相等时产生一个匹配事件,这个事件可以触发中断。把这样一套能力映射到hwtimer框架非常顺:
| hwtimer框架概念 | 先楫TMR硬件行为 |
|---|---|
| prescaler分频 | 配置TMR通道的时钟分频系数 |
| start(cnt) | 设置比较值为cnt,启动计数并开启匹配中断 |
| stop | 停止计数,关闭匹配中断,清除标志 |
| count_get | 读当前计数器值 |
| 周期模式 | 匹配后自动重新装载计数起始值 |
| 单次模式 | 匹配后停止计数,不再触发 |
| 超时通知 | 比较中断服务里调用rt_hwtimer_isr |
这张映射表就是驱动骨架。剩下的事情,无非是把每个格子用HPM SDK的接口填上。如果你已经把上面框架里的概念理解透了,即使HPM SDK接口名变动,也能很快找到对应功能。
3. 手写drv_hwtimer.c:核心代码与实现逻辑
3.1 设备对象与注册流程
先定义一个私有结构体,把RT-Thread框架需要的rt_hwtimer_device和你自己的硬件信息放在一起:
struct hpm_hwtimer_dev { struct rt_hwtimer_device timer; TMR_Type *base; uint8_t channel; uint32_t clock_freq; };base保存TMR外设基地址,channel保存用哪个通道,clock_freq保存进入定时器时钟源的真实频率。先楫HPM6xxx的主频很高,但TMR时钟往往来自独立的时钟生成器,频率不一定等于CPU主频。驱动里必须拿到真实频率,否则后面算分频一定会出错。
然后静态定义一个实例,并填好info和ops:
static struct hpm_hwtimer_dev hwtimer_dev; static const struct rt_hwtimer_info hwtimer_info = { .maxfreq = 100000000UL, .minfreq = 1, .maxcnt = 0xFFFFFFFFUL, .cntmode = HWTIMER_MODE_PERIOD, }; static const struct rt_hwtimer_ops hpm_hwtimer_ops = { .init = hpm_hwtimer_init, .start = hpm_hwtimer_start, .stop = hpm_hwtimer_stop, .count_get = hpm_hwtimer_count_get, .get_countfreq = hpm_hwtimer_get_countfreq, .control = hpm_hwtimer_control, };注册函数很简单,关键是把timer0这个名字定下来:
static int hpm_hwtimer_drv_init(void) { hwtimer_dev.base = HPM_TMR0; hwtimer_dev.channel = 0; hwtimer_dev.clock_freq = 100000000UL; // 以实际时钟树配置为准 hwtimer_dev.timer.info = &hwtimer_info; hwtimer_dev.timer.ops = &hpm_hwtimer_ops; return rt_hwtimer_register(&hwtimer_dev.timer, "timer0", RT_NULL); } INIT_DEVICE_EXPORT(hpm_hwtimer_drv_init);注意INIT_DEVICE_EXPORT的时机。hwtimer属于设备类,放在DEVICE阶段初始化是合适的。如果放在INIT_BOARD_EXPORT,那个时候部分内核机制还没准备好;如果放在INIT_APP_EXPORT,有些依赖hwtimer的应用组件可能已经在更早阶段尝试查找设备,就会找不到。
3.2 逐个实现ops函数
init函数是驱动和硬件打照面的第一关。这里要做的不多,但必须干净:
static rt_err_t hpm_hwtimer_init(struct rt_hwtimer_device *timer, rt_uint32_t prescaler) { struct hpm_hwtimer_dev *dev = rt_container_of(timer, struct hpm_hwtimer_dev, timer); /* 关闭通道、配置分频、清除旧状态 */ tmr_channel_stop_counter(dev->base, dev->channel); tmr_channel_configure(dev->base, dev->channel, &tmr_cfg); timer->prescaler = prescaler; return RT_EOK; }prescaler参数由框架计算好后传进来。我这个例子里的tmr_channel_configure是HPM SDK常见用法,不同HPM SDK版本函数名可能有差异,但逻辑一样。比较重要的是:在init里把通道先停掉,防止上一次残留的计数运行状态影响新配置。
get_countfreq要返回实际计数频率,计算公式很简单:
static rt_err_t hpm_hwtimer_get_countfreq(struct rt_hwtimer_device *timer, rt_uint32_t *freq) { *freq = hwtimer_dev.clock_freq / timer->prescaler; return RT_EOK; }这个函数会被应用层的HWTIMER_CTRL_GET_TIME和框架内部使用。如果分频系数算错,上层看到的时间和真实时间就会对不上。
start是驱动里最重要的函数之一:
static rt_err_t hpm_hwtimer_start(struct rt_hwtimer_device *timer, rt_uint32_t cnt, rt_hwtimer_mode_t mode) { struct hpm_hwtimer_dev *dev = rt_container_of(timer, struct hpm_hwtimer_dev, timer); if (mode == HWTIMER_MODE_ONESHOT) { /* 配置单次模式:匹配后停止 */ } else { /* 配置周期模式:匹配后自动重载 */ } /* 设置比较值 */ tmr_channel_set_cmp(dev->base, dev->channel, cnt); /* 清匹配中断标志,开中断,启动计数器 */ tmr_channel_clear_match_irq_flag(dev->base, dev->channel); tmr_channel_enable_match_irq(dev->base, dev->channel); tmr_channel_start_counter(dev->base, dev->channel); return RT_EOK; }为什么要在设置比较值之前配置模式?因为硬件一旦跑起来,再切模式可能出现"已经匹配过一次但模式还没切对"的窗口。这也是我最早踩坑的地方:先启动了计数器,再配置单次模式,结果第一次超时硬件还按周期模式自动重载,回调被连续触发好几次。
stop和count_get相对简单:
static rt_err_t hpm_hwtimer_stop(struct rt_hwtimer_device *timer) { struct hpm_hwtimer_dev *dev = rt_container_of(timer, struct hpm_hwtimer_dev, timer); tmr_channel_stop_counter(dev->base, dev->channel); tmr_channel_disable_match_irq(dev->base, dev->channel); tmr_channel_clear_match_irq_flag(dev->base, dev->channel); return RT_EOK; } static rt_err_t hpm_hwtimer_count_get(struct rt_hwtimer_device *timer, rt_uint32_t *cnt) { struct hpm_hwtimer_dev *dev = rt_container_of(timer, struct hpm_hwtimer_dev, timer); *cnt = tmr_channel_get_counter(dev->base, dev->channel); return RT_EOK; }先楫部分TMR在读取计数时,为了保持读取一致性,可能需要先锁存计数器的值再读取。如果cnt一直读到0,或者读到跳变的值,优先检查SDK里有没有"锁存/读取暂存"接口。这个细节HPM手册里写得很隐蔽,但基本都能在SDK示例里找到答案。
control函数里处理频率设置和停止命令:
static rt_err_t hpm_hwtimer_control(struct rt_hwtimer_device *timer, rt_hwtimer_ctrl cmd, void *arg) { switch (cmd) { case HWTIMER_CTRL_FREQ_SET: { rt_uint32_t freq = *(rt_uint32_t *)arg; rt_uint32_t prescaler = hwtimer_dev.clock_freq / freq; if (freq > hwtimer_info.maxfreq || freq < hwtimer_info.minfreq) { return -RT_EINVAL; } timer->prescaler = prescaler; return RT_EOK; } case HWTIMER_CTRL_STOP: return hpm_hwtimer_stop(timer); default: return -RT_ENOSYS; } }这里有个很常见的坑:prescaler是整数,除法结果会截断。如果你想设的频率无法整除时钟源,实际计数频率和目标频率会有偏差。比如时钟频率是1MHz,你想分频成30kHz,1000000 / 30000 = 33,实际频率就变成了1000000 / 33 = 30303Hz。如果你的场景对定时有精度要求,要么选择能整除的目标频率,要么在驱动里返回实际频率,让上层知道真实定时时间。
3.3 中断服务函数如何"钩"进RT-Thread
hwtimer超时最终要靠中断来通知。先楫HPM的中断入口需要和RT-Thread中断管理接起来,具体方法看HPM SDK的中断注册接口。
一个典型的ISR长这样:
void hpm_tmr0_ch0_isr(void) { /* 清除匹配中断标志,这一步必须在调用上层回调之前完成 */ tmr_channel_clear_match_irq_flag(HPM_TMR0, 0); /* 通知RT-Thread hwtimer框架,触发应用层回调 */ rt_hwtimer_isr(&hwtimer_dev.timer); }rt_hwtimer_isr是框架提供的中断入口函数,它内部会检查有没有注册超时回调,有的话就调用。需要特别注意的是,应用层回调跑在中断上下文里,不要在回调里做浮点运算、延时、加锁或者长时间打印。如果你需要把超时事件抛给线程处理,应该在回调里只置一个标志,或者用rt_sem_release唤醒一个后台线程,所有耗时处理都放到线程里去。
很多初学者第一次写完驱动,发现回调卡死或者系统复位,十有八九都是因为在回调函数里做了不该做的事。
3.4 接入构建系统:Kconfig与SConscript
驱动文件写完后,还要让构建系统认识它。RT-Thread BSP通常用Kconfig做配置开关,用SConscript做文件收集。
Kconfig片段:
menu "On-chip Peripheral Drivers" config BSP_USING_HWTIMER bool "Enable HWTIMER driver" default n select RT_USING_HWTIMER help Enable HWTIMER driver. if BSP_USING_HWTIMER config BSP_USING_HWTIMER0 bool "Enable TMR0 as hwtimer" default n endif endmenuSConscript片段:
if GetDepend(['BSP_USING_HWTIMER0']): src += ['drv_hwtimer.c']我还习惯在drv_hwtimer.c头部加编译宏,确保Kconfig和源码对应:
#if !defined(BSP_USING_HWTIMER0) #error "BSP_USING_HWTIMER0 not defined" #endif当然这步可选。加上之后的好处是,如果配置没开但文件被强行编进工程,编译阶段就会报错,比运行时找不到设备好排查得多。
4. 应用层验证:从msh命令到周期回调
4.1 先跑一个最小demo
驱动注册好之后,第一时间不是写复杂测试,而是用一个最小demo把链路打通。我把以下代码放到应用层,导出成msh命令:
#include <rtthread.h> #include <rtdevice.h> #include <drivers/hwtimer.h> static rt_err_t hwtimer_timeout_cb(rt_device_t dev, rt_size_t size) { rt_kprintf("hwtimer timeout\n"); return 0; } static void hwtimer_sample(void) { rt_device_t hw_dev = rt_device_find("timer0"); if (!hw_dev) { rt_kprintf("find timer0 failed\n"); return; } if (rt_device_open(hw_dev, RT_DEVICE_OFLAG_RDWR) != RT_EOK) { rt_kprintf("open timer0 failed\n"); return; } rt_device_set_rx_indicate(hw_dev, hwtimer_timeout_cb); rt_uint32_t freq = 10000; rt_err_t ret = rt_device_control(hw_dev, HWTIMER_CTRL_FREQ_SET, &freq); if (ret != RT_EOK) { rt_kprintf("set freq failed\n"); return; } ret = rt_hwtimer_start(hw_dev, 5000, HWTIMER_MODE_PERIOD); if (ret != RT_EOK) { rt_kprintf("start timer failed\n"); return; } } MSH_CMD_EXPORT(hwtimer_sample, run hwtimer sample);这段代码里,freq=10000表示计数频率10kHz,start里的cnt=5000表示计5000个tick,所以超时周期是5000 / 10000 = 0.5秒。这个换算关系一定要清楚,很多问题不是驱动写错,而是应用层把cnt当成毫秒来填。
编译烧录后,在msh里执行hwtimer_sample,如果终端每500毫秒打印一条hwtimer timeout,说明驱动整个链路已经通了一半。接下来再验证单次模式:把HWTIMER_MODE_PERIOD改成HWTIMER_MODE_ONESHOT,重新执行后应该只打印一次hwtimer timeout,然后定时器安静下来。
4.2 用逻辑分析仪验证定时精度
软件打印能证明"事件发生了",但证明不了"时间准不准"。我的习惯是在回调函数里翻转一个GPIO引脚,用逻辑分析仪抓波形,同时测超时周期。
static rt_err_t hwtimer_timeout_cb(rt_device_t dev, rt_size_t size) { rt_pin_write(GPIO_LED_R, !rt_pin_read(GPIO_LED_R)); return 0; }用逻辑分析仪抓GPIO,如果设置500毫秒周期,测出来的实际波形周期也许不是精确的500毫秒,这很正常。误差来源主要有三个:
一是时钟源本身有误差。HPM内部RC或者晶振的精度决定了基础频率,这个误差是所有定时器都躲不开的。二是分频参数取整造成的误差。上面说过,clock_freq / freq无法整除时,实际频率会偏离目标频率。三是中断响应延迟。从硬件匹配到进入ISR再到翻转GPIO,中间有若干条指令执行时间,虽然很短,但对微秒级精度测试有影响。
如果波形周期稳定偏长或偏短,我优先查驱动里的分频计算,把实际get_countfreq返回的值打印出来对照。我曾经遇到一个案例,驱动里设了分频系数,但get_countfreq还傻傻上报时钟源频率,导致上层算出错误的定时时间,整整排查了一个下午,最后发现是这个函数没有除以prescaler。
5. 踩坑实录与排查方法
5.1 设备注册成功却找不到
如果应用层rt_device_find("timer0")返回空指针,先别怀疑驱动注册函数没执行。我建议按这个顺序查:
- 在msh执行
list_device,看系统设备列表里有没有timer0。 - 如果没有,检查Kconfig是否打开了
BSP_USING_HWTIMER0。 - 如果打开了,检查编译日志里有没有编译
drv_hwtimer.c。 - 如果编译了,检查
INIT_DEVICE_EXPORT(hpm_hwtimer_drv_init)是否被编译器优化掉。 - 如果一切正常,打印注册函数返回值,看
rt_hwtimer_register有没有报-RT_EEXIST,可能设备名已经被占用。
一个很容易忽略的现象是:INIT_DEVICE_EXPORT宏在部分编译器优化等级下,如果驱动初始化函数没有显式引用,可能会被链接器丢弃。解决方法是确保函数不是static,或者通过rt_components_init阶段的段收集机制正常链接。RT-Thread官方大部分BSP都依赖这个自动初始化机制,如果你的工程出现"注册函数没跑",多半还是Kconfig或构建脚本的问题。
5.2 频率设置失败或周期漂移
频率设置失败,最常见原因是超出info里minfreq和maxfreq范围。驱动实现里要加边界检查,返回明确的错误码。
周期漂移则更隐蔽。我遇到过三次,原因各不相同:第一次是分频计算没取整,第二次是ISR里清理标志的位置不对,导致一个匹配事件被重复触发,第三次是周期模式下没有重新装载比较值。
先楫TMR周期模式通常有两种做法:一是硬件自动重载,初始化时配置好起始值和比较值;二是靠软件在ISR里重设比较值。如果你选了软件重载,一定记得在ISR里先清标志、再设比较值、最后调用回调。如果顺序反了,可能导致下一次比较值还没更新,硬件又匹配了一次旧的比较值,形成短周期抖动。
5.3 中断不触发
中断不触发是最让人头疼的问题,因为它可能发生在任何一层。我的排查思路是先把RT-Thread和hwtimer框架从链路里摘出去,直接在HPM SDK裸机例子里,看看TMR能不能产生中断。如果不能,说明硬件配置有问题,重点检查时钟使能、中断线使能、PLIC/全局中断状态。如果能,再看RT-Thread侧:
- 注册ISR时是否正确绑定了TMR0通道0的中断号。
- 中断服务函数有没有在RT-Thread中断表里被正确地安装。
- 驱动初始化时有没有意外关闭了对应中断源。
- 匹配中断标志是否在ISR入口立即清除。
另外一个容易忽略的点是优先级。先楫HPM使用中断控制器统一管理外设中断,如果你把某个不相关的中断优先级配得特别高,并且它的ISR里阻塞了很长时间,TMR中断可能迟迟得不到响应。这种情况不算中断不触发,而是"触发了但进不去",实际表现是定时器回调频率远低于预期。
5.4 单次模式不停止
现象是:设置了HWTIMER_MODE_ONESHOT,超时回调却周期性出现。原因通常是硬件通道没有真正进入单次模式,或者ISR里没有主动停止计数器。
一个是硬件层面:部分TMR通道的"单次模式"需要单独配置一个位,如果你只设置了匹配中断,没配置"匹配后停止"的行为,硬件会继续按自由运行模式跑。另一个是软件层面:即使硬件配置正确,某些实现里ISR触发后计数器并没有自动停止,需要在ISR里显式调用stop。我建议在ISR里针对单次模式做一次主动停止,双保险:
if (timer->mode == HWTIMER_MODE_ONESHOT) { hpm_hwtimer_stop(&hwtimer_dev.timer); }不过这个判断最好放在框架层你自有的状态里。如果框架没有暴露mode,那就在驱动模块里自己记一个is_periodic标志位,start时根据mode更新,ISR里按标志位决定要不要主动停。
5.5 通道与外设冲突
先楫HPM的TMR通道既是定时器,也可以配置成PWM输出、输入捕获或脉冲计数。很多芯片内部外设的引脚复用和功能模式相互作用,一旦某个通道被其他驱动占了,hwtimer驱动注册时可能能成功,但真正启动后硬件行为就会错乱。
最典型的情况是:你已经用TMR0的通道0在做PWM,现在又想让hwtimer用同一个通道。两个驱动都往同一组寄存器写配置,最终结果谁也说不清。我在实际项目中就把"hwtimer用的通道"和"电机PWM用的通道"分别固定成不同的TMR外设,并且在Kconfig里明确区分开关,从配置层面杜绝冲突。
排查时发现定时器行为诡异,首先要回头检查这个通道有没有被复用。HPM的TMR功能虽然灵活,但驱动开发最忌讳"一个通道多用"。
6. 从基础篇到进阶的扩展思路
6.1 给hwtimer增加多实例支持
上面示例只注册了一个timer0,但先楫HPM往往有多个TMR外设,一个TMR里还有多个通道。完全可以注册timer0、timer1、timer2等多个hwtimer设备。
推荐做法是把私有结构体改成数组,每个实例保存自己的基地址和通道号:
#define HPM_HWTIMER_DEV_MAX 2 static struct hpm_hwtimer_dev hwtimer_dev[HPM_HWTIMER_DEV_MAX];初始化时根据Kconfig逐项注册。这样应用层就能同时跑多个独立定时器,互不干扰。需要注意:不同通道的中断服务函数必须区分中断号,ISR里也要通过入口参数或全局变量正确判断是哪个设备实例,别把rt_hwtimer_isr敲错成同一个设备。
6.2 更高精度的计时实现
基础篇用单通道32位计数器就够了,但如果你要输出更长周期,或者更高精度,可以考虑把两个32位通道级联成64位计数器这种玩法。不过级联会增加复杂度,而且需要仔细处理进位和高低位读取的一致性问题。
更实用的一种优化是:利用hwtimer做软件闹钟合并。比如应用层同时挂多个超时需求,驱动层可以维护一个最近超时时间,只设置硬件定时器为最近的那个时间点,时间到了再检查所有任务。这样不需要注册很多个硬件定时器,也减轻了中断频率。这个思路在《RT-Thread设备驱动开发指南》进阶内容里会进一步展开,基础篇先把单通道驱动跑通就足够了。
6.3 结合《指南》方法论继续迁移其他外设
我把这次hwtimer的开发流程最后压缩成四步,后续所有外设驱动都可以套用:
第一步,画出框架与硬件的映射关系,明确ops里每个函数对应硬件的哪个行为。第二步,用最小代码把ops填满,注册成设备,跑通最基础的打开和访问。第三步,加中断,把异步事件送进RT-Thread机制,验证事件链路。第四步,接入Kconfig和构建系统,补充错误处理,整理边界条件。
我写UART驱动、SPI驱动、PWM驱动时,都是按这个顺序推进的。不同的是每个外设的映射表会更复杂,但框架方法论不变。所以如果你把这篇hwtimer吃透,再去看《RT-Thread设备驱动开发指南》里其他章节,很多代码都能看懂,剩下的只是硬件细节而已。
最后说一点个人体会。驱动开发这个地方,最忌讳硬抄代码。同一个hwtimer驱动,在STM32上是一种写法,在先楫HPM上又是另一种写法,但设备框架层的逻辑是一样的。我刚开始写驱动时总想找一个"万能模板",后来发现真正可靠的做法是:拿官方BSP里已有的驱动对照着读,搞清楚每一步操作的硬件含义,再跑到目标板上去验证。先楫BSP和RT-Thread源码都是开源的好资料,多读几遍,比自己闭门造车快得多。