文章目录

  • 前言
  • 一、硬件选择
  • 二、原理介绍(UART)
  • 三、硬件连线
  • 三、软件代码
    • 1、视觉部分代码(Openart)
    • 2、控制部分代码(MSPM0)
      • (1) UART部分
      • (2) 计算函数部分
      • (3) 控制部分


前言

这篇博客的代码是博主在备赛电赛的时候写来练手的,结果今年电赛的题目真的差不多,是一个自动瞄准追踪装置,因此在比赛结束之后也是用这些代码写一下这篇博客。方案是两个42步进电机采用同一个驱动模块进行驱动(D36A),主控肯定采用MSPM0G3507。然后3D打印了一个二维云台的结构并进行组装,视觉方面采用openart (后面比赛的时候还是选了树莓派。openart的帧率实在太低了,最多才10-20帧)。本章博客主要是讲这个结合openart和云台进行小球追踪的思路以及代码。
其实按赛题的要求,云台的控制端是可以不使用mspm0系列的芯片进行控制的,完全可以采用openart或者其他模块直接进行控制,但是由于之前写的代码都是mspm0的,所以博主这边还是采用了mspm0的芯片进行控制(由于还有小车的循迹方面,一块mspm0的引脚甚至不够用,最后用了两块mspm0,一块控制小车一块控制云台)


如果无法很好的复现博客里的代码,可以私信作者博取源代码

一、硬件选择

主控:MSPM0G3507
驱动:D36A双路步进电机驱动
电机:42步进电机*2
视觉:Openart

二、原理介绍(UART)

这边主要讲UART的通信原理,UART 是短距离设备间常用的异步串行通信协议,无需同步时钟,仅通过 TX(发送)和 RX(接收)两根信号线实现双向通信,本装置中 Openart 与 MSPM0G3507 即通过它传输小球位置信息。​
其系统含发送器与接收器:发送器将并行数据转为串行数据经 TX 发送,接收器通过 RX 接收串行数据并还原为并行数据。​
数据以帧传输,含起始位(1 位低电平,表传输开始)、数据位(5-9 位,常为 8 位有效信息)、校验位(可选,用于校验准确性)、停止位(1-2 位高电平,表传输结束)。​
波特率(单位 bps)是关键参数,表每秒传输的二进制位数,通信双方需一致(如本装置约定 115200bps),否则会出错。​
实际应用中,Openart 识别小球后,按约定帧格式经 TX 发送位置信息;MSPM0G3507 通过 RX 接收并解析,计算云台转动角度,控制步进电机完成追踪。

三、硬件连线

云台部分硬件连线部分在前几篇篇博客里面已经说过了,可以直接去上一篇里面看,这边附上链接
MSPM0开发学习笔记:D36A驱动的42步进电机二维云台(2025电赛 附源代码及引脚配置)
这边讲一下openart和mspm0之间的连线,两个设备之间采用uart进行通讯,openart上面是有专门用于通信的一个4p接口的。
在这里插入图片描述
从左到右分别是5V GND TX RX;我们采用的方案是openart只负责传递偏差距离,在mspm0中进行pid计算以及云台的控制(这个是为了之后如果openart不适用的话换视觉模块更加方便,事实证明确实应该这样处理,同样的条件下openart只能跑到15帧左右,但是树莓派连接摄像头可以跑到100帧,并且通讯的延迟也更低)

用4p的线接出来之后直接连到mspm0上面的对应引脚(注意TX要接RX,RX接TX)就好了(需要选用使用与UART通信的GPIO引脚,引脚之后在代码中初始化为GPIO引脚的RX和TX)

然后连在mspm0上的线有一点要注意就是,在引脚充足的情况下,尽量避免共用的引脚(下图左边有连线的这些),因为说不准会出什么问题导致后续排查很久。
在这里插入图片描述

三、软件代码

1、视觉部分代码(Openart)

视觉部分采用Python语言实现,IDE采用Openmv
具体代码如下

import sensor
import image
import time
from machine import UART# 初始化UART,波特率115200,对应设备端口可根据实际调整
uart = UART(2, 115200)# 初始化摄像头
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)  # 320x240分辨率
sensor.skip_frames(time=2000)
sensor.set_auto_gain(False)  # 关闭自动增益,避免颜色识别受光强影响
sensor.set_auto_whitebal(False)  # 关闭自动白平衡# 红色HSV阈值范围(可根据实际环境调整)
red_threshold = (30, 100, 15, 127, 15, 127)# 图像中心点坐标
center_x = 160
center_y = 120while True:img = sensor.snapshot()# 查找红色色块blobs = img.find_blobs([red_threshold], pixels_threshold=200, area_threshold=200)if blobs:# 取最大的色块作为目标largest_blob = max(blobs, key=lambda b: b.area())# 计算色块中心坐标blob_x = largest_blob.cx()blob_y = largest_blob.cy()# 计算与中心点的偏差dx = blob_x - center_xdy = blob_y - center_y# 按要求处理偏差值(加500,确保传输为正数)send_dx = dx + 500send_dy = dy + 500# 格式化发送字符串data_str = "X{}{}Y".format(send_dx, send_dy)# 通过UART发送数据uart.write(data_str + "\r\n")print("发送数据:", data_str)  # 调试用time.sleep(0.05)  # 控制发送频率

