【STM32】HAL库 STM32 串口调节PID参数

最近在跟着江协学PID电机控制的代码,但是又不想买模块,也不想画板子,使用DEBUG慢慢调整参数太麻烦了,而且在调整的一瞬间会起飞,于是考虑使用串口来修改PID的参数,串口1打印PID的实时信息,Target,Out等等,只能使用串口2来接收跟处理数据,在这个期间还顺便学了一下FIFO来处理串口信息。已验证(基于STM32F103精英板,使用Clion开发)

先来讲解一下FIFO,FIFO(先进先出)是一种数据处理方式,意味着最先进入的数据最先被处理或输出。它在计算机系统中用于缓冲数据,确保数据的顺序性,对于处理数据帧有不错的效果。

以下是FIFO的文件,使用的使用需要提前定义一个缓冲区跟FIFO_t的结构体,在使用FIFO_Init来初始化.

FIFO.h

#ifndef __FIFO_H__
#define __FIFO_H__

#include <stdint.h>
#include <stdbool.h>


typedef struct {
    uint8_t *buffer;
    uint16_t size;
    uint16_t head;
    uint16_t tail;
} FIFO_t;

void FIFO_Init(FIFO_t *fifo, uint8_t *buf, uint16_t size);

void FIFO_Clear(FIFO_t *fifo);


bool FIFO_IsFull(FIFO_t *fifo);

bool FIFO_IsEmpty(FIFO_t *fifo);

bool FIFO_Push(FIFO_t *fifo, uint8_t data);

bool FIFO_Pop(FIFO_t *fifo, uint8_t *data);

bool FIFO_Peek(FIFO_t *fifo, uint8_t *data, uint8_t Size);

uint16_t FIFO_GetLength(FIFO_t *fifo);

#endif

FIFO.c文件

#include "FIFO.h"

/**
 * FIFO结构体初始化
 * @param fifo FIFO结构体地址
 * @param buf 缓冲区地址
 * @param size 缓冲区大小
 */
void FIFO_Init(FIFO_t *fifo, uint8_t *buf, uint16_t size) {
    fifo->buffer = buf;
    fifo->size = size;
    fifo->head = 0;
    fifo->tail = 0;
}

/**
 * 当前缓冲区是否满
 * @param fifo FIFO结构体地址
 * @return 0 未满, 1缓冲区已满(一位的缓冲)
 */
bool FIFO_IsFull(FIFO_t *fifo) {
    return ((fifo->head + 1) % (fifo->size)) == fifo->tail;
}
/**
 * 当前缓冲区是否空
 * @param fifo FIFO结构体地址
 * @return 1 空, 0未空
 */
bool FIFO_IsEmpty(FIFO_t *fifo) {
    return (fifo->head == fifo->tail);
}
/**
 * 写入一字节
 * @param fifo FIFO结构体地址
 * @param data 写入的数据
 * @return 0,写入失败, 1写入成功
 */
bool FIFO_Push(FIFO_t *fifo, uint8_t data) {
    if (FIFO_IsFull(fifo)) return false;
    fifo->buffer[fifo->head] = data;
    fifo->head = (fifo->head + 1) % fifo->size;
    return true;
}
/**
 * 读取一字节
 * @param fifo FIFO结构体地址
 * @param data 读取的数据(写入这个地址)
 * @return 0 缓冲区为空, 1读取成功
 */
bool FIFO_Pop(FIFO_t *fifo, uint8_t *data) {
    if (FIFO_IsEmpty(fifo)) return false;
    *data = fifo->buffer[fifo->tail];
    fifo->tail = (fifo->tail + 1) % fifo->size;
    return true;
}

/**
 * 预读数据,不读出
 * @param fifo  FIFO结构体地址
 * @param data 读取的数据(写入这个地址)
 * @return 缓冲区为空, 1读取成功
 */
bool FIFO_Peek(FIFO_t *fifo, uint8_t *data, uint8_t Size) {
    if (FIFO_IsEmpty(fifo)) return false;
    for (int i = 0; i < Size; ++i) {
        *data = fifo->buffer[fifo->tail + i];
        data++;
    }
    return true;
}

/**
 * 获取缓冲区当前长度
 * @param fifo FIFO结构体地址
 * @return 当前长度
 */
uint16_t FIFO_GetLength(FIFO_t *fifo) {
    return (fifo->head >= fifo->tail) ?
           (fifo->head - fifo->tail) :
           (fifo->size - fifo->tail + fifo->head);
}

