校电赛决赛一等奖H题源码存档

惊心动魄的小车调试呀

Maixcam

题目一和二

这些题目都要改动一下lab

from maix import image, camera, display, app, audio, time, uart, touchscreen
import math
import struct

camWidth = 320

ts = touchscreen.TouchScreen()
disp = display.Display()

idStart = 0
idEnd = 4

serial_dev = uart.UART("/dev/ttyS0", 115200)
time.sleep_ms(100)

Threshold = [[0, 40, -5, 5, -5, 5]]  # 更宽松的 L/A/B 范围

class LineFollower:
    def __init__(self, img_width=320, img_height=240,
                 lost_frame_threshold=5, recovery_frame_threshold=3,
                 min_area=256, max_width_ratio=0.4, edge_margin=0.0,
                 proximity_limit=60,
                 # 高灵敏度参数(丢线时上 ROI 专用)
                 sensitive_min_area=64,
                 sensitive_max_width_ratio=0.2,
                 sensitive_edge_margin=0.00):
        self.img_w = img_width
        self.img_h = img_height

        # ROI 百分比定义(上/中/下)
        rois_pct = [
            (0,   0.40, 1.0, 0.2, 0.2),  # 上(最远)
            (0,   0.60, 1.0, 0.2, 0.4),  # 中
            (0,   0.80, 1.0, 0.2, 0.9)   # 下(最近)
        ]
        self.rois = []
        for (xp, yp, wp, hp, wgt) in rois_pct:
            x = int(xp * img_width)
            y = int(yp * img_height)
            w = int(wp * img_width)
            h = int(hp * img_height)
            self.rois.append((x, y, w, h, wgt))
        self.weight_sum = sum(r[4] for r in self.rois)

        # 各 ROI 中心 Y 坐标(用于外推)
        self.roi_centers_y = [y + h//2 for (_, y, _, h, _) in self.rois]

        # 内部状态
        self.line_rho = 0
        self.last_rho = img_width // 2
        self.lost_count = 0
        self.recovery_count = 0
        self.lost_frame_cnt = 0
        self.recovery_frame_cnt = 0
        self.confirmed_lost = False

        # 常规参数
        self.lost_frame_threshold = lost_frame_threshold
        self.recovery_frame_threshold = recovery_frame_threshold
        self.min_area = min_area
        self.max_width_ratio = max_width_ratio
        self.edge_margin = edge_margin
        self.proximity_limit = proximity_limit

        # 高灵敏度参数(丢线时上 ROI 专用)
        self.sensitive_min_area = sensitive_min_area
        self.sensitive_max_width_ratio = sensitive_max_width_ratio
        self.sensitive_edge_margin = sensitive_edge_margin

        self.draw_debug = True

    # ---------- 辅助函数 ----------
    def _filter_blobs(self, blobs, roi_x, roi_w, min_area, max_w_ratio, edge_margin):
        """通用过滤:面积、宽度、边缘"""
        valid = []
        for b in blobs:
            if b.area() < min_area:
                continue
            if b.w() > roi_w * max_w_ratio:
                continue
            if (b.x() < roi_x + roi_w * edge_margin or
                (b.x() + b.w()) > roi_x + roi_w * (1 - edge_margin)):
                continue
            valid.append(b)
        return valid

    def _nearest_blob(self, blobs, target_x, limit=None):
        if not blobs:
            return None
        if limit is None:
            limit = self.proximity_limit
        best = None
        best_dist = float('inf')
        for b in blobs:
            dist = abs(b.cx() - target_x)
            if dist < best_dist and dist <= limit:
                best = b
                best_dist = dist
        return best

    def _extrapolate_x(self, x_mid, y_mid, x_down, y_down, y_up):
        dy = y_down - y_mid
        if abs(dy) < 1e-6:
            return x_down
        return int(x_mid + (x_down - x_mid) * (y_up - y_mid) / dy)

    # ---------- 主处理 ----------
    def update(self, img):
        # img.gaussian(3)
        # try:
        #     img.clahe(clip_limit=2.0, tile_grid=(8,8))
        # except:
        #     img.histeq() 
        line_blobs = []           # 用于绘制的实际色块
        roi_blobs = [None, None, None]  # 上中下最终选中的色块

        # 获取各 ROI 的候选色块(常规灵敏度)
        candidates = []
        for idx, (x, y, w, h, wgt) in enumerate(self.rois):
            blobs = img.find_blobs(Threshold, roi=(x, y, w, h),
                                   merge=True, pixels_threshold=self.min_area)
            valid = self._filter_blobs(blobs, x, w,
                                       self.min_area,
                                       self.max_width_ratio,
                                       self.edge_margin)
            candidates.append(valid)

        # ---- 下 ROI:参考 last_rho ----
        ref_down = self.last_rho
        blob_d = self._nearest_blob(candidates[2], ref_down)
        roi_blobs[2] = blob_d

        # ---- 中 ROI:如果有下,则参考下中心,否则参考 last_rho ----
        ref_mid = blob_d.cx() if blob_d else self.last_rho
        blob_m = self._nearest_blob(candidates[1], ref_mid)
        roi_blobs[1] = blob_m

        # ---- 判断是否丢线(中、下同时无效)----
        lose_now = (roi_blobs[2] is None and roi_blobs[1] is None)

        # ---- 上 ROI:平时用外推/邻近,丢线时高灵敏度尽力查找 ----
        if not lose_now:
            # 正常情况:中或下至少有一个有效
            if blob_m is not None and blob_d is not None:
                # 两点一线外推
                x_ext = self._extrapolate_x(blob_m.cx(), self.roi_centers_y[1],
                                            blob_d.cx(), self.roi_centers_y[2],
                                            self.roi_centers_y[0])
                blob_u = self._nearest_blob(candidates[0], x_ext,
                                            limit=self.proximity_limit * 1.5)
            else:
                # 只有一个有效(中或下)
                ref_up = blob_m.cx() if blob_m else (blob_d.cx() if blob_d else self.last_rho)
                blob_u = self._nearest_blob(candidates[0], ref_up)
            roi_blobs[0] = blob_u
        else:
            # ---- 丢线状态:上 ROI 高灵敏度尽力查找线头 ----
            blob_u = None

            # 先尝试从常规候选里找(也许已经过滤出小块)
            if candidates[0]:
                # 取离 last_rho 最近的
                blob_u = self._nearest_blob(candidates[0], self.last_rho,
                                            limit=self.proximity_limit * 2.0)

            # 如果常规候选没找到,用高灵敏度参数重新查找
            if blob_u is None:
                x, y, w, h, _ = self.rois[0]
                blobs_sensitive = img.find_blobs(Threshold, roi=(x, y, w, h),
                                                 merge=True,
                                                 pixels_threshold=self.sensitive_min_area)
                valid_s = self._filter_blobs(blobs_sensitive, x, w,
                                             min_area=self.sensitive_min_area,
                                             max_w_ratio=self.sensitive_max_width_ratio,
                                             edge_margin=self.sensitive_edge_margin)
                if valid_s:
                    # 取面积最大或离 last_rho 最近的,这里用最近
                    blob_u = self._nearest_blob(valid_s, self.last_rho,
                                                limit=self.proximity_limit * 2.5)

            roi_blobs[0] = blob_u

        # ---- 收集用于显示和加权 ----
        centroid_sum = 0
        weight_used = 0
        for idx, b in enumerate(roi_blobs):
            if b is not None:
                line_blobs.append(b)
                centroid_sum += b.cx() * self.rois[idx][4]
                weight_used += self.rois[idx][4]

        # ---- 连续帧计数 ----
        if lose_now:
            self.lost_frame_cnt += 1
            self.recovery_frame_cnt = 0
        else:
            self.recovery_frame_cnt += 1
            self.lost_frame_cnt = 0

        is_lost_confirmed = (self.lost_frame_cnt >= self.lost_frame_threshold)
        is_recovered = (self.recovery_frame_cnt >= self.recovery_frame_threshold)

        if is_lost_confirmed and not self.confirmed_lost:
            self.lost_count += 1
            self.confirmed_lost = True
        elif self.confirmed_lost and is_recovered:
            self.recovery_count += 1
            self.confirmed_lost = False

        # ---- 计算最终线位置 ----
        if not lose_now:
            if weight_used > 0:
                self.line_rho = int(centroid_sum / weight_used)
            else:
                self.line_rho = self.last_rho
            self.last_rho = self.line_rho
        else:
            # 丢线时,有上 ROI 就用上,没有则保持
            if roi_blobs[0] is not None:
                self.line_rho = roi_blobs[0].cx()
                self.last_rho = self.line_rho
                line_blobs.append(roi_blobs[0])  # 确保绘制出来
            else:
                self.line_rho = self.last_rho

        offset = self.line_rho - self.img_w // 2

        # ---- 调试绘制 ----
        if self.draw_debug:
            for b in line_blobs:
                img.draw_rect(b.x(), b.y(), b.w(), b.h(), color=image.COLOR_WHITE)
                img.draw_cross(b.cx(), b.cy(),
                               image.Color.from_rgb(255, 255, 255),
                               size=5, thickness=1)
            img.draw_string(0, 30,
                            'rho=%d off=%d lost=%d' % (self.line_rho, offset, self.lost_count),
                            image.COLOR_WHITE)
            img.draw_string(0, 50,
                            'lost_cnt=%d rec=%d' % (self.lost_frame_cnt, self.recovery_count),
                            image.COLOR_WHITE)

        return offset, self.lost_count, self.recovery_count

# ========== 外部主循环示例 ==========
follower = LineFollower(img_width=320, img_height=240)

# 速度控制参数
BASE_SPEED = 33
TURN_GAIN = BASE_SPEED * 0.0045 * 320 / camWidth
MAX_SPEED = BASE_SPEED
MIN_SPEED = 0

# ========== 巡线模式函数 ==========
def calc_speed(error):
    """计算左右轮速度"""
    left_speed = BASE_SPEED + TURN_GAIN * error
    right_speed = BASE_SPEED - TURN_GAIN * error
    
    left_speed = max(MIN_SPEED, min(MAX_SPEED, left_speed))
    right_speed = max(MIN_SPEED, min(MAX_SPEED, right_speed))
    
    return int(left_speed), int(right_speed)

def send_speed_to_stm32(left_speed, right_speed):
    """打包发送6字节协议到STM32"""
    left_u16 = left_speed & 0xFFFF
    right_u16 = right_speed & 0xFFFF
    
    left_low = left_u16 & 0xFF
    left_high = (left_u16 >> 8) & 0xFF
    right_low = right_u16 & 0xFF
    right_high = (right_u16 >> 8) & 0xFF
    
    checksum = (0xAA + left_low + left_high + right_low + right_high) & 0xFF
    packet = bytes([0xAA, left_low, left_high, right_low, right_high, checksum])
    serial_dev.write(packet)
    return packet


# 提前创建好播放器,避免每次调用都重复初始化,提升响应速度
_player = audio.Player(sample_rate=48000, format=audio.Format.FMT_S16_LE, channel=1)
_player.volume(100)   # 音量 0-100,可按需调整

def beep(freq=2000, duration=0.3):
    """
    使用板载喇叭播放指定频率和时长的提示音(阻塞式)
    :param freq: 频率,单位 Hz,默认 2000
    :param duration: 持续时间,单位 秒,默认 0.8
    """
    # 1. 根据频率和时长生成 PCM 音频数据
    sample_rate = 48000
    num_samples = int(sample_rate * duration)
    amplitude = 32767   # 最大音量
    pcm = bytearray()

    for i in range(num_samples):
        t = i / sample_rate
        sample = amplitude * math.sin(2 * math.pi * freq * t)
        pcm.extend(struct.pack('<h', int(sample)))

    # 2. 播放生成的 PCM 数据(非阻塞,硬件立即开始输出)
    _player.play(bytes(pcm))

    # 3. 阻塞等待播放结束
    time.sleep_ms(int(duration * 1000))

def detectApriltag():
    cam = camera.Camera(320, 240)
    families = image.ApriltagFamilies.TAG36H11

    print("开始识别 AprilTag,按 Ctrl+C 或开发板上的 Home 键退出...")

    while not app.need_exit():
        try:
            img = cam.read()
            if img is None:
                continue

            apriltags = img.find_apriltags(families=families)

            if apriltags:
                ids = [tag.id() for tag in apriltags]
                print(ids)

                img.draw_string(10, 10, "IDs: " + ",".join(map(str, ids)),
                                scale=3, color=image.COLOR_RED)

                for tag in apriltags:
                    corners = tag.corners()
                    for i in range(4):
                        img.draw_line(
                            corners[i][0], corners[i][1],
                            corners[(i + 1) % 4][0], corners[(i + 1) % 4][1],
                            color=image.COLOR_GREEN, thickness=2
                        )

                disp.show(img)
                beep()   # 确保 beep 函数已定义,否则会报错
                print("识别完成,程序退出。")
                del cam          # 先释放摄像头再返回
                return ids       # 返回所有 ID 的列表
            else:
                disp.show(img)

        except Exception as e:
            print(f"识别出错: {e}")

    del cam
    return []      # 如果用户主动退出,返回空列表

def is_in_button(x, y, btn_pos):
    return x > btn_pos[0] * 2 and x < btn_pos[0] * 2 + btn_pos[2] * 2 and y > btn_pos[1] * 2 and y < btn_pos[1] * 2 + btn_pos[3] * 2


if __name__ == "__main__":
    found_id = 0
    delay = 60
    while True:
        ids = detectApriltag()  
        if not ids:
            print("未检测到任何标签或用户主动退出,程序结束。")
            break
        for tag_id in ids:
            if idStart <= tag_id <= idEnd:
                found_id = tag_id
                break

        if found_id is not None:
            print(f"找到范围内 ID: {found_id} (范围 {idStart}~{idEnd})")
            break  
        else:
            print(f"检测到的 ID {ids} 不在范围 [{idStart}, {idEnd}] 内,重新识别...")

    cam = camera.Camera(320, 240)
    loop_label = f"LOOP: {found_id}"
    exit_label = " R U N "
    size = image.string_size(exit_label, scale = 3.0, thickness = 2)
    exit_btn_pos = [0, 0, 8*2 + size.width(), 30 + size.height()]
    while True:
        img = cam.read()
        img.draw_string(8, 30, exit_label, image.COLOR_WHITE, scale = 3.0, thickness = 2)
        img.draw_rect(exit_btn_pos[0], exit_btn_pos[1], exit_btn_pos[2], exit_btn_pos[3],  image.COLOR_WHITE, 2)
        img.draw_string(8, 75, loop_label, image.COLOR_WHITE, scale = 3.0, thickness = 2)
        x, y, pressed = ts.read()
        if is_in_button(x, y, exit_btn_pos):
            break
        disp.show(img)

    
    while True:
        t = time.time_ms()
        img = cam.read()
        if img is None: 
            continue
        offset, lost_cnt, recovery_count = follower.update(img)

        l, r = calc_speed(offset)
        if recovery_count > found_id * 3 - 1:
            delay -= 1
            if delay > 0:
                delay -= 1
                send_speed_to_stm32(l, -r)
            else:
                send_speed_to_stm32(0, 0)
        else:
            send_speed_to_stm32(l, -r)

        disp.show(img)
        print("FPS =", int(1000 / (time.time_ms() - t)), "Offset:", offset, "Lost:", lost_cnt)
        

题目三

from maix import image, camera, display, app, audio, time, uart, touchscreen, ahrs
import math
import struct

from maix.ext_dev import imu

camWidth = 320

ts = touchscreen.TouchScreen()
disp = display.Display()

idStart = 0
idEnd = 4

serial_dev = uart.UART("/dev/ttyS0", 115200)
time.sleep_ms(100)

Threshold = [[0, 40, -6, 6, -10, 6]]  # 更宽松的 L/A/B 范围


kp = 2
ki = 0.01
calibrate_first = False
sensor = imu.IMU("", mode=imu.Mode.DUAL,
                                    acc_scale=imu.AccScale.ACC_SCALE_2G,
                                    acc_odr=imu.AccOdr.ACC_ODR_1000,
                                    gyro_scale=imu.GyroScale.GYRO_SCALE_256DPS,
                                    gyro_odr=imu.GyroOdr.GYRO_ODR_8000)
ahrs_filter = ahrs.MahonyAHRS(kp, ki)

if calibrate_first or not sensor.calib_gyro_exists():
    print("\n\nNeed calibrate fisrt")
    print("now calibrate, please !! don't move !! device, wait for 10 seconds")
    sensor.calib_gyro(10000)
else:
    sensor.load_calib_gyro()

class LineFollower:
    def __init__(self, img_width=320, img_height=240,
                 lost_frame_threshold=5, recovery_frame_threshold=3,
                 min_area=256, max_width_ratio=0.70, edge_margin=0.0,
                 proximity_limit=60,
                 # 高灵敏度参数(丢线时上 ROI 专用)
                 sensitive_min_area=30,
                 sensitive_max_width_ratio=0.8,
                 sensitive_edge_margin=0.0):
        self.img_w = img_width
        self.img_h = img_height

        # ROI 百分比定义(上/中/下)
        rois_pct = [
            (0.4,   0.40, 0.6, 0.2, 0.2),  # 上(最远)
            (0,   0.60, 1.0, 0.2, 0.5),  # 中
            (0,   0.80, 1.0, 0.2, 0.8)   # 下(最近)
        ]
        self.rois = []
        for (xp, yp, wp, hp, wgt) in rois_pct:
            x = int(xp * img_width)
            y = int(yp * img_height)
            w = int(wp * img_width)
            h = int(hp * img_height)
            self.rois.append((x, y, w, h, wgt))
        self.weight_sum = sum(r[4] for r in self.rois)

        # 各 ROI 中心 Y 坐标(用于外推)
        self.roi_centers_y = [y + h//2 for (_, y, _, h, _) in self.rois]

        # 内部状态
        self.line_rho = 0
        self.last_rho = img_width // 2
        self.lost_count = 0
        self.recovery_count = 0
        self.lost_frame_cnt = 0
        self.recovery_frame_cnt = 0
        self.confirmed_lost = False

        # 常规参数
        self.lost_frame_threshold = lost_frame_threshold
        self.recovery_frame_threshold = recovery_frame_threshold
        self.min_area = min_area
        self.max_width_ratio = max_width_ratio
        self.edge_margin = edge_margin
        self.proximity_limit = proximity_limit

        # 高灵敏度参数(丢线时上 ROI 专用)
        self.sensitive_min_area = sensitive_min_area
        self.sensitive_max_width_ratio = sensitive_max_width_ratio
        self.sensitive_edge_margin = sensitive_edge_margin

        self.draw_debug = True

    # ---------- 辅助函数 ----------
    def _filter_blobs(self, blobs, roi_x, roi_w, min_area, max_w_ratio, edge_margin):
        """通用过滤:面积、宽度、边缘"""
        valid = []
        for b in blobs:
            if b.area() < min_area:
                continue
            if b.w() > roi_w * max_w_ratio:
                continue
            if (b.x() < roi_x + roi_w * edge_margin or
                (b.x() + b.w()) > roi_x + roi_w * (1 - edge_margin)):
                continue
            valid.append(b)
        return valid

    def _nearest_blob(self, blobs, target_x, limit=None):
        if not blobs:
            return None
        if limit is None:
            limit = self.proximity_limit
        best = None
        best_dist = float('inf')
        for b in blobs:
            dist = abs(b.cx() - target_x)
            if dist < best_dist and dist <= limit:
                best = b
                best_dist = dist
        return best

    def _extrapolate_x(self, x_mid, y_mid, x_down, y_down, y_up):
        dy = y_down - y_mid
        if abs(dy) < 1e-6:
            return x_down
        return int(x_mid + (x_down - x_mid) * (y_up - y_mid) / dy)       

    # ---------- 主处理 ----------
    def update(self, img):
        # img.gaussian(3)
        # try:
        #     img.clahe(clip_limit=2.0, tile_grid=(8,8))
        # except:
        #     img.histeq() 
        line_blobs = []           # 用于绘制的实际色块
        roi_blobs = [None, None, None]  # 上中下最终选中的色块

        # 获取各 ROI 的候选色块(常规灵敏度)
        candidates = []
        for idx, (x, y, w, h, wgt) in enumerate(self.rois):
            blobs = img.find_blobs(Threshold, roi=(x, y, w, h),
                                   merge=True, pixels_threshold=self.min_area)
            valid = self._filter_blobs(blobs, x, w,
                                       self.min_area,
                                       self.max_width_ratio,
                                       self.edge_margin)
            candidates.append(valid)

        # ---- 下 ROI:参考 last_rho ----
        ref_down = self.last_rho
        blob_d = self._nearest_blob(candidates[2], ref_down)
        roi_blobs[2] = blob_d

        # ---- 中 ROI:如果有下,则参考下中心,否则参考 last_rho ----
        ref_mid = blob_d.cx() if blob_d else self.last_rho
        blob_m = self._nearest_blob(candidates[1], ref_mid)
        roi_blobs[1] = blob_m

        # ---- 判断是否丢线(中、下同时无效)----
        lose_now = (roi_blobs[2] is None and roi_blobs[1] is None)
        lose_down = (roi_blobs[2] is None)

        # ---- 上 ROI:平时用外推/邻近,丢线时高灵敏度尽力查找 ----
        if not lose_now:
            # 正常情况:中或下至少有一个有效
            if blob_m is not None and blob_d is not None:
                # 两点一线外推
                x_ext = self._extrapolate_x(blob_m.cx(), self.roi_centers_y[1],
                                            blob_d.cx(), self.roi_centers_y[2],
                                            self.roi_centers_y[0])
                blob_u = self._nearest_blob(candidates[0], x_ext,
                                            limit=self.proximity_limit * 1.5)
            else:
                # 只有一个有效(中或下)
                ref_up = blob_m.cx() if blob_m else (blob_d.cx() if blob_d else self.last_rho)
                blob_u = self._nearest_blob(candidates[0], ref_up)
            roi_blobs[0] = blob_u
        else:
            # ---- 丢线状态:上 ROI 高灵敏度尽力查找线头 ----
            blob_u = None

            # 先尝试从常规候选里找(也许已经过滤出小块)
            if candidates[0]:
                # 取离 last_rho 最近的
                blob_u = self._nearest_blob(candidates[0], self.last_rho,
                                            limit=self.proximity_limit * 2.0)

            # 如果常规候选没找到,用高灵敏度参数重新查找
            if blob_u is None:
                x, y, w, h, _ = self.rois[0]
                blobs_sensitive = img.find_blobs(Threshold, roi=(x, y, w, h),
                                                 merge=True,
                                                 pixels_threshold=self.sensitive_min_area)
                valid_s = self._filter_blobs(blobs_sensitive, x, w,
                                             min_area=self.sensitive_min_area,
                                             max_w_ratio=self.sensitive_max_width_ratio,
                                             edge_margin=self.sensitive_edge_margin)
                if valid_s:
                    # 取面积最大或离 last_rho 最近的,这里用最近
                    blob_u = self._nearest_blob(valid_s, self.last_rho,
                                                limit=self.proximity_limit * 2.5)

            roi_blobs[0] = blob_u

        # ---- 收集用于显示和加权 ----
        centroid_sum = 0
        weight_used = 0
        for idx, b in enumerate(roi_blobs):
            if b is not None:
                line_blobs.append(b)
                centroid_sum += b.cx() * self.rois[idx][4]
                weight_used += self.rois[idx][4]

        # ---- 连续帧计数 ----
        if lose_now:
            self.lost_frame_cnt += 1
            self.recovery_frame_cnt = 0
        else:
            self.recovery_frame_cnt += 1
            self.lost_frame_cnt = 0

        is_lost_confirmed = (self.lost_frame_cnt >= self.lost_frame_threshold)
        is_recovered = (self.recovery_frame_cnt >= self.recovery_frame_threshold)

        if is_lost_confirmed and not self.confirmed_lost:
            self.lost_count += 1
            self.confirmed_lost = True
        elif self.confirmed_lost and is_recovered:
            self.recovery_count += 1
            self.confirmed_lost = False

        # ---- 计算最终线位置 ----
        if not lose_now:
            if weight_used > 0:
                self.line_rho = int(centroid_sum / weight_used)
            else:
                self.line_rho = self.last_rho
            self.last_rho = self.line_rho
        else:
            # 丢线时,有上 ROI 就用上,没有则保持
            if roi_blobs[0] is not None:
                self.line_rho = roi_blobs[0].cx()
                self.last_rho = self.line_rho
                line_blobs.append(roi_blobs[0])  # 确保绘制出来
            else:
                self.line_rho = self.last_rho

        offset = self.line_rho - self.img_w // 2

        # ---- 调试绘制 ----
        if self.draw_debug:
            for b in line_blobs:
                img.draw_rect(b.x(), b.y(), b.w(), b.h(), color=image.COLOR_WHITE)
                img.draw_cross(b.cx(), b.cy(),
                               image.Color.from_rgb(255, 255, 255),
                               size=5, thickness=1)
            img.draw_string(0, 30,
                            'rho=%d off=%d lost=%d' % (self.line_rho, offset, self.lost_count),
                            image.COLOR_WHITE)
            img.draw_string(0, 50,
                            'lost_cnt=%d rec=%d' % (self.lost_frame_cnt, self.recovery_count),
                            image.COLOR_WHITE)

        return offset, self.lost_count, self.recovery_count, lose_down



# ========== 外部主循环示例 ==========
follower = LineFollower(img_width=320, img_height=240)

# 速度控制参数
BASE_SPEED = 20
TURN_GAIN = BASE_SPEED * 0.009 * 320 / camWidth
MAX_SPEED = BASE_SPEED
MIN_SPEED = 0

# ========== 巡线模式函数 ==========
def calc_speed(error):
    """计算左右轮速度"""
    left_speed = BASE_SPEED + TURN_GAIN * error
    right_speed = BASE_SPEED - TURN_GAIN * error
    
    left_speed = max(MIN_SPEED, min(MAX_SPEED, left_speed))
    right_speed = max(MIN_SPEED, min(MAX_SPEED, right_speed))
    
    return int(left_speed), int(right_speed)

def send_speed_to_stm32(left_speed, right_speed):
    """打包发送6字节协议到STM32"""
    left_u16 = left_speed & 0xFFFF
    right_u16 = right_speed & 0xFFFF
    
    left_low = left_u16 & 0xFF
    left_high = (left_u16 >> 8) & 0xFF
    right_low = right_u16 & 0xFF
    right_high = (right_u16 >> 8) & 0xFF
    
    checksum = (0xAA + left_low + left_high + right_low + right_high) & 0xFF
    packet = bytes([0xAA, left_low, left_high, right_low, right_high, checksum])
    serial_dev.write(packet)
    return packet


# 提前创建好播放器,避免每次调用都重复初始化,提升响应速度
_player = audio.Player(sample_rate=48000, format=audio.Format.FMT_S16_LE, channel=1)
_player.volume(100)   # 音量 0-100,可按需调整

def beep(freq=2000, duration=0.3):
    """
    使用板载喇叭播放指定频率和时长的提示音(阻塞式)
    :param freq: 频率,单位 Hz,默认 2000
    :param duration: 持续时间,单位 秒,默认 0.8
    """
    # 1. 根据频率和时长生成 PCM 音频数据
    sample_rate = 48000
    num_samples = int(sample_rate * duration)
    amplitude = 32767   # 最大音量
    pcm = bytearray()

    for i in range(num_samples):
        t = i / sample_rate
        sample = amplitude * math.sin(2 * math.pi * freq * t)
        pcm.extend(struct.pack('<h', int(sample)))

    # 2. 播放生成的 PCM 数据(非阻塞,硬件立即开始输出)
    _player.play(bytes(pcm))

    # 3. 阻塞等待播放结束
    time.sleep_ms(int(duration * 1000))

def scan_apriltag(resolution=(2560, 1440), win_w=400, win_h=300, step=200, scan_y=0):
    """
    拍摄一张照片,从左到右滑动窗口检测 AprilTag。
    完全内部管理摄像头,不依赖外部 camera 对象。

    参数:
        resolution: (width, height) 摄像头采集分辨率,默认 (2560,1440)
        win_w, win_h: 滑动窗口尺寸,默认 400x300
        step: 水平滑动步长(像素),默认 200
        scan_y: 垂直起始位置(顶部偏移),默认 0

    返回:
        若检测到: dict {
            'id': int,
            'family': str,
            'corners': [(x1,y1), (x2,y2), (x3,y3), (x4,y4)],  # 在原图上的坐标
            'center': (cx, cy)
        }
        若未检测到: None
    """
    # ---------- 1. 内部初始化摄像头(指定分辨率)----------
    cam = camera.Camera(resolution[0], resolution[1])
    time.sleep(0.5)  # 等待传感器稳定
    full_img = cam.read()  # 拍摄一帧
    # 拍摄完成后立即释放摄像头资源(避免占用内存和冲突)
    del cam
    # 注意:某些固件可能没有显式的 close 方法,del 会触发 __del__ 释放

    # ---------- 2. 滑动窗口扫描 ----------
    width, height = resolution
    max_x = width - win_w
    x_start = 0
    detected = False
    result = None

    while x_start <= max_x and not detected:
        # 裁剪当前窗口
        window_img = full_img.crop(x_start, scan_y, win_w, win_h)
        # 实时显示当前扫描窗口(便于观察进度)
        disp.show(window_img)

        tags = window_img.find_apriltags(families=image.ApriltagFamilies.TAG36H11)
        if tags:
            detected = True
            a = tags[0]  # 只取第一个
            # 窗口内坐标转换到原图坐标
            corners_win = a.corners()
            orig_corners = [(int(cx + x_start), int(cy + scan_y)) for (cx, cy) in corners_win]
            cx_win = int(a.x() + a.w() / 2)
            cy_win = int(a.y() + a.h() / 2)
            orig_center = (cx_win + x_start, cy_win + scan_y)

            # 在原图上绘制标记
            for i in range(4):
                x1, y1 = orig_corners[i]
                x2, y2 = orig_corners[(i+1)%4]
                full_img.draw_line(x1, y1, x2, y2, image.COLOR_RED, thickness=2)
            full_img.draw_circle(orig_center[0], orig_center[1], 5, image.COLOR_GREEN, thickness=2)

            ox_text = int(a.x() + a.w() + x_start)
            oy_text = int(a.y() + scan_y)
            full_img.draw_rect(ox_text, oy_text, 100, 30, image.COLOR_BLACK, thickness=-1)
            full_img.draw_string(ox_text, oy_text, f"id: {a.id()}", image.COLOR_WHITE)
            full_img.draw_string(ox_text, oy_text+15, f"family: {a.family()}", image.COLOR_WHITE)

            result = a.id()

            # result = {
            #     'id': a.id(),
            #     'family': a.family(),
            #     'corners': orig_corners,
            #     'center': orig_center
            # }
            break
        else:
            x_start += step

    # ---------- 3. 显示最终结果(缩放至屏幕)----------
    if not detected:
        full_img.draw_string(50, 50, "No AprilTag found", image.COLOR_RED)

    # 缩放图像以适应显示屏
    if full_img.width() != disp.width() or full_img.height() != disp.height():
        full_img = full_img.resize(disp.width(), disp.height())
    disp.show(full_img)

    return result

def clamp_abs(value: float) -> float:
    if value > 0:
        return max(value, 1.0)
    elif value < 0:
        return min(value, -1.0)
    else:
        # value == 0 的情况,按规则归为 1.0
        return 1.0

def execuseTurn(angle: float = 0.0, speed: int = 10, offset: float = 0.5):
    """
    相对旋转指定角度(单位:度)
    angle  > 0  顺时针(或取决于你的底盘定义)
    speed      最大速度(int,绝对值 ≤ speed)
    offset     允许的停止误差范围(度)
    """
    # ---- 初始化,获得当前朝向 ----
    data = sensor.read_all(calib_gryo=True, radian=True)
    t0 = time.ticks_s()
    attitude = ahrs_filter.get_angle(data.acc, data.gyro, data.mag, 0.01, radian=False)
    initial_yaw = attitude.z          # 记录起始偏航角
    last_time = t0

    # 计算目标绝对角度,限制在 [-180, 180)
    target_absolute = initial_yaw + angle
    while target_absolute >= 180:
        target_absolute -= 360
    while target_absolute < -180:
        target_absolute += 360

    # 比例系数,保持 30° 对应满速的设定(可调)
    Kp = speed / 30.0

    while True:
        data = sensor.read_all(calib_gryo=True, radian=True)
        t = time.ticks_s()
        dt = t - last_time
        last_time = t

        attitude = ahrs_filter.get_angle(data.acc, data.gyro, data.mag, dt, radian=False)
        current_yaw = attitude.z

        print(f"pitch: {attitude.x:8.2f}, roll: {attitude.y:8.2f}, yaw: {current_yaw:8.2f}, dt: {int(dt*1000):3d}ms, temp: {data.temp:.1f}")

        # 误差 = 目标绝对角度 - 当前偏航,并规范到 [-180, 180)
        error = target_absolute - current_yaw
        while error > 180:
            error -= 360
        while error < -180:
            error += 360

        # 进入允许范围则停车退出
        if abs(error) <= offset:
            send_speed_to_stm32(0, 0)
            print(f"旋转完成,剩余误差: {error:.2f}°")
            break

        # 比例速度,误差大速度快,误差小速度慢
        motor_speed = Kp * error
        if motor_speed > speed:
            motor_speed = speed
        elif motor_speed < -speed:
            motor_speed = -speed

        motor_speed  = clamp_abs(motor_speed)

        left_speed = int(motor_speed)
        right_speed = int(motor_speed)   # 右轮取反,实现原地旋转
        send_speed_to_stm32(-left_speed, -right_speed)

def blindMove(duration = 1000, speed = 20):
    send_speed_to_stm32(speed, -speed)
    send_speed_to_stm32(speed, -speed)
    send_speed_to_stm32(speed, -speed)
    send_speed_to_stm32(speed, -speed)
    send_speed_to_stm32(speed, -speed)
    send_speed_to_stm32(speed, -speed)
    time.sleep_ms(duration / 2)


def is_in_button(x, y, btn_pos):
    return x > btn_pos[0] * 2 and x < btn_pos[0] * 2 + btn_pos[2] * 2 and y > btn_pos[1] * 2 and y < btn_pos[1] * 2 + btn_pos[3] * 2


if __name__ == "__main__":

    # while True:
    #     execuseTurn(90)
    #     time.sleep_ms(1000)
    #     execuseTurn(-90)
    #     time.sleep_ms(1000)

    found_id = 0
    isLabelRequired = 0
    tag01 = 0
    tag02 = 0
    isMode = 0
    cam = camera.Camera(320, 240)

    change_label = "CHANGE"
    size0 = image.string_size(change_label, scale=1.0, thickness=1)
    change_btn_pos = [150, 0, 8*2 + size0.width(),  size0.height()]

    odd_label = " O D D "
    even_label = " E V E N "

    size1 = image.string_size(odd_label, scale=3.0, thickness=2)
    odd_btn_pos = [8, 30, 8*2 + size1.width(), 30 + size1.height()]

    size2 = image.string_size(even_label, scale=3.0, thickness=2)
    even_btn_pos = [8, odd_btn_pos[3] + 50, 8*2 + size2.width(), 30 + size2.height()]   # 与 odd 底部留 20 间隙

    left_label = " L E F T "
    right_label = " R I G H T "

    size3 = image.string_size(left_label, scale=3.0, thickness=2)
    left_btn_pos = [8, 30, 8*2 + size3.width(), 30 + size3.height()]

    size4 = image.string_size(right_label, scale=3.0, thickness=2)
    right_btn_pos = [8, left_btn_pos[3] + 50, 8*2 + size4.width(), 30 + size4.height()]   # 与 odd 底部留 20 间隙

    isChangePressed = False
    while True:
        
        img = cam.read()
        
        img.draw_string(150, 0, change_label, image.COLOR_WHITE, scale = 1.0, thickness = 1)
        img.draw_rect(change_btn_pos[0], change_btn_pos[1], change_btn_pos[2], change_btn_pos[3],  image.COLOR_RED, 1)

        if isMode == 0 :
            # 画 Odd 按钮文字和矩形
            img.draw_string(odd_btn_pos[0], odd_btn_pos[1] + 15, odd_label, image.COLOR_WHITE, scale=3.0, thickness=2)
            img.draw_rect(odd_btn_pos[0], odd_btn_pos[1], odd_btn_pos[2], odd_btn_pos[3], image.COLOR_WHITE, 2)
            
            # 画 Even 按钮文字和矩形(两者位置对齐)
            img.draw_string(even_btn_pos[0], even_btn_pos[1] + 15, even_label, image.COLOR_WHITE, scale=3.0, thickness=2)
            img.draw_rect(even_btn_pos[0], even_btn_pos[1], even_btn_pos[2], even_btn_pos[3], image.COLOR_WHITE, 2)
        else:
            # 画 Odd 按钮文字和矩形
            img.draw_string(left_btn_pos[0], left_btn_pos[1] + 15, left_label, image.COLOR_WHITE, scale=3.0, thickness=2)
            img.draw_rect(left_btn_pos[0], left_btn_pos[1], left_btn_pos[2], left_btn_pos[3], image.COLOR_WHITE, 2)
            
            # 画 Even 按钮文字和矩形(两者位置对齐)
            img.draw_string(right_btn_pos[0], right_btn_pos[1] + 15, right_label, image.COLOR_WHITE, scale=3.0, thickness=2)
            img.draw_rect(right_btn_pos[0], right_btn_pos[1], right_btn_pos[2], right_btn_pos[3], image.COLOR_WHITE, 2)
        
        x, y, pressed = ts.read()

        if pressed and is_in_button(x, y, change_btn_pos) and isChangePressed is False:
            isMode = 1 - isMode
            isChangePressed = True

        if pressed and is_in_button(x, y, odd_btn_pos):
            isLabelRequired = 0
            break
        if pressed and is_in_button(x, y, even_btn_pos):
            isLabelRequired = 1
            break
        
        if pressed and is_in_button(x, y, left_btn_pos):
            isLabelRequired = 0
            break
        if pressed and is_in_button(x, y, right_btn_pos):
            isLabelRequired = 1
            break
        if not pressed:
            isChangePressed = False

        disp.show(img)
    del cam

    execuseTurn(-90)

    while True:
        ret = scan_apriltag()
        if ret is not None:
            img02 = image.Image(disp.width(), disp.height())
            found_id = ret
            label = f":{ret}"
            img02.draw_string(8, 30, label, image.COLOR_WHITE, scale = 20.0, thickness = 14)
            disp.show(img02)
            tag01 = int(ret)
            beep()
            break

    execuseTurn(90)

    cam = camera.Camera(320, 240)
    while True:
        t = time.time_ms()
        img = cam.read()
        if img is None: 
            continue
        offset, lost_cnt, recovery_count, lose_down = follower.update(img)

        l, r = calc_speed(offset)
        if lost_cnt > 0:
            send_speed_to_stm32(l, -r)
            time.sleep_ms(600)
            send_speed_to_stm32(0, 0)
            break
        else:
            send_speed_to_stm32(l, -r)

        disp.show(img)
        print("FPS =", int(1000 / (time.time_ms() - t)), "Offset:", offset, "Lost:", lost_cnt)
    del cam

    while True:
        ret = scan_apriltag()
        if ret is not None:
            img02 = image.Image(disp.width(), disp.height())
            found_id = ret
            label = f":{ret}"
            img02.draw_string(8, 30, label, image.COLOR_WHITE, scale = 20.0, thickness = 14)
            disp.show(img02)
            tag02 = int(ret)
            beep()
            break
    
    # blindMove(duration = 2000)

    if isMode == 1:
        if isLabelRequired == 1:
            if tag02 % 2 == 1:
                isLabelRequired = 0
        elif tag01 % 2 == 0:
            isLabelRequired = 1



    if isLabelRequired == 1:
        execuseTurn(-55)
        blindMove(duration = 2300)
        cam = camera.Camera(320, 240)
        finishTimerMs = 500
        while True:
            t = time.time_ms()
            img = cam.read()
            if img is None: 
                continue
            offset, lost_cnt, recovery_count , lose_down = follower.update(img)

            l, r = calc_speed(offset)
            if recovery_count > 0 and lost_cnt > recovery_count:
                send_speed_to_stm32(l, -r)
                send_speed_to_stm32(0, 0)
                execuseTurn(83)
                break
            else:
                send_speed_to_stm32(l, -r)

            send_speed_to_stm32(l, -r)

            disp.show(img)
            print("FPS =", int(1000 / (time.time_ms() - t)), "Offset:", offset, "Lost:", lost_cnt)

        blindMove(duration = 2600)
        
        while True:
            t = time.time_ms()
            img = cam.read()
            if img is None: 
                continue
            offset, lost_cnt, recovery_count, lose_down = follower.update(img)

            if lose_down is False:
                finishTimerMs -= int((time.time_ms() - t))
                lastTime = t

            l, r = calc_speed(offset)

            print(f"{finishTimerMs}")

            if finishTimerMs < 0:
                send_speed_to_stm32(0, 0)
                break
            send_speed_to_stm32(l, -r)

            disp.show(img)
            print("FPS =", int(1000 / (time.time_ms() - t)), "Offset:", offset, "Lost:", lost_cnt)
        
        del cam
    else:
        finishTimerMs = 300
        execuseTurn(-10)
        blindMove(duration = 3500)
        execuseTurn(85)
        cam = camera.Camera(320, 240)
        blindMove(duration = 200)

        while True:
            t = time.time_ms()
            img = cam.read()
            if img is None: 
                continue
            offset, lost_cnt, recovery_count , lose_down = follower.update(img)

            l, r = calc_speed(offset)
            if recovery_count > 0 and lost_cnt > recovery_count:
                send_speed_to_stm32(0, 0)
                break
            send_speed_to_stm32(l, -r)

            disp.show(img)
            print("FPS =", int(1000 / (time.time_ms() - t)), "Offset:", offset, "Lost:", lost_cnt)

        execuseTurn(45)
        blindMove(duration = 5500)
        execuseTurn(-100)

        while True:
            t = time.time_ms()
            img = cam.read()
            if img is None: 
                continue
            offset, lost_cnt, recovery_count, lose_down = follower.update(img)

            if lose_down is False:
                finishTimerMs -= int((time.time_ms() - t))
                lastTime = t

            l, r = calc_speed(offset)
        
            print(f"{finishTimerMs}")

            if finishTimerMs < 0:
                send_speed_to_stm32(0, 0)
                break
            send_speed_to_stm32(l, -r)

            disp.show(img)
            print("FPS =", int(1000 / (time.time_ms() - t)), "Offset:", offset, "Lost:", lost_cnt)
        
        del cam

STM32底座

main.c

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2026 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

#include "motor.h"

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

// UART3       ر   
uint8_t rx_buffer;              //    ֽڽ  ջ   
static uint8_t uart3_rx_step = 0;
static uint8_t uart3_rx_checksum = 0;
static uint8_t uart3_rx_checksum_rcv = 0;
static uint8_t uart3_rx_int16_1_low = 0;
static uint8_t uart3_rx_int16_1_high = 0;
static uint8_t uart3_rx_int16_2_low = 0;
static uint8_t uart3_rx_int16_2_high = 0;

int16_t left_speed = 0;
int16_t right_speed = 0;

  
//float base_speed = 0.679325;     
//float kp_turn = 0.35f;        

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

#include <string.h>
#include <stdio.h>

#define RX_BUFFER_SIZE 64

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM1_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  MX_TIM4_Init();
  MX_USART3_UART_Init();
  /* USER CODE BEGIN 2 */
	HAL_UART_Receive_IT(&huart3, &rx_buffer, 1);
	motorInit();

//	HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
//	__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);


  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */

while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    
    // 设置电机目标速度(PID速度闭环会自动调节PWM)
    setSpeedL(left_speed);
    setSpeedR(right_speed);
	
//	HAL_Delay(5);  // 10ms控制周期,与PID周期匹配 
	  
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV8;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV8;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART3)
    {
       
        switch(uart3_rx_step)
        {
            case 0: 
                if(rx_buffer == 0xAA)
                {
                    uart3_rx_step = 1;
                    uart3_rx_checksum = 0xAA;
                    
                 
                    HAL_UART_Transmit(&huart3, &rx_buffer, 1, 100);
                }
                break;
                
            case 1: 
                uart3_rx_int16_1_low = rx_buffer;
                uart3_rx_checksum += rx_buffer;
                uart3_rx_step = 2;
                break;
                
            case 2: 
                uart3_rx_int16_1_high = rx_buffer;
                uart3_rx_checksum += rx_buffer;
                uart3_rx_step = 3;
                break;
                
            case 3: 
                uart3_rx_int16_2_low = rx_buffer;
                uart3_rx_checksum += rx_buffer;
                uart3_rx_step = 4;
                break;
                
            case 4:  
                uart3_rx_int16_2_high = rx_buffer;
                uart3_rx_checksum += rx_buffer;
                uart3_rx_step = 5;
                break;
                
            case 5: 
                uart3_rx_checksum_rcv = rx_buffer;
                if((uart3_rx_checksum & 0xFF) == uart3_rx_checksum_rcv)
                {
                    int16_t value1 = (int16_t)(uart3_rx_int16_1_low | (uart3_rx_int16_1_high << 8));
                    int16_t value2 = (int16_t)(uart3_rx_int16_2_low | (uart3_rx_int16_2_high << 8));
                    
                    left_speed = value1;
                    right_speed = value2;
                    
                   
                    uint8_t response[6] = {
                        0xAA,
                        uart3_rx_int16_1_low,
                        uart3_rx_int16_1_high,
                        uart3_rx_int16_2_low,
                        uart3_rx_int16_2_high,
                        uart3_rx_checksum_rcv
                    };
                    HAL_UART_Transmit(&huart3, response, 6, 100);
                }
                uart3_rx_step = 0;
                break;
        }
        
        HAL_UART_Receive_IT(&huart3, &rx_buffer, 1);
    }
}
/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

