STM32电磁循迹小车实现

该文章已生成可运行项目,
AI助手已提取文章相关产品:

STM32电磁循迹小车完整实现:从原理到代码的深度实践

在智能车竞赛和嵌入式教学项目中,如何让一辆小车“看得见”赛道并稳定行驶,始终是一个核心挑战。传统的红外循迹方案虽然简单易行,但极易受到环境光、地面反光或污渍干扰,导致误判甚至失控。有没有一种方式能彻底摆脱这些光学限制?答案是肯定的—— 电磁循迹

通过检测埋设于赛道中的交变电流导线所产生的磁场,电磁循迹技术实现了对路径的非视觉感知。这种方案不受光照影响、信号稳定、抗干扰能力强,已成为全国大学生智能车竞赛等高水平赛事的主流选择。而以STM32F103为代表的微控制器,凭借其强大的外设集成能力与实时处理性能,成为构建此类系统的理想平台。

本文将带你深入剖析一个功能完备的STM32电磁循迹小车系统,涵盖从传感器采集、电机控制、编码测距到蓝牙遥控的全链路设计,并提供一套经过验证的完整代码框架。更重要的是,我们将打破常规的技术堆砌式讲解,转而以工程思维贯穿始终——每一个模块的设计背后,都有实际问题驱动;每一行代码的实现,都源于真实场景下的调试经验。


我们从小车最核心的能力开始:感知赛道位置。这依赖于一组对称布置的电感线圈。当赛道导线下方通有约20kHz的交流电时,会在周围空间激发交变磁场。车体前端安装的五个电感(L1~L5)会因相对位置不同而感应出不同的电压幅值。中间线圈(L3)正对导线时信号最强,向左或向右偏离则对应侧的感应电压升高。这一物理特性为我们提供了连续的位置反馈。

为了准确捕捉这些微弱信号(通常仅几毫伏),必须经过两级运放放大和带通滤波调理,最终送入STM32的ADC引脚进行模数转换。这里选用STM32F103C8T6内置的12位ADC,参考电压为3.3V,理论分辨率达到约0.8mV/LSB,足以满足精度需求。

关键在于多通道高效采样。若采用轮询方式逐个读取五路ADC值,不仅耗时且占用CPU资源。因此,我们启用ADC1的扫描模式配合DMA传输,实现一次启动后自动完成五通道连续采集,结果直接存入内存数组,极大提升了系统响应速度与稳定性。

void ADC_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);

    // 配置PA0-PA4为模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;        // 扫描模式
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 5;
    ADC_Init(ADC1, &ADC_InitStructure);

    // 设置每个通道的采样时间
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 5, ADC_SampleTime_55Cycles5);

    ADC_Cmd(ADC1, ENABLE);
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

这段初始化代码看似标准,但在实际调试中你会发现几个容易忽略的关键点:首先是采样时间的选择。由于传感器输出阻抗较高,若设置过短的采样周期(如7.5 cycles),会导致充电不足,引入显著误差。实践中推荐使用55.5周期以上,确保ADC输入电容充分充电。其次是校准流程不可跳过——尤其是在更换供电电源或温度变化较大时,重新校准能有效消除偏移误差。

有了数据输入,下一步就是驱动小车运动。我们采用TIM3定时器生成两路PWM信号,分别控制左右轮电机的速度。驱动芯片可选L298N或更高效的TB6612FNG,前者逻辑清晰但发热严重,后者支持更高频率且具备刹车功能,更适合长时间运行。

PWM频率设定尤为关键。太低会产生明显噪音(人耳可闻的“滋滋”声),太高则可能超出驱动芯片响应范围。经实测,10kHz是一个不错的平衡点:既能避开听觉敏感区,又保证MOSFET开关损耗可控。ARR值设为999,即周期为1ms,这样占空比每增加1%,对应比较寄存器加10,便于计算映射。

void TIM3_PWM_Init(u16 arr, u16 psc) {
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

    // PA6, PA7 -> TIM3_CH1, TIM3_CH2
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    TIM_TimeBaseStructure.TIM_Period = arr;
    TIM_TimeBaseStructure.TIM_Prescaler = psc;
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM3, &TIM_OCInitStructure);
    TIM_OC2Init(TIM3, &TIM_OCInitStructure);

    TIM_CtrlPWMOutputs(TIM3, ENABLE);
    TIM_Cmd(TIM3, ENABLE);
}

void Set_Motor_Speed(int left, int right) {
    if (left < 0) left = 0; else if (left > 100) left = 100;
    if (right < 0) right = 0; else if (right > 100) right = 100;

    TIM_SetCompare1(TIM3, left * (ARR_VALUE / 100));
    TIM_SetCompare2(TIM3, right * (ARR_VALUE / 100));
}