/**
 * 清空 FIFO
 * @param fifo FIFO结构体地址
 */
void FIFO_Clear(FIFO_t *fifo) {
    fifo->head = 0;
    fifo->tail = 0;
    for (int i = 0; i < fifo->size; ++i) {
        fifo->buffer[i] = 0;
    }
}

接下来需要使用串口2来测试FIFO是否能够写入并且读取,再加一个写失败保护,防止数据写入太快导致处理不过来,先创立一个串口任务放进主循环里面轮询(每10ms执行一次),建立一个标志位结构体,打开串口2的中断接收,在中断里面设置为1,其中Rx_F是收到一位数据的标志位,Rx_E是接收失败错误标志位,Rx_FRAME是接收到一帧数据的标准位。

在开始初始化串口后,需要使用HAL_UART_Receive_IT(&huart2, &Rx_Data, 1);函数,在中断里面写入FIFO,注串口接收中断只能执行一次,所以接收完需要再次调用在接收中断函数。

在main里面重新定义串口接收回调函数HAL_UART_RxCpltCallback,在里面写入自己的判断逻辑,MyTick是自己写的一个心跳,在1ms定时器中断里面+就好了,这样子写可以方便合理分配每个任务的执行周期,也能节省MCU的执行效率,10ms执行一次判断若当前写入FIFO的数据超过数据帧的长度,就可以开始数据分析或者处理其他逻辑。

Analytic_Fun()函数是解析数据帧的一个函数,后续会写
typedef struct {
    uint8_t Rx_F: 1;
    uint8_t Rx_E: 1;
    uint8_t Rx_FRAME: 1;
} _Sys_F;





void Uart2_Task(void) {
    uint8_t rx_data = 0;
    static uint32_t Tick = 0;
    if (Get_MyTick() - Tick >= UART2_TASK_FREQ) {
        if (Sys_F.Rx_F && !Sys_F.Rx_E) {
            Sys_F.Rx_F = 0;
            if (FIFO_GetLength(&Uart2_FIFO) >= FRAME_SIZE) {
                Sys_F.Rx_FRAME = 1;
            }
        } else if (Sys_F.Rx_E && Sys_F.Rx_F) {
            Sys_F.Rx_F = 0;
            FIFO_Clear(&Uart2_FIFO);
            USART2_printf("接收读取失败,请降低发送速度,重新发送\r\n");
        }
        Analytic_Fun();
        Tick = Get_MyTick();
    }
}




void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        Sys_F.Rx_F = 1;
        if (!FIFO_Push(&Uart2_FIFO, Rx_Data)) {
            Sys_F.Rx_E = 1;
        } else {
            HAL_UART_Receive_IT(&huart2, &Rx_Data, 1);
        }
    }
}


定制串口数据帧,使用串口数据帧有以下优点

1.提高通信可靠性

  • 错误检测与纠正
    数据帧通常包含校验字段(如CRC、奇偶校验等),可检测传输中的错误(如噪声干扰、数据丢失),部分协议还能通过重传机制纠正错误。

  • 数据完整性
    帧结构明确标识数据的起始和结束(通过帧头、帧尾),避免接收方因数据流不连续而解析错误。

2. 结构化数据组织

  • 明确的数据边界
    帧头(Start Flag)和帧尾(Stop Flag)标记数据范围,解决串口流式通信中数据粘包或断帧的问题。

  • 多字段复用
    一帧内可包含地址、命令、负载数据、校验等不同字段,实现复杂协议的封装(如Modbus、CAN协议)。

4. 同步与效率优化

  • 同步时钟恢复
    帧头帮助接收方同步时钟(尤其在异步通信中),减少因波特率偏差导致的采样错误。

  • 减少冗余开销
    相比逐字节发送,数据帧批量传输可降低协议开销(如减少重复的帧头/帧尾占用)。

我这个项目是双环PID,需要调节速度环跟位置环,那么就涉及到多个参数的调节

包头 + 读/写 + 速度环/位置环 + P/I/D + float值(四字节)+ 校验位(XOR) + 包尾
这样子的结构,能够单独设置一个环的一个参数,可以使用AI写一个Python上位机来图形化操作,直接打包发送数据。

对于浮点数的处理,使用(IEEE 754),在线转换网站 https://www.toolhelper.cn/Digit/FractionConvert

