MSPM0开发学习笔记:非阻塞二维云台运动代码

针对二维云台跟踪目标时抖动大、PID 难调的痛点,本篇分析阻塞型步进控制在视觉帧间歇“停‑启”所带来的冲击,并提出在 MSPM0 上以非阻塞方式按最新视觉误差实时刷新步进脉冲频率,从而在保持闭环响应速度的同时压低机械抖动。


前言

在前几篇的博客中,有提到一个追踪红色小球的代码,但是会发现追踪的过程中存在很大的抖动并且响应速度相对较慢,推测其原因是因为步进电机的控制函数是阻塞型的,就是说步进电机一定要以某一个给定的速度走完当前规定的角度步数,才会根据新接受到的视觉误差数据再次进行调整运动,而在接受的这几毫秒,步进电机的频率是0,也就是说会直接停下来,之后又根据新的误差数据马上运动起来,所以会有很大的抖动,非常影响识别,也导致pid很难调,没有办法达到一个很好的效果。

本章博客的目的就是介绍一个新的运动思路以及代码,通过实时根据接受到的视觉数据进行二维云台的步进电机的频率更新,从而避免大幅的抖动以及保证电机数据更新的时效性,更好的瞄准目标。


如果无法很好的复现博客里的代码,可以私信作者博取源代码,电赛期间都在线

一、硬件选择

主控:MSPM0G3507
驱动:D36A双路步进电机驱动
电机:42步进电机*2
视觉:树莓派连接USB摄像头

二、硬件连线

硬件连线部分在前几篇博客里面已经说过了,可以直接去里面看,这边附上链接
机器视觉:树莓派结合摄像头解决2025电赛E题锁定靶心 思路与代码分享

三、软件代码

1、思路

对于非阻塞的控制,一开始的思路是通过GPIO引脚输出PWM波,通过接受UART信息的中断实时更新PWM的占空比从而控制给D36A的ST引脚上升沿脉冲的数量和速度(D36A是42步进电机的闭环集成驱动模块,给ST引脚一个脉冲步进电机就会转动一步,具体可以去看看前几章的博客那边有具体讲到)。但是后面发现可能不能用这个高的频率(树莓派给的视觉误差信息帧率可以达到一百帧)去更新PWM引脚的占空比,一运行这个程序TI芯片就会卡死。

因此后面我们换了一种思路,不用GPIO引脚来输出PWM波来进行控制,而是通过我们自己设置引脚的高低电平进而给ST引脚脉冲,我们只需要设置每一次循环的延时就可以控制给上升沿脉冲的数量与速度。这样就不需要频繁的改变PWM引脚的占空比导致芯片卡死。

2、代码实现

软件部分采用C语言实现,IDE采用keil,基于逐飞库进行编写
这边先进
行一下简单的参数说明,便于理解后面的思路

参数含义以及作用
UART_INDEX串口索引,定义使用的串口(此处为UART_2),用于指定数据接收和发送的硬件串口
UART_BAUDRATE串口波特率,定义串口通信的波特率(与调试口一致),确保与外部设备通信速率匹配
UART_TX_PIN串口发送引脚,定义UART_2的发送引脚(B15),用于串口数据的发送
UART_RX_PIN串口接收引脚,定义UART_2的接收引脚(B16),用于串口数据的接收
UART_PRIORITY串口中断优先级,指定串口接收中断的优先级(使用UART0的中断编号映射到UART2),控制中断响应顺序
PIT_CON_PRIO中频PIT中断优先级,定义用于PID计算的定时器(TIMG12)中断优先级,确保PID计算的实时性
PIT_IMPLUSE_PRIO高频PIT中断优先级,定义用于生成步进电机脉冲的定时器(TIMA0)中断优先级,确保脉冲生成的高精度
LASER_PIN激光控制引脚,定义控制激光的GPIO引脚(A30),用于在接近目标时开启/关闭激光
uart_get_data串口接收数据缓冲区,存储串口接收的原始数据(最大5049字节),避免数据丢失
fifo_get_dataFIFO输出缓冲区,从FIFO中读取数据并用于解析,临时存储待解析的串口数据
get_data单次接收数据字节,存储串口中断中单次接收的一个字节数据
fifo_data_countFIFO数据个数,记录FIFO缓冲区中当前存储的数据字节数,用于判断是否有足够数据可解析
uart_data_fifoFIFO结构体,用于串口数据的缓冲管理,解决串口数据突发接收时的处理压力
PID_CH_XX轴PID通道,定义X方向对应的PID控制器通道(0),用于区分X轴的PID计算
PID_CH_YY轴PID通道,定义Y方向对应的PID控制器通道(1),用于区分Y轴的PID计算
FOC_CH_X_PINX轴步进电机脉冲引脚,定义X轴步进电机的脉冲控制引脚(B11),用于生成X轴电机的驱动脉冲
FOC_CH_Y_PINY轴步进电机脉冲引脚,定义Y轴步进电机的脉冲控制引脚(B12),用于生成Y轴电机的驱动脉冲
ParseResult.valid解析结果有效性标志,标记串口数据解析是否成功(true为有效),用于判断是否使用解析后的数据
ParseResult.first_num解析结果前三位数字,存储串口数据中X方向相关的前三位数字,用于计算X轴误差
ParseResult.second_num解析结果后三位数字,存储串口数据中Y方向相关的后三位数字,用于计算Y轴误差
gpio_statusGPIO状态变量,预留用于存储GPIO引脚的电平状态,暂未在代码中实际使用
pit_statePIT中断触发标志,标记中频PIT定时器(10ms)是否触发中断(1为触发),用于主循环同步中断状态
target_xX轴目标位置,由串口数据解析并转换得到的X轴目标位置,作为X轴PID控制器的目标值
target_yY轴目标位置,由串口数据解析并转换得到的Y轴目标位置,作为Y轴PID控制器的目标值
freq_XX轴PID输出频率,X轴PID控制器计算得到的输出频率(正负表示方向),用于控制X轴电机转速
freq_YY轴PID输出频率,Y轴PID控制器计算得到的输出频率(正负表示方向),用于控制Y轴电机转速
ticks全局计数器,每10ms递增一次,用于计时和控制周期性任务(如帧率统计、搜索间隔)
fps当前帧率,记录每秒接收的有效数据帧数,反映系统数据接收的实时性
fps_ticks帧计数器,累计1秒内接收的有效帧数,用于计算fps
data_proceed数据处理标志,标记是否有新的解析数据需要更新PID目标(true为需要),用于同步主循环与中断的数据流
update_fps帧率更新标志,标记是否需要更新并打印帧率(true为需要),控制帧率的周期性打印
implus_X_countX轴脉冲计数器,每100us递增一次,用于累计X轴脉冲间隔,触发脉冲生成
implus_Y_countY轴脉冲计数器,每100us递增一次,用于累计Y轴脉冲间隔,触发脉冲生成
skip_pits_XX轴脉冲间隔,X轴两次脉冲之间的100us计数间隔(由频率计算得到),决定X轴电机转速
skip_pits_YY轴脉冲间隔,Y轴两次脉冲之间的100us计数间隔(由频率计算得到),决定Y轴电机转速
Impluse_mode手动脉冲模式标志,标记是否处于手动脉冲模式(true为手动),手动模式下暂停自动脉冲生成
no_found目标未找到标志,标记是否丢失目标(true为丢失),用于触发目标搜索逻辑
lost_frames丢失帧数计数器,累计连续丢失的无效数据帧数,用于判断是否进入目标搜索模式
no_lost_frames连续找到帧数计数器,累计连续接收的有效数据帧数,用于判断是否从搜索模式恢复
impluse_count搜索脉冲计数,累计目标丢失时发送的搜索脉冲总数,用于限制搜索范围(避免无限搜索)
step搜索步长,目标丢失时每次发送的脉冲数(18),控制搜索时的移动距离
skip_ticks搜索间隔计数器,记录上次发送搜索脉冲的ticks值,用于控制搜索动作的间隔(每100ms一次)

核心函数

/**
 * 高频PIT中断处理函数(100us触发一次)
 * 功能:生成步进电机脉冲(根据set_freq设置的间隔)
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void pit_impluse_handler(uint32 state, void *ptr){
    if(Impluse_mode == true) return;  // 手动模式下不生成自动脉冲
    
    // 脉冲计数器递增(每100us+1)
    implus_X_count += 1;
    implus_Y_count += 1;
    
    // X轴:计数器达到间隔时,生成一个脉冲
    if(implus_X_count >= skip_pits_X){
        implus_X_count = 0;  // 重置计数器
        // 生成10us高电平脉冲
        gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
    }
    
    // Y轴:同上
    if(implus_Y_count >= skip_pits_Y){
        implus_Y_count = 0;
        gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
    }
}

pit_impluse_handler是一个高频中断处理函数,每 100 微秒触发一次,核心功能是为步进电机生成精确的控制脉冲。
1、函数首先判断是否处于手动脉冲模式(Impluse_mode)若是则暂停自动脉冲生成;否则分别递增 X 轴和 Y 轴的脉冲计数器(每 100 微秒 + 1)。
2、当计数器值达到预设的脉冲间隔(skip_pits_X或skip_pits_Y,由set_freq_X/Y根据目标频率计算得出)时,会生成一个 10 微秒的高电平脉冲(通过控制对应 GPIO 引脚电平)随后重置计数器。这一机制直接决定了电机的转速 —— 间隔越小,单位时间内生成的脉冲越多,电机转动越快,从而实现根据目标频率实时调节电机速度的效果。

/**
 * 中频PIT中断处理函数(10ms触发一次)
 * 功能:执行PID计算、更新电机频率、帧率统计
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void pit_handler (uint32 state, void *ptr)
{
    ticks += 1;  // 全局计数器递增(每10ms+1)
    
    if(Impluse_mode == true) return;  // 手动模式下不执行自动控制
    
    // 若有新数据,执行PID计算并更新频率
    if(data_proceed){
        // 计算X/Y轴PID输出(频率)
        freq_X = pid_calculate_cam(0, target_x);
        freq_Y = pid_calculate_cam(1, target_y);
        
        // 设置X/Y轴频率(包含方向)
        set_freq_X(freq_X);
        set_freq_Y(freq_Y);
        
        data_proceed = false;  // 重置数据处理标志
    }
    
    pit_state = 1;  // 标记PIT中断已触发
    
    // 每100个中断(10ms×100=1s)更新一次帧率
    if(ticks % 100 == 0){
        tick2 += 1;  // 辅助计数递增
        update_fps = true;  // 标记需更新帧率
        fps = fps_ticks;    // 存储当前帧率
        fps_ticks = 0;      // 重置帧计数器
    }
}

pit_handler是一个中频中断处理函数,每 10 毫秒触发一次,主要负责闭环控制逻辑与状态更新。
1、函数首先递增全局计数器(ticks),若处于手动模式则暂停自动控制;
2、当检测到新数据标志(data_proceed)时,会调用 PID 控制器计算 X 轴和 Y 轴的目标频率(freq_X/Y),并通过set_freq_X/Y更新脉冲间隔,完成后重置数据标志。
3、此外,函数会标记中断触发状态(pit_state),并每 100 次中断(即 1 秒)更新一次帧率统计(fps),通过记录有效帧数量反映系统数据接收的实时性。这一函数是连接数据解析与电机控制的核心,确保根据最新误差动态调整电机控制参数。

计时器中断初始化

pit_ms_init(PIT_TIM_G12, 10, pit_handler, NULL);                            
pit_us_init(PIT_TIM_A0, 100, pit_impluse_handler, NULL);    

总体代码这边因为是电赛控制题基础部分第三题的代码,所以额外还多了一些找不到目标时候的手动收缩函数,但是都是一个逻辑上的区别而已,在找到目标之后还是根据以上讲到的这两个函数进行pid的控制逼近中心收敛。

大概思路如下:
在这里插入图片描述

完整代码如下:

//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------Include区域(头文件包含)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

// 通用头文件(包含基础类型、宏定义等)
#include "zf_common_headfile.h"
// PID控制器头文件(提供PID计算相关函数)
#include "mc_pid_controller.h"
// 延时驱动头文件(提供微秒/毫秒级延时函数)
#include "zf_driver_delay.h"
// GPIO驱动头文件(提供GPIO初始化、电平控制等函数)
#include "zf_device_d36a.h"  // 步进电机驱动相关头文件
#include <math.h>            // 数学函数库(用于atan2等计算)
#include <stdint.h>          // 标准整数类型定义
#include <stdio.h>           // 标准输入输出(用于printf)


//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------Define区域(宏定义)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

// 串口配置:使用UART2
#define UART_INDEX              (UART_2   )                   
// 串口波特率(与调试口一致)
#define UART_BAUDRATE           (DEBUG_UART_BAUDRATE)        
// UART2发送引脚(B15)
#define UART_TX_PIN             (UART2_TX_B15  )              
// UART2接收引脚(B16)
#define UART_RX_PIN             (UART2_RX_B16  )              
// 串口中断优先级(使用UART0的中断编号,实际映射到UART2)
#define UART_PRIORITY           (UART0_INT_IRQn)              

// PIT控制器中断优先级(用于PID计算的定时器)
#define PIT_CON_PRIO           (TIMG12_INT_IRQn)   
// 脉冲生成PIT中断优先级(用于生成步进电机脉冲的定时器)
#define PIT_IMPLUSE_PRIO       (TIMA0_INT_IRQn)  

// 激光控制引脚(A30)
#define LASER_PIN                A30                                

// 串口接收数据缓冲区(最大5049字节,用于暂存接收的原始数据)
uint8 uart_get_data[5049];                                        
// FIFO输出缓冲区(用于从FIFO中读取数据并解析)
uint8 fifo_get_data[5049];                                        

// 单次接收的数据字节
uint8 get_data = 0;                                             
// FIFO中存储的数据个数
uint32 fifo_data_count = 0;                                     

// FIFO结构体(用于串口数据的缓冲,解决数据突发接收问题)
fifo_struct uart_data_fifo;

// PID通道定义:X轴和Y轴分别使用0和1通道
#define PID_CH_X                0  
#define PID_CH_Y                1  

// 步进电机脉冲引脚:X轴(B11)、Y轴(B12)
#define FOC_CH_X_PIN                B11  
#define FOC_CH_Y_PIN                B12  


// 解析结果结构体(存储串口数据解析后的有效信息)
typedef struct {          
    bool valid;           // 数据是否有效(解析成功为true)
    int first_num;        // 前三位数字(X方向相关)
    int second_num;       // 后三位数字(Y方向相关)
} ParseResult;

// GPIO状态变量(预留,用于存储GPIO电平状态)
uint8 gpio_status;


//---------------------------------------------------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------函数定义区域(工具函数与回调函数)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

/**
 * 串口接收中断处理函数
 * 功能:在串口收到数据时触发,将数据写入FIFO缓冲区,避免中断中处理耗时逻辑
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void uart_rx_interrupt_handler (uint32 state, void *ptr)
{ 
    // 读取一个字节(查询式,无数据时不阻塞)
    uart_query_byte(UART_INDEX, &get_data);                                     
    // 将接收的字节写入FIFO,实现数据缓冲
    fifo_write_buffer(&uart_data_fifo, &get_data, 1);                           
}


/**
 * 线性映射函数:将0-200的输入映射到300-1000的输出(反向映射)
 * @param input:输入值(0-200)
 * @return 映射后的输出值(300-1000)
 */