motor.c

#include "motor.h"
#include "tim.h"

#define ABS(x) ((x) > 0) ? (x) : -(x)

#define PULSE2SPEED 0.16983125
#define PWM_MAX 999
#define PWM_MIN -999
#define INTEGRAL_LIMIT 99.0f

#define PWM_TIM htim3
#define PID_TIM htim4
#define COUNTER_A_TIM htim1
#define COUNTER_B_TIM htim2

// class GuoguoILoveU
// 线数 13 四倍频率 52
// 10ms 52 * 0.01 = 0.52
// 28.26cm 
// 27.173 -> 5 0.27173
// 
volatile float debug_actualL = 0.0f;
volatile float debug_actualR = 0.0f;
volatile  int16_t testA = 0;
volatile int16_t testB = 0;

typedef struct {
	float p;
	float i;
	float d;
	float targetSpeed;
	float lastErr;
	float totalErr;
	int16_t pwm;
} pid_element_t;

pid_element_t motorL = {
	.p = -40.0f,
	.i = -0.05f,
	.d = -60.0f,
	.targetSpeed = 10.0f,
	.lastErr = 0.0f,
	.totalErr = 0.0f,
	.pwm = 0
};

pid_element_t motorR = {
	.p = -40.0f,
	.i = -0.05f,
	.d = -60.0f,
	.targetSpeed = 10.0f,
	
	.lastErr = 0.0f,
	.totalErr = 0.0f,
	.pwm = 0
};

