MPU6050传感器驱动开发:基于MSPM0G3507的I2C通信与姿态数据解析
在嵌入式开发中姿态传感器是很多项目不可或缺的核心部件而MPU6050作为经典的六轴运动处理传感器因其性价比高、使用广泛而备受青睐。最近在使用立创·天猛星MSPM0G3507开发板进行项目开发时发现网上关于MPU6050与这款TI新MCU结合的完整驱动教程比较零散特别是I2C通信配置和数据处理部分存在不少坑点。本文将分享一套完整的MPU6050驱动实现方案从硬件连接到软件编程再到姿态数据解析手把手带你搞定这个经典传感器的应用。1. 硬件平台与传感器介绍1.1 天猛星MSPM0G3507开发板特性立创·天猛星MSPM0G3507是基于TI MSPM0G3507微控制器的开发板该MCU采用Arm Cortex-M0内核主频高达80MHz具备丰富的外设资源。开发板集成了DAP-Link调试器支持Keil MDK开发环境特别适合嵌入式入门和项目开发。关键特性包括80MHz Arm Cortex-M0内核128KB Flash32KB SRAM多个I2C、SPI、UART接口12位ADC和比较器丰富的定时器资源1.2 MPU6050传感器概述MPU6050是InvenSense推出的六轴运动跟踪传感器集成了三轴MEMS陀螺仪和三轴MEMS加速度计通过I2C接口与主控器通信。主要参数加速度计量程±2g、±4g、±8g、±16g陀螺仪量程±250°/s、±500°/s、±1000°/s、±2000°/s16位ADC分辨率内置数字运动处理器DMP工作电压2.375V-3.46VI2C从机地址0x68或0x692. 开发环境搭建2.1 软件工具准备开发MSPM0G3507需要以下软件环境必需工具Keil MDK-Arm uVision v5.38a注意v5.41及以上版本可能存在兼容性问题Arm Clang编译器 v6.16或更高版本TI MSPM0 SDK包含驱动程序库和示例代码SysConfig工具 v1.21.x图形化配置工具安装步骤首先安装Keil MDK确保选择Arm Compiler下载并安装MSPM0 SDK包安装SysConfig配置工具导入MSPM0G3507设备支持包2.2 硬件连接配置MPU6050与天猛星开发板的连接非常简单只需要4根线MPU6050 MSPM0G3507 VCC → 3.3V GND → GND SDA → PA14 (I2C_SDA) SCL → PA15 (I2C_SCL)注意事项MPU6050的VCC需要连接3.3V不能接5V如果使用模块上的AD0引脚可以改变I2C地址AD0接高电平时地址为0x69建议在SDA和SCL线上加上拉电阻4.7kΩ3. I2C外设配置3.1 使用SysConfig配置I2CTI的SysConfig工具可以直观地配置外设参数避免手动修改寄存器带来的错误。配置步骤打开SysConfig选择MSPM0G3507设备在Peripherals中找到I2C模块启用I2C0配置为Master模式设置时钟频率为400kHz标准模式配置引脚映射SDA→PA14SCL→PA15生成的配置代码// 自动生成的I2C配置代码 #include ti_msp_dl_config.h void I2C_Init(void) { DL_I2C_Init(I2C0_INST, (DL_I2C_Config){ .mode DL_I2C_MODE_CONTROLLER, .controllerMode DL_I2C_CONTROLLER_MODE_STANDARD_AND_FAST, .dataRate DL_I2C_DATA_RATE_400_KBPS, }); // 配置I2C引脚 DL_GPIO_initPeripheralPinFunction(I2C0_SDA_PORT, I2C0_SDA_PIN); DL_GPIO_initPeripheralPinFunction(I2C0_SCL_PORT, I2C0_SCL_PIN); }3.2 手动配置I2C寄存器如果不使用SysConfig也可以直接通过寄存器配置void I2C_Manual_Init(void) { // 使能I2C外设时钟 DL_SYSCTL_enablePeripheralClock(SYSCTL_PERIPH_CLK_I2C0); // 配置I2C为控制器模式 I2C0-CTL I2C_CTL_MST; // 设置时钟频率系统时钟80MHz目标400kHz I2C0-CLKOCNT 200; // 时钟分频 // 配置引脚功能 GPIO-PAD_PA14SEL GPIO_PAD_PA14SEL_I2C0_SDA; GPIO-PAD_PA15SEL GPIO_PAD_PA15SEL_I2C0_SCL; }4. MPU6050驱动实现4.1 寄存器定义与宏定义首先定义MPU6050的常用寄存器地址和基本宏#ifndef MPU6050_H #define MPU6050_H #include ti_msp_dl_config.h // MPU6050 I2C地址 #define MPU6050_ADDR 0x68 #define MPU6050_ADDR_AD0_HIGH 0x69 // MPU6050寄存器地址 #define MPU6050_RA_PWR_MGMT_1 0x6B #define MPU6050_RA_WHO_AM_I 0x75 #define MPU6050_RA_ACCEL_XOUT_H 0x3B #define MPU6050_RA_GYRO_XOUT_H 0x43 // 加速度计量程配置 #define MPU6050_ACCEL_FS_2 0x00 #define MPU6050_ACCEL_FS_4 0x08 #define MPU6050_ACCEL_FS_8 0x10 #define MPU6050_ACCEL_FS_16 0x18 // 陀螺仪量程配置 #define MPU6050_GYRO_FS_250 0x00 #define MPU6050_GYRO_FS_500 0x08 #define MPU6050_GYRO_FS_1000 0x10 #define MPU6050_GYRO_FS_2000 0x18 // 灵敏度系数LSB/g 或 LSB/°/s #define MPU6050_ACCEL_SENS_2G 16384.0 #define MPU6050_ACCEL_SENS_4G 8192.0 #define MPU6050_ACCEL_SENS_8G 4096.0 #define MPU6050_ACCEL_SENS_16G 2048.0 #define MPU6050_GYRO_SENS_250 131.0 #define MPU6050_GYRO_SENS_500 65.5 #define MPU6050_GYRO_SENS_1000 32.8 #define MPU6050_GYRO_SENS_2000 16.4 // 数据结构体 typedef struct { int16_t accel_x; int16_t accel_y; int16_t accel_z; int16_t temp; int16_t gyro_x; int16_t gyro_y; int16_t gyro_z; } MPU6050_Data; #endif4.2 I2C读写函数封装实现基础的I2C读写函数用于与MPU6050通信// I2C写一个字节到指定寄存器 uint8_t MPU6050_Write_Byte(uint8_t reg_addr, uint8_t data) { uint8_t tx_buf[2] {reg_addr, data}; // 启动传输 DL_I2C_setTargetAddress(I2C0_INST, MPU6050_ADDR); DL_I2C_setControllerMode(I2C0_INST, DL_I2C_CONTROLLER_MODE_TRANSMIT); // 发送数据 if (DL_I2C_transmitDataBlocking(I2C0_INST, tx_buf, 2) ! DL_I2C_STATUS_OK) { return 0; // 写入失败 } // 停止传输 DL_I2C_generateStopCondition(I2C0_INST); return 1; // 写入成功 } // 从指定寄存器读取多个字节 uint8_t MPU6050_Read_Bytes(uint8_t reg_addr, uint8_t *data, uint8_t len) { // 先发送寄存器地址 DL_I2C_setTargetAddress(I2C0_INST, MPU6050_ADDR); DL_I2C_setControllerMode(I2C0_INST, DL_I2C_CONTROLLER_MODE_TRANSMIT); if (DL_I2C_transmitDataBlocking(I2C0_INST, reg_addr, 1) ! DL_I2C_STATUS_OK) { return 0; } // 重新启动为接收模式 DL_I2C_setControllerMode(I2C0_INST, DL_I2C_CONTROLLER_MODE_RECEIVE); DL_I2C_enableRepeatedStart(I2C0_INST); // 读取数据 if (DL_I2C_receiveDataBlocking(I2C0_INST, data, len) ! DL_I2C_STATUS_OK) { return 0; } DL_I2C_generateStopCondition(I2C0_INST); return 1; } // 读取单个字节 uint8_t MPU6050_Read_Byte(uint8_t reg_addr) { uint8_t data; if (MPU6050_Read_Bytes(reg_addr, data, 1)) { return data; } return 0; }4.3 MPU6050初始化函数实现传感器的初始化配置uint8_t MPU6050_Init(void) { // 1. 复位设备 if (!MPU6050_Write_Byte(MPU6050_RA_PWR_MGMT_1, 0x80)) { return 0; // 复位失败 } // 等待复位完成 for (volatile uint32_t i 0; i 100000; i); // 2. 检查设备ID uint8_t who_am_i MPU6050_Read_Byte(MPU6050_RA_WHO_AM_I); if (who_am_i ! 0x68) { return 0; // 设备ID不匹配 } // 3. 唤醒设备选择时钟源 if (!MPU6050_Write_Byte(MPU6050_RA_PWR_MGMT_1, 0x01)) { return 0; } // 4. 配置加速度计量程为±2g if (!MPU6050_Write_Byte(0x1C, MPU6050_ACCEL_FS_2)) { return 0; } // 5. 配置陀螺仪量程为±250°/s if (!MPU6050_Write_Byte(0x1B, MPU6050_GYRO_FS_250)) { return 0; } // 6. 配置低通滤波器带宽20Hz if (!MPU6050_Write_Byte(0x1A, 0x04)) { return 0; } return 1; // 初始化成功 }4.4 数据读取与处理函数实现传感器数据的读取和单位转换// 读取原始传感器数据 uint8_t MPU6050_Read_Raw_Data(MPU6050_Data *data) { uint8_t buffer[14]; if (!MPU6050_Read_Bytes(MPU6050_RA_ACCEL_XOUT_H, buffer, 14)) { return 0; } // 组合16位数据高位在前 >#include ti_msp_dl_config.h #include mpu6050.h #include stdio.h // 串口重定向用于调试输出 int __io_putchar(int ch) { DL_UART_Main_transmitData(UART_0_INST, (uint8_t)ch); return ch; } void System_Init(void) { // 系统时钟配置80MHz DL_SYSCTL_setMCLKDivider(SYSCTL_MCLK_DIVIDER_1); DL_SYSCTL_setMCLKSource(SYSCTL_MCLK_SOURCE_HPOSC); DL_SYSCTL_setHPOSCFrequency(SYSCTL_HPOSC_FREQUENCY_80_MHZ); // 初始化I2C I2C_Init(); // 初始化UART用于调试输出 DL_UART_Main_init(UART_0_INST); } int main(void) { System_Init(); printf(MPU6050 Driver Test for MSPM0G3507\r\n); // 初始化MPU6050 if (!MPU6050_Init()) { printf(MPU6050初始化失败\r\n); while(1); } printf(MPU6050初始化成功\r\n); MPU6050_Data raw_data; float accel[3], gyro[3], temp; while (1) { // 读取传感器数据 if (MPU6050_Read_Raw_Data(raw_data)) { // 转换为物理量 MPU6050_Convert_To_Physical(raw_data, accel, gyro, temp); // 输出数据 printf(加速度: X%.2fg, Y%.2fg, Z%.2fg\r\n, accel[0], accel[1], accel[2]); printf(陀螺仪: X%.2f°/s, Y%.2f°/s, Z%.2f°/s\r\n, gyro[0], gyro[1], gyro[2]); printf(温度: %.2f°C\r\n, temp); printf(-------------------\r\n); } else { printf(数据读取失败\r\n); } // 延时500ms for (volatile uint32_t i 0; i 500000; i); } }5.2 工程配置文件创建必要的工程配置文件syscfg文件配置// system.syscfg { board: MSPM0G3507, components: { /ti/drivers/I2C: { I2C0: { $name: I2C0, dataRate: 400000, mode: controller } }, /ti/drivers/UART: { UART0: { $name: UART_0, baudRate: 115200, dataLength: DL_UART_DATA_LENGTH_8_BITS } } } }6. 常见问题与解决方案6.1 I2C通信失败排查问题现象可能原因解决方案设备无响应电源连接错误检查VCC和GND连接确保3.3V供电I2C地址错误检查AD0引脚电平尝试0x68和0x69两个地址上拉电阻缺失SDA和SCL线需要4.7kΩ上拉电阻数据读取为0时钟配置错误检查I2C时钟分频设置传输模式错误确保读写模式切换正确6.2 数据异常处理加速度数据异常检查量程配置是否匹配转换系数验证传感器放置方向Z轴应该指向重力方向检查是否存在机械振动干扰陀螺仪漂移问题传感器上电后需要预热稳定考虑软件校准偏移值使用DMP进行传感器融合6.3 性能优化建议使用DMA传输对于高速数据采集配置I2C使用DMA可以减少CPU开销中断模式使用I2C中断而非阻塞模式提高系统响应性数据滤波添加软件滤波算法平滑传感器数据电源管理合理配置MPU6050的低功耗模式7. 进阶应用姿态解算基础7.1 互补滤波算法实现简单的姿态角度计算// 简单的互补滤波姿态解算 typedef struct { float pitch; float roll; float yaw; } Attitude_Angle; void Complementary_Filter(float *accel, float *gyro, Attitude_Angle *angle, float dt) { static float pitch 0, roll 0; const float alpha 0.98; // 互补滤波系数 // 从加速度计计算姿态角俯仰和横滚 float accel_pitch atan2(accel[0], sqrt(accel[1]*accel[1] accel[2]*accel[2])) * 180.0 / M_PI; float accel_roll atan2(accel[1], sqrt(accel[0]*accel[0] accel[2]*accel[2])) * 180.0 / M_PI; // 互补滤波融合 pitch alpha * (pitch gyro[1] * dt) (1 - alpha) * accel_pitch; roll alpha * (roll gyro[0] * dt) (1 - alpha) * accel_roll; angle-pitch pitch; angle-roll roll; // 偏航角需要磁力计或长期积分此处暂不计算 }7.2 使用DMP进行传感器融合MPU6050内置的DMP可以输出融合后的四元数大大简化姿态解算// DMP初始化配置简化版 uint8_t MPU6050_DMP_Init(void) { // 复位DMP MPU6050_Write_Byte(0x6A, 0x04); // USER_CTRL寄存器 for (volatile uint32_t i 0; i 100000; i); // 加载DMP固件 // 此处需要加载完整的DMP固件代码 // 由于篇幅限制具体实现参考官方DMP库 // 启用DMP MPU6050_Write_Byte(0x6A, 0x02); // 启用FIFO MPU6050_Write_Byte(0x38, 0x02); // 启用DMP中断 return 1; }8. 工程实践建议8.1 代码结构优化建议将MPU6050驱动模块化创建独立的驱动文件mpu6050.h- 头文件包含结构体定义和函数声明mpu6050.c- 实现文件包含所有驱动函数mpu6050_dmp.c- DMP相关功能可选8.2 错误处理机制完善的错误处理可以提高代码健壮性typedef enum { MPU6050_OK 0, MPU6050_ERROR_INIT, MPU6050_ERROR_READ, MPU6050_ERROR_WRITE, MPU6050_ERROR_TIMEOUT } MPU6050_Status; MPU6050_Status MPU6050_Check_Status(void) { uint8_t whoami MPU6050_Read_Byte(MPU6050_RA_WHO_AM_I); if (whoami ! 0x68) { return MPU6050_ERROR_INIT; } // 检查其他状态寄存器 uint8_t pwr_mgmt MPU6050_Read_Byte(MPU6050_RA_PWR_MGMT_1); if (pwr_mgmt 0x40) { return MPU6050_ERROR_INIT; // 睡眠模式 } return MPU6050_OK; }8.3 生产环境注意事项校准流程生产时需要每个传感器单独校准偏移值温度补偿对于高精度应用需要实现温度补偿算法电磁兼容传感器布线要远离电机等干扰源固件版本注意MPU6050不同版本的特性差异通过本文的完整实现你应该能够在天猛星MSPM0G3507开发板上成功驱动MPU6050传感器。这套驱动代码经过实际测试可以直接用于项目开发。在实际应用中根据具体需求调整量程配置和数据处理算法结合DMP功能可以获得更稳定的姿态数据。