int32_t map_0_200_to_1000_300(int32_t input) {
    // 限制输入范围在0-200
    if (input < 0) {
        input = 0;
    } else if (input > 200) {
        input = 200;
    }
    
    // 反向映射公式:input越小,output越大(1000→300)
    int32_t output = 700 - (input * 550) / 200;
    
    return output;
}


/**
 * 取两个整数的较小值
 * @param a:第一个整数
 * @param b:第二个整数
 * @return 较小的整数
 */
int min_int(int a, int b) {                                               
    if (a < b) {
        return a;
    } else {
        return b;
    }
}


/**
 * 解析串口数据中的X-Y坐标信息
 * 格式要求:数据需包含"XxxxxxxE",其中xxxxxx为6位数字(前3位为X相关,后3位为Y相关)
 * @param data:待解析的字符串
 * @return 解析结果(包含有效性、前三位数字、后三位数字)
 */
ParseResult parse_xy_data(const char *data) {
    ParseResult result = {false, 0, 0};  // 初始化结果为无效
    const char *start, *end;
    char num_str[7];  // 存储6位数字(+1位结束符)
    
    // 查找"X"的位置(数据起始标志)
    start = strchr(data, 'X');
    if (!start) {
        return result;  // 未找到"X",数据无效
    }
    start++;  // 跳过"X",指向数字部分
    
    // 查找"E"的位置(数据结束标志)
    end = strchr(start, 'E');
    if (!end) {
        return result;  // 未找到"E",数据无效
    }
    
    // 检查"X"和"E"之间是否正好6位数字
    if (end - start != 6) {
        return result;  // 位数不正确,数据无效
    }
    
    // 提取6位数字并添加结束符
    strncpy(num_str, start, 6);
    num_str[6] = '\0';  
    
    // 检查是否全为数字
    for (int i = 0; i < 6; i++) {
        if (num_str[i] < '0' || num_str[i] > '9') {
            return result;  // 含非数字字符,数据无效
        }
    }
    
    // 拆分前3位和后3位数字
    char first_str[4] = {0};  // 前3位(+结束符)
    char second_str[4] = {0}; // 后3位(+结束符)
    
    strncpy(first_str, num_str, 3);  // 提取前3位
    strncpy(second_str, num_str + 3, 3);  // 提取后3位
    
    // 转换为整数并标记数据有效
    result.first_num = atoi(first_str);
    result.second_num = atoi(second_str);
    result.valid = true;
    
    return result;
}


/**
 * 将误差值转换为舵机角度(通过几何计算)
 * @param output_x:X方向误差值
 * @return 对应的舵机角度(度)
 */
float output_to_servo(float output_x)
{
    // 计算对边长度:误差值×1.8/66(几何比例参数)
    float numerator = output_x * 1.8f / 66.0f;
    // 计算反正切(邻边固定为15,单位:弧度)
    float radian = atan2f(numerator, 15.0f);  
    // 弧度转角度(×180/π)
    int servo_dx = (int)(radian * 180.0f / 3.1415926f);  
    
    return servo_dx;
}


/**
 * 将误差距离映射到脉冲延迟时间(用于调整步进电机速度)
 * @param distance:误差距离(0-1000)
 * @return 延迟时间(300-3000μs,距离越大延迟越小)
 */
uint16 distance_to_delay(int32 distance) {
    // 取距离绝对值(误差非负)
    uint32 abs_dist = (distance < 0) ? -distance : distance;
    // 限制最大距离为1000(避免延迟过小)
    uint32 limited_dist = (abs_dist > 1000) ? 1000 : abs_dist;
    // 反向映射:距离越大,延迟越小(3000→300)
    return 300 - (limited_dist * 200) / 300;  
}


//---------------------------------------------------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------全局变量定义(状态与参数存储)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

uint8_t pit_state = 0;               // PIT中断触发标志(1表示触发)
float target_x = 0, target_y = 0;    // X/Y轴目标位置(由串口数据解析得到)
int16_t freq_X, freq_Y;              // X/Y轴PID计算输出的频率
int ticks = 0;                       // 全局计数器(10ms递增一次)
int fps = 0, fps_ticks = 0;          // 帧率相关:fps为当前帧率,fps_ticks为帧计数
bool data_proceed = false;           // 数据处理标志(1表示需更新PID目标)
bool update_fps = false;             // 帧率更新标志(1表示需打印帧率)


// 脉冲计数与间隔变量(用于高频脉冲生成)
int implus_X_count = 0, implus_Y_count = 0;  // X/Y轴脉冲计数器(100us递增)
float skip_pits_X = 999999.0f, skip_pits_Y = 999999.0f;  // X/Y轴脉冲间隔(100us为单位)

/**
 * 设置X轴步进电机频率
 * @param freq:频率值(正负表示方向,绝对值为频率)
 */
void set_freq_X(float freq){
    // 设置方向引脚:频率>0时正转,否则反转
    gpio_set_level(D36A_IN4_PIN, freq > 0 ? 1 : 0);  
    // 取频率绝对值(仅关心大小)
    freq = fabsf(freq);
    // 频率为0时,间隔设为极大值(不生成脉冲)
    if(freq == 0) skip_pits_X = 999999;
    // 计算脉冲间隔:10000(100us×100)/频率 = 间隔(单位:100us)
    else skip_pits_X = 10000.0f/freq;
}

/**
 * 设置Y轴步进电机频率
 * @param freq:频率值(正负表示方向,绝对值为频率)
 */
void set_freq_Y(float freq){
    // 设置方向引脚:频率<0时正转,否则反转(与X轴逻辑相反)
    gpio_set_level(D36A_IN2_PIN, freq < 0 ? 1 : 0);  
    // 取频率绝对值
    freq = fabsf(freq);
    // 频率为0时不生成脉冲
    if(freq == 0) skip_pits_Y = 999999;
    // 计算脉冲间隔(同X轴)
    else skip_pits_Y = 10000.0f/freq;
}

bool Impluse_mode = false;  // 手动脉冲模式标志(1表示手动模式,暂停自动控制)

/**
 * 手动发送脉冲序列(用于复位、搜索等特定动作)
 * @param ch:通道(0=X轴,1=Y轴)
 * @param impluse:脉冲数(正负表示方向)
 * @param delay_us:基础延迟时间(μs)
 */
void SendImpluse(uint8_t ch,int16 impluse,int16_t delay_us){
    Impluse_mode = true;  // 进入手动模式,暂停自动脉冲生成
    impluse *= 16.5;      // 脉冲数缩放(根据机械结构调整)
    
    if (ch == 0){  // X轴处理
        // 设置方向:脉冲数>0时正转
        gpio_set_level(D36A_IN4_PIN, impluse > 0 ? 1 : 0);  
        impluse = abs(impluse);  // 取绝对值(仅关心脉冲数量)
        // 计算减速点:前70%脉冲正常速度,后30%减速
        uint16_t slow_now = impluse * 0.7;
        uint16_t delay = delay_us;  // 基础延迟
        
        // 循环生成脉冲
        for(uint16_t i = 0;i < impluse;i ++){
            // 超过减速点后,延迟增至3倍(降低速度,避免过冲)
            if(i > slow_now) delay = delay_us * 3;
            // 生成10us高电平脉冲
            gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
            system_delay_us(10);
            gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
            // 脉冲间延迟
            system_delay_us(delay);
        }
    }
    else{  // Y轴处理(逻辑同X轴,引脚不同)
        // 设置方向:脉冲数<0时正转
        gpio_set_level(D36A_IN2_PIN, impluse < 0 ? 1 : 0);
        impluse = abs(impluse);
        uint16_t slow_now = impluse * 0.7;
        uint16_t delay = delay_us;
        
        for(uint16_t i = 0;i < impluse;i ++){
            if(i > slow_now) delay = delay_us * 3;
            gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
            system_delay_us(10);
            gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
            system_delay_us(delay);
        }
    }
    Impluse_mode = false;  // 退出手动模式,恢复自动控制
}

/**
 * 复位Y轴偏移(特定机械结构的复位动作)
 */
void ResetOffset(){
    SendImpluse(1,-100,100);  // Y轴反向发送100脉冲(基础延迟100us)
    system_delay_ms(100);     // 等待动作完成
    SendImpluse(1,50,100);   // Y轴正向发送50脉冲
    system_delay_ms(50);      // 等待动作完成
}

/**
 * X轴搜索动作(用于丢失目标时的范围搜索)
 */
void SearchAtX(){
    ResetOffset();               // 先复位Y轴
    SendImpluse(0,100,1000);    // X轴正向100脉冲(延迟1000us,慢速)
    SendImpluse(0,-100,100);    // X轴反向100脉冲(延迟100us,快速)
    SendImpluse(0,-100,1000);   // X轴反向100脉冲(延迟1000us,慢速)
}


/**
 * 高频PIT中断处理函数(100us触发一次)
 * 功能:生成步进电机脉冲(根据set_freq设置的间隔)
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void pit_impluse_handler(uint32 state, void *ptr){
    if(Impluse_mode == true) return;  // 手动模式下不生成自动脉冲
    
    // 脉冲计数器递增(每100us+1)
    implus_X_count += 1;
    implus_Y_count += 1;
    
    // X轴:计数器达到间隔时,生成一个脉冲
    if(implus_X_count >= skip_pits_X){
        implus_X_count = 0;  // 重置计数器
        // 生成10us高电平脉冲
        gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
    }
    
    // Y轴:同上
    if(implus_Y_count >= skip_pits_Y){
        implus_Y_count = 0;
        gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
    }
}

int tick2 = 0;  // 辅助计数器(用于调试)

/**
 * 中频PIT中断处理函数(10ms触发一次)
 * 功能:执行PID计算、更新电机频率、帧率统计
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void pit_handler (uint32 state, void *ptr)
{
    ticks += 1;  // 全局计数器递增(每10ms+1)
    
    if(Impluse_mode == true) return;  // 手动模式下不执行自动控制
    
    // 若有新数据,执行PID计算并更新频率
    if(data_proceed){
        // 计算X/Y轴PID输出(频率)
        freq_X = pid_calculate_cam(0, target_x);
        freq_Y = pid_calculate_cam(1, target_y);
        
        // 设置X/Y轴频率(包含方向)
        set_freq_X(freq_X);
        set_freq_Y(freq_Y);
        
        data_proceed = false;  // 重置数据处理标志
    }
    
    pit_state = 1;  // 标记PIT中断已触发
    
    // 每100个中断(10ms×100=1s)更新一次帧率
    if(ticks % 100 == 0){
        tick2 += 1;  // 辅助计数递增
        update_fps = true;  // 标记需更新帧率
        fps = fps_ticks;    // 存储当前帧率
        fps_ticks = 0;      // 重置帧计数器
    }
}


//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------主函数(程序入口)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

bool no_found = 0;          // 目标未找到标志
int lost_frames = 0;        // 丢失帧数计数器
int no_lost_frames = 0;     // 连续找到帧数计数器
int impluse_count = 0;      // 搜索脉冲计数
int16_t step = 0;           // 搜索步长
int skip_ticks = 0;         // 搜索间隔计数器

/**
 * 主函数:初始化系统并进入主循环
 */
