news 2026/10/14 18:05:11

STM32F407与OpenMV视觉伺服闭环:激光打靶云台实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F407与OpenMV视觉伺服闭环:激光打靶云台实战

简介:这份资源面向嵌入式视觉与运动控制方向的学习者与参赛者,围绕STM32F407主控、步进电机与OpenMV摄像头搭建激光打靶系统,解决图形识别、闭环循迹与几何参数测量等典型问题。包内共276个文件,以55个h头文件、52个c源文件为核心,配合49个o目标文件、48个crf交叉引用文件及uvprojx、uvoptx等Keil工程配置,另有hex烧录文件、map映射表与py脚本,压缩包约9.16MB,工程结构完整可直接编译。代码实现识别矩形或圆形后,以闭环方式驱动步进电机带动激光笔在图形区域内循迹,并包含形状判断、距离估算与图形面积计算逻辑;同时封装了USART、OLED、LCD、MPU6050、PWM、键盘及超声波等固件库源码,便于二次开发与模块复用。目前已有3174人学习下载,适合备赛智能车、光电设计或嵌入式课程设计时参考完整方案与排错思路。

1. 激光打靶闭环:STM32F407 与 OpenMV 的视觉伺服实战

去年帮一个做机器人竞赛的朋友调一套自动打靶装置,他们原本的方案是纯机械限位加手动校准,靶面稍微挪动十厘米就得重新对半天。后来换成 OpenMV 做视觉识别、STM32F407 驱动步进电机做二维云台,整个系统从「打哪儿指哪儿」变成了「指哪儿打哪儿」。这套方案的核心其实不复杂:OpenMV 负责在图像里找到靶心坐标,STM32F407 负责把像素偏差换算成步进电机的脉冲数,两个芯片通过串口交换数据,形成一个视觉伺服闭环。它适合做自动瞄准、激光指示、智能云台这类需要「看见目标再动」的场景,也适合想入门机器视觉加运动控制的开发者。下面我把这套系统从选型到落地的完整路径拆开讲,包括坐标怎么映射、脉冲怎么算、串口协议怎么定,以及我踩过的那些坑。

2. 硬件选型与坐标映射:为什么不是随便挑个电机就能打准

2.1 主控、视觉模块与执行器的三角关系

STM32F407 在这套系统里扮演的是「运动规划器」的角色。它需要同时处理三件事:接收 OpenMV 发来的靶心坐标、计算云台需要转动的角度、输出脉冲和方向信号给步进电机驱动器。F407 的定时器资源比较充裕,高级定时器 TIM1 和 TIM8 可以输出互补 PWM 带死区控制,通用定时器 TIM2 到 TIM5 也能做脉冲输出,这给两轴独立控制留了余量。主频 168MHz,跑一个简单的比例控制或者带前馈的 PID 完全够用,不需要上更贵的 H7 系列。

OpenMV 这边选型要看靶面大小和识别距离。常见做法是用 OpenMV H7 或者 H7 Plus,前者算力够跑色块检测和 AprilTag 识别,后者多出来的内存可以跑轻量级神经网络。如果靶心是特定颜色的圆形,用find_blobs做色块检测就能拿到质心坐标;如果靶面贴的是 AprilTag 或者二维码,用find_apriltags可以直接拿到标签的六自由度位姿,精度更高但帧率会降。我一般会先问清楚靶面特征再定方案,纯色靶用色块检测,带编码图案的用 AprilTag。

步进电机选 42 或者 57 都行,关键是驱动器要支持细分。细分越高,单步角度越小,理论分辨率越高,但过高的细分在高速时反而容易丢步。常见做法是 16 细分或者 32 细分,配合 1.8 度步距角的电机,单步角度能到 0.1125 度或者 0.05625 度。云台机械结构如果是同步带传动,还要把传动比算进去。

2.2 像素坐标到电机脉冲的映射推导

这是整套系统里最容易翻车的地方。OpenMV 返回的靶心坐标是像素值,比如(160, 120),而步进电机需要的是脉冲数。中间要经过三步换算:像素偏差转角度偏差、角度偏差转脉冲数、脉冲数转定时器比较值。

