1. 从零开始设计STM32F4机器人主板

搞机器人开发的朋友们应该都遇到过这样的问题:市面上现成的控制板要么功能不够灵活,要么价格贵得离谱。去年我接手一个移动机器人项目时,就决定自己设计一块基于STM32F407和ROS的智能主板。没想到这一做就是三个月,踩了不少坑,但也积累了不少实战经验。

这块主板的核心是STM32F407VGT6,选择它主要是因为性能足够强大(168MHz主频,1MB Flash,192KB RAM),而且外设资源丰富。我给它配了4路电机驱动、IMU接口、多路串口,还专门为ROS通信做了优化。最让我自豪的是实现了10ms级别的实时控制周期,这在普通Arduino上是很难做到的。

硬件设计上我走了不少弯路。第一次打板就因为电源设计不合理,上电就冒烟。后来改用四层板设计,把数字电源和模拟电源彻底分开,还加了TVS二极管做保护。现在这块板子可以稳定工作在-20℃到70℃的环境,实测连续工作72小时不出问题。

2. 硬件设计的关键细节

2.1 电源系统设计

电源绝对是主板最核心的部分。我采用航模电池供电,输入电压范围8-24V,需要转换成3.3V、5V和12V三种电压。这里有个坑要注意:普通的LDO在高压差时发热严重,最后我选了TPS5430做12V转5V的DC-DC,再用AMS1117做5V转3.3V。

电池电压检测电路也很有讲究。最初直接用电阻分压,发现测量误差能达到0.5V。后来在PA4_ADC引脚前加了RC滤波(10kΩ+0.1μF),还在软件端做了滑动平均滤波,最终把误差控制在0.1V以内。代码实现是这样的:

#define ADC_SAMPLE_TIMES 32
uint16_t Get_Battery_Voltage(void)
{
    static uint16_t adc_buf[ADC_SAMPLE_TIMES];
    static uint8_t index = 0;
    uint32_t sum = 0;
    
    adc_buf[index++] = ADC_GetValue(ADC_CH4);
    if(index >= ADC_SAMPLE_TIMES) index = 0;
    
    for(int i=0; i<ADC_SAMPLE_TIMES; i++){
        sum += adc_buf[i];
    }
    return (sum / ADC_SAMPLE_TIMES) * 33 / 4096; // 33是分压系数
}

2.2 外设接口布局

为了兼容ROS生态系统,我预留了5路串口:

  • USART1用于调试输出
  • USART2连接蓝牙模块
  • USART3作为与上位机的主通信接口
  • UART4接NRF24L01无线模块
  • UART5备用

电机驱动部分用了4路TB6612FNG,每路都加了光耦隔离。实测驱动12V/2A的直流电机毫无压力,PWM频率调到20kHz以上就听不到高频啸叫声了。这里特别提醒:电机驱动电路的地线一定要够粗,我用的2mm线宽,否则大电流时会有电压跌落。

3. ROS通信的软件架构

3.1 串口协议设计

和ROS通信最头疼的就是数据格式。我自定义了一套二进制协议:

帧头(0xAA) | 命令字 | 数据长度 | 数据区 | 校验和 | 帧尾(0x55)

校验和是前面所有字节的累加和,这样可以检测出99%的传输错误。在STM32端,用DMA+空闲中断的方式接收数据,效率比普通中断方式高得多:

void USART3_IRQHandler(void)
{
    if(USART_GetITStatus(USART3, USART_IT_IDLE) != RESET){
        uint16_t len = USART3->SR; // 清中断标志
        len = USART3->DR;
        len = DMA_GetCurrDataCounter(DMA1_Stream1);
        usart3_rx_len = USART3_RX_BUF_SIZE - len;
        DMA_Cmd(DMA1_Stream1, DISABLE);
        DMA_SetCurrDataCounter(DMA1_Stream1, USART3_RX_BUF_SIZE);
        DMA_Cmd(DMA1_Stream1, ENABLE);
        
        if(usart3_rx_len > 0){
            ros_parser(usart3_rx_buf, usart3_rx_len);
        }
    }
}

3.2 定时任务调度