int main(void) {
    // 初始化系统时钟(80MHz)
    clock_init(SYSTEM_CLOCK_80M);  
    // 初始化调试口(用于printf输出)
    debug_init();
    // 初始化步进电机驱动(配置相关引脚)
    d36a_init();                   
    printf("begin\n");  // 打印启动信息

    // 初始化FIFO(8位数据,缓冲区为uart_get_data,大小64字节)
    fifo_init(&uart_data_fifo, FIFO_DATA_8BIT, uart_get_data, 64);
    // 初始化串口(波特率、引脚)
    uart_init(UART_INDEX, UART_BAUDRATE, UART_TX_PIN, UART_RX_PIN);
    // 使能串口接收中断
    uart_set_interrupt_config(UART_INDEX, UART_INTERRUPT_CONFIG_RX_ENABLE);
    // 设置串口中断优先级
    interrupt_set_priority(UART_PRIORITY, 0);
    // 绑定串口接收中断回调函数
    uart_set_callback(UART_INDEX, uart_rx_interrupt_handler, NULL);

    printf("pid\n");  // 打印PID初始化信息
    // 初始化X轴PID(位置式,Kp=5, Ki=0, Kd=0,输出限幅±1500)
    pid_init(0, PID_POSITIONAL, 5, 0, 0, 1, 0, 1500);
    // 初始化Y轴PID(参数同X轴)
    pid_init(1, PID_POSITIONAL, 5, 0, 0, 1, 0, 1500);
    
    // 初始化激光引脚(推挽输出,初始高电平)
    gpio_init(LASER_PIN, GPO, 1, GPO_PUSH_PULL);
    // 初始化中频PIT(10ms中断,绑定pit_handler)
    pit_ms_init(PIT_TIM_G12, 10, pit_handler, NULL);                            
    // 初始化高频PIT(100us中断,绑定pit_impluse_handler)
    pit_us_init(PIT_TIM_A0, 100, pit_impluse_handler, NULL);                            
    // 设置PIT中断优先级
    interrupt_set_priority(PIT_CON_PRIO, 0);
    interrupt_set_priority(PIT_IMPLUSE_PRIO, 1);

    printf("int\n");  // 打印中断初始化信息
    printf("init\n"); // 打印初始化完成信息
    ResetOffset();    // 执行Y轴复位

    // 主循环(程序核心逻辑)
    while (1) {
        // 1. 处理串口数据(从FIFO中读取并解析)
        fifo_data_count = fifo_used(&uart_data_fifo);  // 获取FIFO中数据量

        // 若FIFO中数据超过6字节(满足最小解析长度)
        if (fifo_data_count > 6) {
            memset(fifo_get_data,0,sizeof(fifo_get_data));  // 清空解析缓冲区
            // 从FIFO读取数据并清空已读部分
            fifo_read_buffer(&uart_data_fifo, fifo_get_data, &fifo_data_count, FIFO_READ_AND_CLEAN);
            fifo_get_data[fifo_data_count] = '\0';  // 添加字符串结束符

            // 解析数据(提取X-Y相关数字)
            ParseResult parsed = parse_xy_data((const char*)fifo_get_data);
            
            // 若解析有效
            if (parsed.valid) {
                // 计算X/Y方向误差(目标值为500,偏离量=实际值-500)
                int32 dx = parsed.first_num - 500 ;   
                int32 dy = parsed.second_num - 500;  
                
                // 过滤无效误差(499为特殊无效值)
                if  (dx!=499 && dy!=499){
                    fps_ticks += 1;  // 有效帧计数+1

                    // 计算目标位置(通过几何转换)
                    float dd_x = output_to_servo(dx);
                    float dd_y = output_to_servo(dy);
                    target_x = dd_x * 16.5 - 67;  // X轴目标位置(机械补偿)
                    target_y = dd_y * 16.5 - 5;   // Y轴目标位置(机械补偿)
                    
                    data_proceed = true;  // 标记需更新PID目标

                    // 动态调整X轴PID参数(小误差时降低增益,避免震荡)
                    if(fabs(target_x)<15) pid_set_params(0, 0.8, 0, 1, 0);
                    else pid_set_params(0, 4, 0, 3, 0);
                    
                    // 动态调整Y轴PID参数(同X轴逻辑)
                    if(fabs(target_y)<15) pid_set_params(1, 0.8, 0, 1, 0);
                    else pid_set_params(1, 4, 0, 3, 0);
                    
                    // 当误差较小时(接近目标),开启激光
                    if(fabs(target_y)<15 && fabs(target_x)<15){
                        gpio_set_level(LASER_PIN,GPIO_HIGH);
                    }else{
                        gpio_set_level(LASER_PIN,GPIO_LOW);
                    }
                    
                    no_lost_frames ++;  // 连续找到帧数+1
                    // 打印目标位置(调试用)
                    printf("x:%f y:%f \n",target_x,target_y);
                }
                else{
                    // 无效误差时,停止电机
                    target_x = 0;
                    target_y = 0;
                    set_freq_X(0);
                    set_freq_Y(0);
                    lost_frames ++;  // 丢失帧数+1
                }
            }
        }
        
        // 2. 打印帧率(每1s一次)
        if(update_fps){
            printf("fps:%d\n",fps);  // 打印当前帧率
            pit_state = 0;
            update_fps = 0;  // 重置帧率更新标志
        }
        
        // 3. 处理PIT中断标志(仅标记用,无实际操作)
        if(pit_state){
            pit_state = 0;
        }
        
        // 4. 目标丢失处理(连续丢失20帧以上)
        printf("n:%d\n",lost_frames);  // 打印丢失帧数(调试用)
        if(lost_frames > 20){
            // 首次进入丢失状态时初始化搜索参数
            if(no_found == false){ 
                no_lost_frames = 0;
                skip_ticks = ticks;  // 记录当前计数
                step = 18;  // 初始搜索步长
            }
            no_found = true;  // 标记目标丢失
        }
        
        // 5. 重新找到目标时,重置丢失状态
        if(no_lost_frames > 0){
            if(no_found == true){
                lost_frames = 0;  // 重置丢失帧数
            }
            no_found = false;  // 标记目标已找到
        }
        
        // 6. 目标丢失时执行搜索动作(每100ms一次)
        if(ticks - skip_ticks > 10 && no_found){
            skip_ticks = ticks;  // 更新搜索间隔计数
            
            // 限制搜索范围(超过±100时反向)
            step = (impluse_count > 100) ? -18 : (impluse_count < -100) ? 18 : step;
            // 发送搜索脉冲(X轴,步长18,延迟180us)
            SendImpluse(0,step,180);
            impluse_count += step;  // 更新搜索脉冲计数
        }
        else
            system_delay_ms(5);  // 主循环小延时(降低CPU占用)
    }
}

MSPM0開發學習筆記:非阻塞二維雲臺運動代碼

針對二維雲台追蹤目標時抖動大、PID 難調的痛點,本篇分析阻塞型步進控制在視覺幀間歇「停‑啟」所帶來的衝擊,並提出在 MSPM0 上以非阻塞方式按最新視覺誤差即時刷新步進脈衝頻率,從而在保持閉環回應速度的同時壓低機械抖動。

來源:https://blog.csdn.net/2403_87969572/article/details/149958611

抓取時間(ISO本地):2026-05-18 05:17:07


文章目錄


前言

在前幾篇的博客中,有提到一個追蹤紅色小球的代碼,但是會發現追蹤的過程中存在很大的抖動並且響應速度相對較慢,推測其原因是因為步進電機的控制函數是阻塞型的,就是說步進電機一定要以某一個給定的速度走完當前規定的角度步數,才會根據新接受到的視覺誤差數據再次進行調整運動,而在接受的這幾毫秒,步進電機的頻率是0,也就是說會直接停下來,之後又根據新的誤差數據馬上運動起來,所以會有很大的抖動,非常影響識別,也導致pid很難調,沒有辦法達到一個很好的效果。

本章博客的目的就是介紹一個新的運動思路以及代碼,通過實時根據接受到的視覺數據進行二維雲臺的步進電機的頻率更新,從而避免大幅的抖動以及保證電機數據更新的時效性,更好的瞄準目標。


如果無法很好的復現博客裡的代碼,可以私信作者博取源代碼,電賽期間都在線

一、硬件選擇

主控:MSPM0G3507
驅動:D36A雙路步進電機驅動
電機:42步進電機*2
視覺:樹莓派連接USB攝像頭

二、硬件連線

硬件連線部分在前幾篇博客裡面已經說過了,可以直接去裡面看,這邊附上鍊接
機器視覺:樹莓派結合攝像頭解決2025電賽E題鎖定靶心 思路與代碼分享

三、軟件代碼

1、思路

對於非阻塞的控制,一開始的思路是通過GPIO引腳輸出PWM波,通過接受UART信息的中斷實時更新PWM的佔空比從而控制給D36A的ST引腳上升沿脈衝的數量和速度(D36A是42步進電機的閉環集成驅動模塊,給ST引腳一個脈衝步進電機就會轉動一步,具體可以去看看前幾章的博客那邊有具體講到)。但是後面發現可能不能用這個高的頻率(樹莓派給的視覺誤差信息幀率可以達到一百幀)去更新PWM引腳的佔空比,一運行這個程序TI芯片就會卡死。

因此後面我們換了一種思路,不用GPIO引腳來輸出PWM波來進行控制,而是通過我們自己設置引腳的高低電平進而給ST引腳脈衝,我們只需要設置每一次循環的延時就可以控制給上升沿脈衝的數量與速度。這樣就不需要頻繁的改變PWM引腳的佔空比導致芯片卡死。

2、代碼實現

軟件部分採用C語言實現,IDE採用keil,基於逐飛庫進行編寫
這邊先進
行一下簡單的參數說明,便於理解後面的思路