先看像素到角度的换算。假设 OpenMV 镜头水平视场角是FOV_H,图像宽度是W,那么每个像素对应的角度是FOV_H / W。靶心在图像中心右侧dx个像素,云台水平方向就需要转动dx * FOV_H / W度。垂直方向同理,用垂直视场角和图像高度算。这里有个前提:镜头畸变要小,或者提前做畸变校正。广角镜头边缘畸变能到百分之几,打靶距离一远,误差就放大了。

角度到脉冲的换算取决于细分和传动比。设电机步距角为step_angle,驱动器细分数为microstep,传动比为ratio,那么云台转动一度需要的脉冲数是(1 / step_angle) * microstep * ratio。举个例子,1.8 度步距角、16 细分、传动比 1:1,转一度需要(1/1.8)*16 ≈ 8.89个脉冲。如果云台转一度对应 8.89 个脉冲,那dx像素对应的脉冲数就是dx * FOV_H / W * 8.89。

实际写代码的时候,我会把这一串换算封装成一个函数,输入像素偏差,输出脉冲数。这样调参的时候只改几个宏定义就行,不用满代码找系数。

// 像素偏差转脉冲数 // dx: 水平像素偏差, dy: 垂直像素偏差 // 返回值: 水平脉冲数, 垂直脉冲数 #define FOV_H_DEG 60.0f // 水平视场角,单位度 #define FOV_V_DEG 45.0f // 垂直视场角,单位度 #define IMG_W 320.0f // 图像宽度 #define IMG_H 240.0f // 图像高度 #define STEP_ANGLE 1.8f // 电机步距角 #define MICROSTEP 16.0f // 驱动器细分 #define GEAR_RATIO 1.0f // 传动比 void pixel_to_pulse(int16_t dx, int16_t dy, int32_t *pulse_x, int32_t *pulse_y) { // 每像素对应的角度 float deg_per_px_x = FOV_H_DEG / IMG_W; float deg_per_px_y = FOV_V_DEG / IMG_H; // 每度对应的脉冲数 float pulse_per_deg = (1.0f / STEP_ANGLE) * MICROSTEP * GEAR_RATIO; // 计算脉冲数,正负号表示方向 *pulse_x = (int32_t)(dx * deg_per_px_x * pulse_per_deg); *pulse_y = (int32_t)(dy * deg_per_px_y * pulse_per_deg); }

这段代码里FOV_H_DEG和FOV_V_DEG需要根据实际镜头标定,不能直接抄镜头规格书上的值,因为规格书给的是全画幅等效值,实际用在 OpenMV 的小靶面上会有偏差。我一般会拿一个已知尺寸的靶子放在固定距离,让 OpenMV 读像素宽度,反推实际视场角。MICROSTEP和GEAR_RATIO根据硬件配置改,改完重新编译烧录就行。

2.3 串口通信协议怎么定才不丢包

OpenMV 和 STM32F407 之间用串口通信,波特率一般设 115200 或者 230400。数据量不大,每帧就几个字节,但协议要定清楚,不然解析起来容易出错。

常见做法是定一个固定帧头加数据加校验的格式。比如帧头用0xAA 0x55,后面跟dx和dy各两个字节,最后加一个字节的校验和。OpenMV 端用uart.write()发,STM32 端用串口中断接收,状态机解析。这样即使偶尔丢一个字节,也能靠帧头重新同步。

# OpenMV 端发送靶心偏差 import ustruct from pyb import UART uart = UART(3, 115200) def send_offset(dx, dy): # 帧头 0xAA 0x55,dx dy 各两字节有符号数,最后校验和 header = b'\xAA\x55' data = ustruct.pack('<hh', dx, dy) # 小端模式,有符号短整型 checksum = sum(data) & 0xFF uart.write(header + data + bytes([checksum]))

STM32 端接收解析的时候,状态机先找0xAA,再找0x55,然后收四个字节数据,最后收校验和。校验通过就更新目标位置,不通过就丢弃重新找帧头。这里有个细节:串口中断里不要做复杂运算,把数据存到缓冲区,在主循环里解析和计算脉冲。中断里做浮点运算容易出玄学问题,血泪经验。

3. STM32F407 步进电机控制:从定时器配置到加减速曲线

3.1 定时器输出脉冲的两种方式与选型

