This repository has been archived on 2025-10-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FPGA_WebLab/src/views/Project/Debugger.vue

533 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="card m-5 bg-base-200 shadow-2xl">
<div class="card-body">
<h2 class="card-title flex justify-between items-center">
<div class="flex items-center gap-2">
<Zap class="w-5 h-5" />
调试器波形捕获
</div>
<div class="flex items-center gap-2">
<button
class="btn btn-sm btn-primary"
@click="
() => {
handleDeleteData();
startCapture();
}
"
:disabled="!captureData"
>
重新捕获
</button>
<button
class="btn btn-sm btn-error"
@click="handleDeleteData"
:disabled="!captureData"
>
清空
</button>
</div>
</h2>
<WaveformDisplay :data="captureData">
<div class="text-center">
<h3 class="text-xl font-semibold text-slate-600 mb-2">
暂无逻辑分析数据
</h3>
<p class="text-sm text-slate-500">点击下方按钮开始捕获</p>
</div>
<button
class="group relative px-8 py-3 bg-gradient-to-r text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 active:scale-95"
:class="{
'from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 focus:ring-blue-300':
!isCapturing,
'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
isCapturing,
}"
@click="isCapturing ? stopCapture() : startCapture()"
>
<span class="flex items-center gap-2">
<template v-if="isCapturing">
<Square class="w-5 h-5" />
停止捕获
</template>
<template v-else>
<Play class="w-5 h-5" />
开始捕获
</template>
</span>
</button>
</WaveformDisplay>
</div>
</div>
<!-- Debugger 通道配置 -->
<div class="card m-5 bg-base-200 shadow-2xl">
<div class="card-body">
<div class="flex justify-between">
<h2 class="card-title mb-4">调试器通道配置</h2>
<div class="flex items-center gap-2">
<button
class="btn btn-sm btn-primary"
@click="addChannel"
:disabled="!configInited"
>
添加通道
</button>
<button
class="btn btn-sm btn-secondary"
@click="showConfigDialog = true"
>
配置
</button>
</div>
</div>
<!-- 配置未初始化时 -->
<div
v-if="!configInited"
class="flex flex-col items-center justify-center py-10"
>
<div class="text-lg text-slate-500 mb-4">请先进行调试器基本配置</div>
<button class="btn btn-primary" @click="showConfigDialog = true">
配置调试器
</button>
</div>
<div v-if="configInited" class="overflow-x-auto flex flex-col gap-10">
<!-- 状态概览 -->
<div
class="stats stats-horizontal bg-base-100 shadow flex justify-between"
>
<div class="stat">
<div class="stat-title">启用端口数</div>
<div class="stat-value text-primary">
{{ config.totalPortNum }}
</div>
<div class="stat-desc">每端口最大32线</div>
</div>
<div class="stat">
<div class="stat-title">最大线宽数</div>
<div class="stat-value text-info">
{{ config.totalPortNum * 32 }}
</div>
<div class="stat-desc">启用端口数 × 32</div>
</div>
<div class="stat">
<div class="stat-title">已用线宽数</div>
<div class="stat-value text-success">
{{ channels.reduce((sum, ch) => sum + ch.width, 0) }}
</div>
<div class="stat-desc">所有通道线宽总和</div>
</div>
<div class="stat">
<div class="stat-title">采样深度</div>
<div class="stat-value text-warning">
{{ config.captureDepth }}
</div>
<div class="stat-desc">每通道采样点数</div>
</div>
<div class="stat">
<div class="stat-title">时钟频率</div>
<div class="stat-value text-accent">{{ config.clkFreq }} MHz</div>
<div class="stat-desc">采样时钟</div>
</div>
</div>
<!-- 通道表格 -->
<div class="space-y-2">
<!-- 表头 -->
<div
class="grid grid-cols-7 justify-items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
>
<span>名称</span>
<span>显示</span>
<span>颜色</span>
<span>触发模式</span>
<span>数据位宽(起始:结尾)</span>
<span>父端口编号</span>
<span>操作</span>
</div>
<!-- 通道列表 -->
<div
v-for="(ch, idx) in channels"
:key="idx"
class="grid grid-cols-7 place-items-center gap-4 p-4 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<input
v-model="ch.name"
class="input input-bordered w-full"
:placeholder="`通道${idx + 1}`"
/>
<input
type="checkbox"
v-model="ch.visible"
class="toggle toggle-primary"
/>
<input
type="color"
v-model="ch.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
/>
<select
v-model="ch.trigger"
class="select select-bordered w-full"
>
<option
v-for="mode in triggerModes"
:key="mode.value"
:value="mode.value"
>
{{ mode.label }}
</option>
</select>
<input
v-model="ch.widthStr"
class="input input-bordered w-full"
placeholder="如0:7"
@change="parseWidthStr(idx)"
/>
<input
type="number"
min="0"
:max="config.totalPortNum - 1"
v-model.number="ch.parentPort"
class="input input-bordered w-full"
/>
<button class="btn btn-error" @click="removeChannel(idx)">
删除
</button>
</div>
<!-- 添加通道按钮 -->
<div class="flex justify-center mt-2">
<button class="btn btn-primary w-100" @click="addChannel">
添加通道
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 配置Dialog -->
<dialog v-if="showConfigDialog" class="modal modal-open">
<form
method="dialog"
class="modal-box max-w-fit"
@submit.prevent="onConfigSubmit"
>
<h3 class="font-bold text-lg mb-4">调试器基本配置</h3>
<div class="flex flex-col gap-4 w-80">
<BaseInputField
v-model="config.clkFreq"
label="时钟频率 (MHz)"
type="number"
min="1"
max="200"
:error="
config.clkFreq < 1 || config.clkFreq > 500 ? '范围1~200' : ''
"
required
/>
<BaseInputField
v-model="config.totalPortNum"
label="启用端口数"
type="number"
min="1"
max="16"
:error="
config.totalPortNum < 1 || config.totalPortNum > 16
? '范围1~16'
: ''
"
required
/>
<BaseInputField
v-model="config.captureDepth"
label="采样深度"
type="number"
min="1"
max="1048576"
:error="
config.captureDepth < 1 || config.captureDepth > 1048576
? '范围1~1048576'
: ''
"
required
/>
</div>
<div class="modal-action mt-6">
<button class="btn btn-primary" type="submit">确定</button>
<button class="btn" type="button" @click="showConfigDialog = false">
取消
</button>
</div>
</form>
</dialog>
</div>
</template>
<script setup lang="ts">
import {
CaptureMode,
ChannelConfig,
DebuggerClient,
DebuggerConfig,
} from "@/APIClient";
import { useAlertStore } from "@/components/Alert";
import BaseInputField from "@/components/InputField/BaseInputField.vue";
import type { LogicDataType } from "@/components/WaveformDisplay";
import WaveformDisplay from "@/components/WaveformDisplay/WaveformDisplay.vue";
import { AuthManager } from "@/utils/AuthManager";
import { useRequiredInjection } from "@/utils/Common";
import { useLocalStorage } from "@vueuse/core";
import axios, { type CancelTokenSource } from "axios";
import { isNull } from "lodash";
import { Play, Square, Zap } from "lucide-vue-next";
import { ref, reactive, computed } from "vue";
interface DebugChannel {
name: string;
visible: boolean;
color: string;
trigger: CaptureMode;
width: number;
widthStr: string;
start: number;
parentPort: number;
}
interface DebuggerSettings {
clkFreq: number;
totalPortNum: number;
captureDepth: number;
}
const triggerModes = [
{ value: CaptureMode.None, label: "x (无关)" },
{ value: CaptureMode.Logic0, label: "0 (低电平)" },
{ value: CaptureMode.Logic1, label: "1 (高电平)" },
{ value: CaptureMode.Rise, label: "↑ (上升沿)" },
{ value: CaptureMode.Fall, label: "↓ (下降沿)" },
];
// 基本配置
const config = reactive<DebuggerSettings>({
clkFreq: 50,
totalPortNum: 1,
captureDepth: 1024,
});
const configInited = ref(false);
const showConfigDialog = ref(false);
function onConfigSubmit() {
configInited.value = true;
showConfigDialog.value = false;
// 清空通道
channels.value = [];
}
// 通道配置
const channels = useLocalStorage<DebugChannel[]>("debugger-channels", []);
const captureData = ref<LogicDataType>();
const alert = useRequiredInjection(useAlertStore);
const isCapturing = ref(false);
const readCancelTokenSource = ref<CancelTokenSource | null>(null);
// 解析widthStr为start/width
function parseWidthStr(idx: number) {
const ch = channels.value[idx];
const match = /^(\d+)\s*:\s*(\d+)$/.exec(ch.widthStr);
if (isNull(match)) {
alert.error("格式错误,应为 起始位:宽度,如 0:7");
ch.widthStr = `${ch.start}:${ch.width}`;
return;
}
const min = Math.min(parseInt(match[1]), parseInt(match[2]));
const max = Math.max(parseInt(match[1]), parseInt(match[2]));
ch.start = min;
ch.width = max - min + 1;
}
function addChannel() {
if (!configInited.value) {
alert.error("请先配置调试器基本参数");
return;
}
if (channels.value.length >= config.totalPortNum * 32) {
alert.error("通道数已达最大线宽数");
return;
}
channels.value.push({
name: `CH${channels.value.length + 1}`,
visible: true,
color: "#00bcd4",
trigger: CaptureMode.None,
width: 1,
widthStr: "0:0",
start: 0,
parentPort: 0,
});
}
function removeChannel(idx: number) {
channels.value.splice(idx, 1);
}
function handleDeleteData() {
captureData.value = undefined;
}
function stopCapture() {
isCapturing.value = false;
if (readCancelTokenSource.value) {
readCancelTokenSource.value.cancel("用户手动停止捕获");
readCancelTokenSource.value = null;
}
}
async function startCapture() {
if (!configInited.value) {
alert.error("请先配置调试器基本参数");
return;
}
if (channels.value.length === 0) {
alert.error("请至少添加一个通道");
return;
}
// 校验通道参数
let usedWires = 0;
for (let i = 0; i < channels.value.length; i++) {
const ch = channels.value[i];
if (!ch.visible) continue;
if (!ch.name) {
alert.error(`通道 ${i + 1} 名称不能为空`);
return;
}
if (ch.width < 1 || ch.width > 32) {
alert.error(`通道 ${i + 1} 数据位宽必须在1到32之间`);
return;
}
if (ch.start < 0 || ch.start + ch.width > 32) {
alert.error(`通道 ${i + 1} 起始位+宽度不能超过32`);
return;
}
if (ch.parentPort < 0 || ch.parentPort >= config.totalPortNum) {
alert.error(`通道 ${i + 1} 父端口编号超出范围`);
return;
}
usedWires += ch.width;
}
if (usedWires > config.totalPortNum * 32) {
alert.error("所有通道线宽总和不能超过最大线宽数");
return;
}
isCapturing.value = true;
const client = AuthManager.createClient(DebuggerClient);
// 构造API配置
const channelConfigs = channels.value
.filter((ch) => ch.visible)
.map(
(ch) =>
new ChannelConfig({
name: ch.name,
color: ch.color,
wireWidth: ch.width,
wireStartIndex: ch.start,
parentPort: ch.parentPort,
mode: ch.trigger,
}),
);
const apiConfig = new DebuggerConfig({
clkFreq: config.clkFreq,
totalPortNum: config.totalPortNum,
captureDepth: config.captureDepth,
triggerNum: 0,
channelConfigs: channelConfigs,
});
try {
// 设置通道模式
let ret = await client.setChannelsMode(apiConfig);
if (!ret) {
alert.error("设置通道模式失败");
isCapturing.value = false;
return;
}
// 启动捕获
ret = await client.startTrigger();
if (!ret) {
alert.error("开始捕获失败,请检查连接");
isCapturing.value = false;
return;
}
// 读取数据
readCancelTokenSource.value = axios.CancelToken.source();
const readDataPromise = client
.readData(apiConfig, readCancelTokenSource.value.token)
.then((data) => {
const enabledChannels = channelConfigs;
const sampleCount = config.captureDepth;
// 解析数据
const y = data.map((cd, idx) => {
const ch = enabledChannels[idx];
const bin = atob(cd.data);
// UInt32数组
const arr = [];
for (let i = 0; i < bin.length; i += 4) {
arr.push(
bin.charCodeAt(i) |
(bin.charCodeAt(i + 1) << 8) |
(bin.charCodeAt(i + 2) << 16) |
(bin.charCodeAt(i + 3) << 24),
);
}
// 截取采样深度
return {
enabled: true,
type: ch.wireWidth === 1 ? ("logic" as const) : ("number" as const),
name: ch.name,
color: ch.color,
value: arr.slice(0, sampleCount),
base: ch.wireWidth === 1 ? ("bin" as const) : ("hex" as const),
};
});
const x: number[] = [];
for (let i = 0; i < sampleCount; i++) {
x.push(i * (1 / config.clkFreq)); // us
}
captureData.value = {
x,
y,
xUnit: "us",
};
})
.catch((error) => {
if (axios.isCancel(error)) {
alert.info("捕获已取消");
} else {
alert.error(`读取数据失败: ${error.message}`);
}
})
.finally(() => {
isCapturing.value = false;
readCancelTokenSource.value = null;
});
} catch (error: any) {
alert.error(`开始捕获失败: ${error.message}`);
isCapturing.value = false;
return;
}
}
</script>