news 2026/9/12 3:48:45

ESP32智能手环实战:低功耗设计、传感器采集与OTA升级

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32智能手环实战:低功耗设计、传感器采集与OTA升级

简介:基于ESP32打造的智能手环完整工程,面向毕业设计、课程设计、工程实训及项目开发等场景,集成心率血氧监测、联网获取时间天气与B站粉丝数、闹钟提醒喝水吃药、秒表、计步等功能,适合单片机学习者从基础到进阶动手实践。资源包共16个文件,压缩包约23.52MB,涵盖C++源码(.cpp/.h)、工程配置(.json/.ini)、Fritzing接线图(.fzz)、PCB预览图、固件工具及说明文档,能够覆盖方案设计、硬件连接、程序烧录到结果验证的全流程。已有445人学习浏览,足见其参考价值。资源内包含完整源码、工程文件与使用说明,目录区分固件与软件模块;即使不会画PCB,也可按接线图用面包板加杜邦线搭建,烧录程序即可复现。项目经严格测试功能正常,可直接运行,也便于在此基础上扩展更多智能交互功能。

1. 用ESP32做智能手环,先把功耗和体积的矛盾想清楚

很多人把ESP32智能手环做成了“带屏幕的计时器”:功能列了一整页,真正戴在手上的体验却不行。问题出在一开始就把方案定成了ESP32-WROOM-32开发板 + 18650电池 + 1.3寸大屏,这套组合是开发板挂手上,不是手环。按课设、毕设、电赛和实训能交卷、也经得起答辩追问的路线,主控应选ESP32-C3或S3,IMU管姿态与计步,MAX30102管心率血氧,0.91寸OLED做显示,再用深度睡眠把待机电流压到µA级。

手环的技术难点不在“功能多”,而在“数据准不准、电够不够用、体积戴不戴得住”。以下方案按可复现的工程流程展开,从硬件选型、传感器采集、显示低功耗到BLE与OTA,每条都能直接落到板子上。

2. 硬件选型与电源树:ESP32手环的地基

手环和开发板最大的区别是硬件必须一次画对。开发板可以用杜邦线改,手环两厘米见方的主板改一次就要报废一版。一次画对依赖三件事:主控选型合理、电源树算得清、布局守纪律。

2.1 主控选型的三条硬约束

手环主控要同时回答三个问题:蓝牙版本能不能撑起低功耗连接,深度睡眠电流能不能进到10µA以内,模块封装和天线适不适合窄长主板。对照这三条,三款常用芯片的差异就很明显。

主控核心SRAM蓝牙深度睡眠电流封装特点
ESP32-WROOM-32LX6 双核520KBBLE 4.2约10µA18×25.5mm,带PCB天线,偏胖
ESP32-C3RISC-V 单核400KBBLE 5.0约5µA13×16.2mm,适合手环主板
ESP32-S3LX7 双核512KBBLE 5.0约7µA算力强,但整机功耗偏高

我一般会选ESP32-C3。手环的显示、心率、姿态计算都吃不满160MHz主频,C3的160MHz RISC-V在息屏态降到40MHz跑传感器任务完全够用;BLE 5.0的Long Range和Coded PHY虽然手环用不上,但连接功耗比BLE 4.2低一截。S3的优势在本地语音唤醒或摄像头识别,这些功能塞进50g以下的手环不现实。

选完芯片看引脚。ESP32-C3的GPIO2、GPIO8、GPIO9是strapping引脚,上电瞬间的电平决定启动模式,不能接容易拉高拉低的器件。手环常用引脚就那么几个:I2C两条、OLED复位一根、马达一根、电池采样一根,避开这三个引脚后布局很宽松。查引脚图时重点看是否有RTC唤醒能力,C3的所有GPIO都能做ext0/ext1唤醒,比ESP32老型号灵活很多。

2.2 电源树:先算峰值电流再定器件

手环电池一般是3.7V聚合物锂电池,容量150mAh到200mAh。电源树从电池到负载只有两级:电池直接给马达和LED供电,电池再经LDO降到3.3V给主控、传感器和OLED。这套结构简单,但每个节点的压降都要算清楚。

