
简介面向 STM32 嵌入式初学者及需要快速接入六轴姿态检测的开发者这份工程提供 ICM-42688-P 在 STM32F103VET6 上的可直接编译示例完成陀螺仪、加速度计与温度数据采集并实时刷新到 OLED 屏幕适合姿态感知、平衡车、云台与可穿戴设备的前期验证。代码保持精简未封装 APEX 与 FIFO 高级功能寄存器配置集中在 Hardware/ICM-42688 目录并随包附上中英文数据手册便于按实际量程、输出速率与滤波参数自行调整。压缩包共 283 个文件以 35 个 .h 头文件、33 个 .c 源文件为核心另含 2 份 PDF 手册、Keil 的 uvprojx/uvoptx 工程文件及启动汇编等整体约 47.57MB目录分层清晰方便对照硬件层与应用层阅读。已有 4392 人学习可作为传感器驱动移植、通信时序排查和 OLED 显示框架复用的参考底稿。1. 姿态角一跳一跳的那晚我把 ICM-42688-P 的驱动从头读了一遍板子第一次通电串口助手里的 roll 角在一秒内跳了十几度手明明放在桌上没动。查了两小时问题不在滤波算法而在驱动层——陀螺量程按 ±2000dps 的灵敏度换算寄存器里配的却是 ±250dps。这套跑在 STM32F103VET6 上的 ICM-42688-P 六轴姿态传感器示例代码把这些最容易出错的地方都摊开了SPI 时序、寄存器初始化、原始数据拼装、零偏校准、姿态解算、串口输出一条链路走完。它适合刚拿到传感器、想先跑通再谈优化的人也适合把驱动当模板往别的 MCU 上搬的熟手。读下去你会发现真正花时间的从来不是算法那几十行。2. 工程骨架与硬件连接从 LQFP100 引脚到 CubeMX 的 SPI 参数拿到一块新板子我习惯先不碰代码先把“电”和“线”确认一遍。ICM-42688-P 是数字接口的六轴器件加速度计和陀螺共用一套寄存器空间对外只有 SPI 或 I2C 两种访问方式。示例工程走的是 SPI理由很实际同样一次采样要读 12 字节原始数据SPI 全双工可以在一次片选内完成I2C 在 400kHz 下读一帧要接近 300μs做 1kHz 以上的姿态更新时这个时间开销不能忽略。主控选 STM32F103VET6Cortex-M3 内核跑 72MHz512KB Flash、64KB SRAMLQFP100 封装引脚够用SPI1 挂在 APB2 上分频后能轻松拿到 9MHz 以上的 SCLK。2.1 接线SPI1 四线加两根中断线的实际接法传感器本身供电范围宽VDD 和 VDDIO 都可以在 1.71V 到 3.6V 之间示例板统一给 3.3V和 MCU 电平一致省掉电平转换。真正容易出问题的是去耦芯片电源脚旁边必须有 100nF 紧贴放置再并一颗 2.2μF 的储能电容否则高 ODR 采样时电源纹波会直接反映到角速度数据上读出来就是一串规律性的毛刺。传感器引脚STM32F103VET6说明SCLKPA5SPI1_SCK时钟主机输出MISOPA6SPI1_MISO传感器数据输出MCU 输入MOSIPA7SPI1_MOSI传感器数据输入MCU 输出CSPA4普通 GPIO片选软件控制低有效INT1PA0EXTI0数据就绪中断可选项VDD / VDDIO3.3V各并 100nF 2.2μFGNDGND与 MCU 共地单点汇合片选之所以不交给 SPI 外设的硬件 NSS是因为读多字节数据时CS 必须在整个传输期间保持低电平。硬件 NSS 在每传完一个字节后就会抬起来传感器会以为一次事务结束后面的数据直接丢失。2.2 CubeMX 配置SPI 速率、时钟极性与复用推挽在 CubeMX 里把 SPI1 设成 Full-Duplex Master数据宽度 8bitNSS 选 SoftwareFirst Bit 选 MSB。时钟极性 CPOL 和相位 CPHA 决定了采样沿ICM-42688-P 支持模式 0 和模式 3示例用模式 3即 CPOLHigh、CPHA2Edge。这两个参数配反的典型现象是能读到数据但全是 0xFF 或规律错位后面排查章节会展开。波特率预分频是另一个要点。SPI1 时钟来自 APB2 的 72MHz预分频取 8 得到 9MHz。手册标称 SPI 读操作可以到 24MHz但那是在传感器评估板上、走线短且阻抗匹配的前提下一旦用杜邦线飞线连接9MHz 已经是比较稳妥的上限再往上走会出现偶发的数据错位且极难复现。/* main.c 里外设初始化之后的调用顺序顺序本身就有讲究 */ MX_GPIO_Init(); /* 先把 CS 拉高防止上电瞬间误触发 */ MX_SPI1_Init(); /* 模式 39MHzMSB first */ MX_USART1_UART_Init(); /* 115200-8-N-1用于输出姿态角 */ printf(icm init: %d\r\n, icm_init()); /* 0 表示成功 */MX_GPIO_Init()必须排在 SPI 初始化之前因为 CS 在 CubeMX 里被配置成普通输出默认电平是低。如果 SPI 先初始化完、CS 还处于低电平传感器会把接下来总线上出现的任何电平跳变当成一次事务的开始初始化序列里第一条写寄存器的命令就有可能被吞掉。2.3 目录结构一份能直接编译的工程是怎么分层的工程按“硬件抽象—器件驱动—应用逻辑”三层切开换 MCU 时只动最底下那一层。目录里没有多余的东西每个文件都能说清自己负责什么。icm42688p_demo/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── icm42688p.h /* 器件寄存器地址、数据结构、对外接口 */ │ │ └── icm42688p_reg.h /* 只放寄存器地址和位域宏方便对照手册 */ │ └── Src/ │ ├── main.c /* 初始化、主循环、串口打印 */ │ ├── icm42688p.c /* SPI 读写、初始化序列、数据换算 */ │ └── attitude.c /* 零偏校准 Mahony 姿态解算 */ ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ └── MDK-ARM/ └── icm42688p_demo.uvprojxicm42688p_reg.h单独拆出来的做法值得学。寄存器地址和位域定义会随着手册版本变化把它们集中在一个文件里升级或者对照手册查错时就不用满工程翻。icm42688p.c里只调用HAL_SPI_TransmitReceive不直接碰 GPIO 寄存器attitude.c只接收已经换算成物理单位的float不知道底下是 SPI 还是 I2C。这种分层听着像教科书但真到要换成 STM32F4 或者国产 M0 平台时你就知道省下来的是整整一天。3. 驱动层拆解SPI 读写时序与初始化序列驱动层是整个项目里最不该“凭感觉写”的部分。ICM-42688-P 的寄存器访问协议很简单地址字节的最高位是读写标志1 表示读、0 表示写其余 7 位是寄存器偏移数据按大端顺序排列。协议简单不代表不会出错绝大多数“传感器没反应”的问题都出在这几行代码上。3.1 三个基础函数读寄存器、写寄存器、连续读所有上层操作都建立在这三个函数上。片选用宏封装是为了将来换引脚时只改一处超时参数统一给 100ms是因为 HAL 库在 SPI 忙等时如果不给超时一旦总线异常程序就死在那儿连串口都来不及打印。#define ICM_CS_PORT GPIOA #define ICM_CS_PIN GPIO_PIN_4 static inline void icm_cs_low(void) { HAL_GPIO_WritePin(ICM_CS_PORT, ICM_CS_PIN, GPIO_PIN_RESET); } static inline void icm_cs_high(void) { HAL_GPIO_WritePin(ICM_CS_PORT, ICM_CS_PIN, GPIO_PIN_SET); } /* 单寄存器读地址 bit7 置 1 */ uint8_t icm_read_reg(uint8_t reg) { uint8_t tx[2] { (uint8_t)(reg | 0x80), 0x00 }; uint8_t rx[2] { 0 }; icm_cs_low(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 100); icm_cs_high(); return rx[1]; /* rx[0] 是读命令期间收到的无效字节 */ } /* 单寄存器写地址 bit7 清 0 */ void icm_write_reg(uint8_t reg, uint8_t val) { uint8_t tx[2] { (uint8_t)(reg 0x7F), val }; icm_cs_low(); HAL_SPI_Transmit(hspi1, tx, 2, 100); icm_cs_high(); } /* 连续读地址只发一次之后时钟连续打出CS 全程保持低 */ void icm_read_regs(uint8_t reg, uint8_t *buf, uint16_t len) { uint8_t addr (uint8_t)(reg | 0x80); icm_cs_low(); HAL_SPI_Transmit(hspi1, addr, 1, 100); HAL_SPI_Receive(hspi1, buf, len, 100); icm_cs_high(); }icm_read_regs是整个驱动的性能关键。一次读 12 字节加速度和陀螺数据如果按单寄存器读要发 12 次地址、拉 12 次片选耗时是连续读的三倍以上。注意这里用的是HAL_SPI_Transmit加HAL_SPI_Receive两段式中间 CS 不抬起传感器内部的地址指针会自动递增这是 ICM-42688-P 手册明确支持的行为。提示HAL_SPI_Receive期间 MOSI 上会持续输出 0xFF 或 0x00具体取决于 HAL 版本。对读操作没有影响但如果总线上挂了多个从设备记得把其他片选都拉高。3.2 初始化序列从上电复位到 1kHz 输出初始化不是把手册里的推荐值抄一遍顺序错了照样跑不起来。下面这段是示例工程里实际用的序列每一步后面都跟着为什么要这么做。uint8_t icm_init(void) { uint8_t id; icm_cs_high(); HAL_Delay(10); /* 上电稳定 */ icm_write_reg(ICM_REG_BANK_SEL, 0x00); /* 明确选中 BANK0 */ id icm_read_reg(ICM_REG_WHO_AM_I); if (id ! 0x47) { return 1; /* 通信都没通别往下配 */ } icm_write_reg(ICM_REG_DEVICE_CONFIG, 0x10); /* bit4 软复位硬件自动清零 */ HAL_Delay(2); while (icm_read_reg(ICM_REG_DEVICE_CONFIG) 0x10) { /* 等复位真正完成实测 1ms 左右 */ } icm_write_reg(ICM_REG_PWR_MGMT0, 0x00); /* 先关掉所有传感器再改配置 */ HAL_Delay(1); icm_write_reg(ICM_REG_GYRO_CONFIG0, 0x06); /* ±2000dpsODR 1kHz */ icm_write_reg(ICM_REG_ACCEL_CONFIG0, 0x06); /* ±16gODR 1kHz */ icm_write_reg(ICM_REG_PWR_MGMT0, 0x0F); /* 陀螺 加速度计均进低噪声模式 */ HAL_Delay(50); /* 陀螺起振手册要求 30ms 以上 */ return 0; }WHO_AM_I的返回值固定是 0x47这是整条链路上唯一一个可以无条件信任的“我还在”信号。把它放在最前面能在配置任何东西之前就把通信问题拦下来省掉后面一大堆“配置了但没效果”的困惑。PWR_MGMT0写两次是有意为之。这个寄存器的位域同时控制陀螺和加速度计的工作模式从关闭状态直接跳到低噪声模式时内部电荷泵和 ADC 需要一段爬升时间先写全 0 让器件彻底静下来再一次性写 0x0F 拉起比“边配边开”稳定得多。置位后那 50ms 的延时别省手册里陀螺的起振时间是 30ms 量级实测在 40ms 附近还有轻微漂移给到 50ms 才干净。GYRO_CONFIG0和ACCEL_CONFIG0的取值 0x06 拆开看是高 3 位 FS_SEL 000 对应 ±2000dps 和 ±16g低 4 位 ODR 0110 对应 1kHz。这两个编码和最终换算系数必须成对记住改一个忘一个数据就会差出几倍而且表面上看起来“有数据在变”很容易被骗过去。3.3 自检温度读数是最好的第一颗螺丝通信通了、配置写进去了接下来该验证数据是不是真的在动。很多人直接去看加速度值其实温度更合适——室温下它应该稳定在 25℃ 附近短时间波动不超过 1℃只要这个数字合理说明 SPI 读通路、字节序、换算公式三件事同时是对的。float icm_read_temp(void) { uint8_t d[2]; icm_read_regs(ICM_REG_TEMP_DATA1, d, 2); /* 0x1D 起始大端 */ int16_t raw (int16_t)((d[0] 8) | d[1]); return raw / 132.48f 25.0f; /* 手册给的灵敏度与零偏 */ }这段代码最有价值的地方在(d[0] 8) | d[1]。ICM-42688-P 的寄存器数据是高字节在前如果按小端习惯写成(d[1] 8) | d[0]温度会变成一个离谱的负数而加速度和陀螺的数据看起来只是“偏大”不仔细对比根本发现不了。用温度做第一颗螺丝就是因为它的正确值区间窄、容易判断。4. 数据链路从 0x1F 开始的 12 字节到可用的姿态角数据从寄存器走到串口中间要过四道关一次性读出来、按大端拼成有符号数、按量程换成物理单位、再去掉零偏送进滤波器。每一道都有各自的坑而且这些坑的错误表现非常像——数据在动但角度不对。4.1 一次突发读 12 字节与符号位处理加速度 X/Y/Z 和陀螺 X/Y/Z 在寄存器空间里是连续的从 0x1F 开始正好 12 字节一次事务读完可以保证六个轴来自同一个采样时刻。分开读的话即使在 1kHz 下两次读之间也隔了几十微秒做高频运动时会产生相位差。void icm_read_data(icm_data_t *out) { uint8_t b[12]; icm_read_regs(ICM_REG_ACCEL_DATA_X1, b, 12); /* 0x1F 起12 字节 */ /* 每两字节一个大端有符号数先移位再强转避免符号扩展出错 */ int16_t ax (int16_t)((b[0] 8) | b[1]); int16_t ay (int16_t)((b[2] 8) | b[3]); int16_t az (int16_t)((b[4] 8) | b[5]); int16_t gx (int16_t)((b[6] 8) | b[7]); int16_t gy (int16_t)((b[8] 8) | b[9]); int16_t gz (int16_t)((b[10] 8) | b[11]); out-acc[0] ax / 2048.0f; /* ±16g 对应 2048 LSB/g */ out-acc[1] ay / 2048.0f; out-acc[2] az / 2048.0f; out-gyro[0] gx / 16.4f; /* ±2000dps 对应 16.4 LSB/dps */ out-gyro[1] gy / 16.4f; out-gyro[2] gz / 16.4f; }(int16_t)((b[0] 8) | b[1])这个写法里b[0] 8会先被提升成int再和b[1]或运算最后强转回int16_t完成符号扩展。如果图省事写成(b[0] 8) | b[1]直接赋给int16_t在部分编译器上行为一致但换一个优化等级就可能出问题。这类细节不写清楚后面调不出来会怀疑人生。4.2 量程换算表与陀螺零偏校准换算系数必须和配置寄存器严格对应这张表建议直接抄进工程注释里改量程时对照着改。加速度量程FS_SEL灵敏度 (LSB/g)陀螺量程FS_SEL灵敏度 (LSB/dps)±16g0002048±2000dps00016.4±8g0014096±1000dps00132.8±4g0108192±500dps01065.5±2g01116384±250dps011131.0零偏校准是第二个必须做的动作。陀螺再贵静止时输出也不会是严格的 0典型值在 ±0.5dps 到 ±2dps 之间直接积分成角度就是一分钟漂十几度。示例工程的做法简单粗暴但有效上电后要求板子静止 1 秒采 1000 组求平均把这个均值当作零偏存起来之后每次读数都减掉。void icm_calibrate_gyro(float bias[3]) { icm_data_t d; float sum[3] {0}; for (int i 0; i 1000; i) { icm_read_data(d); sum[0] d.gyro[0]; sum[1] d.gyro[1]; sum[2] d.gyro[2]; HAL_Delay(1); /* 采样间隔 1ms覆盖 1 秒 */ } for (int i 0; i 3; i) { bias[i] sum[i] / 1000.0f; } }校准期间板子必须真的静止。放在桌面上被风扇吹、或者 USB 线还在轻微晃动都会把振动混进零偏里而且这个错误会被永久写进后续所有角度计算属于典型的“一开始就错后面全错”。4.3 Mahony 互补滤波把角速度和加速度合成姿态角陀螺积分短时准、长时漂加速度计测重力方向长时准、短时被运动干扰。互补滤波就是让两者各管一段频段。示例工程用 Mahony 的四元数形式好处是不用处理欧拉角的万向锁且计算量对 M3 来说完全够用——1kHz 调用一次单次耗时在 70μs 量级。static float q0 1.0f, q1 0.0f, q2 0.0f, q3 0.0f; static float ix 0.0f, iy 0.0f, iz 0.0f; #define KP 0.5f #define KI 0.02f void mahony_update(float gx, float gy, float gz, float ax, float ay, float az, float dt) { float n sqrtf(ax * ax ay * ay az * az); if (n 0.01f) return; /* 自由落体或剧烈冲击跳过修正 */ ax / n; ay / n; az / n; /* 由当前四元数推算出的重力方向 */ float vx 2.0f * (q1 * q3 - q0 * q2); float vy 2.0f * (q0 * q1 q2 * q3); float vz q0 * q0 - q1 * q1 - q2 * q2 q3 * q3; /* 测量重力与估计重力的叉积作为误差反馈 */ float ex ay * vz - az * vy; float ey az * vx - ax * vz; float ez ax * vy - ay * vx; ix ex * KI * dt; iy ey * KI * dt; iz ez * KI * dt; /* 度/秒转弧度/秒后叠加修正量 */ gx gx * 0.0174533f KP * ex ix; gy gy * 0.0174533f KP * ey iy; gz gz * 0.0174533f KP * ez iz; /* 一阶龙格库塔积分四元数 */ q0 (-q1 * gx - q2 * gy - q3 * gz) * 0.5f * dt; q1 ( q0 * gx q2 * gz - q3 * gy) * 0.5f * dt; q2 ( q0 * gy - q1 * gz q3 * gx) * 0.5f * dt; q3 ( q0 * gz q1 * gy - q2 * gx) * 0.5f * dt; n sqrtf(q0 * q0 q1 * q1 q2 * q2 q3 * q3); q0 / n; q1 / n; q2 / n; q3 / n; }KP决定跟随速度KI负责消除陀螺的残余零偏。KP 给大了角度会跟着振动抖给小了响应迟滞0.5 是个比较中庸的起点KI 一般不超过 0.05再大就会出现缓慢的低频振荡。dt必须用实测的调用间隔不能写死 0.001因为主循环里还有串口打印和其他任务实际周期会浮动这个误差会直接变成角度误差。注意if (n 0.01f) return;这一句。当板子自由落体或被猛撞时加速度计测到的不再是重力继续拿它做修正会把姿态往错误方向拽。加了这行判断至少能保证冲击瞬间姿态不崩。4.4 串口输出定长帧比 printf 更适合观察调试阶段最容易忽略的是输出格式。用printf(%f, %f, %f\r\n, ...)看着方便但浮点格式化的长度不固定上位机解析时容易错位而且printf的重入和阻塞会打乱 1kHz 的采样节拍。示例工程改成定长二进制帧或者整数化的文本帧每帧固定 3 个 int16 加一个帧尾上位机一帧一帧对齐就行。/* 每 10ms 输出一次roll/pitch/yaw 放大 100 倍成整数帧尾 0x0D 0x0A */ void attitude_report(float roll, float pitch, float yaw) { int16_t r (int16_t)(roll * 100.0f); int16_t p (int16_t)(pitch * 100.0f); int16_t y (int16_t)(yaw * 100.0f); printf(%d,%d,%d\r\n, r, p, y); }降到 100Hz 输出是有意的1kHz 采样、100Hz 上报中间用累加或者隔次取值既能看清动态过程又不至于把串口带宽占满。用 115200 波特率时一帧十几字节在 1ms 内就能发完不会阻塞主循环。5. 排查清单WHO_AM_I 读到 0x00 和角度漂移的五个现场调试这类传感器最消耗时间的不是写代码而是判断“到底哪一层坏了”。下面五条是实际遇到过的现场按从底层往上的顺序排遇到问题按顺序过一遍通常十分钟内能定位。5.1 通信层的三个坑现象一WHO_AM_I稳定读到 0x00。原因基本锁定在 SPI 没真正跑起来。常见的是 CubeMX 里 MOSI/MISO/SCK 三个脚没配成复用推挽或者配成了复用开漏也有 CS 引脚被误配成复用模式导致软件拉低根本没生效。解决方式是先拿示波器看 SCK 上有没有时钟再看 CS 在读写期间是不是一直保持低电平。两个都对再查 MISO 是否被别的外设占用。现象二WHO_AM_I读到 0xFF。0xFF 意味着 MISO 一直被拉高通常是传感器没上电或者 MISO 悬空。先量 VDD 和 VDDIO 是不是真的到了 3.3V再补一颗 MISO 上的 10kΩ 下拉确认是不是虚焊。还有一种情况是 SPI 模式配错——传感器在错误的时钟沿输出数据主机采样到的全是空闲电平。现象三偶尔读到正确 ID但连续读数据就错位。这是速率问题。飞线连接时 9MHz 已经接近上限杜邦线长过 15cm 就该降到 4.5MHz 甚至 2.25MHz。另一个隐蔽原因是读 12 字节的过程中被高优先级中断打断CS 长时间保持低电平传感器内部状态机超时复位。解决办法是把整段读操作放进临界区或者干脆用 DMA 一次搬完。5.2 数据与算法层的两个坑现象四静止时角度持续单向漂移一分钟能漂十几度。先确认零偏校准有没有真的执行以及校准时板子是不是静止。如果校准做了还漂检查PWR_MGMT0写入后那 50ms 延时够不够陀螺没起振稳定就开始采集零偏本身就在变化。还有一种情况是温度影响——便宜模块上温度变化 10℃ 就能带来 0.5dps 的零偏漂移只能靠KI慢慢收敛或者加温度补偿表。现象五加速度数值看着合理但姿态角在某个方向明显偏大。八成是量程和换算系数没对上。配置写的是 ±8g换算还按 2048 LSB/g 算得到的结果就偏大一倍。这种错误的迷惑性在于数据始终“在动”曲线形状也对只是幅值不对不看静止时的重力模长根本发现不了。养成习惯静止时算一下三轴加速度的模长应该在 1.0g 附近偏太多就是量程错了。6. 二次开发换量程、开 FIFO、把驱动挪到别的 MCU这套代码真正的作用是当模板。实际项目里最先要改的通常是量程——做手势识别用 ±4g 就够做跌倒检测得留到 ±16g 的余量陀螺做慢速云台 ±500dps 足够做高速旋转测量才需要 ±2000dps。改的时候两个地方必须同时动GYRO_CONFIG0/ACCEL_CONFIG0的 FS_SEL 位以及icm_read_data里的除数。我一般会在头文件里定义一组宏把配置值和换算系数绑在一起改一处编译期就会暴露没改的另一处。/* 量程配置与换算系数绑定改一处不用记着改另一处 */ #define ICM_ACC_FS_SEL 0x01 /* 001 ±8g */ #define ICM_ACC_LSB_PER_G 4096.0f #define ICM_GYR_FS_SEL 0x02 /* 010 ±500dps */ #define ICM_GYR_LSB_PER_DPS 65.5f #define ICM_ACC_CONFIG0 ((ICM_ACC_FS_SEL 5) | 0x06) /* 低 4 位 ODR1kHz */ #define ICM_GYR_CONFIG0 ((ICM_GYR_FS_SEL 5) | 0x06)第二个动作是打开 FIFO。主循环里同步读数据采样率一高就会被其他任务挤掉节拍FIFO 能把传感器内部的采样和 MCU 的读取解耦配置好水位中断MCU 每积累 N 组数据才进一次中断一次性把 FIFO 里读空CPU 占用直接降一个量级。流程是使能 FIFO 模式、选择要打包的传感器、设水位阈值、在中断里循环读FIFO_COUNT直到空。具体位域按手头的数据手册填不同批次的寄存器映射偶有调整以手册为准。第三个动作是移植。把icm_read_reg、icm_write_reg、icm_read_regs三个函数抽成函数指针表注册进驱动结构体上层逻辑一行不用改就能换到别的平台。我做这套抽象的血泪经验是一开始觉得“就三个函数直接改 HAL 调用多快”等到第四个平台要接的时候才发现散落在各处的HAL_SPI_Transmit有二十多处。从那以后我每次接新传感器第一件事就是把读写接口先抽出来再写初始化序列。最后是验证方法。改完量程或者移植完别急着看姿态角先做两步静止时看三轴加速度模长是否在 1.0g 附近、陀螺三轴是否接近 0然后绕单个轴缓慢转 90 度看对应角度是不是也跟着走 90 度、另外两个轴是否基本不动。这两步过了再谈滤波参数。希望帮到你。本文还有配套的精品资源点击获取