背景及概述
借助AI的力量,(不懂FreeRTOS的)笔者把FreeRTOS移植到了STM32F4的工程上并调通了。
现在工程中有4个舵机需要控制,需要上位机发送信号来得到控制参数。
下一步应明确通讯协议,在STM32端实现解析函数/串口通信函数/集成FreeRTOS功能。
本文将逐个突破此三要点。
解析函数
上位机数据协议:
- 协议格式 (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
浙公网安备 33010602011771号