void motorInit()
{
	// 启动两个 PWM 通道(使用 TIM4 的 CH1 和 CH2)
	HAL_TIM_PWM_Start(&PWM_TIM, TIM_CHANNEL_1);  // 左电机 PWM (PB6)
	HAL_TIM_PWM_Start(&PWM_TIM, TIM_CHANNEL_2);  // 右电机 PWM (PB7)

	// 设置左电机方向(正转)
	HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_SET);   // AIN1 高
	HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_RESET); // AIN2 低

	// 设置右电机方向(正转)
	HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_SET);   // BIN1 高
	HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_RESET); // BIN2 低

	// 设置两个电机的初始占空比(30%),对应比较值 300(若 Period=999)
	__HAL_TIM_SET_COMPARE(&PWM_TIM, TIM_CHANNEL_1, 0); // 左电机
	__HAL_TIM_SET_COMPARE(&PWM_TIM, TIM_CHANNEL_2, 0); // 右电机
	
	HAL_TIM_Encoder_Start(&COUNTER_A_TIM, TIM_CHANNEL_ALL);//编码器启动,定时器开始计算
	HAL_TIM_Encoder_Start(&COUNTER_B_TIM, TIM_CHANNEL_ALL);
	
	HAL_TIM_Base_Start_IT(&PID_TIM);
}