STM32F407 输出步进电机脉冲有两种常见方式:PWM 模式和定时器中断翻转 IO。PWM 模式适合连续匀速转动,频率由定时器自动重装载值和预分频器决定,CPU 不用干预。中断翻转 IO 适合需要精确控制脉冲数的场景,每进一次中断翻转一次 IO,同时计数,到数了就停。

打靶云台需要精确控制转动角度,所以更适合中断翻转 IO 的方式。但中断频率不能太高,否则 CPU 全在响应中断,主循环跑不动。F407 主频 168MHz,中断里做几个简单操作,跑 20kHz 到 50kHz 的中断频率问题不大。再高就要考虑用 DMA 或者 PWM 加脉冲计数的方式了。

我一般会用 TIM2 或者 TIM5 做脉冲输出,这两个是 32 位定时器,自动重装载值可以设很大,方便做低频脉冲。配置的时候预分频器设成 168-1,这样定时器时钟是 1MHz,自动重装载值设成 50 就是 20kHz 的中断频率。中断服务函数里翻转 IO、更新脉冲计数、判断是否到达目标位置。

// TIM2 中断服务函数,用于步进电机脉冲输出 volatile int32_t pulse_count_x = 0; // 已发送脉冲数 volatile int32_t pulse_target_x = 0; // 目标脉冲数 volatile uint8_t motor_running_x = 0; // 电机运行标志 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (motor_running_x) { // 翻转脉冲引脚 GPIO_ToggleBits(GPIOA, GPIO_Pin_0); // 只在上升沿计数 if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0)) { if (pulse_target_x > 0) { pulse_count_x++; if (pulse_count_x >= pulse_target_x) { motor_running_x = 0; pulse_count_x = 0; } } else if (pulse_target_x < 0) { pulse_count_x--; if (pulse_count_x <= pulse_target_x) { motor_running_x = 0; pulse_count_x = 0; } } } } } }

这段代码里pulse_target_x的正负表示方向,正数正转,负数反转。方向引脚在启动电机之前就要设好,不能在中断里频繁切换,否则驱动器可能丢步。motor_running_x标志位用来启停,主循环里根据 OpenMV 发来的偏差更新pulse_target_x,然后置位motor_running_x启动电机。

3.2 加减速曲线为什么不能省

步进电机有个特性:启动频率不能太高,否则会丢步或者堵转。如果直接给一个很高的脉冲频率让电机从静止启动,电机可能响都不响一下,或者发出嗡嗡声但不转。这就是为什么需要加减速曲线。

常见做法是梯形加减速或者 S 形加减速。梯形加减速分三段:加速段、匀速段、减速段。加速段频率从启动频率线性升到目标频率,减速段反过来。S 形加减速在加速和减速的拐点做平滑处理,冲击更小,但计算量稍大。

在打靶场景里,云台转动角度一般不大,梯形加减速就够用了。实现方式是在定时器中断里动态调整自动重装载值。加速阶段每次中断把重装载值减小一点,频率就升高;减速阶段反过来。重装载值的变化量决定了加速度大小,需要根据电机扭矩和负载惯量调。

// 梯形加减速参数 #define ACCEL_STEPS 200 // 加速段脉冲数 #define DECEL_STEPS 200 // 减速段脉冲数 #define START_ARR 500 // 启动时自动重装载值,对应 2kHz #define TARGET_ARR 50 // 目标自动重装载值,对应 20kHz void update_speed(int32_t current_pulse, int32_t total_pulse) { uint16_t arr; if (current_pulse < ACCEL_STEPS) { // 加速段:ARR 从 START_ARR 线性降到 TARGET_ARR arr = START_ARR - (START_ARR - TARGET_ARR) * current_pulse / ACCEL_STEPS; } else if (current_pulse > total_pulse - DECEL_STEPS) { // 减速段:ARR 从 TARGET_ARR 线性升到 START_ARR int32_t decel_pulse = total_pulse - current_pulse; arr = START_ARR - (START_ARR - TARGET_ARR) * decel_pulse / DECEL_STEPS; } else { // 匀速段 arr = TARGET_ARR; } TIM_SetAutoreload(TIM2, arr); }