代码解释:

red_threshold = (30, 100, 15, 127, 15, 127)这边是颜色阈值,需要根据实际情况进行调整,稳定性不高说实话,因此也可以考虑yolo算法的识别,之后有机会的话也出一期,难度是相对来说比较低的,就是过程会比较麻烦一些。
颜色阈值可以通过openmv IDE自带的阈值编辑器来得到
在这里插入图片描述
在工具这边选机器视觉,然后里面有一个阈值编辑器,打开界面如下
在这里插入图片描述
白色的是被跟踪的像素,通过调整滑块让需要的颜色变成白色就可以。
也可以选用灰度编辑器(上图用的是LAB)但是灰度编辑器一般来说用于筛选黑白会更合适(比如这次电赛E题的白色靶子和黑色边框)
在这里插入图片描述
选完之后吧阈值复制一下黏贴到代码的对应位置就可以了
不过openmvIDE只提供LAB和灰度这两种阈值编辑器,如果有需要别的比如HSV或者BRG这些的是可以自己用opencv写一个程序来进行筛选的。
这边也是写了一个简单的阈值编辑器,完整代码如下:

import cv2
import numpy as np
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTkclass ThresholdEditor:def __init__(self, root):self.root = rootself.root.title("OpenCV阈值编辑器")self.root.geometry("1200x800")self.root.minsize(1000, 700)# 初始化变量self.image = Noneself.video_capture = Noneself.is_camera_active = Falseself.color_mode = "BGR"  # 默认模式# 创建UI组件self.create_widgets()# 初始化滑块值self.init_slider_values()def create_widgets(self):# 创建顶部控制区control_frame = tk.Frame(self.root, padx=10, pady=5)control_frame.pack(fill=tk.X)# 输入源选择input_frame = tk.Frame(control_frame)input_frame.pack(side=tk.LEFT, padx=10)tk.Label(input_frame, text="输入源:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=5)self.camera_btn = tk.Button(input_frame, text="启动摄像头", command=self.toggle_camera,bg="#4CAF50", fg="white", padx=8)self.camera_btn.pack(side=tk.LEFT, padx=5)self.image_btn = tk.Button(input_frame, text="选择图片", command=self.load_image,bg="#2196F3", fg="white", padx=8)self.image_btn.pack(side=tk.LEFT, padx=5)# 颜色模式选择mode_frame = tk.Frame(control_frame)mode_frame.pack(side=tk.LEFT, padx=20)tk.Label(mode_frame, text="颜色模式:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=5)self.mode_var = tk.StringVar(value="BGR")modes = ["BGR", "HSV", "灰度", "LAB"]mode_menu = tk.OptionMenu(mode_frame, self.mode_var, *modes, command=self.change_mode)mode_menu.config(width=8)mode_menu.pack(side=tk.LEFT, padx=5)# 创建滑块区域(带滚动条)slider_container = tk.Frame(self.root)slider_container.pack(fill=tk.X, padx=10, pady=5)self.slider_canvas = tk.Canvas(slider_container)scrollbar = tk.Scrollbar(slider_container, orient="horizontal", command=self.slider_canvas.xview)self.slider_frame = tk.Frame(self.slider_canvas)self.slider_frame.bind("<Configure>",lambda e: self.slider_canvas.configure(scrollregion=self.slider_canvas.bbox("all")))self.slider_canvas.create_window((0, 0), window=self.slider_frame, anchor="nw")self.slider_canvas.configure(xscrollcommand=scrollbar.set)self.slider_canvas.pack(side="left", fill="x", expand=True)scrollbar.pack(side="bottom", fill="x")# 创建图像显示区域display_frame = tk.Frame(self.root)display_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)self.original_frame = tk.Frame(display_frame)self.original_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)tk.Label(self.original_frame, text="原图", font=("Arial", 10, "bold")).pack()self.original_label = tk.Label(self.original_frame, bg="#f0f0f0")self.original_label.pack(fill=tk.BOTH, expand=True)self.processed_frame = tk.Frame(display_frame)self.processed_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)tk.Label(self.processed_frame, text="阈值处理后", font=("Arial", 10, "bold")).pack()self.processed_label = tk.Label(self.processed_frame, bg="#f0f0f0")self.processed_label.pack(fill=tk.BOTH, expand=True)def init_slider_values(self):# 清除现有滑块for widget in self.slider_frame.winfo_children():widget.destroy()# 根据颜色模式创建滑块if self.color_mode in ["BGR", "HSV", "LAB"]:# 三个通道的低阈值self.low_vars = {}for channel in self.get_channels():frame = tk.Frame(self.slider_frame)frame.pack(side=tk.LEFT, padx=10)var = tk.IntVar(value=0)self.low_vars[channel] = vartk.Label(frame, text=f"低{channel}:", width=8).pack(anchor=tk.W)slider = tk.Scale(frame, from_=0, to=self.get_max_value(channel), variable=var, orient=tk.HORIZONTAL, length=200,command=lambda _: self.update_thresholds())slider.pack()tk.Label(frame, textvariable=var, width=5, borderwidth=2, relief="sunken").pack(pady=5)# 三个通道的高阈值self.high_vars = {}for channel in self.get_channels():frame = tk.Frame(self.slider_frame)frame.pack(side=tk.LEFT, padx=10)max_val = self.get_max_value(channel)var = tk.IntVar(value=max_val)self.high_vars[channel] = vartk.Label(frame, text=f"高{channel}:", width=8).pack(anchor=tk.W)slider = tk.Scale(frame, from_=0, to=max_val, variable=var, orient=tk.HORIZONTAL, length=200,command=lambda _: self.update_thresholds())slider.pack()tk.Label(frame, textvariable=var, width=5, borderwidth=2, relief="sunken").pack(pady=5)elif self.color_mode == "灰度":# 灰度模式只有一个通道self.gray_low_var = tk.IntVar(value=0)self.gray_high_var = tk.IntVar(value=255)# 低阈值frame = tk.Frame(self.slider_frame)frame.pack(side=tk.LEFT, padx=10)tk.Label(frame, text="低阈值:", width=8).pack(anchor=tk.W)slider = tk.Scale(frame, from_=0, to=255, variable=self.gray_low_var, orient=tk.HORIZONTAL, length=200,command=lambda _: self.update_thresholds())slider.pack()tk.Label(frame, textvariable=self.gray_low_var, width=5, borderwidth=2, relief="sunken").pack(pady=5)# 高阈值frame = tk.Frame(self.slider_frame)frame.pack(side=tk.LEFT, padx=10)tk.Label(frame, text="高阈值:", width=8).pack(anchor=tk.W)slider = tk.Scale(frame, from_=0, to=255, variable=self.gray_high_var, orient=tk.HORIZONTAL, length=200,command=lambda _: self.update_thresholds())slider.pack()tk.Label(frame, textvariable=self.gray_high_var, width=5, borderwidth=2, relief="sunken").pack(pady=5)def get_channels(self):if self.color_mode == "BGR":return ["B", "G", "R"]elif self.color_mode == "HSV":return ["H", "S", "V"]elif self.color_mode == "LAB":return ["L", "A", "B"]return []def get_max_value(self, channel=None):if self.color_mode == "HSV":# H通道范围是0-179,S和V是0-255return 179 if channel == "H" else 255return 255def change_mode(self, mode):self.color_mode = modeself.init_slider_values()self.update_thresholds()def toggle_camera(self):if self.is_camera_active:# 关闭摄像头if self.video_capture:self.video_capture.release()self.video_capture = Noneself.is_camera_active = Falseself.camera_btn.config(text="启动摄像头", bg="#4CAF50")else:# 打开摄像头self.video_capture = cv2.VideoCapture(0)if not self.video_capture.isOpened():tk.messagebox.showerror("错误", "无法打开摄像头,请检查设备是否正常")self.video_capture = Nonereturnself.is_camera_active = Trueself.camera_btn.config(text="关闭摄像头", bg="#f44336")self.image = None  # 清除已加载的图像self.update_frame()  # 开始更新帧def load_image(self):# 关闭摄像头(如果开启)if self.is_camera_active:self.toggle_camera()# 选择并加载图片file_path = filedialog.askopenfilename(filetypes=[("图像文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif")])if file_path:self.image = cv2.imread(file_path)if self.image is None:tk.messagebox.showerror("错误", "无法加载选中的图片")returnself.update_thresholds()def update_frame(self):if self.is_camera_active and self.video_capture.isOpened():ret, frame = self.video_capture.read()if ret:self.image = frameself.update_thresholds()# 继续更新帧self.root.after(30, self.update_frame)def update_thresholds(self):if self.image is None:return# 复制原图用于显示original = self.image.copy()# 根据颜色模式转换图像并应用阈值if self.color_mode == "HSV":processed = cv2.cvtColor(self.image, cv2.COLOR_BGR2HSV)low = np.array([self.low_vars["H"].get(),self.low_vars["S"].get(),self.low_vars["V"].get()])high = np.array([self.high_vars["H"].get(),self.high_vars["S"].get(),self.high_vars["V"].get()])mask = cv2.inRange(processed, low, high)result = cv2.bitwise_and(original, original, mask=mask)elif self.color_mode == "灰度":processed = cv2.cvtColor(self.image, cv2.COLOR_BGR2GRAY)low = self.gray_low_var.get()high = self.gray_high_var.get()_, result = cv2.threshold(processed, low, high, cv2.THRESH_BINARY)# 转换回BGR以便与原图格式一致result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)elif self.color_mode == "LAB":processed = cv2.cvtColor(self.image, cv2.COLOR_BGR2LAB)low = np.array([self.low_vars["L"].get(),self.low_vars["A"].get(),self.low_vars["B"].get()])high = np.array([self.high_vars["L"].get(),self.high_vars["A"].get(),self.high_vars["B"].get()])mask = cv2.inRange(processed, low, high)result = cv2.bitwise_and(original, original, mask=mask)else:  # BGR模式low = np.array([self.low_vars["B"].get(),self.low_vars["G"].get(),self.low_vars["R"].get()])high = np.array([self.high_vars["B"].get(),self.high_vars["G"].get(),self.high_vars["R"].get()])mask = cv2.inRange(self.image, low, high)result = cv2.bitwise_and(original, original, mask=mask)# 显示图像self.display_image(original, self.original_label)self.display_image(result, self.processed_label)def display_image(self, img, label):# 调整图像大小以适应窗口img = self.resize_image(img, label)# 转换OpenCV图像格式为Tkinter可用格式img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)pil_img = Image.fromarray(img_rgb)tk_img = ImageTk.PhotoImage(image=pil_img)# 更新标签图像label.config(image=tk_img)label.image = tk_img  # 保持引用,防止被垃圾回收def resize_image(self, img, label):# 获取显示区域大小display_width = label.winfo_width()display_height = label.winfo_height()# 如果窗口还没初始化,使用默认大小if display_width <= 1 or display_height <= 1:display_width = 400display_height = 300# 计算调整比例h, w = img.shape[:2]ratio = min(display_width / w, display_height / h)# 调整大小if ratio < 1:new_size = (int(w * ratio), int(h * ratio))return cv2.resize(img, new_size, interpolation=cv2.INTER_AREA)return imgif __name__ == "__main__":root = tk.Tk()# 确保中文显示正常app = ThresholdEditor(root)root.mainloop()