电源轨电压范围主要负载峰值电流
VBAT3.0~4.2V振动马达、MAX30102 LED120mA
3V33.3VESP32-C3、OLED、IMU60mA

充电管理用TP4054或TP4056,注意TP4054最大充电电流设定电阻要按电池容量的0.5C来配,150mAh电池配到75mA充电电流比较安全。LDO别用AMS1117,静态电流和压差都太大,XC6206P332MR或RT9013这类低压差LDO才适合电池场景。

电池电压检测用两个100kΩ电阻分压后送ADC。ESP32-C3的ADC参考电压在出厂时有eFuse校准值,Arduino里直接调analogReadMilliVolts(),IDF里用adc_cali接口读校准系数,比手动按3.3V反推准得多。分压电阻带来约14µA的静态损耗,对150mAh电池来说可接受,但如果要做到5µA级深睡,电阻分压电路必须加MOS管开关,深睡时彻底断掉。

// 硬件引脚映射:ESP32-C3手环最小系统 #define PIN_I2C_SCL 5 // I2C时钟,避开strap引脚 #define PIN_I2C_SDA 4 // I2C数据 #define PIN_OLED_RST 3 // OLED复位 #define PIN_BATT_ADC 0 // 电池分压采样,ADC1_CH0 #define PIN_VIB_MOTOR 2 // 振动马达,低电平驱动 static const float BATT_DIVIDER = 2.0f; // 100k+100k分压,外部再并0.1uF电容滤波 static const float BATT_LOW = 3.5f; // 低于3.5V进低电量告警,留出0.2V压降余量

引脚映射里最重要的一条是避开上电约束脚,GPIO2在这里只做马达输出,就算上电瞬间被拉低也不影响启动。电池采样放在GPIO0是因为它靠近ADC1通道,走线短,分压电阻放芯片附近采样更稳。BATT_LOW取3.5V不是按放电截止电压3.0V算的,而是考虑LDO压降和OLED在临界电压下的闪烁。

2.3 PCB布局:天线净空、IMU位置、OLED隔离

手环PCB布局有三个容易踩的坑。第一是天线的净空区,ESP32-C3的PCB天线正下方和周围一圈不能铺铜,不能走高频线,电池支架最好也别压在天线上方。第二是IMU位置,加速度计要放在主板靠近手腕中心的位置,戴在腕上时离心加速度误差最小。第三是MAX30102和OLED之间的隔离,OLED的电荷泵和驱动电流会干扰PPG模拟前端,两者之间加地隔离带,传感器底下铺完整地平面。

四层板成本不高,信号层、地层、电源层、底层四层结构对手环这种密度很合适。IMU的I2C走线加串联电阻抑制振铃,OLED电源引脚放0.1µF和10µF两只电容,马达两端并一个续流二极管,这些都是老生常谈但决定稳定性的细节。

3. 传感器采集:IMU姿态、心率与温度的落地实现

传感器的数据质量决定手环功能的可信度。这一章按IMU、MAX30102、任务调度三个层面讲,重点放在采样参数和滤波上,这两点是最直接影响数据结果的因素。

3.1 MPU6050六轴读取与互补滤波

MPU6050虽然老,但胜在资料全、寄存器透明,课设和竞赛用它最稳妥。用现成库开发很快,但手环要控采样时序,自己读寄存器更灵活。上电先复位,然后配置量程和数字低通滤波器。