这段代码在每次中断里调用,根据当前脉冲数更新定时器的自动重装载值。ACCEL_STEPS和DECEL_STEPS要根据实际负载调,负载惯量大就设大一点,否则加速太猛会丢步。START_ARR对应启动频率,一般设 1kHz 到 2kHz,太高了电机启动不起来。

3.3 两轴联动的插补思路

打靶云台需要水平轴和垂直轴同时动,才能让激光点沿直线移动到靶心。如果两个轴分别动,先水平再垂直,激光点走的是直角路径,看起来会有点笨。更好的做法是做直线插补,让两个轴按比例同时走。

常见做法是用 Bresenham 算法做插补。假设水平轴需要走dx个脉冲,垂直轴需要走dy个脉冲,取绝对值大的那个作为主轴,主轴每走一步,从轴按dy/dx的比例走。这样两个轴同时到达目标,激光点走的是直线。

在代码里实现的时候,可以在主循环里计算好两个轴的目标脉冲数,然后启动主轴定时器,在主轴中断里根据比例决定从轴是否也发一个脉冲。这样只需要一个定时器就能控制两个轴,节省资源。

4. OpenMV 视觉识别:色块检测与 AprilTag 的取舍

4.1 色块检测的阈值怎么调才稳

色块检测是 OpenMV 最常用的功能,核心是find_blobs函数。它根据 LAB 颜色空间的阈值来分割图像,找到符合颜色范围的连通区域,然后返回每个区域的边界框和质心。调阈值是件很考验耐心的事,光线一变,阈值就得重调。

我一般会先用 OpenMV IDE 里的阈值编辑器,把靶子放在实际光照下,拖动滑块直到靶子被完整框出来。然后记下 LAB 三个通道的阈值范围,写进代码。但这样调出来的阈值只在当时的光照下有效,换一个环境就不行了。更稳的做法是加自动白平衡和自动增益,让 OpenMV 先适应环境光,再做色块检测。

# OpenMV 色块检测 import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240 sensor.set_auto_whitebal(True) # 自动白平衡 sensor.set_auto_gain(True) # 自动增益 sensor.skip_frames(time=2000) # 等待设置生效 # 红色靶心的 LAB 阈值,需要根据实际调整 red_threshold = (30, 100, 15, 127, 15, 127) uart = UART(3, 115200) while True: img = sensor.snapshot() blobs = img.find_blobs([red_threshold], pixels_threshold=100, area_threshold=100) if blobs: # 找最大的色块 largest = max(blobs, key=lambda b: b.pixels()) # 计算质心与图像中心的偏差 dx = largest.cx() - img.width() // 2 dy = largest.cy() - img.height() // 2 send_offset(dx, dy) else: # 没找到靶心,发送一个特殊值表示丢失目标 send_offset(32767, 32767)

这段代码里pixels_threshold和area_threshold用来过滤噪点,值太小会把反光点也当成靶子,值太大又可能漏掉远处的靶子。red_threshold的六个数字分别是 LAB 三个通道的最小值和最大值,需要根据实际靶子颜色调。如果靶子是绿色或者蓝色,改对应的阈值就行。

4.2 AprilTag 识别为什么精度更高但更挑环境

AprilTag 是一种视觉基准标记,类似二维码但专门为机器视觉设计。OpenMV 的find_apriltags函数可以识别 AprilTag 并返回它的位置和旋转角度。相比色块检测,AprilTag 的优势是能给出六自由度位姿,而且对光照变化不那么敏感,因为它是靠黑白对比度识别的,不是靠颜色。

但 AprilTag 也有缺点。首先它需要靶面上贴一个特定图案的标签,不能随便拿个红点就用。其次识别距离受标签尺寸限制,标签太小或者距离太远,识别率会下降。再就是帧率比色块检测低,因为解码运算量更大。

如果打靶场景允许在靶面上贴 AprilTag,我一般会优先用这个方案。识别稳定性好很多,而且能直接拿到靶面的倾斜角度,对云台姿态补偿有帮助。如果靶面不能贴东西,那就只能回到色块检测。