注意选取图片的时候,图片路径不要有中文。具体效果如下,退出之前记得记下调好的阈值
在这里插入图片描述
然后还有一个注意的点就是,把所有的误差值都加上五百,是为了防止负号或十位数个位数的误差导致UART传输过程或传输后的解码读取过程出错,保证是三位正数,之后在MSPM0端的代码解析之后再减去500就好。

2、控制部分代码(MSPM0)

(1) UART部分

初始化部分

#define UART_INDEX              (UART_2   )                           // 默认 UART_1
#define UART_BAUDRATE           (DEBUG_UART_BAUDRATE)                           // 默认 115200
#define UART_TX_PIN             (UART2_TX_B15  )                           // 默认 UART0_TX_A10
#define UART_RX_PIN             (UART2_RX_B16  )                           // 默认 UART1_RX_A11#define UART_PRIORITY           (UART0_INT_IRQn)                                // 对应串口中断的中断编号 在 MIMXRT1064.h 头文件中查看 IRQn_Type 枚举体uint8 uart_get_data[64];                                                        // 串口接收数据缓冲区
uint8 fifo_get_data[64];                                                        // fifo 输出读出缓冲区uint8 get_data = 0;                                                             // 接收数据变量
uint32 fifo_data_count = 0;                                                     // fifo 数据个数fifo_struct uart_data_fifo;

