新闻详情

新闻详情

首页 / 资讯中心 / 详情

STM32嵌入式项目V1封装实战:CAN、FreeRTOS、Flash与PI控制集成

发布时间:2026/9/27 10:52:30来源:尧图网络
STM32嵌入式项目V1封装实战:CAN、FreeRTOS、Flash与PI控制集成
1. 从零散模块到可交付系统V1封装的真实动机做嵌入式项目的人大概都有过这种体验功能一个个都调通了CAN能收发、Flash能读写、FreeRTOS任务跑得也挺欢但当你试图把整个工程交给别人、或者过两个月自己再回来看的时候却发现完全理不清头绪。main.c里塞了八百行初始化代码中断回调里藏着业务逻辑各个模块之间靠全局变量互相“喊话”想改一个采样周期都得翻遍五个文件确认没有副作用。V1封装要解决的就是这个问题。它不是简单的“把代码整理一下”而是把一堆验证过的功能模块按照明确的接口边界、初始化顺序、错误处理策略重新组织成一个可复用、可测试、可交接的工程骨架。关键词里出现的STM32、CAN、FreeRTOS、Flash、PI这几个词基本勾勒出了这个项目的技术栈轮廓一颗STM32芯片作为主控CAN总线负责节点间通信FreeRTOS承载多任务调度Flash用于参数存储或日志记录而PI大概率指的是某种闭环控制算法比例积分控制器或者树莓派之类的上位机协同。这篇文章适合谁看如果你手头正好有一个“功能都通了但结构稀烂”的STM32项目或者你正准备做一个基于CAN和FreeRTOS的多任务系统再或者你只是想知道一个嵌入式项目从“能跑”到“能交付”之间到底差了什么那接下来的内容应该对你有用。我会把V1封装过程中涉及的模块划分逻辑、RTOS任务设计、CAN通信层抽象、Flash参数管理、以及PI控制环的集成方式按照实际操作的顺序拆开讲该给参数给参数该说坑说坑。2. 封装前必须先想清楚的模块边界与依赖方向2.1 为什么“能跑就行”的代码一到封装就出问题很多人在功能验证阶段习惯了一种“扁平化”的写法所有初始化都在main()里顺序调用所有中断服务函数直接操作业务变量任务之间通过全局标志位同步。这种写法在单人开发、功能单一的时候确实效率高但它有一个致命缺陷——依赖关系是隐式的。比如CAN接收中断里直接修改了一个全局的motor_target变量而PI控制任务又去读这个变量表面上看没问题但当你想把PI控制任务单独拿出来测试时就发现它依赖了CAN中断的副作用根本没法独立运行。V1封装的核心动作就是把这些隐式依赖变成显式接口。具体来说我做了三件事第一把每个功能模块的对外接口收敛到一个头文件里外部只能看到函数声明和必要的数据类型内部实现细节全部用static藏起来第二明确每个模块的初始化函数和去初始化函数保证模块可以按任意顺序初始化和反初始化而不出问题第三禁止跨模块直接访问全局变量所有数据交换必须通过函数调用或者RTOS的消息队列。2.2 模块划分的实际方案与依赖方向控制这个项目最终划分成了六个核心模块依赖方向是严格单向的上层可以调用下层下层绝对不能反向依赖上层。模块名称职责依赖的下层模块bsp_canCAN外设初始化、报文收发、ID过滤bsp_clock,bsp_gpiobsp_flashFlash扇区擦写、参数读写、磨损均衡bsp_clockdrv_piPI控制器计算、抗积分饱和、输出限幅无纯算法app_motor电机控制逻辑、状态机drv_pi,bsp_canapp_param参数管理、默认值加载、Flash同步bsp_flashtask_schedulerFreeRTOS任务创建、优先级分配、队列管理所有app层模块这个表格看起来简单但实际划分的时候我反复调整了好几次。最开始我把PI控制器放在了app_motor里面后来发现上位机调试的时候也需要单独跑PI算法做离线仿真就把它抽出来做成了纯算法模块drv_pi不依赖任何硬件。这个调整带来的好处非常明显PI参数整定的时候可以直接在PC上跑单元测试不用每次都烧录到板子上。注意模块划分不是越细越好。我见过有人把GPIO操作也封装成一个模块结果每个引脚都要写一个函数调用链长得离谱。划分的标准应该是“这个模块有没有独立的变化原因”如果两个功能的修改原因总是一样的那它们就应该放在一起。2.3 头文件里应该放什么、不应该放什么封装质量的高低很大程度上取决于头文件的设计。我的原则是头文件只暴露调用者必须知道的东西其他一律不放。具体来说函数声明、枚举类型、结构体定义如果调用者需要创建实例、宏定义如果调用者需要用作参数是必须的而内部使用的宏、内部结构体的成员、static函数的声明绝对不应该出现在头文件里。还有一个容易被忽略的点头文件里不要#include那些只有实现文件才需要的头文件。比如bsp_can.h里如果只用了uint32_t这种标准类型那就只#include stdint.h就够了不要把stm32f4xx_hal.h整个拉进来。这样做的好处是减少编译依赖改一个HAL库的配置不会导致所有包含bsp_can.h的文件全部重编译。3. FreeRTOS任务划分与CAN通信的配合方式3.1 任务数量与优先级的确定逻辑FreeRTOS任务划分最容易犯的错误就是“一个功能一个任务”结果创建了十几个任务每个任务大部分时间都在阻塞调度器光切换上下文就耗掉了不少CPU。我的做法是先梳理出系统中真正需要并发执行的数据流然后按照数据流的实时性要求来分配任务和优先级。这个项目最终只用了四个任务CAN接收任务优先级最高configMAX_PRIORITIES - 2阻塞在CAN接收队列上收到报文后解析并分发到对应的处理队列。之所以给它最高优先级是因为CAN总线的报文如果不及时读走硬件接收FIFO溢出后就会丢帧这个损失是不可接受的。电机控制任务优先级次高configMAX_PRIORITIES - 3以固定的1kHz周期运行从队列中读取最新的目标值执行PI计算输出PWM。这个任务的周期必须严格保证否则PI控制器的积分项会因为采样周期抖动而计算错误。参数管理任务优先级中等tskIDLE_PRIORITY 2负责响应参数修改请求将参数写入Flash以及上电时从Flash加载参数。这个任务的实时性要求不高但写Flash的时候会阻塞较长时间所以优先级不能太高否则会影响控制任务。状态上报任务优先级最低tskIDLE_PRIORITY 1周期性地通过CAN总线发送状态报文周期是100ms。这个任务纯粹是“尽力而为”丢一帧状态报文对系统没有影响。优先级分配的核心原则是周期越短、截止时间越硬的任务优先级越高。但也要注意FreeRTOS的中断优先级和任务优先级是两套不同的体系CAN接收中断的优先级配置必须低于configMAX_SYSCALL_INTERRUPT_PRIORITY否则在中断里调用xQueueSendFromISR会直接触发断言失败。3.2 CAN报文接收与分发的队列设计CAN接收任务和电机控制任务之间的数据传递我用了一个长度为8的消息队列。队列里存的结构体定义是这样的typedef struct { uint32_t can_id; uint8_t data[8]; uint8_t dlc; uint32_t timestamp; } can_rx_msg_t;CAN接收中断里调用xQueueSendFromISR把报文塞进队列接收任务从队列里取出来之后根据can_id判断报文类型再分发到不同的处理逻辑。这里有一个细节值得展开说为什么不在中断里直接解析报文因为解析报文可能涉及浮点运算、查表、甚至调用PI控制器的参数更新函数这些操作在中断上下文里执行时间不可控容易导致其他中断被延迟响应。把解析工作放到任务里中断只负责搬运数据响应时间就稳定在微秒级别。还有一个坑我踩过队列长度设得太小。最开始我设了长度4结果在总线负载高的时候接收任务还没来得及处理队列就满了xQueueSendFromISR返回失败报文直接丢了。后来改成8并且在队列满的时候增加一个溢出计数器通过状态报文上报给上位机方便定位问题。队列长度的选择没有固定公式我的经验值是队列长度 ≥ 任务最坏情况下的处理延迟 / 报文平均到达间隔。比如接收任务最坏情况要5ms才能回到队列读取报文平均每1ms来一帧那队列长度至少要是5留点余量就设8。3.3 中断优先级与任务优先级的常见混淆这个问题值得单独拿出来说因为我在调试过程中至少见过三次因为搞混这两者而导致的问题。FreeRTOS里有两套优先级中断优先级由NVIC配置数值越小优先级越高和任务优先级由xTaskCreate的参数指定数值越大优先级越高。它们之间唯一的联系是中断优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断不能调用任何FreeRTOS的API。具体到这个项目CAN接收中断的抢占优先级我配置为5而configMAX_SYSCALL_INTERRUPT_PRIORITY设置为5在STM32的HAL库中通常对应NVIC分组4即4位抢占优先级。这意味着CAN中断的优先级等于阈值可以安全地调用FromISR结尾的API。如果我把CAN中断优先级设成4那它就不能调用xQueueSendFromISR否则系统会进入configASSERT死循环。提示STM32的NVIC优先级分组一定要在初始化最开始就设置好并且整个工程只设置一次。我见过有人在每个模块的初始化里都调用HAL_NVIC_SetPriorityGrouping结果后面的调用覆盖了前面的设置导致中断优先级完全乱套。4. Flash参数存储的可靠性设计与磨损处理4.1 为什么不能直接把参数写进Flash就完事Flash的物理特性决定了它不能像RAM一样随意读写。STM32内部的Flash擦除粒度是扇区通常16KB到128KB不等写入粒度是半字16位或字32位。这意味着如果你想修改一个字节的参数实际上需要读出整个扇区到RAM修改对应字节擦除整个扇区再把整个扇区写回去。这个过程不仅耗时擦除一个128KB扇区可能需要1-2秒而且擦除次数有限通常10万次左右。如果每次参数变化都直接写Flash两个问题会立刻暴露第一写Flash期间CPU如果从Flash取指令就会阻塞导致控制任务错过截止时间第二频繁擦写会快速消耗Flash寿命。所以参数管理必须做两层缓冲RAM里维护一份当前参数副本只有在上位机明确下发“保存参数”命令时才把RAM副本同步到Flash。4.2 双扇区交替存储与参数校验为了进一步提高可靠性我用了两个扇区交替存储参数。具体做法是在Flash中划分两个相同大小的扇区SECTOR_A和SECTOR_B每个扇区开头放一个头部结构体包含魔数、版本号、参数长度和CRC校验值。上电时先读SECTOR_A的头部如果魔数和CRC都正确就用A区的参数否则读SECTOR_B如果两个都不对就加载默认参数并标记需要重新保存。保存参数的时候总是写入“当前未使用”的那个扇区写完之后更新一个存储在备份寄存器里的“当前有效扇区”标志。这样即使写入过程中断电另一个扇区里的旧参数仍然是完整的系统下次上电会自动回退到旧参数。这个方案的成本很低只需要多占用一个扇区的空间但可靠性提升非常明显。typedef struct { uint32_t magic; // 0x50415241 (PARA) uint16_t version; uint16_t param_len; uint32_t crc32; uint8_t reserved[16]; } flash_param_header_t; typedef struct { float kp; float ki; float integral_limit; float output_limit; uint32_t can_baudrate; uint16_t report_interval_ms; uint8_t reserved[32]; } system_params_t;CRC校验我用的是STM32硬件CRC外设配置成32位多项式0x04C11DB7初始值0xFFFFFFFF。硬件CRC的计算速度比软件查表快很多而且不占用CPU周期。唯一需要注意的是硬件CRC外设一次只能处理32位数据参数结构体需要按字对齐并且长度必须是4的倍数。4.3 写Flash时的任务调度策略写Flash期间CPU会阻塞这是硬件特性决定的没法绕过。但我们可以控制什么时候写把影响降到最低。我的策略是参数管理任务收到保存请求后先通过队列通知电机控制任务“即将写Flash”电机控制任务收到通知后把PWM输出置为安全状态比如占空比归零然后参数管理任务才开始擦写。写完之后再通知电机控制任务恢复输出。这个过程听起来有点小题大做但在实际调试中非常必要。我有一次忘了做这个协调结果写Flash的时候电机控制任务正好在运行PI计算因为Flash阻塞导致采样周期从1ms变成了1.5ms积分项累积出错电机直接飞车了。从那以后任何可能阻塞超过100微秒的操作我都会先让控制任务进入安全状态。5. PI控制环在RTOS环境下的实现细节5.1 位置式PI与增量式PI的选择依据PI控制器有两种常见实现位置式和增量式。位置式直接计算输出绝对值增量式计算输出的变化量。在这个项目里我选择了位置式原因是执行机构电机驱动器需要的是绝对位置或绝对速度指令而不是增量。如果用电机的增量式编码器做反馈增量式PI确实更方便但这里用的是绝对式磁编码器位置式更直接。位置式PI的离散化公式是u(k) Kp * e(k) Ki * T * sum(e(j)) u0其中e(k)是当前误差T是采样周期sum(e(j))是历史误差累积u0是输出偏置。在代码实现中积分项用一个static float变量持续累加每次计算后更新。5.2 抗积分饱和与输出限幅的工程处理积分饱和是PI控制器在实际系统中最常见的问题。当误差长时间存在比如电机堵转积分项会一直累加直到达到浮点数的精度极限。这时候即使误差反向积分项也需要很长时间才能“退饱和”导致系统响应严重滞后。我的处理方式是积分分离加限幅当输出达到限幅值时停止积分累加同时给积分项本身也设一个上下限防止它在输出未饱和时就已经累积过大。具体代码逻辑是这样的float pi_calculate(pi_ctrl_t *ctrl, float setpoint, float feedback) { float error setpoint - feedback; float p_term ctrl-kp * error; // 积分项累加但仅在输出未饱和时进行 if (!ctrl-output_saturated) { ctrl-integral ctrl-ki * ctrl-sample_time * error; // 积分限幅 if (ctrl-integral ctrl-integral_limit) { ctrl-integral ctrl-integral_limit; } else if (ctrl-integral -ctrl-integral_limit) { ctrl-integral -ctrl-integral_limit; } } float output p_term ctrl-integral ctrl-offset; // 输出限幅并记录饱和状态 if (output ctrl-output_limit) { output ctrl-output_limit; ctrl-output_saturated true; } else if (output -ctrl-output_limit) { output -ctrl-output_limit; ctrl-output_saturated true; } else { ctrl-output_saturated false; } return output; }这段代码里有一个细节output_saturated标志是在输出限幅之后更新的但它影响的是下一次积分累加。这意味着当前周期如果输出饱和了下一周期的积分项就不会继续累加给系统一个“喘息”的机会让误差反向。这个逻辑我调了好几版才稳定下来最开始忘了加积分限幅结果电机在堵转测试时积分项直接冲到1e6松开之后电机猛冲了一下差点把联轴器扭断。5.3 PI参数在线整定的实现方式PI参数如果写死在代码里每次调整都要重新编译烧录效率太低。我在参数管理模块里预留了kp和ki两个可写参数通过CAN总线接收上位机的整定命令。整定命令的报文格式很简单CAN ID固定为0x7A0数据区前4字节是kp的浮点数后4字节是ki的浮点数。收到整定命令后参数管理任务更新RAM里的参数副本同时通过队列通知电机控制任务重新加载PI参数。这里有一个并发问题需要注意电机控制任务可能在PI计算的中途被参数更新打断导致kp已经更新但ki还是旧值。我的解决方式是给PI控制器结构体加一个update_pending标志参数管理任务设置这个标志电机控制任务在每次PI计算开始前检查标志如果置位就重新加载参数并清除标志。这样参数更新只会在PI计算的边界发生不会出现半新半旧的情况。6. 封装收尾阶段的接口冻结与版本标记6.1 接口冻结的判断标准V1封装的最后一步是“接口冻结”意思是所有模块的对外函数签名和数据结构定义不再变动。判断是否可以冻结我的标准是所有上层模块的调用需求都已经满足且没有已知的、需要修改接口才能解决的问题。具体来说我会做一次“模拟调用”检查假设我要写一个新的应用层模块只用现有的头文件能不能完成所有需要的操作如果发现某个数据拿不到、某个操作没法触发那就说明接口还有缺口。接口冻结之后任何修改都必须走版本升级流程。我在每个头文件的开头加了一个版本宏格式是MODULE_NAME_VERSION_MAJOR.MINOR.PATCH。主版本号在接口不兼容时递增次版本号在增加功能但保持兼容时递增修订号在修复bug时递增。这个习惯看起来有点形式主义但在多人协作或者长期维护的项目里能省掉很多“这个函数怎么和上次不一样了”的沟通成本。6.2 编译期配置检查与断言封装完成之后我加了一组编译期断言用来检查配置参数之间的依赖关系。比如CAN总线的波特率和采样点配置必须匹配FreeRTOS的configTOTAL_HEAP_SIZE必须大于所有任务堆栈之和加上队列开销Flash参数结构体的大小必须是4的倍数硬件CRC的要求。这些检查用_Static_assert实现编译不通过就直接报错比运行时才发现问题要高效得多。_Static_assert(sizeof(system_params_t) % 4 0, system_params_t size must be multiple of 4 for hardware CRC); _Static_assert(configTOTAL_HEAP_SIZE 8192, FreeRTOS heap too small for current task configuration);还有一个运行时检查在系统启动的最后一个阶段我会遍历所有任务的堆栈检查剩余空间是否大于预设的安全阈值我设的是configMINIMAL_STACK_SIZE的50%。如果某个任务的堆栈使用率超过警戒线就通过状态报文上报一个警告。这个检查在调试阶段帮我发现了好几次堆栈溢出隐患尤其是CAN接收任务因为报文解析里用了snprintf格式化字符串堆栈消耗比预期大不少。6.3 从V1到V2的演进方向预判V1封装完成之后我复盘了一下哪些地方在V2里可能需要调整。首先是CAN通信层目前只支持标准帧如果以后要接CAN FD设备bsp_can的接口需要扩展。其次是参数存储目前是双扇区交替如果参数数量继续增加可能需要引入更复杂的日志式存储结构。最后是PI控制器目前只有位置式如果以后要做速度环和电流环的级联控制可能需要增加增量式PI的实现。这些预判不是为了现在就动手改而是为了在V1的接口设计里留好扩展点。比如bsp_can的发送函数我设计成了can_send(uint32_t id, uint8_t *data, uint8_t dlc, bool is_extended)最后一个参数就是为扩展帧预留的。虽然V1里只传false但接口已经支持了V2要加扩展帧的时候不需要改函数签名。7. 实际调试中遇到的三个典型问题与排查过程7.1 CAN总线在电机启动瞬间的偶发丢帧这个问题困扰了我将近一周。现象是电机静止时CAN通信完全正常一旦电机启动上位机就会偶尔收不到状态报文概率大概在5%左右。最开始我怀疑是CAN收发器的电源被电机干扰了用示波器量了收发器的供电引脚纹波确实比静止时大但还在数据手册允许范围内。后来我把CAN分析仪接到总线上发现丢帧的时候总线上其实有报文只是STM32没有收到。进一步排查发现电机启动瞬间CAN接收中断被延迟了。原因是电机PWM中断的优先级配置成了4比CAN接收中断的优先级5更高PWM中断服务函数里又做了一些浮点运算执行时间大约20微秒。在这20微秒里如果CAN报文到达硬件接收FIFO只有3个深度连续来4帧就会溢出。解决方案有两个方向要么降低PWM中断的执行时间要么提高CAN接收中断的优先级。我选择了后者把CAN接收中断优先级改成4PWM中断改成5。但这里有一个约束CAN接收中断里调用了xQueueSendFromISR所以它的优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY。我把configMAX_SYSCALL_INTERRUPT_PRIORITY也相应调整到了4确保安全。改完之后丢帧率降到了零连续跑了24小时没有再复现。7.2 Flash写入后参数读回不一致的根因定位这个问题出现在参数保存功能的第一次测试中。现象是保存参数后立即读回发现kp的值变了但ki的值是对的。因为kp和ki在结构体里是相邻的我一开始怀疑是Flash写入的字节序问题检查了半字写入的代码没发现异常。后来用调试器直接读Flash里的原始数据发现kp对应的4个字节里高2字节是对的低2字节是全0xFF。这说明写入操作只完成了一半。进一步检查代码发现我在写Flash之前没有等待上一次擦除操作完成。STM32的Flash擦除是异步的调用HAL_FLASHEx_Erase之后需要轮询FLASH_WaitForLastOperation或者等擦除中断我漏掉了这一步导致擦除还没结束就开始写入了。修复方式很简单在擦除和写入之间加一个FLASH_WaitForLastOperation(500)超时时间设500毫秒。但这个问题给我的教训是Flash操作的所有步骤都必须检查返回值不能假设它一定成功。后来我在bsp_flash的每个公开函数里都加了返回值检查上层调用者必须处理错误不允许忽略。7.3 FreeRTOS任务堆栈溢出的隐蔽表现堆栈溢出最讨厌的地方在于它不一定表现为死机有时候只是某个变量莫名其妙变了值或者某个函数返回了错误的结果。我遇到的现象是状态上报任务发送的报文里report_interval_ms字段偶尔会变成一个很大的数但RAM里读出来的参数明明是100。用FreeRTOS的uxTaskGetStackHighWaterMark检查了所有任务的堆栈使用情况发现状态上报任务的剩余堆栈只有12个字48字节而它调用了snprintf来格式化报文snprintf内部至少需要64字节的堆栈。溢出的部分覆盖了相邻内存恰好是参数结构体的副本。解决方式是给状态上报任务的堆栈从configMINIMAL_STACK_SIZE通常128字增加到256字并且把snprintf换成了自己写的轻量级整数转字符串函数避免引入标准库的堆栈开销。改完之后剩余堆栈稳定在100字以上。这个问题的隐蔽性在于它只在特定参数值比如需要格式化的数字位数较多的时候才触发普通的单元测试根本测不出来。8. 关于V1封装这件事的个人体会做完这个V1封装我最大的感受是封装的价值不在于代码看起来多整洁而在于当需求变化时你能以多快的速度定位到需要修改的地方以及修改之后有多大的把握不会引入新的问题。V1之前改一个CAN报文的解析逻辑我需要同时检查中断服务函数、任务处理函数和全局变量定义三个地方V1之后只需要改bsp_can.c里的一个解析函数其他模块完全不受影响。另一个体会是关于“过度封装”的警惕。我在划分模块的时候一度想把每个CAN报文都封装成一个独立的函数结果发现光是函数声明就写了三十多个调用关系反而更复杂了。后来退回到“按功能分组”的方式把相关的报文处理放在同一个函数里用switch-case区分代码量少了一半可读性反而更好。封装的粒度应该由变化的频率决定而不是由功能的独立性决定。最后说一个具体的技巧在封装完成之后我写了一个简单的“冒烟测试”脚本通过CAN总线发送一组预定义的命令然后检查返回的报文是否符合预期。这个脚本只有几十行Python代码但每次修改完代码烧录之后跑一遍能快速确认核心功能没有被破坏。比起手动一个个功能去点效率高太多了。如果你也在做类似的嵌入式项目强烈建议在封装阶段就把这个自动化测试的框架搭起来后面会省很多事。
网站建设高端定制企业官网
RELATED

