1. 项目背景与核心价值

智能物流小车听起来高大上,但说白了就是让一个小车自己认路、送货、避障,完全不用人管。我在工厂里第一次看到这种小车时就被迷住了——它们像有生命一样在车间里穿梭,精准地把零件送到每个工位。后来我自己动手做了一台,才发现背后的技术其实没那么神秘。

这个项目最吸引人的地方在于它的实用性。不管是小型仓库、电子厂还是实验室,都能用上这种自主导航小车。我做的这个基于STM32的智能物流小车,核心就是让几种传感器协同工作:红外传感器负责循着黑线走,超声波传感器避开路上的障碍物,摄像头识别二维码确定送货地点。最难的不是单个传感器的使用,而是让它们配合得天衣无缝。

你可能觉得这么多技术堆在一起会很复杂,但其实STM32芯片已经帮我们处理了大部分脏活累活。我选择的是STM32F4系列,它的运算能力足够强,而且外设接口丰富,接各种传感器都很方便。最重要的是,整个项目的成本可以控制在500元以内,比买现成的物流小车便宜太多了。

2. 硬件系统设计与选型要点

做硬件选型时我踩过不少坑,最后总结出一套性价比最高的方案。主控芯片用STM32F407,价格适中性能足够。电机驱动模块推荐L298N,虽然效率不是最高,但特别皮实耐用了,新手不容易烧模块。千万别贪便宜买那些没品牌的驱动模块,我已经烧坏过三个了。

传感器配置是关键中的关键。红外循迹模块我建议用四路以上的,左右各两个,这样识别路径更准确。超声波避障模块要注意探测角度,最好选择15度左右的小角度模块,避免误检测。摄像头模块用普通的30万像素OV7670就够用了,主要是用来识别二维码,不需要太高清。

电源系统很多人会忽略,但其实特别重要。我最初用9V电池,结果电机一启动电压就骤降,导致单片机重启。后来改成两套电源系统:一套7.4V锂电池给电机供电,另一套降压到5V给控制板供电,问题就解决了。这个小细节花了我两天时间才排查出来,所以提前告诉你们少走弯路。

接线时记得给电机驱动加上滤波电容,我在电机引脚处加了0.1uF的瓷片电容和100uF的电解电容,有效消除了电机干扰。所有信号线最好用双绞线,特别是超声波模块的接线,这样可以减少外部干扰。

3. 软件开发环境搭建

软件开发环境我推荐用STM32CubeIDE,它把STM32CubeMX和IDE整合在一起,配置引脚和生成代码特别方便。安装时记得勾选HAL库,虽然有些人说寄存器操作效率更高,但对新手来说HAL库更容易上手。

新建工程时选择正确的芯片型号,我用的STM32F407VET6,时钟树配置到168MHz主频刚刚好。外设配置要仔细:USART1用于摄像头通信,I2C1接红外传感器,TIM1生成PWM控制电机。GPIO口配置要注意超声波模块的Trig引脚设为推挽输出,Echo引脚设为浮空输入。

调试工具必不可少,ST-Link V2既便宜又好用。接线时注意SWDIO和SWCLK不要接反,我第一次调试时接反了,找了半天问题。调试printf重定向到串口,这样就能实时打印调试信息了。

编译前在工程设置里勾选C99模式,不然有些语法会报错。优化等级建议先用-O0,调试没问题后再改成-O1。如果代码量大可以启用FPU,STM32F4有硬件浮点单元,处理传感器数据时能快很多。

4. 红外循迹模块实战

红外循迹的原理其实很简单,就是利用黑色吸收红外线、白色反射红外线的特性。但实际调试时你会发现很多坑,比如环境光太强会影响检测,地面反光也会造成误判。

我的解决方案是给红外传感器加上遮光罩,用热缩管把发射和接收管包起来,只留前面一个小孔。这样环境光的影响就小多了。另一个技巧是动态阈值调整,不要用固定的检测阈值,而是根据环境光自动调整。

代码实现时我用了状态机模式,这样逻辑更清晰:

#define TRACK_SENSOR_NUM 4
uint8_t track_sensor_values[TRACK_SENSOR_NUM];

void track_sensor_read(void)
{
    for(int i=0; i<TRACK_SENSOR_NUM; i++){
        track_sensor_values[i] = HAL_GPIO_ReadPin(sensor_port[i], sensor_pin[i]);
    }
}