相关函数定义部分

void uart_rx_interrupt_handler (uint32 state, void *ptr)
{ 
//    get_data = uart_read_byte(UART_INDEX);                                    // 接收数据 while 等待式 不建议在中断使用uart_query_byte(UART_INDEX, &get_data);                                     // 接收数据 查询式 有数据会返回 TRUE 没有数据会返回 FALSEfifo_write_buffer(&uart_data_fifo, &get_data, 1);                           // 将数据写入 fifo 中
}// 解析结果结构体
typedef struct {bool valid;           // 数据是否有效int first_num;        // 前三位数字int second_num;       // 后三位数字
} ParseResult;// 解析格式为XnnnnnnY的数据(X开头,Y结尾,中间6位数字)
ParseResult parse_xy_data(const char *data) {ParseResult result = {false, 0, 0};// 检查数据长度是否正确(X + 6位数字 + Y 共8个字符)if (strlen(data) != 8) {return result;}// 检查开头是否为'X',结尾是否为'Y'if (data[0] != 'X' || data[7] != 'Y') {return result;}// 提取中间6位数字并检查是否都是数字for (int i = 1; i <= 6; i++) {if (data[i] < '0' || data[i] > '9') {return result;}}// 提取前三位数字char first_str[4] = {0};strncpy(first_str, &data[1], 3);result.first_num = atoi(first_str);// 提取后三位数字char second_str[4] = {0};strncpy(second_str, &data[4], 3);result.second_num = atoi(second_str);// 标记为有效数据result.valid = true;return result;
}

