欢迎光临
我们一直在努力

05-PID、按键舵机电机与 GL_Math:执行层与算法层代码精读

PID、按键舵机电机与 GL_Math:执行层与算法层代码精读

本篇串起三块底层:Agorithm.c 的 PID/限幅/快开方,Common_Peripherals.c 的蜂鸣器/按键/舵机/无刷,GL_Math.c 的三角与 atan2 查表。 说明它们如何支撑 GPS 与视觉两条业务线。 源码仓库:https://github.com/shuifanyu/TC264-GPS-Vision-Car


目录

  • 层位置
  • pid_param_t:一个结构体两种算法
  • PidLocCtrl 逐行问题意识
  • PidIncCtrl_L/R:增量式实现
  • constrain_float 与 Sqrt_Fast
  • 蜂鸣器与分层自检
  • 按键扫描与标志位
  • Steer_set:角度域限幅再进 PWM
  • BLDC_ctrl:符号表方向
  • 为什么需要 GL_Math
  • func_sin / func_sqrt / fast_atan2
  • 与 GPS/视觉的衔接
  • 改法建议
  • 小结

  • 1. 层位置

    业务:GPS Follow_track / Image steering
    │ 误差 e、目标 duty

    算法:Agorithm.c PID、限幅
    │ 输出 u / duty / Nomal 用法

    执行:Common_Peripherals.c
    Steer_set / BLDC_ctrl / Key / Buzzer


    库:pwm/gpio + GL_Math(方位角等用到的数学)


    2. pid_param_t:一个结构体两种算法

    typedef struct
    {
    float kp, ki, kd;
    float imax; // 积分限幅(意图)

    float out_p, out_i, out_d, out;
    float integrator; // 位置式积分累加
    float last_error;
    float last_derivative; // 增量式里存 e(k)-e(k-1)
    unsigned long last_t;
    } pid_param_t;

    extern pid_param_t I_PID; // 倾向惯性/视觉类
    extern pid_param_t G_PID; // 倾向 GPS 类

    字段位置式用法增量式用法
    integrator Σe 一般不用
    last_error e(k-1) e(k-1)
    last_derivative 可选 e(k-1)-e(k-2) 相关
    out 合成输出 累加后的输出
    imax 积分限幅 少用

    双实例:GPS 与惯性/其它模式可各用一套参数,避免互相污染。


    3. PidLocCtrl 逐行问题意识

    真实代码要点:

    float PidLocCtrl(pid_param_t * pid, float error)
    {
    G_PID.kp = 1.1; // ①
    G_PID.kd = 5;
    I_PID.kp = 2;
    I_PID.kd = 1;

    pid->integrator += error;
    constrain_float(pid->integrator, pid->imax, pid->imax); // ②

    pid->out_p = pid->kp * error;
    pid->out_i = pid->ki * pid->integrator;
    pid->out_d = pid->kd * (error pid->last_error);

    pid->last_error = error;
    pid->out = pid->out_p + pid->out_i + pid->out_d;
    return pid->out;
    }

    ① 参数被函数内写死

    无论菜单改成多少,每次进入 PID 都会把 G_PID/I_PID 的 kp/kd 盖掉。 结果:

    • 在线调参无效
    • 只能改源码重编译
    • 两套 PID 实例在函数里被交叉赋值,语义混乱

    正确做法:PidLocCtrl 只读 pid->kp/ki/kd,初始化/菜单/Flash 负责写参数。

    ② 积分限幅未写回

    constrain_float(pid->integrator, pid->imax, pid->imax);

    函数返回限幅后的值,但这里 丢弃返回值,integrator 仍可能无限增长。

    应写成:

    pid->integrator = constrain_float(pid->integrator, pid->imax, pid->imax);

    或改函数原型为就地限幅。

    ③ 未用 last_t 做 dt

    结构体有 last_t,实现却按 固定周期、dt 并入系数 的简化形式。 若未来周期可变,应显式用 dt 或重新标定 kp/ki。

    ④ 微分为裸差分

    kd * (e – last) 无滤波,e 噪声大时会抖;可在 d 路径加一阶低通。


    4. PidIncCtrl_L/R:增量式实现

    float PidIncCtrl_L(pid_param_t * pid, float error)
    {
    pid->out_p = pid->kp * (error pid->last_error);
    pid->out_i = pid->ki * error;
    pid->out_d = pid->kd * ((error pid->last_error) pid->last_derivative);

    pid->last_derivative = error pid->last_error;
    pid->last_error = error;

    pid->out += pid->out_p + pid->out_i + pid->out_d;
    return pid->out;
    }

    PidIncCtrl_R 与 L 逻辑相同(当前未体现左右不同参数;可留作左右轮增益差异扩展点)。

    与公式对应

    [ \\Delta u=K_p(e-e_{k-1})+K_i e+K_d\\big((e-e_{k-1})-(e_{k-1}-e_{k-2})\\big) ]

    last_derivative 存的是上一次的一阶差分,从而差分得到近似二阶差分。

    缺点(现状)

    • out 无上下限(应在累加后 out = constrain_float(…))
    • 无抗积分饱和专项(增量式相对好一些,但仍需输出限幅)
    • L/R 重复代码,可合并为一个函数

    5. constrain_float 与 Sqrt_Fast

    constrain_float

    float constrain_float(float amt, float low, float high)
    {
    return ((amt)<(low)?(low):((amt)>(high)?(high):(amt)));
    }

    标准夹逼。关键在 调用方是否使用返回值。

    Sqrt_Fast(快速逆平方根风格)

    float Sqrt_Fast(float x)
    {
    float halfx = 0.5f * x;
    float y = x;
    long i = *(long *) &y;
    i = 0x5f3759df (i >> 1);
    y = *(float *) &i;
    y = y * (1.5f (halfx * y * y));
    return y; // 注意:近似 1/sqrt(x),不是 sqrt(x)
    }

    点说明
    魔数 0x5f3759df,Q_rsqrt 初值
    一次牛顿迭代 精度与速度折中
    返回值 更接近 1/√x;若当 √x 用会错
    严格别名 用 union 或 memcpy 更稳妥

    GPS 距离若用 (d=\\sqrt{\\Delta x^2+\\Delta y^2}),需 1/Sqrt_Fast 或直接 func_sqrt/库 sqrt。


    6. 蜂鸣器与分层自检

    void Buzzer_init(void) {
    gpio_init(BUZZER_PIN, GPO, 0, GPO_PUSH_PULL);
    }
    void Buzzer_check(int time2) {
    gpio_set_level(BUZZER_PIN, 1);
    system_delay_ms(time2);
    gpio_set_level(BUZZER_PIN, 0);
    }

    配合 core0_main:

    init 外设 → 短响 50ms
    配置 PIT → 长响 300ms

    以及菜单 keyN_clear 里短响 50ms:操作反馈。

    system_delay_ms 在 ISR 里应避免;菜单在主循环 clear 时蜂鸣是同步的,会短暂卡主循环(50ms),一般可接受。


    7. 按键扫描与标志位

    uint8 key1_flag, key2_flag, key3_flag, key4_flag;
    int Key_close_flag = 0;

    void key_scan(void) {
    key1_state_last = key1_state;
    key1_state = gpio_get_level(KEY1);
    // …
    if (key1_state && !key1_state_last) key1_flag = 1;
    }

    初始化

    gpio_init(KEY1, GPI, GPIO_HIGH, GPI_PULL_UP); // 上拉,按下常见为低

    与注释的差异(要心里有数)

    注释写“检测到按键按下”,实现是 state && !last,即 高电平沿。 若原理图是 低电平有效,条件应改为 (!state && last)。

    现场以万用表/按键实测为准;方向错了会出现“抬手触发/没反应”。

    开关量

    switch1_state 等已定义,便于拨码选科目/模式(进一步可做模式输入)。


    8. Steer_set:角度域限幅再进 PWM

    void Steer_init(void) {
    pwm_init(SERVO_MOTOR_PWM, SERVO_MOTOR_FREQ,
    (uint32)SERVO_MOTOR_DUTY(SERVO_MOTOR_MID));
    }

    void Steer_set(int angle)
    {
    if (angle > SERVO_MOTOR_LMAX) angle = SERVO_MOTOR_LMAX;
    if (angle < SERVO_MOTOR_RMAX) angle = SERVO_MOTOR_RMAX;
    pwm_set_duty(SERVO_MOTOR_PWM, (uint32)SERVO_MOTOR_DUTY(angle));
    }

    概念含义
    SERVO_MOTOR_MID 中值,上电默认回中
    LMAX / RMAX 左右机械限位(命名注意谁是大角)
    SERVO_MOTOR_DUTY() 角度/脉宽 → 比较值宏
    双限幅 保护齿轮,防止 PID 打满

    Steer_text:键 1/2 ±10,键 3/4 直接到左右极限——标定中值与极限的工程函数。

    视觉 duty 与 Steer_set 的量纲

    image_steering_duty 来自 96 – (mid/188)*40,范围约 56~96,更像 脉宽/占空比域。 Steer_set(int angle) 是角度语义。

    对接时要统一:

    若 duty 就是 pwm_set_duty 参数 → 应调 pwm_set_duty 而非 Steer_set(angle)
    或:把视觉输出映射到 angle,再 Steer_set

    量纲不一致是联调时最常见坑之一。


    9. BLDC_ctrl:符号表方向

    void BLDC_init(void)
    {
    pwm_init(PWM_CH1, 1000, 0);
    gpio_init(DIR_CH1, GPO, 1, GPO_PUSH_PULL);
    }

    void BLDC_ctrl(int16 Motor_SPEED)
    {
    if (Motor_SPEED >= 0) {
    pwm_set_duty(PWM_CH1, Motor_SPEED);
    gpio_set_level(DIR_CH1, 1);
    } else {
    pwm_set_duty(PWM_CH1, Motor_SPEED);
    gpio_set_level(DIR_CH1, 0);
    }
    }

    设计说明
    带符号接口 业务侧 BLDC_ctrl(4500) / BLDC_ctrl(0)
    方向脚 DIR_CH1 高低电平
    PWM 绝对值占空比
    频率 init 1000Hz(与驱动匹配)

    Follow_track 里直接查表调用(2000/4500/0),开环速度档;不是编码器闭环。

    Motor_text:±100/±1000 手动摸电机,用于方向与占空比标定。

    安全

    • 无显式 PWM 上限 clamp(4500 等需确认与 PWM 分辨率匹配)
    • 堵转仅靠机械/电池
    • 比赛前应加软件上限

    10. 为什么需要 GL_Math

    动机说明
    Flash/代码体积 标准 math 库可能偏大(视工具链)
    可控精度 竞赛够用即可
    教学/算法训练 自写泰勒、牛顿迭代、查表 atan
    避免许可证/裁剪 依赖更少

    GPS 方位角、距离、IMU 相关运算会用到 sin/cos/atan2/sqrt。


    11. func_sin / func_sqrt / fast_atan2

    func_sqrt(牛顿迭代)

    double func_sqrt(double x)
    {
    double j = 0.0, k = x / 2;
    while (j != k) { // ① 浮点 == 比较
    j = k;
    k = (j + x / j) / 2; // 牛顿迭代
    }
    return j;
    }

    点说明
    迭代式 (k_{n+1}=(k_n+x/k_n)/2)
    初值 x/2,x 很大时可能迭代多
    j != k 依赖浮点最终 bit 相等,应改为 `
    x≤0 未定义,需保护

    py_sqrt / k_sqrt

    递归版牛顿,k_sqrt 用 py_fabs(x1-x0)>=10e-15 停止,比 != 稳妥。

    func_sin(泰勒)

    // 奇次项累加,|temp|>1e-15 时继续
    result = x x^3/3! + x^5/5! ...

    注意:输入应为 弧度;大角度需先归约到 ([-π,π]) 再算,否则阶乘/幂误差与循环次数都会出问题。代码里 func_cos 用 func_sin(π/2-x)。

    func_asin

    级数展开,factorial(2n) 在 n 增大时 long 溢出 风险高,竞赛里小范围角度可用,生产代码应换查表/库。

    fast_atan2(查表 + 线性插值)

    // 1) 把 |y|/|x| 折到 0~45° 相关的 z
    // 2) z 小 → angle≈z
    // 3) 否则表索引 + 插值:
    // alpha = z * 256 – 0.5
    // base = table[index] + (table[index+1]-table[index])*alpha
    // 4) 按象限翻转符号 / 用 π±base

    | 优点 | 比泰勒 atan 快且内存可控(257 个 double) | | 精度 | 与表分辨率、插值有关,导航够用 | | 用途 | 方位角 atan2(Δy,Δx) |

    与 Agorithm.c 的 Sqrt_Fast 对比:一个是三角查表,一个是开方快速近似,服务不同热点。


    12. 与 GPS/视觉的衔接

    GPS: Nomal_Error → PidLocCtrl → (理想) Steer_set
    Distance → 到点;N → BLDC_ctrl 档位

    视觉: mid_avg → duty 公式 → 低通 → pwm/Steer

    菜单: 改 flag、测 Steer_text/Motor_text、蜂鸣反馈

    模块依赖执行层依赖算法层
    Follow_track BLDC_ctrl(已直接调用) 可接 PID
    steering_image 舵机 PWM 可选 PID
    Menu 测试页 Steer_set / BLDC_ctrl

    13. 改法建议

    13.1 PID

    float PidLocCtrl(pid_param_t * pid, float error)
    {
    pid->integrator += error;
    pid->integrator = constrain_float(pid->integrator, pid->imax, pid->imax);
    pid->out_p = pid->kp * error;
    pid->out_i = pid->ki * pid->integrator;
    pid->out_d = pid->kd * (error pid->last_error);
    pid->last_error = error;
    pid->out = constrain_float(pid->out_p + pid->out_i + pid->out_d,
    OUT_LIMIT, OUT_LIMIT);
    return pid->out;
    }

    参数初始化放 pid_init(&G_PID, kp, ki, kd, imax)。

    13.2 增量式

    累加后:

    pid->out = constrain_float(pid->out, u_min, u_max);

    合并 L/R:

    float PidIncCtrl(pid_param_t *pid, float e) { ... }

    13.3 执行层

    • BLDC_ctrl 内部软件限幅
    • 明确 image_steering_duty 与 Steer_set 量纲
    • 按键有效电平与原理图一致

    13.4 GL_Math

    • sqrt 停止条件用 epsilon
    • 大角先归约再泰勒
    • 导航路径优先 fast_atan2,调试可用库函数对照

    14. 小结

    层文件核心问题
    算法 Agorithm.c 位置式/增量式、限幅是否真正生效
    执行 Common_Peripherals.c 键标、舵机限位、BLDC 符号+PWM
    数学 GL_Math.c 迭代/级数/查表的速度与精度

    读竞赛代码不要只背“有 PID”:

    参数从哪来?
    限幅有没有写回?
    输出量纲和执行器是否一致?
    自写数学用在哪条链路?

    把这四问写进精读,文章就有实质,而不是目录。


    作者:shuifanyu 标签:PID 嵌入式 智能车 TC264 GL_Math 代码精读

    源码:code/Agorithm.c/h、code/Common_Peripherals.c、code/GL_Math.c 仓库:https://github.com/shuifanyu/TC264-GPS-Vision-Car

    赞(0)
    未经允许不得转载:171主机测评 » 05-PID、按键舵机电机与 GL_Math:执行层与算法层代码精读
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址