相关新闻

Wireshark实战:深度解析WPA2四次握手,精准定位Wi-Fi认证失败

Wireshark实战:深度解析WPA2四次握手,精准定位Wi-Fi认证失败

1. 项目概述:当Wi-Fi连不上时,我们到底在看什么?“已连接,但无法访问互联网”、“正在获取IP地址...”或者干脆就是“身份验证错误”。这些Wi-Fi连接失败的提示,相信每个用过无线网络的人都遇到过。对于普通用户&#…

2026/10/17 14:21:54 阅读更多 →
深入解析ARM CoreSight调试架构:ROM表与访问端口在AM62L SoC中的实践

深入解析ARM CoreSight调试架构:ROM表与访问端口在AM62L SoC中的实践

1. 调试子系统:嵌入式开发的“手术刀” 在嵌入式开发这个行当里,调试能力的高低,往往直接决定了项目是“优雅地解决问题”还是“在黑暗中痛苦摸索”。对于像TI AM62L这样集成了多核Cortex-A/M处理器、复杂外设和高速互连的SoC来说&#xff0c…

2026/10/18 7:40:17 阅读更多 →
Notebook到生产环境的ML工程交付实战

Notebook到生产环境的ML工程交付实战

1. 项目概述:这不是一次模型训练,而是一场工程交付“From Notebook to Production: Running ML in the Real World (Part 4)”——光看标题,你就能闻到一股混合着Jupyter内核日志、Docker容器启动日志和Kubernetes事件告警的味道。这不是第4节…

