背景及概述

借助AI的力量,(不懂FreeRTOS的)笔者把FreeRTOS移植到了STM32F4的工程上并调通了。
现在工程中有4个舵机需要控制,需要上位机发送信号来得到控制参数。
下一步应明确通讯协议,在STM32端实现解析函数/串口通信函数/集成FreeRTOS功能。
本文将逐个突破此三要点。

解析函数

上位机数据协议:

  1. 协议格式 (LD150舵机)
[0x55][0x55][ID][长度][命令][数据...][校验和]
  2字节   1字节 1字节  1字节   N字节    1字节

帧头: 0x55 0x55
ID: 舵机ID (1-4) 或 0xFE (广播)
数据: 每组5字节 = ID + time_low + time_high + pos_low + pos_high
位置: 0-1000 对应 0-240° (转换为 0-180°)

定义全局量

// 全局解析结果(volatile 确保中断安全)
volatile int g_servo_s1 = 0;
volatile int g_servo_s2 = 0;
volatile int g_servo_s3 = 0;
volatile int g_servo_s4 = 0;
volatile int g_delay_ms = 1000;
volatile bool g_new_command_ready = false;

解析函数实现

/* 解析4个舵机二进制协议 */
void parse_servo_frame(const uint8_t *data, uint16_t len)
{
    if (len < SERVO_FRAME_MIN_LEN) return;

    // 检查帧头 0x55 0x55
    if (data[0] != 0x55 || data[1] != 0x55) return;

    // 检查广播ID或有效ID
    uint8_t servo_id = data[2];
    if (servo_id != SERVO_BROADCAST_ID && (servo_id < 1 || servo_id > 4)) return;

    // 检查数据长度
    uint8_t data_len = data[3];
    uint8_t expected_len = 4 + data_len + 1; // header(2) + id(1) + len(1) + data(n) + checksum(1)
    if (len < expected_len) return;

    // 检查命令 (0x03 = 位置写命令)
    if (data[4] != SERVO_CMD_POS_WRITE) return;

    // 验证校验和
    uint8_t calc_sum = calc_checksum(data, expected_len - 1);
    if (calc_sum != data[expected_len - 1]) {
        // 校验和错误
        return;
    }

    // 解析舵机数据 (每个舵机5字节: id + time_low + time_high + pos_low + pos_high)
    uint8_t *frame_data = (uint8_t*)&data[5];
    uint8_t servo_count = data_len / 5;

    for (uint8_t i = 0; i < servo_count; i++) {
        uint8_t id = frame_data[i * 5];
        // 位置: pos = pos_high * 256 + pos_low, 范围0-1000对应0-240度
        uint16_t pos = frame_data[i * 5 + 3] | (frame_data[i * 5 + 4] << 8);

        // 转换为角度 (0-240度 -> 0-180度)
        uint16_t angle = (pos * 180) / 240;
        if (angle > 180) angle = 180;

        // 更新对应舵机
        switch (id) {
            case 1: g_servo_s1 = angle; break;
            case 2: g_servo_s2 = angle; break;
            case 3: g_servo_s3 = angle; break;
            case 4: g_servo_s4 = angle; break;
        }
    }

    // 解析运动时间 (用于DELAY)
    if (servo_count > 0) {
        uint16_t move_time = frame_data[1] | (frame_data[2] << 8);
        g_delay_ms = move_time;  // 单位ms
    }

    g_new_command_ready = true;
}

串口通信函数

先配置好串口4,开DMA接收和IDLE中断,配置的代码不是重点就不放上来了。