#include <Wire.h> #define MPU6050_ADDR 0x68 static void imu_write_reg(uint8_t reg, uint8_t val) { Wire.beginTransmission(MPU6050_ADDR); Wire.write(reg); Wire.write(val); Wire.endTransmission(); } static void imu_read_xyz(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz) { Wire.beginTransmission(MPU6050_ADDR); Wire.write(0x3B); // ACCEL_XOUT_H Wire.endTransmission(false); // 不发停止位,连续读 Wire.requestFrom(MPU6050_ADDR, 14); if (Wire.available() < 14) return; *ax = (Wire.read() << 8) | Wire.read(); // 大端,先高字节 *ay = (Wire.read() << 8) | Wire.read(); *az = (Wire.read() << 8) | Wire.read(); Wire.read(); Wire.read(); // 跳过温度寄存器 *gx = (Wire.read() << 8) | Wire.read(); *gy = (Wire.read() << 8) | Wire.read(); *gz = (Wire.read() << 8) | Wire.read(); } void imu_init() { Wire.begin(4, 5); imu_write_reg(0x6B, 0x80); // 复位整个芯片 delay(100); imu_write_reg(0x6B, 0x01); // 退出睡眠,时钟源选PLL X轴陀螺 imu_write_reg(0x1A, 0x03); // DLPF带宽44Hz,滤掉高频振动 imu_write_reg(0x1B, 0x08); // 陀螺仪±500dps,灵敏度65.5 LSB/dps imu_write_reg(0x1C, 0x08); // 加速度±4g,灵敏度8192 LSB/g }

说几个参数的含义。DLPF设0x03后加速度带宽44Hz、陀螺仪带宽42Hz,走路步频大概2Hz,手势翻转在5Hz以内,44Hz足够保留有效信号,又能滤掉电机和电网的50Hz干扰。加速度量程选±4g而不是±2g,是因为手环佩戴时走路产生的冲击接近3g,±2g容易饱和。

姿态角用互补滤波就够了,不需要上卡尔曼。互补滤波的优点是计算量小、参数直观,实时性反而比卡尔曼好。

// 互补滤波:陀螺积分负责短时平滑,加速度计负责长时校正 // 采样定时器每10ms调用一次update_attitude static float pitch = 0, roll = 0; static const float dt = 0.01f; // 10ms static const float alpha = 0.98f; // 陀螺仪权重 void update_attitude(float gx, float gy, float gz, float ax, float ay, float az) { pitch += gy * dt; // 陀螺积分的pitch增量 roll += gx * dt; float acc_pitch = atan2f(ax, az) * 57.2958f; // 转角度 float acc_roll = atan2f(ay, az) * 57.2958f; pitch = alpha * pitch + (1.0f - alpha) * acc_pitch; roll = alpha * roll + (1.0f - alpha) * acc_roll; }

alpha取0.98的意思是陀螺积分占98%,加速度计校正占2%。这个值不是拍脑袋定的,它对应的时间常数为alpha*dt/(1-alpha)约0.5秒,意味着加速度计需要约半秒去修正陀螺漂移。手环姿态更新是短时的,0.5秒的校正时间足以覆盖10秒级的积分漂移。如果采样率改了,alpha要用时间常数公式重新算,直接抄会出问题。

3.2 MAX30102心率血氧:采样参数决定信号能不能用

MAX30102的原始信号噪声大,能不能算出稳定心率,关键在采样配置。手上戴紧一点可以减少运动伪差,但更重要的是把ADC量程和LED电流配到合适档位。寄存器0x09是SpO2配置字节,同时决定采样率和脉宽;0x0A和0x0C分别控制红光和红外LED电流。

// MAX30102关键参数配置 max30102_write_reg(0x09, 0x13); // 采样率100Hz,脉宽411us,ADC量程2048nA max30102_write_reg(0x0A, 0x24); // 红光LED电流约7.2mA max30102_write_reg(0x0C, 0x24); // 红外LED电流约7.2mA max30102_write_reg(0x08, 0x07); // FIFO:每个采样点包含红光+红外,写满进FIFO

采样率100Hz对应每个心跳周期60到100个采样点,波谷检测精度足够。脉宽411µs是ADC对LED反射光积分的时间,脉宽越长信噪比越高,但功耗也越大。ADC量程2048nA是中间档,肤色浅的人信号会偏小,肤色深的人容易饱和,实操时先看原始波形再决定加减档。

心率计算的代码不复杂,但滤波才是核心。原始PPG信号里叠加了大量低频基线漂移和高频噪声,先做0.5到5Hz的带通滤波,再在滑动窗口里找波峰,相邻波峰间隔换算成BPM。