參數含義以及作用
UART_INDEX串口索引,定義使用的串口(此處為UART_2),用於指定數據接收和發送的硬件串口
UART_BAUDRATE串口波特率,定義串口通信的波特率(與調試口一致),確保與外部設備通信速率匹配
UART_TX_PIN串口發送引腳,定義UART_2的發送引腳(B15),用於串口數據的發送
UART_RX_PIN串口接收引腳,定義UART_2的接收引腳(B16),用於串口數據的接收
UART_PRIORITY串口中斷優先級,指定串口接收中斷的優先級(使用UART0的中斷編號映射到UART2),控制中斷響應順序
PIT_CON_PRIO中頻PIT中斷優先級,定義用於PID計算的定時器(TIMG12)中斷優先級,確保PID計算的實時性
PIT_IMPLUSE_PRIO高頻PIT中斷優先級,定義用於生成步進電機脈衝的定時器(TIMA0)中斷優先級,確保脈衝生成的高精度
LASER_PIN激光控制引腳,定義控制激光的GPIO引腳(A30),用於在接近目標時開啟/關閉激光
uart_get_data串口接收數據緩衝區,存儲串口接收的原始數據(最大5049字節),避免數據丟失
fifo_get_dataFIFO輸出緩衝區,從FIFO中讀取數據並用於解析,臨時存儲待解析的串口數據
get_data單次接收數據字節,存儲串口中斷中單次接收的一個字節數據
fifo_data_countFIFO數據個數,記錄FIFO緩衝區中當前存儲的數據字節數,用於判斷是否有足夠數據可解析
uart_data_fifoFIFO結構體,用於串口數據的緩衝管理,解決串口數據突發接收時的處理壓力
PID_CH_XX軸PID通道,定義X方向對應的PID控制器通道(0),用於區分X軸的PID計算
PID_CH_YY軸PID通道,定義Y方向對應的PID控制器通道(1),用於區分Y軸的PID計算
FOC_CH_X_PINX軸步進電機脈衝引腳,定義X軸步進電機的脈衝控制引腳(B11),用於生成X軸電機的驅動脈衝
FOC_CH_Y_PINY軸步進電機脈衝引腳,定義Y軸步進電機的脈衝控制引腳(B12),用於生成Y軸電機的驅動脈衝
ParseResult.valid解析結果有效性標誌,標記串口數據解析是否成功(true為有效),用於判斷是否使用解析後的數據
ParseResult.first_num解析結果前三位數字,存儲串口數據中X方向相關的前三位數字,用於計算X軸誤差
ParseResult.second_num解析結果後三位數字,存儲串口數據中Y方向相關的後三位數字,用於計算Y軸誤差
gpio_statusGPIO狀態變量,預留用於存儲GPIO引腳的電平狀態,暫未在代碼中實際使用
pit_statePIT中斷觸發標誌,標記中頻PIT定時器(10ms)是否觸發中斷(1為觸發),用於主循環同步中斷狀態
target_xX軸目標位置,由串口數據解析並轉換得到的X軸目標位置,作為X軸PID控制器的目標值
target_yY軸目標位置,由串口數據解析並轉換得到的Y軸目標位置,作為Y軸PID控制器的目標值
freq_XX軸PID輸出頻率,X軸PID控制器計算得到的輸出頻率(正負表示方向),用於控制X軸電機轉速
freq_YY軸PID輸出頻率,Y軸PID控制器計算得到的輸出頻率(正負表示方向),用於控制Y軸電機轉速
ticks全局計數器,每10ms遞增一次,用於計時和控制週期性任務(如幀率統計、搜索間隔)
fps當前幀率,記錄每秒接收的有效數據幀數,反映系統數據接收的實時性
fps_ticks幀計數器,累計1秒內接收的有效幀數,用於計算fps
data_proceed數據處理標誌,標記是否有新的解析數據需要更新PID目標(true為需要),用於同步主循環與中斷的數據流
update_fps幀率更新標誌,標記是否需要更新並打印幀率(true為需要),控制幀率的週期性打印
implus_X_countX軸脈衝計數器,每100us遞增一次,用於累計X軸脈衝間隔,觸發脈衝生成
implus_Y_countY軸脈衝計數器,每100us遞增一次,用於累計Y軸脈衝間隔,觸發脈衝生成
skip_pits_XX軸脈衝間隔,X軸兩次脈衝之間的100us計數間隔(由頻率計算得到),決定X軸電機轉速
skip_pits_YY軸脈衝間隔,Y軸兩次脈衝之間的100us計數間隔(由頻率計算得到),決定Y軸電機轉速
Impluse_mode手動脈衝模式標誌,標記是否處於手動脈衝模式(true為手動),手動模式下暫停自動脈衝生成
no_found目標未找到標誌,標記是否丟失目標(true為丟失),用於觸發目標搜索邏輯
lost_frames丟失幀數計數器,累計連續丟失的無效數據幀數,用於判斷是否進入目標搜索模式
no_lost_frames連續找到幀數計數器,累計連續接收的有效數據幀數,用於判斷是否從搜索模式恢復
impluse_count搜索脈衝計數,累計目標丟失時發送的搜索脈衝總數,用於限制搜索範圍(避免無限搜索)
step搜索步長,目標丟失時每次發送的脈衝數(18),控制搜索時的移動距離
skip_ticks搜索間隔計數器,記錄上次發送搜索脈衝的ticks值,用於控制搜索動作的間隔(每100ms一次)

核心函數

/**
 * 高頻PIT中斷處理函數(100us觸發一次)
 * 功能:生成步進電機脈衝(根據set_freq設置的間隔)
 * @param state:中斷狀態(未使用)
 * @param ptr:用戶參數(未使用)
 */
void pit_impluse_handler(uint32 state, void *ptr){
    if(Impluse_mode == true) return;  // 手動模式下不生成自動脈衝
    
    // 脈衝計數器遞增(每100us+1)
    implus_X_count += 1;
    implus_Y_count += 1;
    
    // X軸:計數器達到間隔時,生成一個脈衝
    if(implus_X_count >= skip_pits_X){
        implus_X_count = 0;  // 重置計數器
        // 生成10us高電平脈衝
        gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
    }
    
    // Y軸:同上
    if(implus_Y_count >= skip_pits_Y){
        implus_Y_count = 0;
        gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
    }
}

pit_impluse_handler是一個高頻中斷處理函數,每 100 微秒觸發一次,核心功能是為步進電機生成精確的控制脈衝。
1、函數首先判斷是否處於手動脈衝模式(Impluse_mode)若是則暫停自動脈衝生成;否則分別遞增 X 軸和 Y 軸的脈衝計數器(每 100 微秒 + 1)。
2、當計數器值達到預設的脈衝間隔(skip_pits_X或skip_pits_Y,由set_freq_X/Y根據目標頻率計算得出)時,會生成一個 10 微秒的高電平脈衝(通過控制對應 GPIO 引腳電平)隨後重置計數器。這一機制直接決定了電機的轉速 —— 間隔越小,單位時間內生成的脈衝越多,電機轉動越快,從而實現根據目標頻率實時調節電機速度的效果。

/**
 * 中頻PIT中斷處理函數(10ms觸發一次)
 * 功能:執行PID計算、更新電機頻率、幀率統計
 * @param state:中斷狀態(未使用)
 * @param ptr:用戶參數(未使用)
 */
void pit_handler (uint32 state, void *ptr)
{
    ticks += 1;  // 全局計數器遞增(每10ms+1)
    
    if(Impluse_mode == true) return;  // 手動模式下不執行自動控制
    
    // 若有新數據,執行PID計算並更新頻率
    if(data_proceed){
        // 計算X/Y軸PID輸出(頻率)
        freq_X = pid_calculate_cam(0, target_x);
        freq_Y = pid_calculate_cam(1, target_y);
        
        // 設置X/Y軸頻率(包含方向)
        set_freq_X(freq_X);
        set_freq_Y(freq_Y);
        
        data_proceed = false;  // 重置數據處理標誌
    }
    
    pit_state = 1;  // 標記PIT中斷已觸發
    
    // 每100箇中斷(10ms×100=1s)更新一次幀率
    if(ticks % 100 == 0){
        tick2 += 1;  // 輔助計數遞增
        update_fps = true;  // 標記需更新幀率
        fps = fps_ticks;    // 存儲當前幀率
        fps_ticks = 0;      // 重置幀計數器
    }
}

pit_handler是一箇中頻中斷處理函數,每 10 毫秒觸發一次,主要負責閉環控制邏輯與狀態更新。
1、函數首先遞增全局計數器(ticks),若處於手動模式則暫停自動控制;
2、當檢測到新數據標誌(data_proceed)時,會調用 PID 控制器計算 X 軸和 Y 軸的目標頻率(freq_X/Y),並通過set_freq_X/Y更新脈衝間隔,完成後重置數據標誌。
3、此外,函數會標記中斷觸發狀態(pit_state),並每 100 次中斷(即 1 秒)更新一次幀率統計(fps),通過記錄有效幀數量反映系統數據接收的實時性。這一函數是連接數據解析與電機控制的核心,確保根據最新誤差動態調整電機控制參數。

計時器中斷初始化

pit_ms_init(PIT_TIM_G12, 10, pit_handler, NULL);                            
pit_us_init(PIT_TIM_A0, 100, pit_impluse_handler, NULL);    

總體代碼這邊因為是電賽控制題基礎部分第三題的代碼,所以額外還多了一些找不到目標時候的手動收縮函數,但是都是一個邏輯上的區別而已,在找到目標之後還是根據以上講到的這兩個函數進行pid的控制逼近中心收斂。

大概思路如下:
在這裡插入圖片描述

完整代碼如下:

//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------Include區域(頭文件包含)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

// 通用頭文件(包含基礎類型、宏定義等)
#include "zf_common_headfile.h"
// PID控制器頭文件(提供PID計算相關函數)
#include "mc_pid_controller.h"
// 延時驅動頭文件(提供微秒/毫秒級延時函數)
#include "zf_driver_delay.h"
// GPIO驅動頭文件(提供GPIO初始化、電平控制等函數)
#include "zf_device_d36a.h"  // 步進電機驅動相關頭文件
#include <math.h>            // 數學函數庫(用於atan2等計算)
#include <stdint.h>          // 標準整數類型定義
#include <stdio.h>           // 標準輸入輸出(用於printf)


//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------Define區域(宏定義)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

// 串口配置:使用UART2
#define UART_INDEX              (UART_2   )                   
// 串口波特率(與調試口一致)
#define UART_BAUDRATE           (DEBUG_UART_BAUDRATE)        
// UART2發送引腳(B15)
#define UART_TX_PIN             (UART2_TX_B15  )              
// UART2接收引腳(B16)
#define UART_RX_PIN             (UART2_RX_B16  )              
// 串口中斷優先級(使用UART0的中斷編號,實際映射到UART2)
#define UART_PRIORITY           (UART0_INT_IRQn)              

// PIT控制器中斷優先級(用於PID計算的定時器)
#define PIT_CON_PRIO           (TIMG12_INT_IRQn)   
// 脈衝生成PIT中斷優先級(用於生成步進電機脈衝的定時器)
#define PIT_IMPLUSE_PRIO       (TIMA0_INT_IRQn)  

// 激光控制引腳(A30)
#define LASER_PIN                A30                                

// 串口接收數據緩衝區(最大5049字節,用於暫存接收的原始數據)
uint8 uart_get_data[5049];                                        
// FIFO輸出緩衝區(用於從FIFO中讀取數據並解析)
uint8 fifo_get_data[5049];                                        

// 單次接收的數據字節
uint8 get_data = 0;                                             
// FIFO中存儲的數據個數
uint32 fifo_data_count = 0;                                     

// FIFO結構體(用於串口數據的緩衝,解決數據突發接收問題)
fifo_struct uart_data_fifo;

// PID通道定義:X軸和Y軸分別使用0和1通道
#define PID_CH_X                0  
#define PID_CH_Y                1  

// 步進電機脈衝引腳:X軸(B11)、Y軸(B12)
#define FOC_CH_X_PIN                B11  
#define FOC_CH_Y_PIN                B12  


// 解析結果結構體(存儲串口數據解析後的有效信息)
typedef struct {          
    bool valid;           // 數據是否有效(解析成功為true)
    int first_num;        // 前三位數字(X方向相關)
    int second_num;       // 後三位數字(Y方向相關)
} ParseResult;

// GPIO狀態變量(預留,用於存儲GPIO電平狀態)
uint8 gpio_status;


//---------------------------------------------------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------函數定義區域(工具函數與回調函數)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

/**
 * 串口接收中斷處理函數
 * 功能:在串口收到數據時觸發,將數據寫入FIFO緩衝區,避免中斷中處理耗時邏輯
 * @param state:中斷狀態(未使用)
 * @param ptr:用戶參數(未使用)
 */
void uart_rx_interrupt_handler (uint32 state, void *ptr)
{ 
    // 讀取一個字節(查詢式,無數據時不阻塞)
    uart_query_byte(UART_INDEX, &get_data);                                     
    // 將接收的字節寫入FIFO,實現數據緩衝
    fifo_write_buffer(&uart_data_fifo, &get_data, 1);                           
}


/**
 * 線性映射函數:將0-200的輸入映射到300-1000的輸出(反向映射)
 * @param input:輸入值(0-200)
 * @return 映射後的輸出值(300-1000)
 */
int32_t map_0_200_to_1000_300(int32_t input) {
    // 限制輸入範圍在0-200
    if (input < 0) {
        input = 0;
    } else if (input > 200) {
        input = 200;
    }
    
    // 反向映射公式:input越小,output越大(1000→300)
    int32_t output = 700 - (input * 550) / 200;
    
    return output;
}


/**
 * 取兩個整數的較小值
 * @param a:第一個整數
 * @param b:第二個整數
 * @return 較小的整數
 */
int min_int(int a, int b) {                                               
    if (a < b) {
        return a;
    } else {
        return b;
    }
}


/**
 * 解析串口數據中的X-Y座標信息
 * 格式要求:數據需包含"XxxxxxxE",其中xxxxxx為6位數字(前3位為X相關,後3位為Y相關)
 * @param data:待解析的字符串
 * @return 解析結果(包含有效性、前三位數字、後三位數字)
 */