主函数部分

uint8 gpio_status;
int main (void)
{//SYSCFG_DL_init();clock_init(SYSTEM_CLOCK_80M);   // 时钟配置及系统初始化<务必保留>d36a_init();//debug_init();                   // 调试端口初始化// 此处编写用户代码 例如外设初始化代码等fifo_init(&uart_data_fifo, FIFO_DATA_8BIT, uart_get_data, 64);              // 初始化 fifo 挂载缓冲区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_INDEX 的中断优先级为 0uart_set_callback(UART_INDEX, uart_rx_interrupt_handler, NULL);			    // 定义中断接收函数// 参数说明:通道、模式、kp(比例)、kpp(二次项)、ki(积分)、kd(微分)、kdd(额外项)、最大输出限制pid_init(PID_CH_X, PID_POSITIONAL, 0.6f, 0.0f, 0.02f, 0.2f, 0.0f, 200.0f);  // X方向PIDpid_init(PID_CH_Y, PID_POSITIONAL, 0.4f, 0.0f, 0.15f, 0.0f, 0.0f, 100.0f);  // Y方向PID// 注:实际参数(0.5f等)需要根据硬件调试,max_limit是输出最大值(如舵机角度范围)uart_write_string(UART_INDEX, "UART Text.");                                // 输出测试信息uart_write_byte(UART_INDEX, '\r');                                          // 输出回车uart_write_byte(UART_INDEX, '\n');                                          // 输出换行// 此处编写用户代码 例如外设初始化代码等while(true){fifo_data_count = fifo_used(&uart_data_fifo);                   // 查看FIFO是否有数据
if(fifo_data_count != 0)                                        // 有数据可读
{// 从FIFO读取数据(最多读取31字节,留1字节给终止符)uint32_t read_len = (fifo_data_count > 31) ? 31 : fifo_data_count;fifo_read_buffer(&uart_data_fifo, fifo_get_data, &read_len, FIFO_READ_AND_CLEAN);// 添加字符串终止符(确保parse_xy_data能正确识别字符串结尾)fifo_get_data[read_len] = '\0';// 解析接收到的数据ParseResult res = parse_xy_data((const char*)fifo_get_data);// 定义发送缓冲区(避免栈溢出,固定长度足够存储响应)char response[50];if (res.valid){// 解析成功:格式化响应(例如"138,118")//sprintf(response, "%d,%d\r\n", res.first_num, res.second_num);//uart_write_string(UART_INDEX, response);int dx = res.first_num - 128;  // 前三位数字减128int dy = res.second_num - 128; // 后三位数字减128float pid_out_x = pid_calculate(PID_CH_X, (float)dx);  // X方向PID输出float pid_out_y = pid_calculate(PID_CH_Y, (float)dy);  // Y方向PID输出// 示例:计算output_x=10时的舵机控制量int ddx = output_to_servo(dx);int ddy = output_to_servo(dy);				// 5. 使用PID输出(示例:发送到串口查看结果)float servo_x=ddx*16;float servo_y=ddy*-16;int speed_x=map_0_200_to_1000_300(servo_x);int speed_y=map_0_200_to_1000_300(servo_y);int speed=min_int(speed_x,speed_y);d36a_set_angle_both(servo_y,servo_x,speed);//d36a_set_angle(D36A_MOTOR_B,servo_x,300);sprintf(response, "dx=%d, dy=%d | sex=%d, sey=%d \r\n",dx, dy, (int)servo_x, (int)servo_y);uart_write_string(UART_INDEX, response);}}system_delay_ms(10);}
}

(2) 计算函数部分

int32_t map_0_200_to_1000_300(int32_t input) {// 限制输入值在0-200范围内if (input < 0) {input = 0;} else if (input > 200) {input = 200;}// 线性映射公式:output = (input - in_min) * (out_max - out_min) / (in_max - in_min) + out_min// 这里是反向映射,1000到300int32_t output = 700 - (input * 550) / 200;return output;
}int min_int(int a, int b) {if (a < b) {return a;} else {return b;}
}int output_to_servo(float output_x)
{// 1. 计算atan2的第一个参数:output_x * 1.8 / 66float numerator = output_x * 1.8f / 66.0f;// 2. 计算反正切(atan2(对边, 邻边)),结果为弧度float radian = atan2f(numerator, 15.0f);  // 第二个参数固定为15(与Python一致)// 3. 将弧度转换为角度(乘以180/π),并转换为整数int servo_dx = (int)(radian * 180.0f / 3.1415926f);  // 用3.1415926提高精度return servo_dx;
}void pid_init(uint8_t ch, PID_Mode mode, float kp, float kpp, float ki, float kd, float kdd, float max_limit)
{if (ch >= PID_MAX_CHANNEL) return;PID_Controller* pid = &pid_controllers[ch];memset(pid, 0, sizeof(PID_Controller));pid->mode = mode;pid->kp = kp;pid->kpp = kpp;pid->ki = ki;pid->kd = kd;pid->kdd = kdd;pid->max_limit = max_limit;
}float pid_calculate(uint8_t ch, float error)
{if (ch >= PID_MAX_CHANNEL) return 0.0f;PID_Controller* pid = &pid_controllers[ch];// »ý·ÖÀÛ¼Ópid->integral += error;// »ý·ÖÏÞ·ù£¬·ÀÖ¹»ý·Ö±¥ºÍif (pid->integral > pid->max_limit) pid->integral = pid->max_limit;if (pid->integral < -pid->max_limit) pid->integral = -pid->max_limit;// Îó²î΢·Öfloat derivative = error - pid->prev_error;// ¼ÆËãÊä³öfloat output = pid->kp * error + pid->ki * pid->integral + pid->kd * derivative ;// ¸üÐÂÉÏÒ»´ÎÎó²îpid->prev_error = error;// ÏÞ·ùÊä³öif (output > pid->max_limit) return pid->max_limit;if (output < -pid->max_limit) return -pid->max_limit;return output;
}#define PID_CH_X                0  // X方向PID通道
#define PID_CH_Y                1  // Y方向PID通道

函数解释
一、map_0_200_to_1000_300

将输入值限制在 0~200 范围内,再线性反向映射到 1000~300 范围(输入越小,输出越大)。
output=700−input×550200\text{output} = 700 - \frac{\text{input} \times 550}{200}output=700200input×550
二、min_int
返回两个整数中的较小值

三、output_to_servo
将输入值output_x转换为伺服电机的角度偏移量(基于反正切计算)。
计算对边长度
numerator=output_x×1.866.0\text{numerator} = \frac{\text{output\_x} \times 1.8}{66.0}numerator=66.0output_x×1.8
计算弧度(反正切)
radian=atan2(numerator,15.0)\text{radian} = \text{atan2}(\text{numerator}, 15.0)radian=atan2(numerator,15.0)
弧度转角度(整数)
servo_dx=⌊radian×180.03.1415926⌉(取整)\text{servo\_dx} = \left\lfloor \text{radian} \times \frac{180.0}{3.1415926} \right\rceil \quad (\text{取整})servo_dx=radian×3.1415926180.0(取整)
四、pid_init
初始化指定通道的 PID 控制器,设置控制模式、比例系数(kp、kpp)、积分系数(ki)、微分系数(kd、kdd)及输出最大值限制。

五、pid_calculate
计算指定通道的 PID 控制器输出,包含积分限幅和输出限幅功能。
积分项累加
integral=integral+error\text{integral} = \text{integral} + \text{error}integral=integral+error
积分限幅
integral={max_limitif integral>max_limit−max_limitif integral<−max_limitintegralotherwise\text{integral} = \begin{cases} \text{max\_limit} & \text{if } \text{integral} > \text{max\_limit} \\ -\text{max\_limit} & \text{if } \text{integral} < -\text{max\_limit} \\ \text{integral} & \text{otherwise} \end{cases}integral=max_limitmax_limitintegralif integral>max_limitif integral<max_limitotherwise
微分项计算
derivative=error−prev_error\text{derivative} = \text{error} - \text{prev\_error}derivative=errorprev_error
PID输出
output=kp×error+ki×integral+kd×derivative\text{output} = kp \times \text{error} + ki \times \text{integral} + kd \times \text{derivative}output=kp×error+ki×integral+kd×derivative
输出限幅
output={max_limitif output>max_limit−max_limitif output<−max_limitoutputotherwise\text{output} = \begin{cases} \text{max\_limit} & \text{if } \text{output} > \text{max\_limit} \\ -\text{max\_limit} & \text{if } \text{output} < -\text{max\_limit} \\ \text{output} & \text{otherwise} \end{cases}output=max_limitmax_limitoutputif output>max_limitif output<max_limitotherwise

这些函数里面的很多值都是需要根据实际设备调整的,比如*16是因为42电机对角度进行了16细分,需要乘于16才是正常的值,然后pid不用说肯定是要自己调的,output_to_servo中的值也需要根据实际情况进行调整

(3) 控制部分

控制部分的函数只有一个就是d36a_set_angle_both,具体的内容在前两章都讲过这边就不赘述了,可以直接参考之前的博客
MSPM0开发学习笔记:D36A驱动的42步进电机二维云台(2025电赛 附源代码及引脚配置)
值得注意的一点是这个控制是阻塞型的,就是电机需要运动完这个指令才会从openart那边接收到新的误差参数进行下一步调整,并且在接受信息的这一瞬间电机的速度直接就是0,而不是根据误差实时调整频率以及方向,所以效果并不是非常准并且电机会有较大的抖动,虽然大体效果是还可以的但是仍需要精进。后续的博客会发在电赛期间写的非阻塞控制代码,基础部分定位第二题一秒不到第二题2.6秒左右,相对来说还是一个不错的成绩的。

如果无法很好的复现博客里的代码,可以私信作者博取源代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/917701.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/917701.shtml
英文地址,请注明出处:http://en.pswp.cn/news/917701.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【CTF-WEB-SQL】SQL注入基本流程(sql-labs的Less11)(用burp抓取post然后用sqlmap盲注)

题目 从第11less开始&#xff0c;就是POST表单了burp抓取数据包将抓取到的数据包存放到桌面&#xff0c;保存为post.txt数据包内容如下&#xff1a;POST /Less-11/ HTTP/1.1 Host: 223.112.39.132:44537 Content-Length: 39 Cache-Control: max-age0 Accept-Language: zh-CN,zh…

WPF 与 Winform :Windows 桌面开发该用谁?

WPF 与 Winform :Windows 桌面开发该用谁? 一、 WPF 与 Winform的概念 WPF:颜值与实力并存的 “后起之秀” Winform:简单直接的 “老前辈” 二、WPF 与 Winform 的核心差异 1. 设计理念:分离 vs 耦合 2. 布局系统:灵活适配 vs 固定坐标 3. 视觉效果:绚丽动画 vs 朴素原生…

【Git学习】入门与基础

目录 Git的安装 Git 配置用户信息 Git 初始化本地仓库 Git 工作区、暂存区和版本库 Git 跟踪文件 Git 修改文件 Git 删除文件 Git 撤销本地文件的修改 Git 取消暂存 Git 跳过暂存区 Git 版本回退 Git 撤销提交 Git 设置忽略文件 Git 比较文件差异 Git 代码托管平台…

idea添加gitlab访问令牌

1.按下图所示顺序操作gitlab,获取到对应的token;2.填写对应的gitlab地址和第一步获取的token

人工智能领域、图欧科技、IMYAI智能助手2025年5月更新月报

2025年5月IMYAI平台技术动态与模型更新综述 摘要&#xff1a; 本文整理了2025年5月期间IMYAI平台发布的主要技术更新、新模型上线信息及功能调整公告&#xff0c;涵盖DeepSeek、Gemini、Claude、即梦、Suno等模型动态及平台功能优化。 正文&#xff1a; 一、 模型更新与上线Dee…

机器人权利:真实还是虚幻,机器人权利研究如何可能,道德权利与法律权利

一、机器人权利&#xff1a;真实还是虚幻&#xff1f;机器人权利的讨论源于技术进步对传统法律与伦理体系的冲击&#xff0c;其真实性取决于技术发展阶段与社会接受度的互动。当前&#xff0c;机器人权利仍呈现“虚幻与真实交织”的特征&#xff1a;技术基础&#xff1a;从工具…

通信小白产品学习碎片01

1. 云中继&#xff08;Cloud Media Relay&#xff09; 运营商在Volte/Vonr场景中引入的核心网关键功能&#xff0c;用于优化媒体流的传输路径&#xff0c;解决跨运营商、跨地域通信时的网络绕行问题。 传统&#xff1a;A终端—>A核心网—>跨网互联点—>B核心网—>…

⭐CVPR2025 3D 生成新框架|Kiss3DGen 让 2D 扩散模型玩转 3D 资产生成

⭐CVPR 3D 生成新框架&#xff5c;Kiss3DGen 让 2D 扩散模型玩转 3D 资产生成 &#x1f4c4;论文题目&#xff1a;Kiss3DGen: Repurposing Image Diffusion Models for 3D Asset Generation ✍️作者及机构&#xff1a;Jiantao Lin、Xin Yang、Meixi Chen 等&#xff08;HKUST …

HTTP基本结构

目录前言1. 概念2. HTTP基本格式2.1 抓包原理2.2 抓包软件使用2.3 抓包结果3. HTTP请求3.1 URL3.2 方法3.3 版本号3.4 HTTP报头3.4 正文部分4. HTTP响应4.1 HTTP状态码4.2 其他部分总结前言 本篇文章介绍HTTP的基本结构。 1. 概念 HTTP全称为超文本传输协议&#xff0c;是一…

CVPR优秀论文 | DashGaussian:在200秒内优化三维高斯点绘制

本文选自gongzhonghao【图灵学术SCI论文辅导】关注我们&#xff0c;掌握更多顶会顶刊发文资讯1.导读1.1 论文基本信息论文标题&#xff1a;DashGaussian: Optimizing 3D Gaussian Splatting in 200 Seconds作者&#xff1a;Youyu Chen、Junjun Jiang、Kui Jiang、Xiao Tang、Zh…

知识蒸馏 - 基于KL散度的知识蒸馏 HelloWorld 示例 采用PyTorch 内置函数F.kl_div的实现方式

知识蒸馏 - 基于KL散度的知识蒸馏 HelloWorld 示例 采用PyTorch 内置函数F.kl_div的实现方式 flyfish kl_div 是 Kullback-Leibler Divergence的英文缩写。 其中&#xff0c;KL 对应提出该概念的两位学者&#xff08;Kullback 和 Leibler&#xff09;的姓氏首字母“div”是 div…

C语言基础_补充知识、数据类型转换、选择结构

0、补充知识&#xff1a; 原码、反码、补码的知识&#xff1a; 计算机中原码转补码&#xff0c;正数不变&#xff0c;负数是符号位不变&#xff0c;其余各位取反码加一。负数的补码转原码应该是补码减一然后再取反&#xff0c;为什么负数的补码转原码是补码取反然后再加一&…

ubuntu自动重启BUG排查指南

当 Ubuntu 系统意外重启时&#xff0c;排查原因需要从系统日志、硬件状态和定时任务等多个方面入手。 示例&#xff1a;通过日志检查重启原因 last -x | head | tac 此命令显示最近的关机和重启记录。如果记录中包含 shutdown 或 crash&#xff0c;则可能是人为操作或系统故障导…

2. JS 有哪些数据类型

总结 基础类型&#xff08;7 种&#xff09;&#xff1a;number, string, boolean, null, undefined, symbol, bigint引用类型&#xff08;对象及其子类&#xff09;&#xff1a;object, array, function, date, regexp, map, set 等 判断方式推荐&#xff1a; 基础类型&#x…

pipeline方法关系抽取--课堂笔记

Pipeline方法课堂笔记 一、Pipeline方法原理 pipeline方法是指在实体识别已经完成的基础上再进行实体之间关系的抽取. pipeline方法流程&#xff1a; 先对输入的句子进行实体抽取&#xff0c;将识别出的实体分别组合&#xff1b;然后再进行关系分类. 注意&#xff1a;这两个子过…

linux系统离线环境安装clickhouse客户端

1、下载离线安装包&#xff1a; 方式1&#xff1a;网站直接下载 链接&#xff1a;https://packagecloud.io/altinity/clickhouse 注意要下载同一版本的四个包 方式2&#xff1a;夸克网盘分享 链接&#xff1a;https://pan.quark.cn/s/7e77e6a1bc5f 2、将本地下载的安装包上传…

GPT-5的诞生之痛:AI帝国的现实危机

目录 前言 一、“俄里翁”的陨落&#xff1a;一场梦碎的代际飞跃 二、扎克伯格的“抄家式”突袭 三、天才的诅咒&#xff1a;当AI聪明到无法与我们对话 四、烧钱的无底洞与微软的影子 结语&#xff1a;AI帝国的黄昏&#xff0c;还是黎明前的黑暗&#xff1f; &#x1f3a…

探索设计模式的宝库:Java-Design-Patterns

在软件开发领域&#xff0c;设计模式是解决常见问题的经典方案&#xff0c;它们如同建筑师的蓝图&#xff0c;为开发者提供了经过验证的最佳实践。今天我要向大家介绍一个GitHub上的明星项目——java-design-patterns&#xff0c;这是一个全面、实用且持续更新的设计模式宝藏项…

JavaScript中的作用域、闭包、定时器 由浅入深

1. JavaScript中的作用域是什么&#xff1f; 作用域&#xff08;Scope&#xff09;是程序中定义变量的区域&#xff0c;它决定了变量的可访问性&#xff08;可见性&#xff09;。在JavaScript中&#xff0c;作用域主要分为三种&#xff1a;全局作用域、函数作用域和块级作用域&…

仓库管理系统-11-前端之头部区域Header的用户登录和退出功能

文章目录 1 登录功能 1.1 登录页面(Login.vue) 1.1.1 页面布局 1.1.2 初始化数据 1.1.3 confirm方法 1.1.4 UserController.java(登录接口) 1.1.5 Login.vue 1.2 登录页面的路由 1.2.1 创建路由文件(router/index.js) 1.2.2 注册路由器(main.js) 1.2.3 路由视图(App.vue) 2 退出…