ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

手把手写RT-Thread hwtimer驱动:先楫BSP完整实现

手把手写RT-Thread hwtimer驱动:先楫BSP完整实现 做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和opsstatic 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你想分频成30kHz1000000 / 30000 33实际频率就变成了1000000 / 33 30303Hz。如果你的场景对定时有精度要求要么选择能整除的目标频率要么在驱动里返回实际频率让上层知道真实定时时间。3.3 中断服务函数如何钩进RT-Threadhwtimer超时最终要靠中断来通知。先楫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);这段代码里freq10000表示计数频率10kHzstart里的cnt5000表示计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源码都是开源的好资料多读几遍比自己闭门造车快得多。
返回列表