在接收到一帧数据后就会开始解析数据,首先需要读取第一位,判断是否是包头,不是包头直接丢弃开始下一位的检测,为了防止数据发送太频繁,会导致任务阻塞在解析任务里面,给他一个限制,使其每次处理两帧或者处理两位无效数据,这样子不仅能合理的接收解析串口并同步数据帧,不然会导致调帧。

如果包头正确会继续进入解析状态,校验位判断与包尾的判断,都符合的情况就会执行赋值操作,

这边浮点数的处理我使用的是联合体的方法来拼凑一个浮点数,浮点数在计算机中的存储方式主要遵循IEEE 754标准,在接收串口数据时,使用的是uint8_t,赋值完后直接调用float值就好,PID_Structure *Uart_Pid是定义的一个PID结构体,读者可以改成其他的形式赋值也可,根据第3位判断是调整哪个环,然后使用一个switch来对PID三个参数赋值

数据包格式
[0] 	0xAA(包头)
[1] 	0x00(写),0x01(读)
[2] 	0x00(速度环),0x01(位置环)
[3] 	0x00(P),0x01(I), 0x02(D)
[4-7]	修改的数值(浮点数IEEE 754格式)小端发送
[8]  	校验位([1-7]异或位)
[9]  	0x55(包尾)



typedef union {
    float f;
    uint8_t bytes[4];
} FloatUnion;

/**
 * 数据帧校验位(异或位)
 * @param data 接收的数据
 * @param len 数据长度
 * @return 检验数据
 */
uint8_t CalcChecksum(uint8_t *data, uint8_t len) {
    uint8_t sum = 0;
    for (uint8_t i = 0; i < len; i++) {
        sum ^= data[i];
    }
    return sum;
}


/**
 * 数据帧解析函数
 */
void Analytic_Fun(void) {
    uint8_t parse_count = 0;
    FloatUnion fu;
    static uint8_t frame_buf[FRAME_SIZE] = {0};
    PID_Structure *Uart_Pid = NULL;

    if (Sys_F.Rx_FRAME) {
        Sys_F.Rx_FRAME = 0;
        while (FIFO_GetLength(&Uart2_FIFO) >= FRAME_SIZE && parse_count < MAX_PARSE_COUNT) {
            parse_count++;
            for (int i = 0; i < FRAME_SIZE; ++i) {
                FIFO_Pop(&Uart2_FIFO, &frame_buf[i]);
                if (frame_buf[0] != FRAME_HEAD && i == 0)break;
            }
            if (frame_buf[0] == FRAME_HEAD) {
                if (CalcChecksum(&frame_buf[1], 7) == frame_buf[FRAME_SIZE - 2] &&
                    frame_buf[FRAME_SIZE - 1] == FRAME_TAIL) {
                    Uart_Pid = frame_buf[2] ? &Motor_Sta.Place_PID : &Motor_Sta.Speed_PID;
                    if (!frame_buf[1]) {
                        for (int i = 0; i < 4; i++) {   //处理浮点数
                            fu.bytes[i] = frame_buf[4 + i];  //
                        }
                        switch (frame_buf[3]) {
                            case 0x00:
                                Uart_Pid->Kp = fu.f;
                                break;
                            case 0x01:
                                Uart_Pid->Ki = fu.f;
                                break;
                            case 0x02:
                                Uart_Pid->Kd = fu.f;
                                break;
                        }
                    } else {
                        USART2_printf("当前 %s 的参数为 Kp = %.2f Ki = %.2f Kd = %.2f \r\n",
                                      (frame_buf[2] ? "位置环" : "速度环"), Uart_Pid->Kp, Uart_Pid->Ki, Uart_Pid->Kd);
                    }
                }
            }
        }
    }
}

烧录验证:

发送指令 AA 01 01 00 00 00 00 00 00 55(获取位置环参数)

发送AA 01 00 00 00 00 00 00 01 55(获取速度环参数)

 

调整PID参数,设置速度环的Kp参数为0.1   AA 00 00 00 00 00 40 3F D5 55,然后再获取速度环参数,可以看到已经修改

测试有乱码的情况下,在数据包前写入BB CC 乱码,也能处理数据帧以外的乱码

测试FIFO快速写入,勾选图中红方框,50ms发送也能处理的了数据

20ms的情况下测试,也能很好的处理数据

修改参数改成10ms发送两帧,写入太快就开始报错了

使用AI写一个Python打包数据,方便调试流程

第一次写博客,标题跟目录都不是很会操作,以上代码可以参考,因为不好截取,只上传的部分核心代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值