ParseResult parse_xy_data(const char *data) {
    ParseResult result = {false, 0, 0};  // 初始化結果為無效
    const char *start, *end;
    char num_str[7];  // 存儲6位數字(+1位結束符)
    
    // 查找"X"的位置(數據起始標誌)
    start = strchr(data, 'X');
    if (!start) {
        return result;  // 未找到"X",數據無效
    }
    start++;  // 跳過"X",指向數字部分
    
    // 查找"E"的位置(數據結束標誌)
    end = strchr(start, 'E');
    if (!end) {
        return result;  // 未找到"E",數據無效
    }
    
    // 檢查"X"和"E"之間是否正好6位數字
    if (end - start != 6) {
        return result;  // 位數不正確,數據無效
    }
    
    // 提取6位數字並添加結束符
    strncpy(num_str, start, 6);
    num_str[6] = '\0';  
    
    // 檢查是否全為數字
    for (int i = 0; i < 6; i++) {
        if (num_str[i] < '0' || num_str[i] > '9') {
            return result;  // 含非數字字符,數據無效
        }
    }
    
    // 拆分前3位和後3位數字
    char first_str[4] = {0};  // 前3位(+結束符)
    char second_str[4] = {0}; // 後3位(+結束符)
    
    strncpy(first_str, num_str, 3);  // 提取前3位
    strncpy(second_str, num_str + 3, 3);  // 提取後3位
    
    // 轉換為整數並標記數據有效
    result.first_num = atoi(first_str);
    result.second_num = atoi(second_str);
    result.valid = true;
    
    return result;
}


/**
 * 將誤差值轉換為舵機角度(通過幾何計算)
 * @param output_x:X方向誤差值
 * @return 對應的舵機角度(度)
 */
float output_to_servo(float output_x)
{
    // 計算對邊長度:誤差值×1.8/66(幾何比例參數)
    float numerator = output_x * 1.8f / 66.0f;
    // 計算反正切(鄰邊固定為15,單位:弧度)
    float radian = atan2f(numerator, 15.0f);  
    // 弧度轉角度(×180/π)
    int servo_dx = (int)(radian * 180.0f / 3.1415926f);  
    
    return servo_dx;
}


/**
 * 將誤差距離映射到脈衝延遲時間(用於調整步進電機速度)
 * @param distance:誤差距離(0-1000)
 * @return 延遲時間(300-3000μs,距離越大延遲越小)
 */
uint16 distance_to_delay(int32 distance) {
    // 取距離絕對值(誤差非負)
    uint32 abs_dist = (distance < 0) ? -distance : distance;
    // 限制最大距離為1000(避免延遲過小)
    uint32 limited_dist = (abs_dist > 1000) ? 1000 : abs_dist;
    // 反向映射:距離越大,延遲越小(3000→300)
    return 300 - (limited_dist * 200) / 300;  
}


//---------------------------------------------------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------全局變量定義(狀態與參數存儲)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

uint8_t pit_state = 0;               // PIT中斷觸發標誌(1表示觸發)
float target_x = 0, target_y = 0;    // X/Y軸目標位置(由串口數據解析得到)
int16_t freq_X, freq_Y;              // X/Y軸PID計算輸出的頻率
int ticks = 0;                       // 全局計數器(10ms遞增一次)
int fps = 0, fps_ticks = 0;          // 幀率相關:fps為當前幀率,fps_ticks為幀計數
bool data_proceed = false;           // 數據處理標誌(1表示需更新PID目標)
bool update_fps = false;             // 幀率更新標誌(1表示需打印幀率)


// 脈衝計數與間隔變量(用於高頻脈衝生成)
int implus_X_count = 0, implus_Y_count = 0;  // X/Y軸脈衝計數器(100us遞增)
float skip_pits_X = 999999.0f, skip_pits_Y = 999999.0f;  // X/Y軸脈衝間隔(100us為單位)

/**
 * 設置X軸步進電機頻率
 * @param freq:頻率值(正負表示方向,絕對值為頻率)
 */
void set_freq_X(float freq){
    // 設置方向引腳:頻率>0時正轉,否則反轉
    gpio_set_level(D36A_IN4_PIN, freq > 0 ? 1 : 0);  
    // 取頻率絕對值(僅關心大小)
    freq = fabsf(freq);
    // 頻率為0時,間隔設為極大值(不生成脈衝)
    if(freq == 0) skip_pits_X = 999999;
    // 計算脈衝間隔:10000(100us×100)/頻率 = 間隔(單位:100us)
    else skip_pits_X = 10000.0f/freq;
}

/**
 * 設置Y軸步進電機頻率
 * @param freq:頻率值(正負表示方向,絕對值為頻率)
 */
void set_freq_Y(float freq){
    // 設置方向引腳:頻率<0時正轉,否則反轉(與X軸邏輯相反)
    gpio_set_level(D36A_IN2_PIN, freq < 0 ? 1 : 0);  
    // 取頻率絕對值
    freq = fabsf(freq);
    // 頻率為0時不生成脈衝
    if(freq == 0) skip_pits_Y = 999999;
    // 計算脈衝間隔(同X軸)
    else skip_pits_Y = 10000.0f/freq;
}

bool Impluse_mode = false;  // 手動脈衝模式標誌(1表示手動模式,暫停自動控制)

/**
 * 手動發送脈衝序列(用於復位、搜索等特定動作)
 * @param ch:通道(0=X軸,1=Y軸)
 * @param impluse:脈衝數(正負表示方向)
 * @param delay_us:基礎延遲時間(μs)
 */
void SendImpluse(uint8_t ch,int16 impluse,int16_t delay_us){
    Impluse_mode = true;  // 進入手動模式,暫停自動脈衝生成
    impluse *= 16.5;      // 脈衝數縮放(根據機械結構調整)
    
    if (ch == 0){  // X軸處理
        // 設置方向:脈衝數>0時正轉
        gpio_set_level(D36A_IN4_PIN, impluse > 0 ? 1 : 0);  
        impluse = abs(impluse);  // 取絕對值(僅關心脈衝數量)
        // 計算減速點:前70%脈衝正常速度,後30%減速
        uint16_t slow_now = impluse * 0.7;
        uint16_t delay = delay_us;  // 基礎延遲
        
        // 循環生成脈衝
        for(uint16_t i = 0;i < impluse;i ++){
            // 超過減速點後,延遲增至3倍(降低速度,避免過沖)
            if(i > slow_now) delay = delay_us * 3;
            // 生成10us高電平脈衝
            gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
            system_delay_us(10);
            gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
            // 脈衝間延遲
            system_delay_us(delay);
        }
    }
    else{  // Y軸處理(邏輯同X軸,引腳不同)
        // 設置方向:脈衝數<0時正轉
        gpio_set_level(D36A_IN2_PIN, impluse < 0 ? 1 : 0);
        impluse = abs(impluse);
        uint16_t slow_now = impluse * 0.7;
        uint16_t delay = delay_us;
        
        for(uint16_t i = 0;i < impluse;i ++){
            if(i > slow_now) delay = delay_us * 3;
            gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
            system_delay_us(10);
            gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
            system_delay_us(delay);
        }
    }
    Impluse_mode = false;  // 退出手動模式,恢復自動控制
}

/**
 * 復位Y軸偏移(特定機械結構的復位動作)
 */
void ResetOffset(){
    SendImpluse(1,-100,100);  // Y軸反向發送100脈衝(基礎延遲100us)
    system_delay_ms(100);     // 等待動作完成
    SendImpluse(1,50,100);   // Y軸正向發送50脈衝
    system_delay_ms(50);      // 等待動作完成
}

/**
 * X軸搜索動作(用於丟失目標時的範圍搜索)
 */
void SearchAtX(){
    ResetOffset();               // 先復位Y軸
    SendImpluse(0,100,1000);    // X軸正向100脈衝(延遲1000us,慢速)
    SendImpluse(0,-100,100);    // X軸反向100脈衝(延遲100us,快速)
    SendImpluse(0,-100,1000);   // X軸反向100脈衝(延遲1000us,慢速)
}


/**
 * 高頻PIT中斷處理函數(100us觸發一次)
 * 功能:生成步進電機脈衝(根據set_freq設置的間隔)
 * @param state:中斷狀態(未使用)
 * @param ptr:用戶參數(未使用)
 */
void pit_impluse_handler(uint32 state, void *ptr){
    if(Impluse_mode == true) return;  // 手動模式下不生成自動脈衝
    
    // 脈衝計數器遞增(每100us+1)
    implus_X_count += 1;
    implus_Y_count += 1;
    
    // X軸:計數器達到間隔時,生成一個脈衝
    if(implus_X_count >= skip_pits_X){
        implus_X_count = 0;  // 重置計數器
        // 生成10us高電平脈衝
        gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
    }
    
    // Y軸:同上
    if(implus_Y_count >= skip_pits_Y){
        implus_Y_count = 0;
        gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
    }
}

int tick2 = 0;  // 輔助計數器(用於調試)

/**
 * 中頻PIT中斷處理函數(10ms觸發一次)
 * 功能:執行PID計算、更新電機頻率、幀率統計
 * @param state:中斷狀態(未使用)
 * @param ptr:用戶參數(未使用)
 */
void pit_handler (uint32 state, void *ptr)
{
    ticks += 1;  // 全局計數器遞增(每10ms+1)
    
    if(Impluse_mode == true) return;  // 手動模式下不執行自動控制
    
    // 若有新數據,執行PID計算並更新頻率
    if(data_proceed){
        // 計算X/Y軸PID輸出(頻率)
        freq_X = pid_calculate_cam(0, target_x);
        freq_Y = pid_calculate_cam(1, target_y);
        
        // 設置X/Y軸頻率(包含方向)
        set_freq_X(freq_X);
        set_freq_Y(freq_Y);
        
        data_proceed = false;  // 重置數據處理標誌
    }
    
    pit_state = 1;  // 標記PIT中斷已觸發
    
    // 每100箇中斷(10ms×100=1s)更新一次幀率
    if(ticks % 100 == 0){
        tick2 += 1;  // 輔助計數遞增
        update_fps = true;  // 標記需更新幀率
        fps = fps_ticks;    // 存儲當前幀率
        fps_ticks = 0;      // 重置幀計數器
    }
}


//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------主函數(程序入口)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

bool no_found = 0;          // 目標未找到標誌
int lost_frames = 0;        // 丟失幀數計數器
int no_lost_frames = 0;     // 連續找到幀數計數器
int impluse_count = 0;      // 搜索脈衝計數
int16_t step = 0;           // 搜索步長
int skip_ticks = 0;         // 搜索間隔計數器

/**
 * 主函數:初始化系統並進入主循環
 */