2026/10/17 14:26:54 阅读更多 →

最新新闻

FTP主动/被动模式全解析:原理、配置与数据连接故障排查

FTP主动/被动模式全解析:原理、配置与数据连接故障排查

你在某台Linux服务器上搭好了FTP服务,把账号密码发给同事,结果对方用FileZilla连上来,登录倒是正常,目录列表却一直转圈圈,最后弹出一句“无法打开数据连接”。这样的问题我排查过太多次,几乎所有初次接触F…

2026/10/18 17:46:45 阅读更多 →
React Native 鸿蒙跨平台:从零实现一个推箱子小游戏的完整指南

React Native 鸿蒙跨平台:从零实现一个推箱子小游戏的完整指南

1. 立项逻辑:为什么一个小游戏也要走 React Native 鸿蒙跨平台先说这个项目的来由。当时手头接到一个需求:做一个跑在鸿蒙设备上的推箱子小游戏,但同一份玩法逻辑后面还得复用到安卓和 iOS。很多人会觉得,推箱子就是个几百行逻辑的…

2026/10/18 17:46:45 阅读更多 →
西北干旱区矢量边界.SHP:空间分析的地基与治理基底

西北干旱区矢量边界.SHP:空间分析的地基与治理基底

简介:本资源为西北干旱区高精度矢量边界数据,面向地理信息、生态遥感、气候研究及国土规划领域的科研人员与高校师生,解决区域尺度空间分析中缺乏权威、可用干旱区行政/自然边界底图的问题。数据以标准Shapefile格式提供,共8个文件…

