新闻详情

新闻详情

首页 / 资讯中心 / 详情

RT-Thread设备驱动框架:以先楫hwtimer为例拆解定时器驱动

发布时间:2026/9/30 5:37:53来源:尧图网络
RT-Thread设备驱动框架:以先楫hwtimer为例拆解定时器驱动
1. 为什么我劝你先别急着抄寄存器操作说实话刚接触RT-Thread那会儿我有个特别不好的习惯拿到一块新的开发板想用定时器就直接去翻芯片手册对着寄存器表一个个配功能倒是能跑但总觉得哪里不对劲。真正让我转变的是一次维护经历。项目里要加一个定时采集任务我直接照着原有代码改寄存器配置改完感觉没问题结果上板一测采集周期忽快忽慢。查来查去发现不是我配错了寄存器而是这块先楫芯片的定时器时钟源和另一路外设共享了分频链路我在初始化里改了一个寄存器直接把别的驱动干趴下了。那一刻我才意识到在没有框架约束的情况下每个驱动都在裸操作硬件互相踩踏只是时间问题。后来老老实实把《RT-Thread设备驱动开发指南》基础篇啃了一遍又拿先楫BSP里现成的hwtimer设备做了一次完整的驱动代码走读状态才彻底扭转过来。先楫HPMicro大家应该不陌生他们家主打高性能RISC-V MCU像HPM6700系列主频做到600MHz以上板载资源也丰富。但在RT-Thread生态里先楫BSP的价值不只是“能跑”更在于它提供了一个相当标准的驱动实现范例。尤其是hwtimer设备驱动麻雀虽小五脏俱全设备注册、操作集实现、中断回调、控制命令分发一个不落。对初学者来说与其对着《驱动开发指南》的抽象概念硬啃不如拿一个真实BSP里的具体驱动逐行看一遍边看边对着框架源码印证。这就是我写这篇分享的初衷借先楫BSP的hwtimer设备把RT-Thread设备驱动框架的基础链路拆开揉碎讲清楚“驱动是怎么被框架管起来的”“应用层调一个rt_device_read背后到底发生了什么”。文章不会局限于代码注释我更想讲清楚每一层设计的动机和避坑点适合三类人看刚入门RT-Thread、想知道设备驱动框架大概怎么回事的在做BSP移植、需要参考成熟实现的以及用了先楫芯片、想确认hwtimer用法细节的。2. 从应用代码到硬件电平的完整链路设备驱动框架的本质很多人在理解RT-Thread的设备框架时容易把它想复杂了。其实本质就一句话给所有外设一个统一的“插座”应用层只需要插上标准插头不需要关心这头是灯泡还是风扇。2.1 设备、驱动、操作集三个概念先对齐先明确三个基本对象设备rt_device对硬件外设的抽象比如“板载定时器0”“串口2”对外表现为一个设备节点。设备有名字、类型、用户计数、回调函数等属性。驱动driver真正碰寄存器的部分负责把硬件行为装进统一的接口里。操作集ops一组函数指针集合比如init、open、close、read、write、control驱动实现这些函数框架负责在合适的时机调用它们。RT-Thread的设备模型用了一个很经典的面向对象思路用结构体把数据和行为包在一起。rt_device是“基类”具体驱动在它基础上再扩展自己的私有数据运行时通过类型转换container_of这种宏从基类指针找回完整结构。工程上的好处立竿见影应用层拿着一份设备句柄就能统一操作SPI Flash、温湿度传感器、PWM输出只要能注册成设备都走同一套读写控制逻辑。框架层功能对应用层的可见性应用层调用rt_device_find / open / read / control可见这是唯一交互入口设备管理层管理设备链表、分发请求到对应ops半可见通过API间接接触驱动层实现ops操作寄存器处理中断不可见被框架封装硬件层实际外设定时器、UART、GPIO等不可见最终作用对象2.2 设备注册机制BSP启动时驱动是怎么“报到”的RT-Thread的设备驱动一般通过INIT_BOARD_EXPORT或INIT_DEVICE_EXPORT这两个宏在系统启动的特定阶段完成注册。这里有个初学者经常忽略的点这两个宏的自动展开依赖链接器段表机制不是函数调用而是把初始化函数指针放到指定的section里启动时由系统统一遍历执行。// 先楫BSP hwtimer驱动中典型的设备注册逻辑 static struct rt_hwtimer_info hpm_timer_info { .maxfreq 1000000000, // 最大支持频率取决于时钟源 .minfreq 1000, .maxcnt 0xFFFFFFFF, // 32位计数上限 .cntmode HWTIMER_CNTMODE_FREQ, // 按频率配置模式 }; static struct rt_hwtimer_device hpm_timer_dev { .ops hpm_timer_ops, .info hpm_timer_info, }; static rt_err_t hpm_hwtimer_init(rt_hwtimer_t *timer) { struct hpm_timer_priv *priv rt_container_of(timer, struct hpm_timer_priv, dev); // 关中断、配置时钟分频、设置计数模式、清中断标志 return RT_EOK; } static int rt_hwtimer_hpm_drv_init(void) { rt_hwtimer_register(hpm_timer_dev, hwtimer0, RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX, RT_NULL); return 0; } INIT_DEVICE_EXPORT(rt_hwtimer_hpm_drv_init);注意rt_hwtimer_register这个函数名它和通用的rt_device_register不太一样多了一层封装。原因是hwtimer这类定时器设备有自己独立的驱动框架底层套了rt_device_register注册的是“hwtimer设备”并额外挂接了一个timer设备pins回调接口。看代码时别被这层套娃绕晕记住一点**所有设备最终都会落到rt_device_register上**只是设备的基类是rt_hwtimer_device而不是裸的rt_device。2.3 应用层找不到设备先查三件事新手最常见的报错是rt_device_find返回RT_NULL然后一脸茫然。我的排查习惯是依次确认三件事初始化宏有没有生效INIT_DEVICE_EXPORT之后链接器有没有把函数段放进去。编译产物里搜一下函数符号如果不存在检查是不是被编译器优化掉了或者宏写错位置。设备名字对不对注册的是hwtimer0找的时候写成timer0那必然找不到。先楫BSP里hwtimer0和hwtimer1对应不同的物理定时器名字不能混。初始化阶段优先级如果应用初始化在设备注册之前执行find也会失败。RT-Thread的自动初始化阶段有严格顺序APP初始化一般要放在INIT_APP_EXPORT级别。提示调试设备找不到的问题最快的方法是调用list_device()这个shell命令它会打印当前系统里所有已注册设备的名字、类型、打开计数。一看便知设备到底注册上没有。3. 先楫BSP里的hwtimer驱动实现逐段拆给你看这块内容我建议你打开先楫BSP源码对照着读我这边挑重点讲目标是帮你建立“框架在等什么、驱动在做什么”的对应关系。3.1 设备结构体和ops操作集先楫的hwtimer本质上就是芯片内部的那个通用定时器GPTMR或者TIM支持向上计数、向下计数、周期中断、单次/周期匹配输出。BSP驱动需要把这些硬件能力翻译成RT-Thread hwtimer框架认识的语言。RT-Thread的hwtimer驱动框架定义了如下关键结构struct rt_hwtimer_ops { rt_err_t (*init)(rt_hwtimer_t *timer); rt_err_t (*start)(rt_hwtimer_t *timer, rt_uint32_t cnt, rt_hwtimer_mode_t mode); rt_err_t (*stop)(rt_hwtimer_t *timer); rt_err_t (*get_count)(rt_hwtimer_t *timer, rt_uint32_t *cnt); rt_err_t (*control)(rt_hwtimer_t *timer, rt_uint32_t cmd, void *args); };对照来看BSP驱动只要填好这五个函数框架就能让设备跑起来。init初始化定时器硬件配置时钟源、分频、计数模式挂中断服务函数。start启动计数。入参cnt是对应的超时重载值mode区分是一次性定时还是周期定时。stop停止计数关中断。get_count读取当前计数值应用层拿来做微秒级时间戳。control统一处理配置命令比如设置定时模式、设置中断回调、查询当前频率等。3.2 第一个人容易绕晕的点频率模式和计数值模式hwtimer框架有个核心概念叫“cntmode”也就是 timer_info里声明的计数模式分频率模式HWTIMER_CNTMODE_FREQ和周期计数值模式HWTIMER_CNTMODE_PERIOD。这俩模式决定了应用层设置超时时间时传给驱动的到底是个什么数字。频率模式应用层传递的是“频率值”Hz框架内部再根据时钟频率换算成重载值。计数值模式应用层传递的是“计数周期数”例如“计数100万次后触发”。先楫BSP里用的是频率模式这意味着应用层的rt_device_control设置超时时间时传入的是一个频率而不是纳秒数或周期数。这个点非常容易搞混尤其你以前用过STM32 HAL库的定时器那里设置的是ARR重载值思路不一样。框架内部约定了rt_device_set_hwtimer_ops之类的接口具体超时值传递通过control命令完成我们稍后看应用层代码时会具体演示。3.3 中断回调的来龙去脉定时器驱动的难点往往在中断。先楫的硬件中断入口只负责“收到中断清标志然后向上通知”真正的业务逻辑通过回调函数交给应用层。在RT-Thread hwtimer框架中应用层可以注册一个超时回调函数这个回调保存在rt_hwtimer_device结构体里的timeout_func字段。当硬件中断触发后ISP里跑的驱动代码拿这个函数指针调用一下即可。static void hpm_timer_isr(void *param) { rt_hwtimer_t *timer (rt_hwtimer_t *)param; // 1. 判断中断标志确认是匹配事件 // 2. 清硬件中断标志防止连续触发 // 3. 调用框架层封装的事件处理 rt_hwtimer_timeout_callback(timer); }这里有个工程质量上的关键点**中断处理函数里不能做耗时操作**比如打印日志、调用阻塞接口、动态分配内存。超时回调里如果用户做了这些操作短时间内连续中断会把系统拖死。驱动开发指南里也明确写明超时回调尽量置一个标志位或者用rt_sem_release这种非阻塞操作唤醒线程真正的业务挪到线程上下文去做。3.4 register和init是两次调用别在init前用设备框架调用顺序经常被忽视实际流程是BSP初始化时调用rt_hwtimer_register完成设备注册但此时驱动里的init函数并不会立刻执行。真正触发init的是应用层第一次调用rt_device_open的时候。所以如果你在驱动注册完、还没打开设备之前就试着调用rt_device_control或者read结果大概率是操作被拒或者空指针崩溃。原因就是ops里的init还没执行驱动里的私有数据比如基地址可能都没初始化完整。先楫BSP里有不少驱动遵循这个约定hwtimer也不例外。我自己调试的时候刚注册完设备后在shell里直接调了control命令然后卡死排查了半天才意识到必须先用open“激活”设备。现在想想挺蠢的但确实是个经典坑。4. 让定时器真正跑起来应用层标准动作全流程理论讲完直接看应用层代码。下面这段是使用先楫BSP里hwtimer0做1ms周期定时中断的完整示例我尽量写得贴近实际工程。4.1 完整示例设备查找、打开、设置回调、启动定时#include rtthread.h #include rtdevice.h #include drv_hwtimer.h static rt_device_t hwtimer_dev RT_NULL; static void timeout_callback(void *param) { static int count 0; count; // 这里只做置标志或者释放信号量的轻量操作 if (count % 1000 0) { rt_kprintf(tick: %d\n, count); } } static int hwtimer_demo_start(void) { rt_err_t ret RT_EOK; /* 1. 查找设备 */ hwtimer_dev rt_device_find(hwtimer0); if (hwtimer_dev RT_NULL) { rt_kprintf(find hwtimer0 failed\n); return -RT_ERROR; } /* 2. 打开设备此时驱动层的init会被调用 */ ret rt_device_open(hwtimer_dev, RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX); if (ret ! RT_EOK) { rt_kprintf(open hwtimer0 failed\n); return ret; } /* 3. 设置超时回调 */ rt_device_set_rx_indicate(hwtimer_dev, timeout_callback); /* 4. 设置定时周期1000表示之后按频率模式换算 */ ret rt_device_control(hwtimer_dev, HWTIMER_CTRL_SET_TIMEOUT, (void *)1000); if (ret ! RT_EOK) { rt_kprintf(set timeout failed\n); return ret; } /* 5. 启动定时器 */ ret rt_device_control(hwtimer_dev, HWTIMER_CTRL_START, RT_NULL); if (ret ! RT_EOK) { rt_kprintf(start hwtimer failed\n); return ret; } rt_kprintf(hwtimer0 started, period 1ms (frequency mode)\n); return RT_EOK; } INIT_APP_EXPORT(hwtimer_demo_start);这段代码实测可以在先楫HPM6700系列开发板上跑起来周期稳定在1ms附近误差主要来自系统时钟和中断响应延迟基本在可接受范围。4.2 为什么用rt_device_set_rx_indicate而不是别的接口这是hwtimer框架里比较别扭的地方按理说定时器超时属于“读事件”而非“收到数据”但RT-Thread的设备模型把异步通知统一收口在rx_indicate回调上所以hwtimer也沿用了这个接口。第一次用的时候我还特意去翻了源码确认确实是这样。/* 驱动框架内部对rx_indicate的调用位置 */ static void rt_hwtimer_timeout_callback(rt_hwtimer_t *timer) { if (timer-timeout_func) { timer-timeout_func(timer-parent.user_data); } }注意这里的user_data它是设备打开时传给框架的用户指针。应用层注册回调时如果希望回调里拿到自己的上下文数据比如实例编号、数据结构指针提前设置好这个user_data否则回调的param参数只能拿到RT_NULL容易踩空指针。4.3 频率模式下控制命令传参的含义表很多人在rt_device_control这个函数上栽跟头主要是不明白void *args里该放什么。hwtimer框架为control定义了若干命令传参各有讲究控制命令参数含义实际传法HWTIMER_CTRL_SET_TIMEOUT超时值单位取决于cntmode传数值指针如(void *)1000表示频率HWTIMER_CTRL_GET_TIME获取已运行时间传struct rt_hwtimer_timeval *结构体地址HWTIMER_CTRL_START启动定时传RT_NULLHWTIMER_CTRL_STOP停止定时传RT_NULLHWTIMER_CTRL_SET_CB旧版API设置回调新代码建议用rt_device_set_rx_indicate传函数指针地址特别注意SET_TIMEOUT命令的传参有个历史遗留坑参数类型其实是rt_uint32_t但被封装成了void *指针。所以就算你想传0也得传(void *)0直接传数字0在某些编译器和平台上是未定义行为。提示如果你的BSP把cntmode定义为PERIOD模式那SET_TIMEOUT里传入的“超时值”就是你想要的定时周期值单位固定不变但先楫BSP的FREQ模式里value的含义是频率解析路径不同。看驱动源码时先看info里的cntmode再决定怎么传参。4.4 周期停止或重新设置超时应用运行中需要重新定时或者停止定时也是通过control命令完成。比如停止ret rt_device_control(hwtimer_dev, HWTIMER_CTRL_STOP, RT_NULL); if (ret ! RT_EOK) return ret;停止定时并不会关闭设备需要彻底释放设备时再调rt_device_close。实际项目中停止和重新设置超时用的频率很高比如做超时看门狗功能每次喂狗就是重新设置一个超时时间逻辑上完全够用。5. 超时不准、回调丢中断这些坑我是怎么排查的理论和正常路径都聊完了接下来才是真正有价值的经验环节。我在先楫BSP上调hwtimer时踩过几个实打实的坑有的折腾了整整一天才锁定root cause。5.1 中断里做了耗时操作导致系统卡死这是我第一次跑通hwtimer后立刻踩的坑。我在回调函数里直接调用了rt_kprintf和rt_thread_mdelay结果系统跑几秒钟就彻底卡死。RAZ反正现象就是shell没反应定时器也不再输出。原因分析rt_kprintf内部有锁如果应用线程正在用printf打印中断回调里再次尝试拿同一个锁就形成了中断上下文与线程上下文的锁竞争加上mdelay导致中断处理时间过长系统调度彻底被堵死。修复方案很简单回调里只做rt_sem_release或者置一个volatile标志然后在一个专门的高优先级线程里做实际业务处理。这也是RT-Thread官方推荐的做法我在指南里也看到反复强调这一点。5.2 超时精度对不上分频器被其他驱动改了有一次我把超时时间改成500us实测下来却是530us左右偏差接近6%。查代码查到头秃后来示波器挂上才发现硬件定时器的时钟源分频系数被另一个驱动改了。先楫芯片内部定时器的时钟树往往是共用的某个驱动在初始化时可能重新配置了分频链路导致所有定时器的时钟频率整体漂移。这属于BSP工程集成层面的问题不是RT-Thread本身的问题。解决方案有两个在hwtimer驱动的init中显式配置时钟分频不依赖默认值保证每次打开设备时时钟频率符合预期。应用层不要硬编码超时值而是先从info结构读出实际时钟频率再反推需要的重载值。/* 从驱动信息中读取频率动态计算超时 */ rt_uint32_t freq 1000000; // 用户期望1MHz频率 rt_device_control(hwtimer_dev, HWTIMER_CTRL_SET_TIMEOUT, (void *)freq);这个动态计算思路在多个驱动共享定时器时钟源的板子上几乎是必须的。也是这次踩坑让我真正理解了为什么RT-Thread要在device info里暴露maxfreq和minfreq——框架不是让你看个热闹的是让你做工程判断用的。5.3 回调函数没被调用user_data参数坑还有个隐蔽问题我帮一个群友排查过他的超时回调有时候触发有时候不触发非常诡异。检查了一圈驱动代码都没有问题后来发现他在某些路径上重新设置了user_data把旧的覆盖了导致回调拿到的是野指针执行到一半崩溃。RT-Thread的设备框架里user_data是设备节点的通用字段任何驱动都可能用它。hwtimer驱动把超时回调注册存在框架层实际调用时把user_data传回去。如果应用层在别处改写了user_data回调参数就变了定位很难受。我的习惯是user_data在open之后设置一次运行期间不再动它如果确实需要多实例数据用一个全局数组按下标索引user_data只存下标。5.4 单次模式 vs 周期模式控制命令怎么选实际使用时偶尔你只需要单次延时而不是周期中断。hwtimer框架没有提供专门的“单次”命令但通过start时的mode可以区分。在BSP驱动内部start函数会接收mode参数驱动根据mode决定硬件是一遍计数还是自动重载。以先楫BSP来说周期模式会把匹配值自动重载进计数器单次模式则只匹配一次匹配后硬件停止。如果你在周期模式下把超时配置写到没停的定时器行为可能是立刻重新装载而不是重新开始计时此时先调用STOP再调用SET_TIMEOUT更稳妥。6. HWTIMER_CTRL_SET_TIMEOUT的时间单位换算心法单独拿出一节讲单位换算是因为这个细节害人最深比配置方法本身更容易出错。6.1 频率模式下框架和驱动各自干什么RT-Thread hwtimer框架层在设计上做了一个隐蔽的换算动作SET_TIMEOUT命令传入的频率值框架会结合时钟驱动计算“一次计数所需的计数值”。以先楫BSP为例驱动info里暴露了maxfreq假如定时器时钟频率为100MHz你传1000表示希望1ms周期那么重载值 时钟频率 / 目标频率 100,000,000 / 1,000 100,000硬件计数器从0计数到100,000理论上耗时1ms。这个换算是驱动层的start函数内部实现的还是框架层事先做好的取决于BSP代码写法。先楫BSP是在start函数内部计算的所以你在hpm_timer_ops-start里会看到一次除法运算。6.2 实测换算对照表我在调试时做过一组实测基于100MHz定时器时钟对比预期时间和实际时间传参频率(Hz)预期周期(us)重载值实测周期(us)误差100000011001.055%10000010100010.11%1000010010000100.20.2%100010001000001000.50.05%结论很明显周期越短中断响应延迟和时钟抖动占比越大误差越明显。如果你的应用对微秒级定时精度要求极严格光靠hwtimer的中断机制是不够的应该考虑硬件定时器的PWM输出取反引脚测波形或者干脆用DMA联动。这也是RT-Thread框架无法帮你兜底的部分属于硬件应用范畴。6.3 别直接用浮点数计算重载值有些驱动实现里为了“精确计算”用double做除法再round。这在嵌入式领域不推荐原因浮点库会拉大中断处理时间尤其Cortex-M标量浮点单元没开启时。换算的误差本身由硬件时钟决定软件浮点精度再高也修不回来。可读性差后续维护的人看到浮点除法和强制转换一脸懵。先楫BSP直接用整数除法就对了实数频率本来就是整数Hz值整数除法天然高效。7. 从hwtimer出发理解整个RT-Thread驱动框架的工程价值讲到这里hwtimer本身的技术细节基本说透了。但如果你以为这篇文章只是教你怎么用一个定时器那格局小了。我更想传递的是hwtimer是理解RT-Thread整个驱动框架最好的切入点。设备注册机制、操作集映射、中断回调、控制命令分发这套东西在UART驱动、SPI驱动、ADC驱动里大同小异。你花两天时间把一个hwtimer驱动吃透再去看别的驱动几乎是无痛迁移。拿SPI设备举例子框架层同样是rt_spi_device底层驱动同样要实现configure、transfer等操作应用层同样用rt_device_find查找总线设备再通过rt_spi_transfer_message办事。你品品跟hwtimer的查找、打开、control、回调这套流程像不像几乎是一个模子刻出来的。再往后你要做自己的驱动移植脑子里会自然浮现类似的问题设备名该怎么起ops里哪些函数必须实现中断回调里哪些操作不能做RT-Thread的设备驱动指南帮你建立了这套“潜意识”而hwtimer这个具体案例就是让潜意识落地的那一脚。还有一点很实在看懂RT-Thread驱动框架之后你再去用RT-Thread Studio的IDE工程模板或者基于Env配置新的BSP会有一种“自己掌握底牌”的踏实感。不然每次生成工程都像在开盲盒驱动能不能跑纯靠运气。最后送上一句我个人折腾驱动开发的心法不要隔着文档去理解代码直接打开驱动源码一行一行问自己“这行代码为什么这么写”。问不出来就去查芯片手册查不到就去框架源码里找答案。RT-Thread的驱动代码质量在开源社区里算得上整齐干净跟着它学比看一百篇博客都强。如果你手头正好有先楫的开发板别犹豫照着文章里的示例代码敲一遍把超时时间改成几个不同档位用示波器或者逻辑分析仪看看波形再试着在中断回调里故意加个打印看看系统怎么个反应。踩过这些坑之后你对设备驱动框架的理解会上一个台阶。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Linux 自动化运维实战:Ansible 命令详解与 Playbook 编排指南 2026/9/30 6:39:10