/**
 * @Breif 设置左轮PWM
 * @Input pwm数值 范围 -1000 ~ 1000
 */
void setL(int16_t pwm)
{
	if(pwm > 1000 || pwm < -1000) return;
	if(pwm < 0) {
		HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_RESET);
		HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_SET);
	} else {
		HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN2_Pin, GPIO_PIN_RESET);
		HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN1_Pin, GPIO_PIN_SET);
	}
	__HAL_TIM_SET_COMPARE(&PWM_TIM, TIM_CHANNEL_1, ABS(pwm));
	testA = ABS(pwm);
}

/**
 * @Breif 设置右轮PWM
 * @Input pwm数值 范围 -1000 ~ 1000
 */
void setR(int16_t pwm)
{
	if(pwm > 1000 || pwm < -1000) return;
	if(pwm < 0) {
		HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_RESET);
		HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_SET);
	} else {
		HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN2_Pin, GPIO_PIN_RESET);
		HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN1_Pin, GPIO_PIN_SET);
	}
	__HAL_TIM_SET_COMPARE(&PWM_TIM, TIM_CHANNEL_2, ABS(pwm));
	testB = ABS(pwm);
}

/**
 * @Breif 读出左轮脉冲
 * @output 输出左轮脉冲
 */
int16_t getL()
{
	int16_t current = __HAL_TIM_GET_COUNTER(&COUNTER_A_TIM);
	__HAL_TIM_SET_COUNTER(&COUNTER_A_TIM,0);
	return current;
}

