简介面向电子设计竞赛参赛者的智能控制小车C语言程序包提供了可复用的控制与调试框架。压缩包共4个文件均为.c源代码整体仅4KB程序按功能模块拆分包括主程序、矩阵输入、液晶显示、串口通信等部分便于快速理解控制流程并在现有硬件上移植修改。已有722人浏览学习适合备赛初期对照代码结构、梳理控制逻辑。内容涵盖GPIO读取传感器信号、PWM调速、PID闭环控制等小车常见控制环节可支撑自动寻迹、避障等赛题核心功能模块化划分既方便自行扩展也为后续加入路径规划算法如A*留出接口。通过研读原始驱动与调度代码还能掌握传感器数据解析、状态显示和上位机交互的实现思路同时涉及嵌入式驱动与算法设计适合在此基础上二次开发有助于提高现场调试验证效率是电赛智能车方向一份轻量实用的参考资料。1. 智能控制小车程序电赛赛题里最值得先啃的一块硬骨头不管你是第一次接触电子设计竞赛还是已经搭过几辆车但跑不稳智能控制小车这道赛题始终是绕不开的入场券。它的难点不在单个模块而在把C语言程序、传感器采集、电机驱动和决策逻辑塞进同一颗MCU里让它们在几十毫秒内协同工作。我见过太多队伍卡在“车能走但走不直”“能巡线但一加速就飞”这类问题上说到底不是硬件不行而是程序架构和参数处理出了问题。这篇文章从一个实际可跑的C语言工程出发把主控选型、外围电路、PID闭环、状态机切换和现场调试讲透适合准备电赛赛题的学生也适合刚入手智能车开发、想快速跑通一套完整流程的从业者。2. 硬件选型与控制架构为什么主流方案是STM32加灰度传感器2.1 主控、电机与传感器的选型逻辑智能控制小车的硬件方案看似五花八门实际在电赛赛题场景下真正被验证过的高性价比组合就那几套。主控方面Cortex-M3或M4内核的MCU是绝对主力原因很直接电赛要求现场写代码、现场调参芯片资料全、库函数丰富、下载调试方便出了问题能快速查到解决方案。8位单片机不是不能用但遇到多路传感器采集加PID运算加无线通信同时跑的时候资源紧张会让你怀疑人生。电机部分常见的坑在于只看空载转速而忽略扭矩。普通TT马达适合练习但真要上赛道建议用带编码器的直流减速电机。编码器的意义不是让你炫技而是让速度闭环有据可依——没有编码器反馈的PID纯粹是盲调有了编码器你才知道当前实际转速和目标转速差了多少这直接决定了小车能不能走直线。电机驱动芯片选TB6612或者DRV8833这类集成H桥方案比离散MOS管搭建省心得多电流余量至少要留出电机堵转时的一倍以上。传感器配置是另一个关键决策点。灰度传感器阵列做循迹是电赛赛题的经典考法常见的有四路、五路、八路三种规格。四路够用但前瞻性差过交叉线容易误判八路采集数据丰富但对安装精度要求高差一点就整体偏移。我一般推荐五路灰度加一个超声波模块的组合灰度管循迹超声波管避障两者通过状态机切换覆盖赛道上的绝大多数情况。在选择灰度传感器模块时注意看有没有电位器可调阈值这比固定阈值的模块灵活得多现场适应不同光照条件的能力完全不一样。2.2 引脚分配与基础驱动代码框架选型定了之后第一件事不是写逻辑而是把引脚分配固定下来。这里的教训是别在代码里到处硬编码引脚号先在头文件里统一宏定义。不同电赛赛题现场提供的赛道环境不同你很可能需要临时换传感器位置到时候只改一个配置文件就能适配比在几十个文件里找引脚定义要舒服得多。// pin_config.h #ifndef PIN_CONFIG_H #define PIN_CONFIG_H // 电机PWM与方向引脚 #define MOTOR_LEFT_PWM TIM1_CH1 // PA8 #define MOTOR_LEFT_DIR1 GPIO_PIN_1 // PA1 #define MOTOR_LEFT_DIR2 GPIO_PIN_0 // PA0 #define MOTOR_RIGHT_PWM TIM1_CH2 // PA9 #define MOTOR_RIGHT_DIR1 GPIO_PIN_3 // PA3 #define MOTOR_RIGHT_DIR2 GPIO_PIN_2 // PA2 // 编码器输入引脚 #define ENC_LEFT_A GPIO_PIN_6 // PA6 #define ENC_LEFT_B GPIO_PIN_7 // PA7 #define ENC_RIGHT_A GPIO_PIN_6 // PB6 #define ENC_RIGHT_B GPIO_PIN_7 // PB7 // 五路灰度传感器 GPIO 输入 #define GRAY_S1 GPIO_PIN_0 // PC0 #define GRAY_S2 GPIO_PIN_1 // PC1 #define GRAY_S3 GPIO_PIN_2 // PC2 #define GRAY_S4 GPIO_PIN_3 // PC3 #define GRAY_S5 GPIO_PIN_4 // PC4 // 超声波模块 #define ULTRASONIC_TRIG GPIO_PIN_8 // PB8 #define ULTRASONIC_ECHO GPIO_PIN_9 // PB9 #endif这段配置的逻辑是把所有硬件相关的引脚变化收敛到一个头文件里。逻辑说明很简单后面的驱动代码、应用代码都只引用这些宏名不直接跟寄存器或GPIO端口打交道。参数说明上面这个分配假设主控引脚充裕如果你用的是小封装芯片优先保证编码器引脚靠近定时器通道因为编码器模式依赖定时器的硬件捕获换到普通IO口上软件模拟会占用大量CPU时间。驱动代码框架上我习惯分四层底层硬件驱动、模块驱动、策略层、主循环调度。很多新手把所有代码堆在一个main.c里几百行下来根本没法调试。分层的核心价值在于灰度传感器模块出问题你只需要查那一个文件不需要把电机和超声波代码翻一遍。主循环里只做三件事采集传感器数据、更新状态机、执行决策输出每轮循环控制在5到10毫秒以内保证响应速度。3. 核心C语言模块PWM电机控制、编码器测速与PID闭环3.1 定时器PWM输出与电机驱动板控制电机控制的底层是PWMPWM的底层是定时器。你要做的不是把某个引脚电平拉高拉低而是让定时器按照设定的占空比连续输出脉冲。使用标准库配置PWM输出有几个关键步骤使能定时器时钟、配置GPIO复用功能、设置定时器周期和比较值、使能输出通道。占空比的计算方式为比较值除以周期值例如周期设为1000比较值为600对应占空比60%。// motor_pwm.c - 基于STM32标准库的PWM初始化与设置 #include stm32f10x.h #include pin_config.h void Motor_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 方向引脚配置为推挽输出 GPIO_InitStructure.GPIO_Pin MOTOR_LEFT_DIR1 | MOTOR_LEFT_DIR2 | MOTOR_RIGHT_DIR1 | MOTOR_RIGHT_DIR2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); } void Motor_PWM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; OCInitTypeDef OC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM1, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz/(711) 1MHz TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 999; // PWM频率 1MHz/(9991) 1kHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); OC_InitStructure.TIM_OCMode TIM_OCMode_PWM1; OC_InitStructure.TIM_OutputState TIM_OutputState_Enable; OC_InitStructure.TIM_Pulse 0; // 初始占空比0 OC_InitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, OC_InitStructure); // 左轮PWM TIM_OC2Init(TIM1, OC_InitStructure); // 右轮PWM TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须开主输出使能 } void Motor_SetSpeed(int left_speed, int right_speed) { // left_speed/right_speed范围 -1000 ~ 1000负数反转方向 if (left_speed 0) { GPIO_SetBits(GPIOA, MOTOR_LEFT_DIR1); GPIO_ResetBits(GPIOA, MOTOR_LEFT_DIR2); } else { GPIO_ResetBits(GPIOA, MOTOR_LEFT_DIR1); GPIO_SetBits(GPIOA, MOTOR_LEFT_DIR2); left_speed -left_speed; } // 右轮逻辑与左轮相同代码略 TIM_SetCompare1(TIM1, left_speed); TIM_SetCompare2(TIM1, right_speed); }这段代码的逻辑说明PWM频率选择1kHz在电机控制里比较均衡——频率太低电机转动会有一顿一顿的噪声影响低速线性度频率太高驱动芯片的开关损耗会上升而且对定时器分频配置也不友好。占空比直接映射到速度值范围定为0到1000方便后续PID直接在这个量纲上做运算。参数说明TIM_Period设为999而不是1000是因为定时器计数从0开始所以实际脉冲周期为1000个计数单位。TIM_Prescaler分频系数71配合72MHz主频得到1MHz计数频率再除以1000得到1kHz的PWM频率这套参数匹配比较成熟。有一个特别容易忽略的坑TIM1是高级定时器初始化之后必须调用TIM_CtrlPWMOutputs使能主输出否则PWM波形根本不会出现在引脚上。这个问题在调试时非常隐蔽因为代码编译运行都不报错只有用示波器或逻辑分析仪才能发现引脚一直为低电平。3.2 编码器捕获与速度计算编码器测速的底层原理是利用定时器的编码器模式硬件自动根据A、B两相脉冲的相位关系判断旋转方向并计数。这样做的好处是CPU完全不需要参与脉冲计数过程计数器的值由硬件累加CPU只在固定时间间隔去读取一次计数值然后换算成实际转速。// encoder.c - 编码器模式初始化与速度读取 #include stm32f10x.h #include pin_config.h int16_t last_count_left 0; int16_t last_count_right 0; float speed_left 0.0f; float speed_right 0.0f; void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3, ENABLE); // 左轮编码器PA6/PA7 - TIM3_CH1/CH2 // 右轮编码器PB6/PB7 - TIM4_CH1/CH2部分芯片映射 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Pin ENC_LEFT_A | ENC_LEFT_B; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler 0; // 编码器模式不分频 TIM_TimeBaseStructure.TIM_Period 65535; // 16位计数上限自然溢出 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM3, TIM_ICInitStructure); // TIM_Channel_2 配置与上面类似代码略 TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM3, ENABLE); } void Speed_Calculate(uint16_t dt_ms) { int16_t count_left TIM3-CNT; int16_t delta_left count_left - last_count_left; last_count_left count_left; // 减速比1:30单圈脉冲数13转换为mm/s // 轮胎直径65mm周长约204mm // 每脉冲对应距离 204 / (13*30*4) ≈ 0.13mm speed_left (float)delta_left * 0.13f / (float)dt_ms * 1000.0f; }代码逻辑说明编码器接口模式下定时器的计数器值会随着电机转动自动增减正转加、反转减。读取两次读数之间的差值再乘以每个脉冲对应的物理距离就得到这一段时间内的平均速度。参数说明TIM_ICFilter设为0x0F是为了给输入信号加一个滤波窗口防止电机换向产生的毛刺信号被误计为有效脉冲——这个在PWM占空比较大时尤其重要电机电流突变会产生干扰。dt_ms是两次计算之间的时间间隔通常和主循环周期保持一致保证速度数据的时效性。一个踩过的坑是编码器计数器和PID用的是同一个定时器中断如果PID运行频率高于编码器读取频率速度反馈会滞后一拍导致系统看起来在震荡。解决办法是把编码器读取和速度计算放在同一个10毫秒定时器中断里PID也以同样的周期执行确保数据同步。3.3 增量式PID在C语言里的落地速度环和方向环必然是PID控的这是智能小车的核心逻辑。位置式PID需要累计所有历史误差容易积分饱和增量式PID只输出最近几次误差的差值计算量小且天然抗积分饱和更适合单片机场景。增量式的输出量是本次控制量的增量最终的控制量需要在外部累加。// pid.c - 增量式PID实现 #include pid.h typedef struct { float Kp; // 比例系数 float Ki; // 积分系数 float Kd; // 微分系数 float target; // 目标值 float error_last; // 上一次误差 float error_prev; // 上上次误差 float output; // 当前输出 float output_max; // 输出限幅 float integral_limit; // 积分分离阈值 } PID_TypeDef; void PID_Init(PID_TypeDef *pid, float Kp, float Ki, float Kd, float output_max) { pid-Kp Kp; pid-Ki Ki; pid-Kd Kd; pid-output_max output_max; pid-error_last 0; pid-error_prev 0; pid-output 0; } float PID_Update(PID_TypeDef *pid, float current) { float error pid-target - current; // 积分分离误差过大时关闭积分项防止积分饱和 if (fabs(error) pid-integral_limit) { pid-Ki 0.0f; } float p_term pid-Kp * (error - pid-error_last); float i_term pid-Ki * error; float d_term pid-Kd * (error - 2 * pid-error_last pid-error_prev); pid-output p_term i_term d_term; // 输出限幅 if (pid-output pid-output_max) pid-output pid-output_max; else if (pid-output -pid-output_max) pid-output -pid-output_max; pid-error_prev pid-error_last; pid-error_last error; return pid-output; }代码逻辑说明增量式PID的三个分量分别对应——比例项Kp*(error - error_last)表示误差的变化趋势积分项Ki*error用于消除稳态误差微分项Kd*(error - 2*error_last error_prev)表示误差变化加速度对抖动有抑制作用。输出在外部累加这样即使长时间达不到目标输出也只会缓慢爬升而不至于直接冲到限幅值。参数说明integral_limit这个阈值是经验值通常设为目标值的20%左右例如目标车速500mm/s时阈值设为100误差超过100就切断积分避免上坡或大弯道时积分疯狂累加。PID调参的落地步骤我后面会专门讲这里先说一个重要的认知Kp调响应速度Ki调稳态精度Kd调阻尼。Kp太大小车会高频震荡Ki太大低速时会来回冲Kd太大转向会迟钝甚至反向。实际调参顺序永远是先Kp再Kd最后微调Ki不要三者同时动。4. 感知与决策灰度寻迹、超声波避障与状态机4.1 灰度传感器阵列的数据采集与边界判断灰度传感器的原理是不同颜色表面对红外光的反射率不同传感器模块输出模拟电压或数字电平。模拟输出的传感器更灵活因为你可以通过ADC读取具体的电压值来判断当前灰度等级而不是只有黑和白两个离散状态。电赛赛题里赛道背景和线条颜色通常是给定的比如白底黑线或蓝底白线你需要在现场根据实际传感器返回值重新标定阈值而不能用固定的出厂值。// gray_sensor.c - 五路灰度传感器读取与线位置计算 #include stm32f10x.h #include pin_config.h // 传感器结构体原始值 二值化结果 typedef struct { uint16_t raw[5]; uint8_t bin[5]; uint16_t threshold[5]; } GraySensor_TypeDef; GraySensor_TypeDef gray; // 读取五路传感器模拟电压值 void Gray_ReadRaw(void) { for (int i 0; i 5; i) { // 实际使用ADC多通道扫描这里省略ADC配置细节 gray.raw[i] ADC_GetConversionValue(ADC1); } } // 二值化与线位置提取 // 返回 -2~2正值表示线偏右负值表示线偏左0表示线居中 int Gray_GetLinePosition(void) { int weight_sum 0; int bin_sum 0; for (int i 0; i 5; i) { gray.bin[i] (gray.raw[i] gray.threshold[i]) ? 1 : 0; weight_sum gray.bin[i] * (i - 2); bin_sum gray.bin[i]; } if (bin_sum 0) { // 全黑或全白保留上一次位置进入丢线状态 return GRAY_LOST_LINE; } // 位置 加权和 / 激活传感器数量归一化到 -2~2 return (weight_sum * 2) / bin_sum; }代码逻辑说明Gray_GetLinePosition用加权平均法计算线的位置偏移中间传感器的权重为0左边为负右边为正激活数量作为分母归一化。这样即使不同路段激活的传感器数量不同位置值依然有可比性。GRAY_LOST_LINE是一个特殊返回码表示传感器阵列完全没检测到线这时候小车不能按正常循迹逻辑走需要切换到搜索模式。参数说明threshold数组需要现场标定标定方式很简单——把小车放在纯白背景上读一次原始值放在黑线上读一次原始值取两者中点作为阈值。这个操作现场用串口打印数值做一遍只需要两分钟但能避免后续大量调参时间。另外一个实用技巧左右两端的传感器阈值可以单独调低一点灵敏度因为在弯道处线可能只进入传感器视野的一部分灵敏度太低会过早丢线。4.2 超声波避障模块与测距滤波超声波测距的基本原理是发射声波脉冲测量回波返回的时间乘以声速再除以2得到距离。这个原理不难难的是声波会被环境噪声和多重反射干扰。电赛现场经常有多个队伍同时调试超声波串扰严重表现为距离值突然跳变到几米又跳回来。解决思路有两个方向一是给发射信号编码发一串特定模式的脉冲而不是单个脉冲二是在软件里做滑动窗口滤波或中值滤波。// ultrasonic.c - 超声波测距与中值滤波 #include stm32f10x.h #include pin_config.h #define SONIC_SPEED_MM_US 0.34f // 声速 mm/us约340m/s #define MAX_DISTANCE_MM 4000 #define FILTER_WINDOW 5 uint16_t dist_history[FILTER_WINDOW]; uint8_t dist_index 0; // 发送触发脉冲测量回波高电平持续时间 uint16_t Ultrasonic_ReadRaw(void) { GPIO_SetBits(GPIOB, ULTRASONIC_TRIG); Delay_us(20); GPIO_ResetBits(GPIOB, ULTRASONIC_TRIG); // 等待ECHO变高超时返回0 uint16_t timeout 0; while (!GPIO_ReadInputDataBit(GPIOB, ULTRASONIC_ECHO) timeout 10000) timeout; if (timeout 10000) return 0; // 测量高电平持续时间 uint16_t duration 0; while (GPIO_ReadInputDataBit(GPIOB, ULTRASONIC_ECHO) duration 30000) { Delay_us(1); duration; } return duration * SONIC_SPEED_MM_US; } // 中值滤波消除单点异常值 uint16_t Ultrasonic_Filtered(void) { uint16_t window[FILTER_WINDOW]; for (int i 0; i FILTER_WINDOW; i) { window[i] dist_history[(dist_index i) % FILTER_WINDOW]; } // 简单冒泡排序取中值 for (int i 0; i FILTER_WINDOW - 1; i) { for (int j i 1; j FILTER_WINDOW; j) { if (window[i] window[j]) { uint16_t temp window[i]; window[i] window[j]; window[j] temp; } } } return window[FILTER_WINDOW / 2]; }代码逻辑说明Ultrasonic_ReadRaw的核心瓶颈在于Delay_us(1)是软件延时占用CPU时间所以超声波测距不适合和PWM控制放在同一高频中断里执行。常见做法是把超声波测距放到主循环里以50Hz的频率调用或者在定时器中断里标记一个“需要测距”的标志位主循环看到标志位再执行。中值滤波的实现思路是保留最近5次测量值排序后取中位数这样即使某次测量被噪声污染也不会对决策产生剧烈影响。参数说明MAX_DISTANCE_MM设为4000是因为电赛赛道的避障距离一般在20到50厘米之间超出这个范围认为障碍物不存在避免远处无关物体触发误避障。FILTER_WINDOW窗口大小需要权衡太大响应变慢太小滤波效果差5是一个比较折中的值。4.3 状态机从单任务到多任务切换有了循迹和避障两个功能就必须引入状态机来管理它们。不用状态机的做法是写一堆if else嵌套代码一多就变成意大利面条改一个逻辑要翻半天。状态机的核心思想是任何时候小车处于一个明确的状态只有特定条件满足时才能切换到另一个状态。对智能小车来说典型的状态包括循迹、避障、搜索、停止。// fsm.c - 小车运行状态机 typedef enum { STATE_LINE_FOLLOW, // 循迹模式 STATE_OBSTACLE_AVOID, // 避障模式 STATE_SEARCH, // 丢线搜索模式 STATE_STOP // 停止模式 } CarState_TypeDef; CarState_TypeDef current_state STATE_LINE_FOLLOW; void FSM_Update(GraySensor_TypeDef *gray, uint16_t distance) { switch (current_state) { case STATE_LINE_FOLLOW: // 前方障碍物小于30cm切换到避障 if (distance 300) { current_state STATE_OBSTACLE_AVOID; } // 丢线超过500ms切换到搜索 else if (gray-lost_time 500) { current_state STATE_SEARCH; } break; case STATE_OBSTACLE_AVOID: // 障碍物已通过且重新找到线回到循迹 if (distance 500 gray-line_found) { current_state STATE_LINE_FOLLOW; } break; case STATE_SEARCH: // 搜索到线立即回循迹 if (gray-line_found) { current_state STATE_LINE_FOLLOW; } break; case STATE_STOP: // 手动恢复或外部指令唤醒 break; } }状态机的逻辑说明每个状态只看自己关心的条件循迹状态关注“什么时候该去避障”和“什么时候丢线”避障状态关注“障碍是否已通过”搜索状态关注“线是否重新出现”。这样做的好处是状态切换的条件清晰不会出现两个逻辑互相打架。参数说明distance 300是避障触发阈值distance 500是解除阈值两个阈值之间留了200mm的滞回区间作用是防止小车在障碍物边缘反复横跳。这个滞回的设计非常关键如果没有它小车会在障碍物边缘来回切换状态表现为车身抖动然后停在原地。5. 调试避坑指南传感器、电机、PID三大高频翻车点5.1 现象PWM输出正常但电机不转有段时间我调试一台车示波器看PWM波形完全正常占空比也是预期值但电机就是纹丝不动。检查了供电、接线、驱动板使能脚最后发现是方向引脚配置出了问题。某些电机驱动板的使能脚是独立引脚需要额外拉高不能只靠PWM信号驱动。具体来说我用的驱动板要求EN引脚为高电平而初始化代码漏掉了这个引脚的配置导致驱动芯片内部输出级完全关闭。解决方法是确认驱动板的数据手册把所有使能引脚加入初始化函数用一个宏定义统一管理。从那以后我检查电机不转的固定顺序是——先量驱动板供电再量EN引脚电平最后量PWM引脚波形。这三步能覆盖大约九成的不转问题。5.2 现象PID调节不收敛小车原地高频抖动PID调参时最常遇到的情况是Kp调大之后小车开始左右高频摆动声音听起来像在颤抖。这不是电机问题是比例增益太大导致过冲每轮控制都在朝目标值猛冲然后越过形成周期振荡。我见过有人调了一个下午Kp越加越大以为加大能更快到达目标实际越调越糟。解决方法是先把Kp降回原来的一半然后观察小车是否稳定稳定后再逐步加Kd增加阻尼Kd可以抑制过冲让系统更快收敛。Kp和Kd要配合着调不能只动一个。一个参考策略是Kp每次增加20%Kd每次增加40%到60%同时观察运动轨迹直到临界稳定后再把两个值都乘以0.8留出余量。5.3 现象巡线小车高速冲出赛道小车在直道上跑得很好一进弯道就飞出去这是循迹类小车最常见的问题。原因往往是控制频率跟不上弯道信息变化。灰度传感器的布局决定了前瞻距离前瞻越短越晚发现弯道留给控制系统的反应时间越少。当车速超过某个值后感知延迟和控制延迟叠加小车根本来不及转弯。解决思路是两个方向并行一是程序上把控制周期从20ms缩短到10ms让每个控制周期内速度变化更小转向更平滑二是物理上调整传感器支架让灰度阵列更靠前增加前瞻距离。如果硬件不方便改就只能降低入弯速度在检测到左右传感器激活时立即设置一个限速标志限制最大车速。5.4 现象超声波距离值跳变避障乱触发超声波数据在电赛现场特别容易受干扰。一次调试中小车距离障碍物还有40cm超声波却返回了120cm的值避障完全没触发直接撞上了障碍物。排查后发现是多台小车同时工作声波串扰导致测距错误。单次测量不可靠必须做滤波这是铁律。解决方法是做了两级防护软件上使用中值滤波加滑动平均的组合——先取5次测量值的中位数再对连续3个中位数取平均。硬件上在超声波模块周围加了一层海绵包裹减少侧面声波干扰。这样处理之后距离数据基本稳定在±2cm以内满足避障要求。6. 参数整定与实跑验证让小车稳下来的三个习惯6.1 速度环和方向环分开调不要同时动很多同学喜欢在车没跑起来的时候同时调速度PID和转向PID结果车又抖又偏完全无从下手。我的习惯是先在直道上把速度环调稳把Kp、Ki、Kd三个值确定下来让小车直行时速度波动控制在±5%以内。速度环稳定后再切换到转向调试。转向环的输入不再是固定的速度差而是灰度传感器计算出的线位置偏差输出是左右轮的附加速度补偿。转向环调参时只用直线段看小车能否走直然后增加一个小弯道看转向响应是否及时最后才上大弯和连续弯。6.2 每组参数都做记录形成调参表调参最忌讳的是拍脑袋改参数不记录结果。我以前的教训是一次调参花了一个多小时感觉找到了合适的参数结果重启后忘了保存全部白调。后来我强制自己每调一组参数就填一张表记录Kp、Ki、Kd、车速、弯道表现、直道表现和备注。这张表看起来原始但积累了几十组数据后你能总结出规律——比如弯道飞线时应该优先加大Kd还是降车速不同速度段的最佳Kp范围是多少。6.3 实跑验证的固定套路参数确定后不要直接跑全场按三段式验证。第一段是直道往返验证速度稳定性和两轮同步性记录实际跑完同一段路的时间连续三次误差应在正负2%以内。第二段是S弯和直角弯组合验证转向响应和丢线恢复能力如果丢线后重新找到线的时间超过1秒说明搜索策略太保守。第三段才是带障碍物的完整赛道验证状态机切换的可靠性。每次验证后回到电脑前看串口打印的传感器数据和状态切换记录这比盯着车跑更直观。能做到这三条小车基本具备稳定完成一条电赛赛道的底子了。从那以后我每次调完参数都会强制走一遍这三步哪怕只是改了0.1的Kp也不会偷懒跳过实跑验证。这个习惯帮我在比赛现场省下了大量时间——现场一般只有一到两小时测试机会没有平时积累的调参表和验证流程基本就是看运气。希望这份拆解和这些踩过的坑能让你在调车路上少走一段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?