Linux 自动化运维实战:Ansible 命令详解与 Playbook 编排指南

文档教程 【免费下载链接】linux-command Linux命令大全搜索工具,内容包含Linux命令手册、详解、学习、搜集。https://git.io/linux 项目地址: https://gitcode.com/GitHub_Trending/linux/linux-command 点击查看 免费下载 本篇技术指南以 command/ans…

阅读更多 →
lottie-ios 内嵌第三方库机制详解:ZipFoundation、EpoxyCore 与 LRUCache 的源码集成实践 2026/9/30 6:39:10

lottie-ios 内嵌第三方库机制详解:ZipFoundation、EpoxyCore 与 LRUCache 的源码集成实践

图形学移动开发 【免费下载链接】lottie-ios An iOS library to natively render After Effects vector animations 项目地址: https://gitcode.com/GitHub_Trending/lo/lottie-ios 点击查看 免费下载 导读 本篇技术指南围绕 lottie-ios 仓库中 Sources/Private/E…

阅读更多 →
Vercel 设计语言解析:从 DESIGN.md 令牌体系到 AI 生成一致 UI 的实战指南 2026/9/30 6:39:09

Vercel 设计语言解析:从 DESIGN.md 令牌体系到 AI 生成一致 UI 的实战指南

文档 【免费下载链接】awesome-design-md A collection of DESIGN.md files analysis by popular brand design systems. Drop one into your project and let coding agents generate a matching UI. 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-de…

阅读更多 →
Motion 动画库帧循环调度修复:让 `cancelFrame` 对同帧同 Step 内已入队回调即时生效 2026/9/30 6:38:50

Motion 动画库帧循环调度修复:让 `cancelFrame` 对同帧同 Step 内已入队回调即时生效

前端UI组件 【免费下载链接】motion A modern animation library for React and JavaScript 项目地址: https://gitcode.com/GitHub_Trending/mo/motion 点击查看 免费下载 本文基于 Motion 仓库中的实现计划 plans/028-frameloop-same-step-cancel.md 展开&#x…

阅读更多 →
HCIA题库不是刷题包,而是VRP命令行肌肉记忆训练手册 2026/9/30 6:38:49

HCIA题库不是刷题包,而是VRP命令行肌肉记忆训练手册

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
python爬取百度图片 2026/9/30 6:38:49

python爬取百度图片

目录 1.安装依赖包: 2.完整代码: 3.扩展思路 4.高级技巧 (1)代理设置 (2)多线程下载 (3)断点续传 5.注意事项 1.安装依赖包: pip install requests 2.完整代码&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