533 lines
16 KiB
Vue
533 lines
16 KiB
Vue
<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>
|