相关资讯

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

较早相关资讯

最新相关资讯

IndexedDB 实战心得:从 onupgradeneeded 到 transaction 作用域,配 TaoToken 统一 Key 调试异步链路 2026/9/27 11:39:39

IndexedDB 实战心得:从 onupgradeneeded 到 transaction 作用域,配 TaoToken 统一 Key 调试异步链路

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

阅读更多 →
避坑指南:上海原单外贸一条街建站图解步骤 2026/9/27 11:39:33

避坑指南:上海原单外贸一条街建站图解步骤

避坑指南:上海原单外贸一条街建站图解步骤 找建站公司最怕什么?怕被坑高价,花大钱做个慢如蜗牛的站。别急着掏钱,先看懂这份 图解步骤…

阅读更多 →
GPT-5.6 64 Agent 数学登月屠夫榜:TaoToken 统一 Key 接入配置实战 2026/9/27 11:39:26

GPT-5.6 64 Agent 数学登月屠夫榜:TaoToken 统一 Key 接入配置实战

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

阅读更多 →
基于Trae开发的自动表关联查询工具:TaoToken统一Key接入与settings.json配置实战 2026/9/27 11:39:00

基于Trae开发的自动表关联查询工具:TaoToken统一Key接入与settings.json配置实战

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

阅读更多 →
新手入门必看:3步搞定wordpress删除plugins避坑指南 2026/9/27 11:39:00

新手入门必看:3步搞定wordpress删除plugins避坑指南

新手入门必看:3步搞定wordpress删除plugins避坑指南 自己不会代码想做网站,却卡在 wordpress 删除plugins 这一步?别急,这其实是 新手入门…

阅读更多 →
Egg 项目接入 egg-mongoose:从 config.default.js 配置到常用 Mongoose 方法实战 2026/9/27 11:39:00

Egg 项目接入 egg-mongoose:从 config.default.js 配置到常用 Mongoose 方法实战

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

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

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