void UART4_IRQHandler(void)
{
  /* USER CODE BEGIN UART4_IRQn 0 */

	uint32_t flag = 0;
	uint32_t temp;
	flag = __HAL_UART_GET_FLAG(&huart4, UART_FLAG_IDLE);
	if(flag != RESET)
	{
		__HAL_UART_CLEAR_IDLEFLAG(&huart4);
		HAL_UART_DMAStop(&huart4);
		temp = __HAL_DMA_GET_COUNTER(&hdma_uart4_rx);
		uint16_t rx_len = UART4_RX_BUFFER_SIZE - temp;
		parse_uart4_command((char*)uart4_rx_buffer, rx_len);
		HAL_UART_Receive_DMA(&huart4, uart4_rx_buffer, UART4_RX_BUFFER_SIZE);
	}

  /* USER CODE END UART4_IRQn 0 */
  HAL_UART_IRQHandler(&huart4);
  /* USER CODE BEGIN UART4_IRQn 1 */

  /* USER CODE END UART4_IRQn 1 */
}

在parse_uart4_command中调用parse_servo_frame。

void parse_uart4_command(char *data, uint16_t len)
{
    if (len == 0 || len >= UART4_RX_BUFFER_SIZE) return;

    // 解析LD150二进制协议 (帧头 0x55 0x55)
    if (len >= SERVO_FRAME_MIN_LEN && (uint8_t)data[0] == 0x55 && (uint8_t)data[1] == 0x55) {
        parse_servo_frame((const uint8_t*)data, len);
    }
}

FreeRTOS集成

对于不熟悉FreeRTOS的开发者来说,重头戏来了。
首先解释下领域信息:
要实现周期性精确调度,就必须用到以下函数:
void vTaskDelayUntil(TickType_t pxPreviousWakeTime, TickType_t xTimeIncrement)。
pxPreviousWakeTime:指向一个变量,该变量保存任务最后一次解除阻塞的时间。第一次使用前,该变量必须初始化为当前时间。
xTimeIncrement:周期循环时间。当时间等于 (
pxPreviousWakeTime + xTimeIncrement) 时,任务解除阻塞。
因此,还需要pdMS_TO_TICKS(),将毫秒转换为FreeRTOS能理解的节拍数,作为xTimeIncrement;
xTaskGetTickCount()获取自系统启动以来的时钟节拍数,作为pxPreviousWakeTime。
这样,代码的逻辑就清晰起来了。
初始化调用pdMS_TO_TICKS和xTaskGetTickCount。
之后在循环中用g_new_command_ready标志判断parse_servo_frame解析有没有成功,有的话就Servo_SetPulse驱动舵机。
由于舵机对应的通道和目标角度都是全局且在其他位置定义的,所以代码可以用一行表示,很简洁。
驱动任务结束后,再把g_new_command_ready置0。
若g_new_command_ready为假,则直接跳过执行舵机驱动,等待下一次延时。

/* 舵机控制命令任务 - 处理UART4收到的舵机指令 */
void vTaskServoControl(void *pvParameters)
{
    TickType_t xLastWakeTime;
    const TickType_t xFrequency = pdMS_TO_TICKS(10);  // 10ms检查一次

    xLastWakeTime = xTaskGetTickCount();

    for(;;)
    {
        // 检查是否有新命令
        if (g_new_command_ready)
        {
            // 解析LD150舵机二进制协议帧
            // 位置范围: 0-1000 (对应0-240度)
            // 注意: Servo_Channel枚举从0开始
            Servo_SetPulse(SERVO_CH1, g_servo_s1);
            Servo_SetPulse(SERVO_CH2, g_servo_s2);
            Servo_SetPulse(SERVO_CH3, g_servo_s3);
            Servo_SetPulse(SERVO_CH4, g_servo_s4);

            // 清除标志
            g_new_command_ready = false;
        }

        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

数据来源链路:

UART4 DMA+IDLE中断 
    ↓
parse_uart4_command() 
    ↓ (检测0x55 0x55帧头)
parse_servo_frame() 
    ↓ (解析ID、位置、校验和)
g_servo_s1~s5 = position
    ↓
g_new_command_ready = true
    ↓
vTaskServoControl() 读取并更新PWM
posted on 2026-03-16 15:42  快乐的乙炔  阅读(30)  评论(0)    收藏  举报