int main(void) {
    // 初始化系統時鐘(80MHz)
    clock_init(SYSTEM_CLOCK_80M);  
    // 初始化調試口(用於printf輸出)
    debug_init();
    // 初始化步進電機驅動(配置相關引腳)
    d36a_init();                   
    printf("begin\n");  // 打印啟動信息

    // 初始化FIFO(8位數據,緩衝區為uart_get_data,大小64字節)
    fifo_init(&uart_data_fifo, FIFO_DATA_8BIT, uart_get_data, 64);
    // 初始化串口(波特率、引腳)
    uart_init(UART_INDEX, UART_BAUDRATE, UART_TX_PIN, UART_RX_PIN);
    // 使能串口接收中斷
    uart_set_interrupt_config(UART_INDEX, UART_INTERRUPT_CONFIG_RX_ENABLE);
    // 設置串口中斷優先級
    interrupt_set_priority(UART_PRIORITY, 0);
    // 綁定串口接收中斷回調函數
    uart_set_callback(UART_INDEX, uart_rx_interrupt_handler, NULL);

    printf("pid\n");  // 打印PID初始化信息
    // 初始化X軸PID(位置式,Kp=5, Ki=0, Kd=0,輸出限幅±1500)
    pid_init(0, PID_POSITIONAL, 5, 0, 0, 1, 0, 1500);
    // 初始化Y軸PID(參數同X軸)
    pid_init(1, PID_POSITIONAL, 5, 0, 0, 1, 0, 1500);
    
    // 初始化激光引腳(推輓輸出,初始高電平)
    gpio_init(LASER_PIN, GPO, 1, GPO_PUSH_PULL);
    // 初始化中頻PIT(10ms中斷,綁定pit_handler)
    pit_ms_init(PIT_TIM_G12, 10, pit_handler, NULL);                            
    // 初始化高頻PIT(100us中斷,綁定pit_impluse_handler)
    pit_us_init(PIT_TIM_A0, 100, pit_impluse_handler, NULL);                            
    // 設置PIT中斷優先級
    interrupt_set_priority(PIT_CON_PRIO, 0);
    interrupt_set_priority(PIT_IMPLUSE_PRIO, 1);

    printf("int\n");  // 打印中斷初始化信息
    printf("init\n"); // 打印初始化完成信息
    ResetOffset();    // 執行Y軸復位

    // 主循環(程序核心邏輯)
    while (1) {
        // 1. 處理串口數據(從FIFO中讀取並解析)
        fifo_data_count = fifo_used(&uart_data_fifo);  // 獲取FIFO中數據量

        // 若FIFO中數據超過6字節(滿足最小解析長度)
        if (fifo_data_count > 6) {
            memset(fifo_get_data,0,sizeof(fifo_get_data));  // 清空解析緩衝區
            // 從FIFO讀取數據並清空已讀部分
            fifo_read_buffer(&uart_data_fifo, fifo_get_data, &fifo_data_count, FIFO_READ_AND_CLEAN);
            fifo_get_data[fifo_data_count] = '\0';  // 添加字符串結束符

            // 解析數據(提取X-Y相關數字)
            ParseResult parsed = parse_xy_data((const char*)fifo_get_data);
            
            // 若解析有效
            if (parsed.valid) {
                // 計算X/Y方向誤差(目標值為500,偏離量=實際值-500)
                int32 dx = parsed.first_num - 500 ;   
                int32 dy = parsed.second_num - 500;  
                
                // 過濾無效誤差(499為特殊無效值)
                if  (dx!=499 && dy!=499){
                    fps_ticks += 1;  // 有效幀計數+1

                    // 計算目標位置(通過幾何轉換)
                    float dd_x = output_to_servo(dx);
                    float dd_y = output_to_servo(dy);
                    target_x = dd_x * 16.5 - 67;  // X軸目標位置(機械補償)
                    target_y = dd_y * 16.5 - 5;   // Y軸目標位置(機械補償)
                    
                    data_proceed = true;  // 標記需更新PID目標

                    // 動態調整X軸PID參數(小誤差時降低增益,避免震盪)
                    if(fabs(target_x)<15) pid_set_params(0, 0.8, 0, 1, 0);
                    else pid_set_params(0, 4, 0, 3, 0);
                    
                    // 動態調整Y軸PID參數(同X軸邏輯)
                    if(fabs(target_y)<15) pid_set_params(1, 0.8, 0, 1, 0);
                    else pid_set_params(1, 4, 0, 3, 0);
                    
                    // 當誤差較小時(接近目標),開啟激光
                    if(fabs(target_y)<15 && fabs(target_x)<15){
                        gpio_set_level(LASER_PIN,GPIO_HIGH);
                    }else{
                        gpio_set_level(LASER_PIN,GPIO_LOW);
                    }
                    
                    no_lost_frames ++;  // 連續找到幀數+1
                    // 打印目標位置(調試用)
                    printf("x:%f y:%f \n",target_x,target_y);
                }
                else{
                    // 無效誤差時,停止電機
                    target_x = 0;
                    target_y = 0;
                    set_freq_X(0);
                    set_freq_Y(0);
                    lost_frames ++;  // 丟失幀數+1
                }
            }
        }
        
        // 2. 打印幀率(每1s一次)
        if(update_fps){
            printf("fps:%d\n",fps);  // 打印當前幀率
            pit_state = 0;
            update_fps = 0;  // 重置幀率更新標誌
        }
        
        // 3. 處理PIT中斷標誌(僅標記用,無實際操作)
        if(pit_state){
            pit_state = 0;
        }
        
        // 4. 目標丟失處理(連續丟失20幀以上)
        printf("n:%d\n",lost_frames);  // 打印丟失幀數(調試用)
        if(lost_frames > 20){
            // 首次進入丟失狀態時初始化搜索參數
            if(no_found == false){ 
                no_lost_frames = 0;
                skip_ticks = ticks;  // 記錄當前計數
                step = 18;  // 初始搜索步長
            }
            no_found = true;  // 標記目標丟失
        }
        
        // 5. 重新找到目標時,重置丟失狀態
        if(no_lost_frames > 0){
            if(no_found == true){
                lost_frames = 0;  // 重置丟失幀數
            }
            no_found = false;  // 標記目標已找到
        }
        
        // 6. 目標丟失時執行搜索動作(每100ms一次)
        if(ticks - skip_ticks > 10 && no_found){
            skip_ticks = ticks;  // 更新搜索間隔計數
            
            // 限制搜索範圍(超過±100時反向)
            step = (impluse_count > 100) ? -18 : (impluse_count < -100) ? 18 : step;
            // 發送搜索脈衝(X軸,步長18,延遲180us)
            SendImpluse(0,step,180);
            impluse_count += step;  // 更新搜索脈衝計數
        }
        else
            system_delay_ms(5);  // 主循環小延時(降低CPU佔用)
    }
}

MSPM0开发学习笔记:非阻塞二维云台运动代码

The post diagnoses why blocking step‑per moves make a two‑axis gimbal hunt and jitter whenever vision packets arrive sporadically, then presents an MSPM0 firmware pattern that recomputes step timing on every UART update instead of forcing each move to completion—keeping motors energized between frames so tracking loops stay smooth and PID gains easier to stabilize.


前言

在前几篇的博客中,有提到一个追踪红色小球的代码,但是会发现追踪的过程中存在很大的抖动并且响应速度相对较慢,推测其原因是因为步进电机的控制函数是阻塞型的,就是说步进电机一定要以某一个给定的速度走完当前规定的角度步数,才会根据新接受到的视觉误差数据再次进行调整运动,而在接受的这几毫秒,步进电机的频率是0,也就是说会直接停下来,之后又根据新的误差数据马上运动起来,所以会有很大的抖动,非常影响识别,也导致pid很难调,没有办法达到一个很好的效果。

本章博客的目的就是介绍一个新的运动思路以及代码,通过实时根据接受到的视觉数据进行二维云台的步进电机的频率更新,从而避免大幅的抖动以及保证电机数据更新的时效性,更好的瞄准目标。


如果无法很好的复现博客里的代码,可以私信作者博取源代码,电赛期间都在线

一、硬件选择

主控:MSPM0G3507
驱动:D36A双路步进电机驱动
电机:42步进电机*2
视觉:树莓派连接USB摄像头

二、硬件连线

硬件连线部分在前几篇博客里面已经说过了,可以直接去里面看,这边附上链接
机器视觉:树莓派结合摄像头解决2025电赛E题锁定靶心 思路与代码分享

三、软件代码

1、思路

对于非阻塞的控制,一开始的思路是通过GPIO引脚输出PWM波,通过接受UART信息的中断实时更新PWM的占空比从而控制给D36A的ST引脚上升沿脉冲的数量和速度(D36A是42步进电机的闭环集成驱动模块,给ST引脚一个脉冲步进电机就会转动一步,具体可以去看看前几章的博客那边有具体讲到)。但是后面发现可能不能用这个高的频率(树莓派给的视觉误差信息帧率可以达到一百帧)去更新PWM引脚的占空比,一运行这个程序TI芯片就会卡死。

因此后面我们换了一种思路,不用GPIO引脚来输出PWM波来进行控制,而是通过我们自己设置引脚的高低电平进而给ST引脚脉冲,我们只需要设置每一次循环的延时就可以控制给上升沿脉冲的数量与速度。这样就不需要频繁的改变PWM引脚的占空比导致芯片卡死。

2、代码实现

软件部分采用C语言实现,IDE采用keil,基于逐飞库进行编写
这边先进
行一下简单的参数说明,便于理解后面的思路

参数含义以及作用
UART_INDEX串口索引,定义使用的串口(此处为UART_2),用于指定数据接收和发送的硬件串口
UART_BAUDRATE串口波特率,定义串口通信的波特率(与调试口一致),确保与外部设备通信速率匹配
UART_TX_PIN串口发送引脚,定义UART_2的发送引脚(B15),用于串口数据的发送
UART_RX_PIN串口接收引脚,定义UART_2的接收引脚(B16),用于串口数据的接收
UART_PRIORITY串口中断优先级,指定串口接收中断的优先级(使用UART0的中断编号映射到UART2),控制中断响应顺序
PIT_CON_PRIO中频PIT中断优先级,定义用于PID计算的定时器(TIMG12)中断优先级,确保PID计算的实时性
PIT_IMPLUSE_PRIO高频PIT中断优先级,定义用于生成步进电机脉冲的定时器(TIMA0)中断优先级,确保脉冲生成的高精度
LASER_PIN激光控制引脚,定义控制激光的GPIO引脚(A30),用于在接近目标时开启/关闭激光
uart_get_data串口接收数据缓冲区,存储串口接收的原始数据(最大5049字节),避免数据丢失
fifo_get_dataFIFO输出缓冲区,从FIFO中读取数据并用于解析,临时存储待解析的串口数据
get_data单次接收数据字节,存储串口中断中单次接收的一个字节数据
fifo_data_countFIFO数据个数,记录FIFO缓冲区中当前存储的数据字节数,用于判断是否有足够数据可解析
uart_data_fifoFIFO结构体,用于串口数据的缓冲管理,解决串口数据突发接收时的处理压力
PID_CH_XX轴PID通道,定义X方向对应的PID控制器通道(0),用于区分X轴的PID计算
PID_CH_YY轴PID通道,定义Y方向对应的PID控制器通道(1),用于区分Y轴的PID计算
FOC_CH_X_PINX轴步进电机脉冲引脚,定义X轴步进电机的脉冲控制引脚(B11),用于生成X轴电机的驱动脉冲
FOC_CH_Y_PINY轴步进电机脉冲引脚,定义Y轴步进电机的脉冲控制引脚(B12),用于生成Y轴电机的驱动脉冲
ParseResult.valid解析结果有效性标志,标记串口数据解析是否成功(true为有效),用于判断是否使用解析后的数据
ParseResult.first_num解析结果前三位数字,存储串口数据中X方向相关的前三位数字,用于计算X轴误差
ParseResult.second_num解析结果后三位数字,存储串口数据中Y方向相关的后三位数字,用于计算Y轴误差
gpio_statusGPIO状态变量,预留用于存储GPIO引脚的电平状态,暂未在代码中实际使用
pit_statePIT中断触发标志,标记中频PIT定时器(10ms)是否触发中断(1为触发),用于主循环同步中断状态
target_xX轴目标位置,由串口数据解析并转换得到的X轴目标位置,作为X轴PID控制器的目标值
target_yY轴目标位置,由串口数据解析并转换得到的Y轴目标位置,作为Y轴PID控制器的目标值
freq_XX轴PID输出频率,X轴PID控制器计算得到的输出频率(正负表示方向),用于控制X轴电机转速
freq_YY轴PID输出频率,Y轴PID控制器计算得到的输出频率(正负表示方向),用于控制Y轴电机转速
ticks全局计数器,每10ms递增一次,用于计时和控制周期性任务(如帧率统计、搜索间隔)
fps当前帧率,记录每秒接收的有效数据帧数,反映系统数据接收的实时性
fps_ticks帧计数器,累计1秒内接收的有效帧数,用于计算fps
data_proceed数据处理标志,标记是否有新的解析数据需要更新PID目标(true为需要),用于同步主循环与中断的数据流
update_fps帧率更新标志,标记是否需要更新并打印帧率(true为需要),控制帧率的周期性打印
implus_X_countX轴脉冲计数器,每100us递增一次,用于累计X轴脉冲间隔,触发脉冲生成
implus_Y_countY轴脉冲计数器,每100us递增一次,用于累计Y轴脉冲间隔,触发脉冲生成
skip_pits_XX轴脉冲间隔,X轴两次脉冲之间的100us计数间隔(由频率计算得到),决定X轴电机转速
skip_pits_YY轴脉冲间隔,Y轴两次脉冲之间的100us计数间隔(由频率计算得到),决定Y轴电机转速
Impluse_mode手动脉冲模式标志,标记是否处于手动脉冲模式(true为手动),手动模式下暂停自动脉冲生成
no_found目标未找到标志,标记是否丢失目标(true为丢失),用于触发目标搜索逻辑
lost_frames丢失帧数计数器,累计连续丢失的无效数据帧数,用于判断是否进入目标搜索模式
no_lost_frames连续找到帧数计数器,累计连续接收的有效数据帧数,用于判断是否从搜索模式恢复
impluse_count搜索脉冲计数,累计目标丢失时发送的搜索脉冲总数,用于限制搜索范围(避免无限搜索)
step搜索步长,目标丢失时每次发送的脉冲数(18),控制搜索时的移动距离
skip_ticks搜索间隔计数器,记录上次发送搜索脉冲的ticks值,用于控制搜索动作的间隔(每100ms一次)

核心函数