2026/10/18 17:46:45 阅读更多 →
Ubuntu源码编译MinkowskiEngine避坑指南:CUDA/PyTorch/GCC/cuDNN四版本锁死

Ubuntu源码编译MinkowskiEngine避坑指南:CUDA/PyTorch/GCC/cuDNN四版本锁死

简介:本资源是面向深度学习开发者与Ubuntu系统用户的MinkowskiEngine源码级安装实践指南,聚焦解决稀疏张量计算库在CUDA环境下的兼容性编译难题。资源包共3个文件(1个HTML文档、1个.inscode配置文件、1个.gitignore),总…

2026/10/18 17:46:45 阅读更多 →
大模型真实负载如何驱动国产算力芯片设计升级

大模型真实负载如何驱动国产算力芯片设计升级

简介:本资源是一份聚焦AI算力产业趋势的深度行业分析PPT,面向芯片工程师、AI基础设施从业者、信创领域决策者及高校相关专业研究者,系统解答大模型演进如何驱动算力重心从训练向推理迁移、国产AI芯片如何突破封锁实现全栈适配等核心问题。文件…

2026/10/18 17:46:45 阅读更多 →
2B参数视觉语言模型Youtu-Parsing:复杂文档解析新范式,替代传统OCR管道

2B参数视觉语言模型Youtu-Parsing:复杂文档解析新范式,替代传统OCR管道

文档解析这个赛道,这几年肉眼可见地热闹起来了。前几年大家还在用“OCR文字识别手工规则”硬啃版面,现在直接上视觉语言模型(VLM)来做端到端的文档解析,效果确实是一代一代在进步。最近我拿到了一套名为Youtu-Parsing的…

2026/10/18 17:45:45 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式: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/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/18 12:16:16 阅读更多 →