# OpenMV AprilTag 识别 import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) # AprilTag 用灰度图 sensor.set_framesize(sensor.VGA) # 640x480,分辨率高识别距离远 sensor.skip_frames(time=2000) uart = UART(3, 115200) while True: img = sensor.snapshot() tags = img.find_apriltags(families=image.TAG36H11) if tags: tag = tags[0] # 取第一个识别到的标签 dx = tag.cx() - img.width() // 2 dy = tag.cy() - img.height() // 2 send_offset(dx, dy) else: send_offset(32767, 32767)

这里families参数指定 AprilTag 的家族,常见的是TAG36H11,编码容量和识别率比较平衡。sensor.set_framesize设成 VGA 是为了在更远距离识别到标签,但帧率会降到 30fps 以下。如果对实时性要求高,可以降到 QVGA,但识别距离会缩短。

4.3 目标丢失与误识别怎么处理

视觉识别最怕两件事:目标丢了不知道,或者把别的东西当成目标。目标丢了如果还继续发旧坐标,云台就会往错误方向转。误识别更危险,可能让激光打到不该打的地方。

处理目标丢失的常见做法是设一个超时计数器。OpenMV 每帧都发数据,STM32 端如果连续几帧没收到有效数据,就认为目标丢失,停止电机或者让云台回到安全位置。超时阈值一般设 3 到 5 帧,太短了容易误判,太长了响应慢。

误识别可以用多帧确认来过滤。连续几帧都检测到目标,而且位置变化不大,才认为是真目标。如果某一帧突然跳到一个很远的位置,很可能是误识别,直接丢弃。这个逻辑可以放在 OpenMV 端做,也可以放在 STM32 端做。我一般放在 OpenMV 端,因为那边有图像信息,判断更准。

5. 避坑与排查:打不准、抖不停、丢目标怎么查

5.1 激光点始终偏离靶心一个固定距离

现象是每次打靶,激光点都偏在靶心同一个方向,偏差大小差不多。原因通常是机械零位没对准,或者像素到角度的换算系数有系统性误差。解决方法是先做一次静态标定:把靶子放在正前方,手动调整云台让激光打在靶心上,记录此时 OpenMV 读到的像素偏差。如果像素偏差不为零,说明光轴和摄像头光轴不平行,需要机械调整或者软件补偿。如果像素偏差为零但激光还是偏,那就是激光器和摄像头的相对位置有固定偏移,在代码里加一个偏移量补偿就行。

5.2 云台在靶心附近来回抖动

现象是激光点接近靶心时,云台开始左右或者上下小幅抖动,停不下来。原因是控制死区设得太小,或者比例系数太大。像素偏差换算成脉冲数之后,如果偏差只有一个像素,换算出来的脉冲数可能只有几个,电机走这几个脉冲后偏差可能变成反方向的一个像素,于是又往回走,形成振荡。解决办法是设一个死区,偏差小于一定像素数就不动,比如 3 个像素以内认为已经对准。另外比例系数不要设太大,宁可慢一点也不要抖。

5.3 串口数据偶尔乱码导致云台乱转

现象是大部分时候正常,偶尔云台会突然往一个方向猛转。原因是串口通信受干扰,数据错位,STM32 解析出了错误的偏差值。解决办法是加校验和,校验不过就丢弃整帧数据。另外串口线要用屏蔽线,走线远离电机驱动线,电机驱动线的电流突变会耦合到串口线上。如果还不行,降低波特率试试,115200 不行就降到 57600。

5.4 步进电机丢步导致累积误差

现象是打靶几次之后,激光点越来越偏,但每次偏差方向不固定。原因是电机丢步,可能是加速太猛、负载太大或者细分设得太高。解决办法是降低加速度,把ACCEL_STEPS设大一点,让速度变化更平缓。如果负载确实大,换扭矩更大的电机或者降低细分。另外检查驱动器电流设置,电流太小扭矩不够也会丢步。

5.5 OpenMV 帧率太低导致响应滞后

现象是靶子移动时,激光点追不上,总是慢半拍。原因是 OpenMV 帧率太低,或者串口发送频率太低。解决办法是降低图像分辨率,QVGA 不够就降到 QQVGA,但识别距离会缩短。另外find_blobs的pixels_threshold设大一点,减少候选区域数量,也能提高帧率。如果用了 AprilTag,换回色块检测能明显提升帧率。

6. 进阶技巧:用比例控制加前馈把打靶时间压到半秒内