// 红外通道IR做心率计算,红光通道RED只用于血氧比计算 // 带通滤波后用简单的峰值计数法:窗口内找局部最大值,间隔小于0.35s则忽略 int bpm_from_ir_waveform(int32_t ir_samples[], int n) { int peaks = 0; for (int i = 1; i < n - 1; i++) { if (ir_samples[i] > ir_samples[i-1] && ir_samples[i] > ir_samples[i+1]) { peaks++; // 波峰计数,1分钟后的波峰数除以时间得到BPM i += 5; // 跳过一个不应期,避免抖动导致重复计数 } } return peaks * 60 / (n / 100); // n对应秒数,按100Hz采样率折算 }

这个简化版在做课设时够用,但要留意两个坑。一是运动状态下PPG信号会被动作干扰,计步和心率同时出现时,心率结果必须和加速度计数据做相关性判断,运动强度超过阈值就显示“运动干扰”而不是硬算。二是LED电流不能一直开最大,每次测量前先跑一个自动增益校准,用信号峰值去反推LED电流,这是手环行业里的通用做法。

3.3 FreeRTOS任务划分:数据流别用全局变量串

手环的传感器任务、显示任务、BLE任务并行跑,用全局结构体共享数据容易出两类问题:任务切换时读到半更新状态,或者多个任务同时写一个变量导致数据错乱。底层FreeRTOS提供了队列和信号量,按任务职责划分数据流比裸机轮询干净得多。