注意 Set_Motor_Speed 函数中的边界检查,这是防止异常参数导致硬件损坏的基本防护措施。此外,在实际部署中建议加入软启动机制——即从零速逐步加速至目标值,避免瞬间大电流冲击电机绕组。

现在的问题是:如何知道小车跑了多远?是否已完成预设任务?这就需要引入编码器。我们在左轮轴上安装了一个增量式光电编码器(例如PPR=360),通过A/B相输出脉冲来判断旋转方向和角度。每当A相上升沿到来时触发外部中断,在ISR中读取B相电平决定计数增减。

有人可能会问:为什么不使用定时器的编码器接口模式?确实,STM32支持硬件解码,但考虑到主控资源紧张(同时运行ADC、PWM、UART等),且本系统对编码器分辨率要求不高,采用外部中断+软件判向的方式更为灵活可控。

volatile long encoder_left_count = 0;

void EXTI9_5_IRQHandler(void) {
    if (EXTI_GetITStatus(EXTI_Line8) != RESET) {
        if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_9))
            encoder_left_count++;
        else
            encoder_left_count--;
        EXTI_ClearITPendingBit(EXTI_Line8);
    }
}

int Check_Completed_Laps(void) {
    long total_pulses = encoder_left_count;
    int laps_done = total_pulses / PULSES_PER_LAP;
    return (laps_done >= TARGET_LAPS);
}

这里有一个隐藏陷阱:中断服务程序执行时间过长可能导致漏脉冲。因此务必保持ISR极简,不做任何延时或复杂运算。另外,PULSES_PER_LAP需根据实际赛道周长标定。例如,若编码器每转输出360脉冲,轮胎周长为20cm,赛道一圈约4米,则每圈对应约7200个脉冲(400 / 0.2 * 360)。这个数值必须通过实地测试修正,否则“定圈停止”将成为一句空话。

为了让操作更加便捷,我们集成了HC-05蓝牙模块,通过串口与手机APP通信。用户无需靠近小车即可发送指令,如启停、切换模式、手动控制方向等。相比物理按键,这种方式极大简化了调试流程。

char rx_data = 0;
_Bool auto_mode = 0;

void USART2_IRQHandler(void) {
    if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) {
        rx_data = USART_ReceiveData(USART2);

        switch(rx_data) {
            case 'F': auto_mode = 0; Set_Motor_Speed(60, 60); break;
            case 'B': auto_mode = 0; Set_Motor_Speed(40, 40); break;
            case 'L': auto_mode = 0; Set_Motor_Speed(30, 60); break;
            case 'R': auto_mode = 0; Set_Motor_Speed(60, 30); break;
            case 'S': Set_Motor_Speed(0, 0); break;
            case 'T': auto_mode = 1; break;
            default: break;
        }
        USART_ClearITPendingBit(USART2, USART_IT_RXNE);
    }
}

值得注意的是,一旦进入自动循迹模式(收到’T’命令),主循环应优先执行循迹算法而非等待新指令。这意味着我们需要在接收到’T’后仅设置标志位,具体动作由主控逻辑决定。这样的设计分离使得系统状态更清晰,也避免了中断中调用复杂函数带来的不确定性。

整个系统的协同工作依赖于合理的主控架构。上电后依次初始化各外设,进入主循环等待蓝牙命令。一旦开启自动模式,便以固定周期(如10ms)读取ADC数据,经归一化处理后计算偏差值。常见的做法是使用五点定位法:

int get_deviation(int *adc_val) {
    int max_index = 0;
    for(int i=1; i<5; i++)
        if(adc_val[i] > adc_val[max_index]) max_index = i;

    switch(max_index) {
        case 0: return -2;
        case 1: return -1;
        case 2: return 0;
        case 3: return 1;
        case 4: return 2;
        default: return 0;
    }
}

该方法虽简单,但在曲率较大的弯道会出现跳跃式偏差跳变。进阶方案可采用加权平均法,例如 (val[4]*2 + val[3] - val[1] - val[0]*2) / (sum) ,得到更平滑的连续输出,利于后续PD/PID控制算法稳定调节差速。

电源设计同样是成败关键。建议使用LM2596将7.4V锂电池降至5V供给传感器和蓝牙模块,再经AMS1117稳压至3.3V供MCU使用。切勿共地不良或共用线径过细的导线,否则电机启停时的大电流波动会引发ADC基准漂移,造成误判。

最后别忘了安全机制。长时间未检测到有效电磁信号时,应判定为脱轨并自动停车,防止飞车撞毁。可通过设置超时计数器实现:“若连续N次采样最大值低于阈值,则进入保护模式”。

这种高度集成的设计思路,不仅解决了传统循迹方案的痛点,也为未来扩展打下坚实基础——无论是加入OLED显示实时参数,还是升级为RTOS任务调度,亦或是融合陀螺仪实现姿态补偿,皆可在此框架上从容演进。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

本文章已经生成可运行项目

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值