void track_line_following(void)
{
    track_sensor_read();
    
    if(track_sensor_values[0]==0 && track_sensor_values[3]==1){
        // 左偏,向右微调
        motor_adjust(0.8, 1.0);
    }
    else if(track_sensor_values[0]==1 && track_sensor_values[3]==0){
        // 右偏,向左微调
        motor_adjust(1.0, 0.8);
    }
    else if(track_sensor_values[1]==0 || track_sensor_values[2]==0){
        // 在轨道上,直行
        motor_adjust(1.0, 1.0);
    }
    else{
        // 丢失轨道,停止
        motor_stop();
    }
}

实际测试时发现小车会左右摇摆,这是典型的P参数过大现象。后来我加入了PID控制,调整了微分参数,小车运行就平稳多了。建议先用P控制,慢慢加入I和D参数,不要一开始就用完整的PID。

5. 超声波避障算法优化

超声波避障听起来简单,但要做到稳定可靠需要不少技巧。我最开始用的方法是检测到障碍物就立即停止,结果小车动不动就急刹,运行起来一顿一顿的。

后来改成了分级预警机制:当检测到50cm内有障碍物时减速,30cm内转向,15cm内紧急停止。这样运行就平滑多了。超声波测距的代码要注意增加滤波处理,因为偶尔会有误检测:

#define ULTRASONIC_NUM 3
float ultrasonic_distances[ULTRASONIC_NUM];

float ultrasonic_filter(uint8_t sensor_id)
{
    static float filter_buf[ULTRASONIC_NUM][5];
    static uint8_t index[ULTRASONIC_NUM] = {0};
    
    // 新测量值放入滤波器
    filter_buf[sensor_id][index[sensor_id]] = measure_distance(sensor_id);
    index[sensor_id] = (index[sensor_id] + 1) % 5;
    
    // 中值滤波
    float temp[5];
    memcpy(temp, filter_buf[sensor_id], sizeof(temp));
    
    // 排序取中值
    for(int i=0; i<4; i++){
        for(int j=i+1; j<5; j++){
            if(temp[i] > temp[j]){
                float tmp = temp[i];
                temp[i] = temp[j];
                temp[j] = tmp;
            }
        }
    }
    return temp[2];
}

void obstacle_avoidance(void)
{
    for(int i=0; i<ULTRASONIC_NUM; i++){
        ultrasonic_distances[i] = ultrasonic_filter(i);
    }
    
    if(ultrasonic_distances[0] < 15.0){
        // 正前方紧急障碍
        emergency_stop();
    }
    else if(ultrasonic_distances[0] < 30.0){
        // 前方有障碍,选择转向方向
        if(ultrasonic_distances[1] > ultrasonic_distances[2]){
            turn_left();
        }else{
            turn_right();
        }
    }
    else if(ultrasonic_distances[0] < 50.0){
        // 减速慢行
        set_speed(0.5);
    }
}

超声波模块的安装位置也很重要。我最初装在车头正中间,结果小车经常斜着撞上障碍物。后来改成左中右三个模块,覆盖120度范围,避障效果就好多了。

6. 摄像头二维码识别实现

二维码识别是整个系统中最吃计算资源的环节。STM32F4虽然性能不错,但直接处理图像还是很吃力。我的解决方案是先用摄像头自带的识别功能,只让单片机处理识别结果。

OV7670摄像头需要配置为QQVGA格式(160x120),这样处理速度够快。通过UART接收摄像头模块已经处理好的二维码数据:

typedef struct {
    uint8_t data[32];
    uint8_t length;
    uint8_t valid;
} qrcode_data_t;

qrcode_data_t current_qrcode;

void qrcode_task(void)
{
    static uint8_t rx_buf[64];
    static uint8_t rx_index = 0;
    
    while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)){
        uint8_t ch = huart1.Instance->DR;
        
        if(ch == 0xFF){
            // 帧头
            rx_index = 0;
        }
        else if(rx_index < sizeof(rx_buf)){
            rx_buf[rx_index++] = ch;
            
            if(rx_index >= 2 && rx_buf[rx_index-1] == 0xFE){
                // 帧尾,解析数据
                parse_qrcode(rx_buf, rx_index-1);
                rx_index = 0;
            }
        }
    }
}