/**
 * @Breif 读出右轮脉冲
 * @output 输出右轮脉冲
 */
int16_t getR()
{
	int16_t current = __HAL_TIM_GET_COUNTER(&COUNTER_B_TIM);
	__HAL_TIM_SET_COUNTER(&COUNTER_B_TIM,0);
	return current;
}


void setSpeedL(float speed) {
	motorL.targetSpeed = speed;
}

void setSpeedR(float speed) {
	motorR.targetSpeed = -speed;
}

int16_t pidCalcUnit(pid_element_t* i, float err)
{
    i->pwm += (i->p * err + i->i * i->totalErr + i->d * (err - i->lastErr));
    if (i->pwm > PWM_MAX) i->pwm = PWM_MAX;
    if (i->pwm < PWM_MIN) i->pwm = PWM_MIN;
    i->totalErr += err;
    if (i->totalErr > INTEGRAL_LIMIT) i->totalErr = INTEGRAL_LIMIT;
    if (i->totalErr < -INTEGRAL_LIMIT) i->totalErr = -INTEGRAL_LIMIT;
    i->lastErr = err;
    return i->pwm;
}

void pidCalc()
{
    // 先读取脉冲数并计算实际速度
    int16_t pulseL = getL();
    int16_t pulseR = getR();
    
    float actualL = PULSE2SPEED * pulseL;
    float actualR = PULSE2SPEED * pulseR;
    
    // 赋值给调试变量
    debug_actualL = actualL;
    debug_actualR = actualR;
    
    // 计算误差
    float errL = actualL - motorL.targetSpeed ;
    float errR = actualR - motorR.targetSpeed;
    
    // 执行 PID 并设置电机
    setL(pidCalcUnit(&motorL, errL));
    setR(pidCalcUnit(&motorR, errR));
}


void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	pidCalc();
}

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