ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32步进电机S型加减速与SpTA算法实战

STM32步进电机S型加减速与SpTA算法实战 简介本资源是一套面向嵌入式电机控制工程师与STM32进阶开发者的SpTA加减速算法实践方案聚焦步进电机高精度、低振动运动控制痛点适用于自动化设备、3D打印、精密定位等对启停平滑性要求严苛的场景。压缩包为ZIP格式共含多个核心模块文件具体数量未提供主要包括C语言源码工程含定时器/PWM初始化、S型T梯形曲线参数计算与实时调度逻辑、加减速效果可视化模拟器支持曲线动态预览与参数调试、以及关键曲线计算参数配置表涵盖最大速度、加减速度斜率、平顶持续时间等可调项整体大小24.43MB。已有1380人学习下载开发者可直接移植至STM32F1/F4系列平台快速实现基于ARM Cortex-M内核的高效SpTA算法部署并借助配套工具完成参数整定、性能验证与运动曲线优化显著缩短电机控制算法落地周期。1. 项目概述为什么S型曲线加减速在STM32步进电机控制中不是“可选项”而是“必选项”你手头正调试一块STM32F407开发板驱动着一个28BYJ-48或NEMA17步进电机用的是常见的ULN2003或TB6600驱动芯片。刚上电电机“咔哒”一声猛冲出去接着在目标位置前“哐当”撞停——这不是电机坏了是你还在用最原始的“全速启停”或粗糙的T梯形加减速。这种控制方式在低速、轻载时勉强能用一旦速度提上去、负载稍有变化失步、抖动、啸叫、定位不准就全来了。我第一次在激光雕刻机项目里遇到这个问题时光调加减速参数就花了整整三天改一次代码烧一次固件串口打印一堆脉冲计数最后发现不是电机问题是加减速曲线本身就不匹配机械惯性。S型曲线S-curve和T梯形曲线Trapezoidal profile不是两种并列的“算法风格”而是加减速控制演进的两个代际。T梯形曲线只有三个阶段加速→匀速→减速加速度在切换点突变导致 jerk加加速度无穷大——这就像开车时司机一脚油门踩到底再一脚急刹乘客脖子都快甩断了。而S型曲线把加速度本身也做成平滑变化让jerk连续可控从而彻底消除冲击。SpTASmoothed Profile with Time-Accuracy Trade-off Analysis则是工程落地的关键它不是纯理论上的理想S型而是针对STM32有限的RAM、Flash和CPU主频比如F4系列168MHz在精度、实时性和资源占用之间做的精巧平衡。它把S型曲线拆解成多段微小线性插值用查表增量计算代替浮点运算让F103这种64KB Flash、20KB RAM的芯片也能跑出接近F4的运动平顺度。这个标题里的每个词都是硬核信号“STM32”意味着你要直面寄存器级定时器配置、中断优先级嵌套、DMA搬运脉冲数据“步进电机”决定了你必须处理细分驱动、堵转检测、相序校验这些底层物理约束“高效”二字直接否定了用浮点库开根号、调sin/cos函数的写法“S型T梯形”说明你得兼容两种模式——有些场合比如快速点对点搬运T梯形更省资源“SpTA”则告诉你这不是抄个开源库改改参数就能跑通的它需要你亲手推导时间-位移-速度-加速度四阶关系再把它压进STM32的32位整数运算框架里。如果你正在做CNC雕刻、3D打印机Z轴升降、自动售货机货道精准推料或者任何要求“静音、准停、无抖动”的机电系统那么这篇内容就是你跳过试错周期、直接复现工业级运动控制的实操手册。2. 核心原理拆解从牛顿第二定律到STM32定时器寄存器的完整映射2.1 S型曲线的数学本质不是“画一条S形”而是控制jerk的积分链很多人以为S型曲线就是用sigmoid函数 y 1/(1e^(-x)) 去拟合位置曲线这是典型误区。真正决定运动品质的是jerk加加速度——即加速度对时间的变化率。机械系统能承受的jerk有物理上限导轨润滑不足时jerk500 m/s³就会啸叫丝杠预紧力不够时jerk200 m/s³就会产生微振动而人耳可感知的振动阈值甚至低至50 m/s³。S型曲线的核心是把jerk设计成分段恒定值再逐级积分得到加速度、速度、位移jerk(t) J₀ → a(t) ∫jerk dt J₀·t a₀ → v(t) ∫a(t) dt ½J₀·t² a₀·t v₀ → s(t) ∫v(t) dt ⅙J₀·t³ ½a₀·t² v₀·t s₀看出来没位置s(t)是t的三次多项式但它的三次项系数直接由jerk J₀决定。SpTA算法的“Smoothed Profile”部分就是把整个运动过程划分为7段经典S型或5段简化SpTA每段分配不同的jerk值确保起点和终点的jerk、加速度、速度、位移全部连续。例如一段典型的7段S型阶段jerk加速度速度位移物理意义1J线性上升抛物线上升三次曲线上升从静止开始柔和加速20恒定线性上升抛物线上升加速度达最大匀加速3-J线性下降抛物线上升三次曲线上升加速度归零速度达峰值400恒定线性上升匀速运行5-J线性下降抛物线下降三次曲线下降开始柔和减速60恒定负值线性下降抛物线下降匀减速7J线性上升抛物线下降三次曲线下降加速度归零平稳停稳提示SpTA的“Trade-off Analysis”就体现在这里——7段S型精度最高但需计算7组参数、7次插值而SpTA常采用5段去掉了阶段2和6的恒定加速度段用更小的jerk值延长阶段1/3/5牺牲一点最高速度换取更小的RAM占用和更快的中断响应。我在F103上实测5段SpTA比7段节省37%的RAM而定位误差仅增加0.08个脉冲对1.8°步进电机而言可忽略。2.2 T梯形曲线是S型的“降维特例”但绝非过时方案T梯形曲线常被贬为“低端”其实它在特定场景下不可替代。它的数学表达极其简洁加速段t∈[0, t₁]: a a_max, v a_max·t, s ½a_max·t² 匀速段t∈[t₁, t₂]: a 0, v v_max, s ½a_max·t₁² v_max·(t - t₁) 减速段t∈[t₂, t₃]: a -a_max, v v_max - a_max·(t - t₂), s s₂ v_max·(t - t₂) - ½a_max·(t - t₂)²关键参数只有三个最大加速度a_max、最大速度v_max、总位移s_total。计算复杂度O(1)中断服务程序ISR里几条汇编指令就能完成。我在做AGV小车的紧急制动逻辑时就强制切回T梯形——因为S型曲线在急停时需重新规划整条轨迹而T梯形只需设a_max为-2g20μs内就能生成下一个脉冲间隔。SpTA算法的高明之处在于它把T梯形作为S型的“兜底模式”当目标位移某个阈值比如50个脉冲或实时计算剩余时间3ms时自动降级为T梯形避免小行程运动反而因插值计算引入延迟。2.3 STM32定时器如何把数学公式变成真实脉冲从ARR/PSC到PWM频率的硬核换算所有算法最终都要落到TIMx定时器上。以TIM2为例核心寄存器就三个PSCPrescaler预分频器决定计数器时钟频率。若系统主频72MHzPSC71则计数器时钟1MHz即1μs/计数。ARRAuto-reload Register自动重装载值决定计数周期。若ARR999则定时器溢出周期1000μs1ms。CNTCounter Register当前计数值读取它可知已过去多少时间。但步进电机要的不是“定时中断”而是精确的脉冲间隔。假设电机细分为1600脉冲/转要求最大转速100RPM则最大脉冲频率100×1600/60≈2667Hz即脉冲间隔≈375μs。这时ARR不能固定——因为S型曲线要求脉冲间隔持续变化加速时从1000μs逐步减到375μs减速时再逐步拉长。正确做法是用TIM2作为主定时器配置为向上计数中断频率设为10kHz即每100μs进一次中断在中断服务程序中根据当前运动阶段和已执行时间t查表或计算出下一个脉冲间隔Δt动态修改TIM3的ARR值TIM3专用于输出脉冲TIM3-ARR Δt × (主频/(PSC1))启动TIM3它会在ARR个时钟周期后触发更新事件翻转GPIO输出电平产生一个脉冲。注意这里必须用两个定时器单一定时器无法兼顾“高精度时间基准”和“动态脉冲生成”。我曾试图用一个TIM的PWM模式结果发现改变CCR值会引入2-3个时钟周期的延迟导致高速段脉冲抖动。双定时器方案虽多占一个外设但实测抖动0.5μs完全满足±0.1%的工业级精度要求。3. SpTA算法实现从理论公式到STM32 C代码的逐行解析3.1 参数预计算为什么要把浮点运算“离线化”到PC端SpTA最反直觉的设计是所有浮点运算都在上位机完成。你在PC上用Python写一个参数生成脚本输入电机最大转速RPM、丝杠导程mm/rev、负载质量kg、期望jerkm/s³它会输出一个结构体数组直接复制进STM32代码// spta_params.h —— 这是PC脚本生成的不是STM32运行时计算的 typedef struct { uint32_t total_steps; // 总脉冲数 uint32_t accel_phase1; // 阶段1脉冲数 uint32_t accel_phase2; // 阶段2脉冲数匀加速段 uint32_t cruise_steps; // 匀速段脉冲数 int32_t jerk_coeff; // jerk系数单位pulse/ms³整数放大1000倍 uint32_t min_pulse_us; // 最小脉冲间隔μs对应最大速度 } SpTA_Params; const SpTA_Params spta_config { .total_steps 12800, .accel_phase1 850, .accel_phase2 1200, .cruise_steps 10200, .jerk_coeff 2450, // 实际jerk 2.45 m/s³ .min_pulse_us 375 };为什么这么做因为STM32F103的CM3内核没有硬件浮点单元sqrtf()函数调用一次耗时120μs而我们的中断周期才100μsPC端用NumPy秒级算完STM32只做整数加减乘除。我测试过同样生成1000个脉冲间隔浮点实时计算耗时142ms而查表线性插值仅需8.3ms——快了17倍。3.2 中断服务程序ISR100μs内必须完成的三件事TIM2的中断服务程序是整个系统的命脉必须严格控制在100μs内否则会丢中断。它只做三件事volatile uint32_t current_step 0; volatile uint8_t motion_state MOTION_IDLE; // IDLE/RUNNING/STOPPING void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 1. 更新当前步数和时间戳关键用DWT周期计数器替代SysTick误差1ns static uint32_t last_cycle 0; uint32_t now_cycle DWT-CYCCNT; uint32_t delta_us (now_cycle - last_cycle) / (SystemCoreClock / 1000000); last_cycle now_cycle; // 2. 根据当前step和预计算参数查表获取下一脉冲间隔 uint32_t next_interval_us; if (motion_state MOTION_RUNNING) { next_interval_us spta_get_next_interval(current_step, spta_config); current_step; } else { next_interval_us spta_config.min_pulse_us; // 保持最小间隔防抖 } // 3. 动态设置TIM3的ARR触发下一个脉冲 uint32_t arr_val next_interval_us * (72000000 / (TIM3_PSC 1)) / 1000000; TIM_SetAutoreload(TIM3, arr_val); } }重点看spta_get_next_interval()函数——它不调用任何math.h只做查表和线性插值uint32_t spta_get_next_interval(uint32_t step, const SpTA_Params* p) { if (step p-accel_phase1) { // 阶段1jerk恒定间隔按二次函数减小 // interval base - k1 * step² k1查表预计算 return p-min_pulse_us (uint32_t)(0.00012 * step * step); // 整数近似误差0.3μs } else if (step p-accel_phase1 p-accel_phase2) { // 阶段2加速度恒定间隔线性减小 uint32_t idx step - p-accel_phase1; return p-min_pulse_us (uint32_t)(0.85 * idx); // 斜率预计算好 } else if (step p-total_steps - p-accel_phase1 - p-accel_phase2) { // 匀速段固定最小间隔 return p-min_pulse_us; } else { // 减速段镜像加速段 uint32_t rev_step p-total_steps - step; if (rev_step p-accel_phase1) { return p-min_pulse_us (uint32_t)(0.00012 * rev_step * rev_step); } else { return p-min_pulse_us (uint32_t)(0.85 * (rev_step - p-accel_phase1)); } } }实操心得DWT-CYCCNTData Watchpoint and Trace cycle counter是隐藏神器它在CM3内核中独立计时精度等于CPU主频比SysTick的1ms分辨率高三个数量级。我在F103上实测用DWT测100μs间隔标准差仅±0.8ns而SysTick误差达±12μs。但必须先启用CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;3.3 SpTA的“时间-精度权衡”实战如何用1KB RAM跑出0.01%定位精度SpTA的Trade-off Analysis具体怎么落地看内存占用对比方案RAM占用定位误差计算耗时适用场景浮点S型7段3.2KB±0.002脉冲142msF4/F7高端设备科研验证查表S型1024点4.1KB±0.001脉冲12ms需要极致精度的CNCSpTA5段插值1.0KB±0.08脉冲8.3msF103/F0主流应用T梯形0.2KB±0.5脉冲0.8msAGV急停、简单搬运SpTA的1KB是怎么省出来的关键在分段线性插值。不存1024个脉冲间隔值只存5个关键点的间隔值起始、阶段1结束、阶段2结束、匀速结束、终止中间值用两点间直线插值得到// 预存5个关键点单位μs const uint16_t spta_keypoints[5] {1000, 650, 375, 375, 1000}; uint32_t spta_interpolate(uint32_t step, uint32_t total_steps) { uint32_t seg_len total_steps / 4; // 4段等长 uint8_t seg_idx step / seg_len; uint32_t local_step step % seg_len; if (seg_idx 4) return spta_keypoints[4]; // 线性插值y y0 (y1-y0) * x / seg_len int32_t dy spta_keypoints[seg_idx1] - spta_keypoints[seg_idx]; return spta_keypoints[seg_idx] (dy * local_step) / seg_len; }这个函数只用整数运算编译后汇编指令仅17条执行时间2.1μs。而1024点查表需要uint16_t table[1024]占2KB RAM且Cache命中率低。SpTA用1/4的RAM达到99.2%的精度保真度——这就是工程智慧不追求理论最优而追求在约束下的帕累托最优。4. 工程落地细节从电路连接到抗干扰的21个硬核经验4.1 步进电机驱动电路的致命陷阱共地、滤波、续流二极管一个都不能少算法再完美硬件一塌糊涂就全白干。我踩过的最痛的坑是驱动芯片共地错误。用TB6600驱动NEMA17时把STM32的GND、TB6600的GND、电源GND接在不同点结果电机在高速时突然失步示波器一看脉冲信号上叠加了12V开关噪声。正确接法必须是“星型单点接地”所有GNDMCU、驱动芯片、电源拧在一起接到电源负极铜箔上TB6600的DIR/EN/STEP信号线必须串100Ω电阻抑制高频振铃电机绕组两端并联100nF陶瓷电容吸收反电动势电源入口加LC滤波100μH电感 1000μF电解电容。注意28BYJ-48这种减速电机千万别直接接TB6600它的相电流仅200mA而TB6600最小输出电流1.5A会烧毁电机。必须用ULN2003或专用小电流驱动芯片并在每相串联10Ω限流电阻。4.2 STM32定时器配置的魔鬼细节为什么TIM2用内部时钟TIM3用外部时钟很多教程说“用一个定时器就行”但实际必须双定时器协同TIM2主定时器配置为内部时钟源CK_INT预分频PSC7172MHz→1MHzARR99100μs中断。它只负责提供精确时间基准不做任何输出。TIM3脉冲生成器配置为外部时钟模式1ETR把TIM2的更新事件UG作为触发源。这样TIM3的计数完全同步于TIM2避免两个定时器各自计数产生的相位漂移。关键代码// TIM2配置时间基准 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 99; // 100μs TIM_TimeBaseStructure.TIM_Prescaler 71; // 1MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // TIM3配置脉冲输出 TIM_TimeBaseStructure.TIM_Period 1000; // 初始值后续动态改 TIM_TimeBaseStructure.TIM_Prescaler 0; // 直接用ETR时钟 TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 启用TIM2的更新中断 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 配置TIM3为外部时钟模式 TIM_ETRClockModeConfig(TIM3, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0); TIM_SelectInputTrigger(TIM3, TIM_TS_ETRF); TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_External1);4.3 抗干扰实战清单让电机啸叫消失的21个操作PCB布线电机走线必须远离信号线至少3mm间距且用地平面隔离电源去耦每个IC的VDD/VSS引脚旁放0.1μF陶瓷电容TB6600输入端加100μF钽电容GPIO配置STEP/DIR引脚设为推挽输出速度50MHz无上拉/下拉中断优先级TIM2中断设为最高NVIC_SetPriority(TIM2_IRQn, 0)避免被其他中断打断DMA禁用绝对不要用DMA搬运脉冲数据——DMA请求有延迟破坏实时性看门狗启用IWDG喂狗放在主循环防止死机导致电机飞车堵转检测在匀速段监测脉冲间隔稳定性连续5次偏差5%则报堵转温度监控TB6600散热片贴NTC热敏电阻70℃自动降速机械共振规避实测电机固有频率如NEMA17约120Hz在S型曲线中避开该频段细分设置28BYJ-48用1/8细分NEMA17用1/16细分平衡精度与扭矩电源纹波用示波器测电机供电电压纹波100mV必须加LC滤波外壳接地金属外壳必须单点接大地否则成天线辐射干扰软件滤波对编码器反馈值做中值滤波剔除异常脉冲启动保护首次上电执行“慢速自检旋转”确认无卡滞再进入高速模式电缆选型电机线用带屏蔽层的双绞线屏蔽层单端接地固件升级预留DFU接口避免每次调试都拆机烧录日志记录用UART DMA发送运动状态当前step、速度、错误码波特率2M电压监测ADC采样VCC4.75V时强制降速防止驱动芯片欠压EMC测试在30-1000MHz频段做辐射发射测试超标点加磁环机械预紧丝杠螺母预紧力调至0.02mm间隙消除反向间隙环境适应-10℃~60℃全温区老化测试-40℃时脉冲间隔自动15%。踩坑实录我在做一台医疗样本分装仪时电机在45℃环境连续运行2小时后开始失步。查了一周才发现是TB6600的散热硅脂干裂表面温度虚低实际结温超125℃触发热保护。后来改用导热系数≥3.0W/mK的硅脂并在固件里加入温度补偿算法——每升高10℃jerk系数自动下调8%彻底解决。5. 常见问题排查从“电机不动”到“定位漂移”的终极速查表现象可能原因排查步骤解决方案电机完全不转STEP信号无输出用示波器测PA0假设STEP接PA0确认有方波检查GPIO初始化是否遗漏补全RCC时钟使能、GPIO模式配置电机抖动严重脉冲间隔抖动5%抓取100个脉冲间隔看标准差检查DWT计时是否启用启用DWT禁用SysTick加速无力最大加速度设置过高降低jerk_coeff值观察是否改善用万用表测驱动芯片输出电压是否跌落按电机扭矩曲线重新计算a_max定位偏移±2脉冲机械反向间隙手动推动负载测空行程在S型曲线末尾加“微进-回退”补偿固件中加入间隙补偿算法高速时啸叫电机固有频率被激发用手机APP测噪声频谱调整S型曲线中匀速段速度避开120Hz/240Hz等谐波点修改cruise_speed参数突然失步电源电压跌落示波器测VCC看加速瞬间是否4.5V检查电源功率是否足够换用更大功率电源加输入电容串口乱码UART中断被TIM2抢占检查NVIC优先级确保UART优先级低于TIM2或改用DMA发送设置UART优先级为3TIM2为0烧毁驱动芯片电机相线短路用万用表测AB/BC/CA电阻正常应1Ω检查接线是否碰线更换电机加熔断器定时器不进中断NVIC未使能检查NVIC_Init()是否调用用调试器看NVIC_ISPR寄存器对应位是否置1补全NVIC配置位置累计误差增大编码器丢脉冲用逻辑分析仪抓编码器A/B相看是否有毛刺检查上拉电阻是否缺失加施密特触发器整形换10kΩ上拉电阻电机发热异常细分电流过大用钳形表测相电流对比驱动芯片拨码设置与电机额定电流调整TB6600拨码开关降低电流无法启动EN信号被拉低测EN引脚电压正常应为3.3V检查是否误接了低电平有效改为高电平有效或反相EN信号运行中随机停机看门狗超时在主循环加IWDG_ReloadCounter()用调试器看是否卡在某处增加喂狗点优化长循环串口打印卡死printf重定向阻塞改用sprintfHAL_UART_Transmit()禁用半主机调试删除__io_putchar()重定向用DMA发送电机方向相反DIR信号极性错误交换DIR信号线或在固件中反转dir_output变量修改GPIO_Write()逻辑低速爬行不稳细分微步不均匀换用更高细分档位如1/32检查驱动芯片是否支持所选细分改TB6600拨码为1/32确认电机兼容电磁干扰通讯失败电机线未屏蔽临时用锡纸包裹电机线加磁环换屏蔽双绞线磁环套在电机线入口处定时器计数不准PSC/ARR值溢出检查PSC*(ARR1)是否超过2³²用32位变量计算改用更大的PSC减小ARR无法烧录固件JTAG被禁用检查是否执行了__HAL_AFIO_REMAP_SWJ_DISABLE()用ST-Link Utility擦除短接BOOT0引脚用系统存储器启动多电机不同步定时器不同步确保所有TIMx使用同一时钟源用TIMx-EGRTIM_EventSource_Update强制同步在主定时器中断里触发从定时器更新事件电源指示灯闪烁过流保护触发测驱动芯片FAULT引脚检查电机是否卡死手动转动电机清理异物重启独家技巧用STM32CubeMX生成基础代码后手动删除所有HAL_Delay()调用。这个函数基于SysTick而SysTick和TIM2中断嵌套会导致不可预测的延迟。所有延时改用DWT计时void dwt_delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t delay_cycles us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) delay_cycles); }实测精度±0.3μs比HAL_Delay()稳定10倍。6. 进阶扩展从单轴SpTA到多轴协同的工业级架构SpTA算法的价值不仅在于单轴平滑更在于它为多轴协同打下基础。当你需要控制X/Y/Z三轴联动切割时S型曲线的jerk连续性保证了各轴加速度变化同步避免因某轴突变导致机械应力集中。我的做法是主从架构用一个STM32F4作为主控制器运行全局运动规划多个STM32F1作为从机各控一轴通过CAN总线接收脉冲数和jerk参数时间同步主控制器广播一个“运动开始时间戳”所有从机用DWT对齐误差10ns插补算法在主控端用Bresenham直线插补或G代码解析器把G1 X10 Y5 Z2分解为各轴的spta_config结构体故障隔离任一从机报堵转主控立即冻结所有轴避免连锁故障。这套架构已在两台量产设备中验证一台桌面级CNC雕刻机F103×3一台工业级激光打标机F407主控F103×4。关键指标三轴同步误差0.05mm100mm/s紧急停止响应3ms连续72小时运行无失步固件OTA升级成功率100%。最后分享一个小技巧SpTA算法可以无缝迁移到伺服电机控制。只需把“脉冲输出”换成“PWM输出”把“步数”换成“编码器位置”核心的jerk控制逻辑完全复用。我在一个AGV项目中用同一套SpTA代码同时控制步进电机货叉升降和伺服电机驱动轮仅需修改底层驱动层上层运动规划零改动——这才是真正可复用的工业级设计。我在实际项目中发现最影响交付周期的从来不是算法多复杂而是硬件选型和抗干扰设计。花三天调通SpTA算法可能要用两周解决电机啸叫和通讯干扰。所以建议你拿到新项目时先用示波器测清所有信号质量再动手写代码。毕竟再优美的S型曲线也救不了一根被干扰的STEP线。本文还有配套的精品资源点击获取
返回列表