/**
 * 高频PIT中断处理函数(100us触发一次)
 * 功能:生成步进电机脉冲(根据set_freq设置的间隔)
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void pit_impluse_handler(uint32 state, void *ptr){
    if(Impluse_mode == true) return;  // 手动模式下不生成自动脉冲
    
    // 脉冲计数器递增(每100us+1)
    implus_X_count += 1;
    implus_Y_count += 1;
    
    // X轴:计数器达到间隔时,生成一个脉冲
    if(implus_X_count >= skip_pits_X){
        implus_X_count = 0;  // 重置计数器
        // 生成10us高电平脉冲
        gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
    }
    
    // Y轴:同上
    if(implus_Y_count >= skip_pits_Y){
        implus_Y_count = 0;
        gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
    }
}

pit_impluse_handler是一个高频中断处理函数,每 100 微秒触发一次,核心功能是为步进电机生成精确的控制脉冲。
1、函数首先判断是否处于手动脉冲模式(Impluse_mode)若是则暂停自动脉冲生成;否则分别递增 X 轴和 Y 轴的脉冲计数器(每 100 微秒 + 1)。
2、当计数器值达到预设的脉冲间隔(skip_pits_X或skip_pits_Y,由set_freq_X/Y根据目标频率计算得出)时,会生成一个 10 微秒的高电平脉冲(通过控制对应 GPIO 引脚电平)随后重置计数器。这一机制直接决定了电机的转速 —— 间隔越小,单位时间内生成的脉冲越多,电机转动越快,从而实现根据目标频率实时调节电机速度的效果。

/**
 * 中频PIT中断处理函数(10ms触发一次)
 * 功能:执行PID计算、更新电机频率、帧率统计
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void pit_handler (uint32 state, void *ptr)
{
    ticks += 1;  // 全局计数器递增(每10ms+1)
    
    if(Impluse_mode == true) return;  // 手动模式下不执行自动控制
    
    // 若有新数据,执行PID计算并更新频率
    if(data_proceed){
        // 计算X/Y轴PID输出(频率)
        freq_X = pid_calculate_cam(0, target_x);
        freq_Y = pid_calculate_cam(1, target_y);
        
        // 设置X/Y轴频率(包含方向)
        set_freq_X(freq_X);
        set_freq_Y(freq_Y);
        
        data_proceed = false;  // 重置数据处理标志
    }
    
    pit_state = 1;  // 标记PIT中断已触发
    
    // 每100个中断(10ms×100=1s)更新一次帧率
    if(ticks % 100 == 0){
        tick2 += 1;  // 辅助计数递增
        update_fps = true;  // 标记需更新帧率
        fps = fps_ticks;    // 存储当前帧率
        fps_ticks = 0;      // 重置帧计数器
    }
}

pit_handler是一个中频中断处理函数,每 10 毫秒触发一次,主要负责闭环控制逻辑与状态更新。
1、函数首先递增全局计数器(ticks),若处于手动模式则暂停自动控制;
2、当检测到新数据标志(data_proceed)时,会调用 PID 控制器计算 X 轴和 Y 轴的目标频率(freq_X/Y),并通过set_freq_X/Y更新脉冲间隔,完成后重置数据标志。
3、此外,函数会标记中断触发状态(pit_state),并每 100 次中断(即 1 秒)更新一次帧率统计(fps),通过记录有效帧数量反映系统数据接收的实时性。这一函数是连接数据解析与电机控制的核心,确保根据最新误差动态调整电机控制参数。

计时器中断初始化

pit_ms_init(PIT_TIM_G12, 10, pit_handler, NULL);                            
pit_us_init(PIT_TIM_A0, 100, pit_impluse_handler, NULL);    

总体代码这边因为是电赛控制题基础部分第三题的代码,所以额外还多了一些找不到目标时候的手动收缩函数,但是都是一个逻辑上的区别而已,在找到目标之后还是根据以上讲到的这两个函数进行pid的控制逼近中心收敛。

大概思路如下:
在这里插入图片描述

完整代码如下:

//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------Include区域(头文件包含)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

// 通用头文件(包含基础类型、宏定义等)
#include "zf_common_headfile.h"
// PID控制器头文件(提供PID计算相关函数)
#include "mc_pid_controller.h"
// 延时驱动头文件(提供微秒/毫秒级延时函数)
#include "zf_driver_delay.h"
// GPIO驱动头文件(提供GPIO初始化、电平控制等函数)
#include "zf_device_d36a.h"  // 步进电机驱动相关头文件
#include <math.h>            // 数学函数库(用于atan2等计算)
#include <stdint.h>          // 标准整数类型定义
#include <stdio.h>           // 标准输入输出(用于printf)


//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------Define区域(宏定义)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

// 串口配置:使用UART2
#define UART_INDEX              (UART_2   )                   
// 串口波特率(与调试口一致)
#define UART_BAUDRATE           (DEBUG_UART_BAUDRATE)        
// UART2发送引脚(B15)
#define UART_TX_PIN             (UART2_TX_B15  )              
// UART2接收引脚(B16)
#define UART_RX_PIN             (UART2_RX_B16  )              
// 串口中断优先级(使用UART0的中断编号,实际映射到UART2)
#define UART_PRIORITY           (UART0_INT_IRQn)              

// PIT控制器中断优先级(用于PID计算的定时器)
#define PIT_CON_PRIO           (TIMG12_INT_IRQn)   
// 脉冲生成PIT中断优先级(用于生成步进电机脉冲的定时器)
#define PIT_IMPLUSE_PRIO       (TIMA0_INT_IRQn)  

// 激光控制引脚(A30)
#define LASER_PIN                A30                                

// 串口接收数据缓冲区(最大5049字节,用于暂存接收的原始数据)
uint8 uart_get_data[5049];                                        
// FIFO输出缓冲区(用于从FIFO中读取数据并解析)
uint8 fifo_get_data[5049];                                        

// 单次接收的数据字节
uint8 get_data = 0;                                             
// FIFO中存储的数据个数
uint32 fifo_data_count = 0;                                     

// FIFO结构体(用于串口数据的缓冲,解决数据突发接收问题)
fifo_struct uart_data_fifo;

// PID通道定义:X轴和Y轴分别使用0和1通道
#define PID_CH_X                0  
#define PID_CH_Y                1  

// 步进电机脉冲引脚:X轴(B11)、Y轴(B12)
#define FOC_CH_X_PIN                B11  
#define FOC_CH_Y_PIN                B12  


// 解析结果结构体(存储串口数据解析后的有效信息)
typedef struct {          
    bool valid;           // 数据是否有效(解析成功为true)
    int first_num;        // 前三位数字(X方向相关)
    int second_num;       // 后三位数字(Y方向相关)
} ParseResult;

// GPIO状态变量(预留,用于存储GPIO电平状态)
uint8 gpio_status;


//---------------------------------------------------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------函数定义区域(工具函数与回调函数)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

/**
 * 串口接收中断处理函数
 * 功能:在串口收到数据时触发,将数据写入FIFO缓冲区,避免中断中处理耗时逻辑
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void uart_rx_interrupt_handler (uint32 state, void *ptr)
{ 
    // 读取一个字节(查询式,无数据时不阻塞)
    uart_query_byte(UART_INDEX, &get_data);                                     
    // 将接收的字节写入FIFO,实现数据缓冲
    fifo_write_buffer(&uart_data_fifo, &get_data, 1);                           
}


/**
 * 线性映射函数:将0-200的输入映射到300-1000的输出(反向映射)
 * @param input:输入值(0-200)
 * @return 映射后的输出值(300-1000)
 */
int32_t map_0_200_to_1000_300(int32_t input) {
    // 限制输入范围在0-200
    if (input < 0) {
        input = 0;
    } else if (input > 200) {
        input = 200;
    }
    
    // 反向映射公式:input越小,output越大(1000→300)
    int32_t output = 700 - (input * 550) / 200;
    
    return output;
}


/**
 * 取两个整数的较小值
 * @param a:第一个整数
 * @param b:第二个整数
 * @return 较小的整数
 */
int min_int(int a, int b) {                                               
    if (a < b) {
        return a;
    } else {
        return b;
    }
}

/**
 * 解析串口数据中的X-Y坐标信息
 * 格式要求:数据需包含"XxxxxxxE",其中xxxxxx为6位数字(前3位为X相关,后3位为Y相关)
 * @param data:待解析的字符串
 * @return 解析结果(包含有效性、前三位数字、后三位数字)
 */
ParseResult parse_xy_data(const char *data) {
    ParseResult result = {false, 0, 0};  // 初始化结果为无效
    const char *start, *end;
    char num_str[7];  // 存储6位数字(+1位结束符)
    
    // 查找"X"的位置(数据起始标志)
    start = strchr(data, 'X');
    if (!start) {
        return result;  // 未找到"X",数据无效
    }
    start++;  // 跳过"X",指向数字部分
    
    // 查找"E"的位置(数据结束标志)
    end = strchr(start, 'E');
    if (!end) {
        return result;  // 未找到"E",数据无效
    }
    
    // 检查"X"和"E"之间是否正好6位数字
    if (end - start != 6) {
        return result;  // 位数不正确,数据无效
    }
    
    // 提取6位数字并添加结束符
    strncpy(num_str, start, 6);
    num_str[6] = '\0';  
    
    // 检查是否全为数字
    for (int i = 0; i < 6; i++) {
        if (num_str[i] < '0' || num_str[i] > '9') {
            return result;  // 含非数字字符,数据无效
        }
    }
    
    // 拆分前3位和后3位数字
    char first_str[4] = {0};  // 前3位(+结束符)
    char second_str[4] = {0}; // 后3位(+结束符)
    
    strncpy(first_str, num_str, 3);  // 提取前3位
    strncpy(second_str, num_str + 3, 3);  // 提取后3位
    
    // 转换为整数并标记数据有效
    result.first_num = atoi(first_str);
    result.second_num = atoi(second_str);
    result.valid = true;
    
    return result;
}


/**
 * 将误差值转换为舵机角度(通过几何计算)
 * @param output_x:X方向误差值
 * @return 对应的舵机角度(度)
 */
float output_to_servo(float output_x)
{
    // 计算对边长度:误差值×1.8/66(几何比例参数)
    float numerator = output_x * 1.8f / 66.0f;
    // 计算反正切(邻边固定为15,单位:弧度)
    float radian = atan2f(numerator, 15.0f);  
    // 弧度转角度(×180/π)
    int servo_dx = (int)(radian * 180.0f / 3.1415926f);  
    
    return servo_dx;
}


/**
 * 将误差距离映射到脉冲延迟时间(用于调整步进电机速度)
 * @param distance:误差距离(0-1000)
 * @return 延迟时间(300-3000μs,距离越大延迟越小)
 */
uint16 distance_to_delay(int32 distance) {
    // 取距离绝对值(误差非负)
    uint32 abs_dist = (distance < 0) ? -distance : distance;
    // 限制最大距离为1000(避免延迟过小)
    uint32 limited_dist = (abs_dist > 1000) ? 1000 : abs_dist;
    // 反向映射:距离越大,延迟越小(3000→300)
    return 300 - (limited_dist * 200) / 300;  
}


//---------------------------------------------------------------------------------------------------------------------------------------------------
// -----------------------------------------------------------------全局变量定义(状态与参数存储)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

uint8_t pit_state = 0;               // PIT中断触发标志(1表示触发)
float target_x = 0, target_y = 0;    // X/Y轴目标位置(由串口数据解析得到)
int16_t freq_X, freq_Y;              // X/Y轴PID计算输出的频率
int ticks = 0;                       // 全局计数器(10ms递增一次)
int fps = 0, fps_ticks = 0;          // 帧率相关:fps为当前帧率,fps_ticks为帧计数
bool data_proceed = false;           // 数据处理标志(1表示需更新PID目标)
bool update_fps = false;             // 帧率更新标志(1表示需打印帧率)


// 脉冲计数与间隔变量(用于高频脉冲生成)
int implus_X_count = 0, implus_Y_count = 0;  // X/Y轴脉冲计数器(100us递增)
float skip_pits_X = 999999.0f, skip_pits_Y = 999999.0f;  // X/Y轴脉冲间隔(100us为单位)

/**
 * 设置X轴步进电机频率
 * @param freq:频率值(正负表示方向,绝对值为频率)
 */
void set_freq_X(float freq){
    // 设置方向引脚:频率>0时正转,否则反转
    gpio_set_level(D36A_IN4_PIN, freq > 0 ? 1 : 0);  
    // 取频率绝对值(仅关心大小)
    freq = fabsf(freq);
    // 频率为0时,间隔设为极大值(不生成脉冲)
    if(freq == 0) skip_pits_X = 999999;
    // 计算脉冲间隔:10000(100us×100)/频率 = 间隔(单位:100us)
    else skip_pits_X = 10000.0f/freq;
}

/**
 * 设置Y轴步进电机频率
 * @param freq:频率值(正负表示方向,绝对值为频率)
 */