实时性对机器人控制至关重要。我设计了三级定时系统:

  • 1ms中断:用于IMU数据采集
  • 5ms中断:执行电机PID控制
  • 10ms任务:在main循环中处理ROS通信

关键是要合理分配任务优先级。比如IMU数据采集必须严格按时执行,而数据发送可以适当延迟。这是定时器配置代码:

void TIM3_IRQHandler(void) // 1ms中断
{
    if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){
        static uint8_t cnt = 0;
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
        
        IMU_Update(); // 1ms采集一次
        
        if(++cnt >= 5){
            cnt = 0;
            Motor_PID_Update(); // 5ms执行一次
        }
    }
}

4. 灯光效果与状态指示

4.1 WS2812B驱动优化

板子上集成了4颗WS2812B RGB灯,用SPI+DMA驱动比GPIO模拟时序稳定得多。具体做法是把PB15配置为SPI2_MOSI,然后用DMA发送经过特殊编码的数据:

void WS2812_Send(uint32_t rgb)
{
    uint8_t buf[24]; // 每个灯24bit数据
    for(int i=0; i<24; i++){
        buf[i] = (rgb & (1<<(23-i))) ? 0xFC : 0xC0;
    }
    DMA_Cmd(DMA1_Stream4, DISABLE);
    DMA_SetCurrDataCounter(DMA1_Stream4, 24);
    DMA_Cmd(DMA1_Stream4, ENABLE);
    SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE);
    while(DMA_GetFlagStatus(DMA1_Stream4, DMA_FLAG_TCIF4) == RESET);
    DMA_ClearFlag(DMA1_Stream4, DMA_FLAG_TCIF4);
}

4.2 状态机设计

灯光不只是为了好看,更是重要的状态指示。我设计了以下几种模式:

  • 启动时:流水灯效果
  • 正常运行时:呼吸灯
  • 低电量时:红色闪烁
  • 通信异常时:黄色快闪

状态切换通过一个简单的状态机实现:

typedef enum{
    SYS_STARTUP,
    SYS_NORMAL,
    SYS_LOW_POWER,
    SYS_ERROR
}SystemState;

void LED_Update(void)
{
    static uint32_t last_time = 0;
    if(HAL_GetTick() - last_time < 100) return;
    last_time = HAL_GetTick();
    
    switch(sys_state){
        case SYS_STARTUP:
            // 流水灯代码
            break;
        case SYS_NORMAL:
            // 呼吸灯代码
            break;
        case SYS_LOW_POWER:
            // 闪烁代码
            break;
    }
}

5. 实际应用与性能测试

5.1 里程计校准

用麦克纳姆轮的全向移动底盘,里程计校准特别重要。我开发了一套基于山外调试助手的校准工具,通过发送特定指令让机器人走固定路径,然后对比实际位移和编码器数据来修正参数。

关键是要考虑轮子打滑的情况。我的做法是让机器人以不同速度做加减速测试,记录下编码器数据和实际移动距离的关系,然后用最小二乘法拟合出校正系数:

void Odom_Calibrate(float actual_dist)
{
    static float enc_sum[4] = {0};
    static uint8_t cnt = 0;
    
    enc_sum[0] += motor[0].encoder;
    // 其他三个电机同理
    
    if(++cnt >= 10){
        float ratio = actual_dist / ((enc_sum[0]+enc_sum[1]+enc_sum[2]+enc_sum[3])/40.0);
        EEPROM_WriteFloat(ODOM_RATIO_ADDR, ratio);
        cnt = 0;
        memset(enc_sum, 0, sizeof(enc_sum));
    }
}

5.2 实时性测试

用逻辑分析仪抓取了关键时序:

  • IMU数据采集:严格1ms间隔,抖动<50μs
  • 电机PWM更新:5ms间隔,抖动<100μs
  • ROS消息响应延迟:平均8ms,最坏情况15ms

这个性能完全能满足大部分移动机器人的需求。有个小技巧:在RTOS环境下,把关键任务放在高优先级线程,并适当调大栈空间,能显著减少任务切换带来的抖动。

Logo

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

更多推荐