void parse_qrcode(uint8_t *data, uint8_t len)
{
    if(len < 3) return;
    
    // 校验和验证
    uint8_t checksum = 0;
    for(int i=0; i<len-1; i++){
        checksum ^= data[i];
    }
    
    if(checksum == data[len-1]){
        memcpy(current_qrcode.data, data, len-1);
        current_qrcode.length = len-1;
        current_qrcode.valid = 1;
    }
}

二维码内容格式我自定义了一套简单协议:前两个字节表示目的地编号,后面是校验信息。比如"01A5"表示送往1号站台。这样解析起来简单,不容易出错。

光照条件对识别率影响很大。我加了两个LED补光灯,在光线暗的时候自动开启。识别距离控制在10-30cm之间,太远了识别率会下降。

7. 多传感器数据融合策略

单个传感器再准确也有局限,多传感器融合才是王道。但融合算法不能太复杂,STM32会算不过来。我用的方法是加权投票机制,简单又有效。

首先给每个传感器分配可信度权重:红外循迹最可靠权重0.5,超声波避障权重0.3,摄像头定位权重0.2。然后根据各传感器的输出做决策:

typedef struct {
    float track_weight;    // 循迹权重
    float ultrasonic_weight; // 避障权重
    float camera_weight;   // 视觉权重
    
    uint8_t track_direction; // 循迹建议方向
    uint8_t ultrasonic_direction; // 避障建议方向
    uint8_t camera_direction; // 视觉建议方向
} sensor_fusion_t;

uint8_t decision_making(sensor_fusion_t *fusion)
{
    float score[3] = {0}; // 0:左转 1:直行 2:右转
    
    // 循迹输入
    switch(fusion->track_direction){
        case TURN_LEFT: score[0] += fusion->track_weight; break;
        case GO_STRAIGHT: score[1] += fusion->track_weight; break;
        case TURN_RIGHT: score[2] += fusion->track_weight; break;
    }
    
    // 避障输入
    switch(fusion->ultrasonic_direction){
        case TURN_LEFT: score[0] += fusion->ultrasonic_weight; break;
        case GO_STRAIGHT: score[1] += fusion->ultrasonic_weight; break;
        case TURN_RIGHT: score[2] += fusion->ultrasonic_weight; break;
    }
    
    // 视觉输入
    switch(fusion->camera_direction){
        case TURN_LEFT: score[0] += fusion->camera_weight; break;
        case GO_STRAIGHT: score[1] += fusion->camera_weight; break;
        case TURN_RIGHT: score[2] += fusion->camera_weight; break;
    }
    
    // 找出最高分
    uint8_t decision = GO_STRAIGHT;
    float max_score = score[1];
    
    if(score[0] > max_score){
        max_score = score[0];
        decision = TURN_LEFT;
    }
    if(score[2] > max_score){
        decision = TURN_RIGHT;
    }
    
    return decision;
}

遇到传感器冲突时,比如红外说要直行但超声波检测到障碍物,这时候优先级别最高的传感器有一票否决权。我把紧急避障的优先级设为最高,任何时候检测到近距离障碍都优先处理。

数据融合的频率也很重要。红外传感器响应最快,每10ms读取一次;超声波每50ms更新一次;摄像头每100ms处理一帧。这样分配系统资源,既能保证实时性又不会过载。

8. 电机控制与PID调参

电机控制是决定小车运行品质的关键。我用的是PWM控制直流电机,通过调节占空比来控制速度。但直流电机的非线性特性很明显,需要PID算法来补偿。

先实现一个简单的PWM输出函数:

#define MOTOR_LEFT_TIM_CHANNEL TIM_CHANNEL_1
#define MOTOR_RIGHT_TIM_CHANNEL TIM_CHANNEL_2

void set_motor_speed(float left_speed, float right_speed)
{
    // 限制速度范围
    left_speed = constrain(left_speed, -1.0, 1.0);
    right_speed = constrain(right_speed, -1.0, 1.0);
    
    // 转换为PWM占空比
    uint32_t left_pwm = (uint32_t)(fabs(left_speed) * 1000);
    uint32_t right_pwm = (uint32_t)(fabs(right_speed) * 1000);
    
    if(left_speed >= 0){
        HAL_GPIO_WritePin(MOTOR_LEFT_DIR_GPIO, MOTOR_LEFT_DIR_PIN, GPIO_PIN_SET);
    }else{
        HAL_GPIO_WritePin(MOTOR_LEFT_DIR_GPIO, MOTOR_LEFT_DIR_PIN, GPIO_PIN_RESET);
    }
    
    if(right_speed >= 0){
        HAL_GPIO_WritePin(MOTOR_RIGHT_DIR_GPIO, MOTOR_RIGHT_DIR_PIN, GPIO_PIN_SET);
    }else{
        HAL_GPIO_WritePin(MOTOR_RIGHT_DIR_GPIO, MOTOR_RIGHT_DIR_PIN, GPIO_PIN_RESET);
    }
    
    __HAL_TIM_SET_COMPARE(&htim1, MOTOR_LEFT_TIM_CHANNEL, left_pwm);
    __HAL_TIM_SET_COMPARE(&htim1, MOTOR_RIGHT_TIM_CHANNEL, right_pwm);
}