任务优先级栈大小周期/触发职责
SensorRead3204810ms读IMU和MAX30102,打包入队
Display23072消息触发刷新OLED页面
BleServer14096事件触发维护连接,通知数据
PowerMonitor410241s采样电池电压,切换工作状态
QueueHandle_t sensorQueue; typedef struct { int16_t ax, ay, az, gx, gy, gz; uint32_t ir, red; uint32_t timestamp; } SensorPacket; void sensorTask(void *arg) { SensorPacket pkt; for (;;) { imu_read_xyz(&pkt.ax, &pkt.ay, &pkt.az, &pkt.gx, &pkt.gy, &pkt.gz); max30102_read_fifo(&pkt.ir, &pkt.red); pkt.timestamp = millis(); xQueueOverwrite(sensorQueue, &pkt); // 覆盖旧包,只保留最新数据 vTaskDelay(pdMS_TO_TICKS(10)); } }

xQueueOverwrite和xQueueSend的区别在于,用overwrite时队列永远只保留最新一包,显示任务读到的是当前时刻的数据,不会被旧的积压数据追赶。Display任务里用xQueuePeek读数据但不弹出,因为显示逻辑可能多次读取同一包来做局部刷新。

4. OLED显示与低功耗策略:手环续航的关键在状态机

手环待机功耗大头在OLED和蓝牙。OLED每个亮着的像素都是一个电流点,时钟常亮跑下来5到15mA,这个数字在150mAh电池面前吃不住。续航的思路不是把屏幕调暗,而是让系统在“显示”“待机”“深睡”之间切换。

4.1 0.91 OLED的驱动与刷新策略

0.91寸OLED 128×32,SSD1306驱动,I2C接口,正好是搜“esp32 oled 128*32”时最常见的型号。Adafruit SSD1306库初始化很简单,但要注意复位引脚处理,C3上复位脚不能悬空。

#include <Adafruit_SSD1306.h> #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 32 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, PIN_OLED_RST); void oled_init() { if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // 常见原因是I2C地址不是0x3C,而是0x3D // 或者OLED的RESET引脚没有接高电平 } display.clearDisplay(); }

显示策略上最值得做的是局部刷新。SSD1306是按页寻址的,128×32分成4个8像素高的页,更新一个心率数字只需要改两个页里的几个字节。Adafruit库的display()会把整块缓冲推给屏幕,这反而比手动操作寄存器慢。

// 局部刷新:只清掉数字所在的区域,保留图标和边框 display.fillRect(30, 8, 48, 24, SSD1306_BLACK); // 清掉旧数字 display.setTextSize(2); display.setCursor(32, 8); display.print(bpm); // 打印新心率值 display.display();

这个片段看起来平淡,但对手环很重要。全屏clearDisplay()再重画的耗时能到30ms以上,局部更新只需要5ms左右,每按一次按键少耗一些CPU时间,这些时间在低功耗状态下直接转化为续航。页面切换时用整页更新,同一个页面内的数字变化用局部更新,这算是我推荐的默认策略。

4.2 手环低功耗状态机:四个状态按需切换

手环不能只有“亮屏”和“熄屏”两个状态,要按外设工作状态拆成四档。从RUN到DEEP_SLEEP逐级降低功耗,每降一级都要付出唤醒时间的代价。

状态CPU与外设典型电流进入条件退出方式
RUNOLED亮,传感器50Hz,Wi-Fi关40~60mA用户操作无操作5s
SENSEOLED灭,IMU 5Hz,BLE保持1.5~3mA自动灭屏抬腕中断
LIGHT_SLEEPCPU暂停,SRAM保持,IMU运动唤醒100~300µA静止30s加速度计中断
DEEP_SLEEPRTC保留,OTA标志保存5~10µA夜间或长待机外部RTC定时唤醒
#include "esp_pm.h" #include "esp_sleep.h" void enter_light_sleep() { esp_pm_config_esp32c3_t pm_cfg = { .max_freq_mhz = 80, // 运行时最高频率限到80MHz .min_freq_mhz = 40, // 空闲自动降频 .light_sleep_enable = true }; esp_pm_configure(&pm_cfg); esp_sleep_enable_ext0_wakeup(GPIO_NUM_2, 0); // IMU中断脚低电平唤醒 esp_light_sleep_start(); }

进LIGHT_SLEEP前要把没必要的外设关掉,特别是UART和Wi-Fi。BLE如果开着,要让协议栈进Modem Sleep模式,Wi-Fi在这种场景下直接关闭。C3从LIGHT_SLEEP唤醒只需要几毫秒,唤醒后SRAM里的数据都在,传感器任务能无缝续跑,所以灭屏后的短暂静止用LIGHT_SLEEP很合适。

DEEP_SLEEP省的是那100多µA的差距,但代价是唤醒后要重新初始化外设、重新分配网络资源。手环一般在检测到夜里长时间不动后切进DEEP_SLEEP,用ESP32内部RTC定时器每天早上定时唤醒一次,或者保留一个GPIO让用户按键唤醒。

4.3 抬腕唤醒与防误触

抬腕唤醒不能靠陀螺仪数据的简单阈值。走路时手臂摆动幅度很大,抬手看表却是一个先抬后转的复合动作,光看加速度幅值容易误触发。我一般用MPU6050的运动中断做第一层唤醒,等系统从LIGHT_SLEEP醒来后,主控再算一次姿态变化是否符合抬腕特征。

// MPU6050运动中断配置,三个寄存器配合使用 imu_write_reg(0x1F, 10); // MOT_THR:运动阈值 imu_write_reg(0x20, 5); // MOT_DUR:持续5ms,抗抖动 imu_write_reg(0x38, 0x40); // INT_ENABLE:使能MOT中断

唤醒后主控用3.1节的互补滤波姿态角做二次判断。仰角从负值变成接近0°,再配合一个横滚角变化,判定为“抬腕看表”,点亮OLED。这个两层判断把走路摆臂的误触发压到很低,也是竞赛答辩时能讲出亮点的设计点。

5. BLE通信、OTA升级与功耗实测

最后这部分是手环项目从“能跑”到“能交付”的分水岭。BLE服务设计、OTA分区和实测方法这三点,直接决定产品演示时是否可靠。

5.1 BLE GATT服务怎么设计才省电

手环和手机建立BLE连接后,连接间隔是功耗的第一关键参数。连接间隔30ms时功耗是200ms的好几倍,手环这种数据量很小的场景,200ms的间隔完全够用,一次通知发心率、步数、电量三个值,每包负载也就20字节。

#include <NimBLEDevice.h> NimBLEService *svc = NimBLEDevice::createService("180D"); NimBLECharacteristic *hrChr = svc->createCharacteristic("2A37", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); NimBLECharacteristic *cmdChr = svc->createCharacteristic("FF01", NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR);

心率用标准0x2A37特征,手机端App不用专门适配就能识别。自定义指令走FF01,用Write而不是Write No Response的话,手机能确认手环收到的指令,适合OTA触发这类关键操作。BLE芯片上NimBLE库比传统bluedroid库省约100KB内存和一部分连接功耗,手环这种资源紧张场景更合适。

5.2 双分区OTA:现场改代码不用拆壳

OTA对手环不是锦上添花,是刚需。手环装进壳子后没有USB口,固件出问题只能拆壳烧录。用双OTA分区方案,从手机下发固件包,升级失败还能回滚到上一个版本。

# 4MB Flash 双OTA分区表 nvs, data, nvs, 0x9000, 0x6000, otadata, data, ota, 0xf000, 0x2000, app0, app, ota_0, 0x11000, 0x1C0000, app1, app, ota_1, 0x1d1000,0x1C0000, coredump, data, coredump,0x391000,0x10000, spiffs, data, spiffs, 0x3A1000,0x5F000,
const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL); esp_ota_handle_t ota_handle; esp_ota_begin(ota_part, OTA_SIZE_UNKNOWN, &ota_handle); // 每收到1KB就对esp_ota_write写入,写完后校验整个镜像 esp_ota_write(ota_handle, buf, len); esp_ota_end(ota_handle); esp_ota_set_boot_partition(ota_part); esp_restart();

C3的4MB Flash跑一对1.75MB的app分区绰绰有余,手环固件编译出来通常不到1MB。OTA写flash时要关中断,否则BLE数据会丢包,写之前告诉手机端暂停发送,写完一个块再恢复。这个细节在实际传输大固件时非常影响成功率。

5.3 用10Ω采样电阻实测各状态电流

续航不能只靠芯片手册上的数字,手册里的低功耗值是裸芯片状态,外设全挂上去后要重测。手边没有专业功耗分析仪时,用10Ω采样电阻加示波器就能看到各状态的真实电流。

测法不复杂:采样电阻串在电池正极,示波器探头跨在电阻两端测电压,1mV对应0.1mA。万用表串联测平均电流,示波器看瞬时波形,两者互相印证。我自己的手环实测参考值:RUN亮屏40mA,SENSE待机2mA,LIGHT_SLEEP 180µA,DEEP_SLEEP 7µA。不同品牌模块之间差20%是正常的,拿到板子先测状态切换的毛刺。

续航估算按一天的使用场景算:亮屏累计30分钟,SENSE态16小时,LIGHT_SLEEP 7小时,DEEP_SLEEP 30分钟。150mAh电池的理论续航约29小时,再乘0.85的放电效率系数,实际在24小时左右。如果这个数值达不到预期,优先看两个地方:MAX30102的LED电流是不是一直在最大档,以及BLE连接间隔是不是被手机端拉到了非常短。把功耗曲线放稳之后,再回头微调LED电流到信号刚够用的档位,每降一档电流通常能换来10%以上的续航提升。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 3:47:47

第二日总结:高效工作者的秘密武器

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

作者头像 李华
网站建设 2026/9/12 3:47:45

微服务分布式事务Seata全解析:核心原理与实战落地

1. 先搞清楚&#xff1a;Seata到底解决的是什么问题我见过不少团队&#xff0c;微服务拆分做了一半&#xff0c;订单、库存、账户各自独立成服务&#xff0c;数据库也分了库&#xff0c;结果到了对账环节发现数据对不上——订单扣款成功了&#xff0c;库存却超卖&#xff1b;用…

作者头像 李华
网站建设 2026/9/12 3:47:14

Python自动化测试中的POM设计模式详解

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

作者头像 李华
网站建设 2026/9/12 3:45:27

Flutter测试组合库鸿蒙适配实战与优化

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

作者头像 李华
网站建设 2026/9/12 3:45:25

林业虫害识别实战:YOLOv8n轻量模型端到端部署指南

简介&#xff1a;本资源是一套完整的林业虫害图像智能识别毕业设计项目&#xff0c;面向计算机、人工智能及相关专业本科生&#xff0c;专为毕业设计、课程设计及实战能力提升打造。项目基于Python开发&#xff0c;集成训练好的深度学习模型、2000张真实林业虫害标注图片&#…

作者头像 李华
网站建设 2026/9/12 3:44:43

PolarDB从节点故障排查实战与优化指南

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

作者头像 李华