前面讲的都是「看到偏差再动」的反馈控制,响应速度受限于视觉帧率和电机速度。如果想把打靶时间压到半秒以内,需要加前馈控制。思路是:当靶子移动时,根据靶子移动的速度提前给电机一个补偿量,而不是等偏差出现了再追。

具体做法是在 OpenMV 端计算靶心坐标的变化率,把dx、dy和它们的差分一起发给 STM32。STM32 端用比例项加微分项算控制量,比例项负责消除当前偏差,微分项负责预测偏差变化趋势。微分项系数不能太大,否则噪声会被放大,云台抖得更厉害。

// 比例加微分控制 float kp = 0.8f; // 比例系数 float kd = 0.3f; // 微分系数 int16_t last_dx = 0, last_dy = 0; void control_update(int16_t dx, int16_t dy) { // 微分项:当前偏差与上次偏差的差 int16_t d_dx = dx - last_dx; int16_t d_dy = dy - last_dy; // 控制量 = 比例项 + 微分项 float ctrl_x = kp * dx + kd * d_dx; float ctrl_y = kp * dy + kd * d_dy; // 转成脉冲数 int32_t pulse_x, pulse_y; pixel_to_pulse((int16_t)ctrl_x, (int16_t)ctrl_y, &pulse_x, &pulse_y); // 更新目标脉冲数,启动电机 pulse_target_x = pulse_x; pulse_target_y = pulse_y; motor_running_x = 1; motor_running_y = 1; last_dx = dx; last_dy = dy; }

这段代码里kp和kd需要实际调。kp太大云台会抖,太小响应慢。kd用来抑制超调,但太大会放大噪声。我一般先把kd设为零,调kp到云台刚好不抖,然后慢慢加kd直到响应速度满意。调参是个耐心活,急不得。

另一个技巧是分段控制。偏差大的时候用大比例系数快速靠近,偏差小的时候用小比例系数精细对准。这样既能快速响应,又不会在靶心附近抖动。实现方式就是根据偏差大小切换kp值,代码里加个判断就行。

最后说一个我自己的习惯:每次改完参数,先用示波器看脉冲波形,确认频率和脉冲数对得上,再上电机。直接上电机调参,丢步了都不知道是参数问题还是机械问题,排查起来很费时间。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 18:04:22

高性能炫酷时钟的工程化实现:时间推演与渲染分层

1. 为什么一个“炫酷的时钟”值得花三小时重写三次&#xff1f; 你有没有在某个深夜刷网页时&#xff0c;被一个悬浮在页面右下角、指针拖着光尾划过渐变星空背景的时钟突然拽住视线&#xff1f;不是因为它多准&#xff0c;而是它像活的一样——秒针跳动时表盘微微呼吸&#xf…

作者头像 李华
网站建设 2026/10/14 18:04:02

LFM线性调频信号与脉冲压缩:原理、参数选型与工程避坑指南

简介&#xff1a;压缩包内含基于Verilog实现的LFM信号下变频与匹配滤波全套代码&#xff0c;面向在Vivado环境中从事FPGA雷达信号处理设计的开发者。LFM信号频率随时间线性变化&#xff0c;是雷达脉冲压缩的关键波形&#xff0c;下变频用于将高频回波搬移至基带&#xff0c;匹配…

作者头像 李华
网站建设 2026/10/14 18:03:49

交通灯控制实验精讲:从计数器到状态机,Verilog与面包板实现

简介&#xff1a;面向电子工程与计算机科学专业学生的数字逻辑实验配套资源&#xff0c;主题为交通灯控制&#xff0c;聚焦基于Altera Quartus II平台的时序逻辑设计。内容涵盖VHDL/Verilog编写的红绿灯控制方案&#xff0c;包括信号定义、计数器定时&#xff08;如红灯30秒、绿…

作者头像 李华
网站建设 2026/10/14 18:03:49

[Git-6] Git标签管理

一、标签概述 1、标签引入 标签&#xff08;Tag&#xff09;通常用于为某个commit取一个容易识别的名称&#xff0c;例如 v1.0.0 这样具有特殊意义的名字&#xff0c;方便定位正式发布版本、测试版本或其他重要节点。 标签的作用是什么呢&#xff1f;以前为了区分不同的commit&…

作者头像 李华