然后实现PID控制器:

typedef struct {
    float kp, ki, kd;
    float integral;
    float prev_error;
    uint32_t prev_time;
} pid_controller_t;

pid_controller_t left_pid = {0.8, 0.2, 0.1, 0, 0, 0};
pid_controller_t right_pid = {0.8, 0.2, 0.1, 0, 0, 0};

float pid_update(pid_controller_t *pid, float error)
{
    uint32_t current_time = HAL_GetTick();
    float dt = (current_time - pid->prev_time) / 1000.0;
    if(dt <= 0) dt = 0.01;
    
    // 比例项
    float p_term = pid->kp * error;
    
    // 积分项
    pid->integral += error * dt;
    float i_term = pid->ki * pid->integral;
    
    // 微分项
    float derivative = (error - pid->prev_error) / dt;
    float d_term = pid->kd * derivative;
    
    pid->prev_error = error;
    pid->prev_time = current_time;
    
    return p_term + i_term + d_term;
}

调参是个耐心活。我先调P参数,让小车能大致循迹但不振荡;然后加D参数抑制超调;最后加I参数消除静差。每个参数都要慢慢微调,一次只调一个参数,观察效果后再调整。

9. 系统稳定性优化技巧

稳定性问题往往出现在细节处。电源稳定性是第一关,我在电源入口处加了TVS管和稳压芯片,有效抑制了电压波动。电机干扰是第二关,每个电机都并联了104电容,电源线用了磁环滤波。

软件方面看门狗是必须的,我设置了1秒的超时时间:

IWDG_HandleTypeDef hiwdg;

void iwdg_init(void)
{
    hiwdg.Instance = IWDG;
    hiwdg.Init.Prescaler = IWDG_PRESCALER_32;
    hiwdg.Init.Reload = 1000; // 1秒超时
    HAL_IWDG_Init(&hiwdg);
}

void feed_dog(void)
{
    HAL_IWDG_Refresh(&hiwdg);
}

在主循环中定期喂狗,但如果某个任务执行时间太长,就需要分次喂狗:

void main_loop(void)
{
    while(1){
        feed_dog();
        track_sensor_read();
        
        feed_dog();
        ultrasonic_update();
        
        feed_dog();
        qrcode_process();
        
        feed_dog();
        sensor_fusion();
        
        feed_dog();
        motor_control();
        
        HAL_Delay(10);
    }
}

异常处理也很重要。我为每个模块都设计了超时检测,比如超声波超过100ms没有返回数据就认为故障,自动切换到降级模式。摄像头识别连续失败5次就重启摄像头模块。

10. 实际部署与性能测试

实验室环境理想,但真实环境复杂多了。我在车间测试时遇到了各种问题:地面反光影响红外传感器,金属设备干扰超声波,光照变化影响摄像头识别。

针对这些问题我做了很多优化:给红外传感器加了偏振片减少反光影响,超声波模块增加了温度补偿,摄像头增加了自动曝光控制。

性能测试要系统化。我设计了一套测试流程:先测试直线循迹精度,再测试弯道通过性,然后测试避障反应时间,最后测试整体配送准确率。

测试数据要记录分析:

测试项目预期指标实测结果达标情况
直线循迹误差<2cm1.5cm✓
90度弯通过速度>0.3m/s0.35m/s✓
避障反应时间<0.5s0.3s✓
二维码识别距离10-30cm8-35cm△
连续运行时间>4小时5小时✓

从测试结果看,二维码识别距离范围需要优化。我调整了摄像头焦距和识别算法,最终达到了10-30cm的稳定识别范围。

长期运行测试发现了电源管理的问题。小车空闲时功耗还有优化空间,我增加了休眠模式,当检测到长时间无任务时自动进入低功耗模式,电池续航时间提升了30%。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