void set_freq_Y(float freq){
    // 设置方向引脚:频率<0时正转,否则反转(与X轴逻辑相反)
    gpio_set_level(D36A_IN2_PIN, freq < 0 ? 1 : 0);  
    // 取频率绝对值
    freq = fabsf(freq);
    // 频率为0时不生成脉冲
    if(freq == 0) skip_pits_Y = 999999;
    // 计算脉冲间隔(同X轴)
    else skip_pits_Y = 10000.0f/freq;
}

bool Impluse_mode = false;  // 手动脉冲模式标志(1表示手动模式,暂停自动控制)

/**
 * 手动发送脉冲序列(用于复位、搜索等特定动作)
 * @param ch:通道(0=X轴,1=Y轴)
 * @param impluse:脉冲数(正负表示方向)
 * @param delay_us:基础延迟时间(μs)
 */
void SendImpluse(uint8_t ch,int16 impluse,int16_t delay_us){
    Impluse_mode = true;  // 进入手动模式,暂停自动脉冲生成
    impluse *= 16.5;      // 脉冲数缩放(根据机械结构调整)
    
    if (ch == 0){  // X轴处理
        // 设置方向:脉冲数>0时正转
        gpio_set_level(D36A_IN4_PIN, impluse > 0 ? 1 : 0);  
        impluse = abs(impluse);  // 取绝对值(仅关心脉冲数量)
        // 计算减速点:前70%脉冲正常速度,后30%减速
        uint16_t slow_now = impluse * 0.7;
        uint16_t delay = delay_us;  // 基础延迟
        
        // 循环生成脉冲
        for(uint16_t i = 0;i < impluse;i ++){
            // 超过减速点后,延迟增至3倍(降低速度,避免过冲)
            if(i > slow_now) delay = delay_us * 3;
            // 生成10us高电平脉冲
            gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
            system_delay_us(10);
            gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
            // 脉冲间延迟
            system_delay_us(delay);
        }
    }
    else{  // Y轴处理(逻辑同X轴,引脚不同)
        // 设置方向:脉冲数<0时正转
        gpio_set_level(D36A_IN2_PIN, impluse < 0 ? 1 : 0);
        impluse = abs(impluse);
        uint16_t slow_now = impluse * 0.7;
        uint16_t delay = delay_us;
        
        for(uint16_t i = 0;i < impluse;i ++){
            if(i > slow_now) delay = delay_us * 3;
            gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
            system_delay_us(10);
            gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
            system_delay_us(delay);
        }
    }
    Impluse_mode = false;  // 退出手动模式,恢复自动控制
}

/**
 * 复位Y轴偏移(特定机械结构的复位动作)
 */
void ResetOffset(){
    SendImpluse(1,-100,100);  // Y轴反向发送100脉冲(基础延迟100us)
    system_delay_ms(100);     // 等待动作完成
    SendImpluse(1,50,100);   // Y轴正向发送50脉冲
    system_delay_ms(50);      // 等待动作完成
}

/**
 * X轴搜索动作(用于丢失目标时的范围搜索)
 */
void SearchAtX(){
    ResetOffset();               // 先复位Y轴
    SendImpluse(0,100,1000);    // X轴正向100脉冲(延迟1000us,慢速)
    SendImpluse(0,-100,100);    // X轴反向100脉冲(延迟100us,快速)
    SendImpluse(0,-100,1000);   // X轴反向100脉冲(延迟1000us,慢速)
}


/**
 * 高频PIT中断处理函数(100us触发一次)
 * 功能:生成步进电机脉冲(根据set_freq设置的间隔)
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void pit_impluse_handler(uint32 state, void *ptr){
    if(Impluse_mode == true) return;  // 手动模式下不生成自动脉冲
    
    // 脉冲计数器递增(每100us+1)
    implus_X_count += 1;
    implus_Y_count += 1;
    
    // X轴:计数器达到间隔时,生成一个脉冲
    if(implus_X_count >= skip_pits_X){
        implus_X_count = 0;  // 重置计数器
        // 生成10us高电平脉冲
        gpio_set_level(FOC_CH_X_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_X_PIN,GPIO_LOW);
    }
    
    // Y轴:同上
    if(implus_Y_count >= skip_pits_Y){
        implus_Y_count = 0;
        gpio_set_level(FOC_CH_Y_PIN,GPIO_HIGH);
        system_delay_us(10);
        gpio_set_level(FOC_CH_Y_PIN,GPIO_LOW);
    }
}

int tick2 = 0;  // 辅助计数器(用于调试)

/**
 * 中频PIT中断处理函数(10ms触发一次)
 * 功能:执行PID计算、更新电机频率、帧率统计
 * @param state:中断状态(未使用)
 * @param ptr:用户参数(未使用)
 */
void pit_handler (uint32 state, void *ptr)
{
    ticks += 1;  // 全局计数器递增(每10ms+1)
    
    if(Impluse_mode == true) return;  // 手动模式下不执行自动控制
    
    // 若有新数据,执行PID计算并更新频率
    if(data_proceed){
        // 计算X/Y轴PID输出(频率)
        freq_X = pid_calculate_cam(0, target_x);
        freq_Y = pid_calculate_cam(1, target_y);
        
        // 设置X/Y轴频率(包含方向)
        set_freq_X(freq_X);
        set_freq_Y(freq_Y);
        
        data_proceed = false;  // 重置数据处理标志
    }
    
    pit_state = 1;  // 标记PIT中断已触发
    
    // 每100个中断(10ms×100=1s)更新一次帧率
    if(ticks % 100 == 0){
        tick2 += 1;  // 辅助计数递增
        update_fps = true;  // 标记需更新帧率
        fps = fps_ticks;    // 存储当前帧率
        fps_ticks = 0;      // 重置帧计数器
    }
}


//---------------------------------------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------主函数(程序入口)------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------------------------------------------------

bool no_found = 0;          // 目标未找到标志
int lost_frames = 0;        // 丢失帧数计数器
int no_lost_frames = 0;     // 连续找到帧数计数器
int impluse_count = 0;      // 搜索脉冲计数
int16_t step = 0;           // 搜索步长
int skip_ticks = 0;         // 搜索间隔计数器

/**
 * 主函数:初始化系统并进入主循环
 */
int main(void) {
    // 初始化系统时钟(80MHz)
    clock_init(SYSTEM_CLOCK_80M);  
    // 初始化调试口(用于printf输出)
    debug_init();
    // 初始化步进电机驱动(配置相关引脚)
    d36a_init();                   
    printf("begin\n");  // 打印启动信息

    // 初始化FIFO(8位数据,缓冲区为uart_get_data,大小64字节)
    fifo_init(&uart_data_fifo, FIFO_DATA_8BIT, uart_get_data, 64);
    // 初始化串口(波特率、引脚)
    uart_init(UART_INDEX, UART_BAUDRATE, UART_TX_PIN, UART_RX_PIN);
    // 使能串口接收中断
    uart_set_interrupt_config(UART_INDEX, UART_INTERRUPT_CONFIG_RX_ENABLE);
    // 设置串口中断优先级
    interrupt_set_priority(UART_PRIORITY, 0);
    // 绑定串口接收中断回调函数
    uart_set_callback(UART_INDEX, uart_rx_interrupt_handler, NULL);

    printf("pid\n");  // 打印PID初始化信息
    // 初始化X轴PID(位置式,Kp=5, Ki=0, Kd=0,输出限幅±1500)
    pid_init(0, PID_POSITIONAL, 5, 0, 0, 1, 0, 1500);
    // 初始化Y轴PID(参数同X轴)
    pid_init(1, PID_POSITIONAL, 5, 0, 0, 1, 0, 1500);
    
    // 初始化激光引脚(推挽输出,初始高电平)
    gpio_init(LASER_PIN, GPO, 1, GPO_PUSH_PULL);
    // 初始化中频PIT(10ms中断,绑定pit_handler)
    pit_ms_init(PIT_TIM_G12, 10, pit_handler, NULL);                            
    // 初始化高频PIT(100us中断,绑定pit_impluse_handler)
    pit_us_init(PIT_TIM_A0, 100, pit_impluse_handler, NULL);                            
    // 设置PIT中断优先级
    interrupt_set_priority(PIT_CON_PRIO, 0);
    interrupt_set_priority(PIT_IMPLUSE_PRIO, 1);

    printf("int\n");  // 打印中断初始化信息
    printf("init\n"); // 打印初始化完成信息
    ResetOffset();    // 执行Y轴复位

    // 主循环(程序核心逻辑)
    while (1) {
        // 1. 处理串口数据(从FIFO中读取并解析)
        fifo_data_count = fifo_used(&uart_data_fifo);  // 获取FIFO中数据量

        // 若FIFO中数据超过6字节(满足最小解析长度)
        if (fifo_data_count > 6) {
            memset(fifo_get_data,0,sizeof(fifo_get_data));  // 清空解析缓冲区
            // 从FIFO读取数据并清空已读部分
            fifo_read_buffer(&uart_data_fifo, fifo_get_data, &fifo_data_count, FIFO_READ_AND_CLEAN);
            fifo_get_data[fifo_data_count] = '\0';  // 添加字符串结束符

// 解析数据(提取X-Y相关数字)
            ParseResult parsed = parse_xy_data((const char*)fifo_get_data);
            
            // 若解析有效
            if (parsed.valid) {
                // 计算X/Y方向误差(目标值为500,偏离量=实际值-500)
                int32 dx = parsed.first_num - 500 ;   
                int32 dy = parsed.second_num - 500;  
                
                // 过滤无效误差(499为特殊无效值)
                if  (dx!=499 && dy!=499){
                    fps_ticks += 1;  // 有效帧计数+1

                    // 计算目标位置(通过几何转换)
                    float dd_x = output_to_servo(dx);
                    float dd_y = output_to_servo(dy);
                    target_x = dd_x * 16.5 - 67;  // X轴目标位置(机械补偿)
                    target_y = dd_y * 16.5 - 5;   // Y轴目标位置(机械补偿)
                    
                    data_proceed = true;  // 标记需更新PID目标

                    // 动态调整X轴PID参数(小误差时降低增益,避免震荡)
                    if(fabs(target_x)<15) pid_set_params(0, 0.8, 0, 1, 0);
                    else pid_set_params(0, 4, 0, 3, 0);
                    
                    // 动态调整Y轴PID参数(同X轴逻辑)
                    if(fabs(target_y)<15) pid_set_params(1, 0.8, 0, 1, 0);
                    else pid_set_params(1, 4, 0, 3, 0);
                    
                    // 当误差较小时(接近目标),开启激光
                    if(fabs(target_y)<15 && fabs(target_x)<15){
                        gpio_set_level(LASER_PIN,GPIO_HIGH);
                    }else{
                        gpio_set_level(LASER_PIN,GPIO_LOW);
                    }
                    
                    no_lost_frames ++;  // 连续找到帧数+1
                    // 打印目标位置(调试用)
                    printf("x:%f y:%f \n",target_x,target_y);
                }
                else{
                    // 无效误差时,停止电机
                    target_x = 0;
                    target_y = 0;
                    set_freq_X(0);
                    set_freq_Y(0);
                    lost_frames ++;  // 丢失帧数+1
                }
            }
        }
        
        // 2. 打印帧率(每1s一次)
        if(update_fps){
            printf("fps:%d\n",fps);  // 打印当前帧率
            pit_state = 0;
            update_fps = 0;  // 重置帧率更新标志
        }
        
        // 3. 处理PIT中断标志(仅标记用,无实际操作)
        if(pit_state){
            pit_state = 0;
        }
        
        // 4. 目标丢失处理(连续丢失20帧以上)
        printf("n:%d\n",lost_frames);  // 打印丢失帧数(调试用)
        if(lost_frames > 20){
            // 首次进入丢失状态时初始化搜索参数
            if(no_found == false){ 
                no_lost_frames = 0;
                skip_ticks = ticks;  // 记录当前计数
                step = 18;  // 初始搜索步长
            }
            no_found = true;  // 标记目标丢失
        }
        
        // 5. 重新找到目标时,重置丢失状态
        if(no_lost_frames > 0){
            if(no_found == true){
                lost_frames = 0;  // 重置丢失帧数
            }
            no_found = false;  // 标记目标已找到
        }
        
        // 6. 目标丢失时执行搜索动作(每100ms一次)
        if(ticks - skip_ticks > 10 && no_found){
            skip_ticks = ticks;  // 更新搜索间隔计数
            
            // 限制搜索范围(超过±100时反向)
            step = (impluse_count > 100) ? -18 : (impluse_count < -100) ? 18 : step;
            // 发送搜索脉冲(X轴,步长18,延迟180us)
            SendImpluse(0,step,180);
            impluse_count += step;  // 更新搜索脉冲计数
        }
        else
            system_delay_ms(5);  // 主循环小延时(降低CPU占用)
    }
}