feat: 添加嵌入式逻辑分析仪

This commit is contained in:
SikongJueluo 2025-07-16 21:53:49 +08:00
parent 8e19587a16
commit e3b769b24e
No known key found for this signature in database
10 changed files with 360 additions and 13 deletions

View File

@ -111,7 +111,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
new SignalTriggerConfig({
signalIndex: index,
operator: SignalOperator.Equal,
value: SignalValue.Logic1,
value: SignalValue.NotCare,
}),
),
);
@ -229,7 +229,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
signalConfigs.forEach((signal) => {
signal.operator = SignalOperator.Equal;
signal.value = SignalValue.Logic1;
signal.value = SignalValue.NotCare;
});
alert?.info("配置已重置", 2000);

View File

@ -0,0 +1,238 @@
<template>
<div
class="w-full"
:class="{
'h-48': !analyzer.logicData.value,
'h-150': analyzer.logicData.value,
}"
>
<v-chart
v-if="analyzer.logicData.value"
class="w-full h-full"
:option="option"
autoresize
:update-options="updateOptions"
/>
<div
v-else
class="w-full h-full flex flex-col gap-6 items-center justify-center"
>
<div class="text-center">
<h3 class="text-xl font-semibold text-slate-600 mb-2">
暂无逻辑分析数据
</h3>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, shallowRef } from "vue";
import VChart from "vue-echarts";
// Echarts
import { use } from "echarts/core";
import { LineChart } from "echarts/charts";
import {
TooltipComponent,
GridComponent,
DataZoomComponent,
AxisPointerComponent,
ToolboxComponent,
} from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import type { ComposeOption } from "echarts/core";
import type { LineSeriesOption } from "echarts/charts";
import type {
AxisPointerComponentOption,
TooltipComponentOption,
GridComponentOption,
DataZoomComponentOption,
} from "echarts/components";
import type {
ToolboxComponentOption,
XAXisOption,
YAXisOption,
} from "echarts/types/dist/shared";
import { useRequiredInjection } from "@/utils/Common";
import { isUndefined } from "lodash";
import { useWaveformManager } from "./WaveformManager";
use([
TooltipComponent,
ToolboxComponent,
GridComponent,
AxisPointerComponent,
DataZoomComponent,
LineChart,
CanvasRenderer,
]);
type EChartsOption = ComposeOption<
| AxisPointerComponentOption
| TooltipComponentOption
| ToolboxComponentOption
| GridComponentOption
| DataZoomComponentOption
| LineSeriesOption
>;
const analyzer = useRequiredInjection(useWaveformManager);
//
const updateOptions = shallowRef({
notMerge: false,
lazyUpdate: true,
silent: false,
});
const option = computed((): EChartsOption => {
if (isUndefined(analyzer.logicData.value)) return {};
// 使y
const enabledChannels = analyzer.logicData.value.y.filter(channel => channel.enabled);
const enabledChannelIndices = analyzer.logicData.value.y
.map((channel, index) => (channel.enabled ? index : -1))
.filter((index) => index !== -1);
const channelCount = enabledChannels.length;
const channelSpacing = 2; //
//
if (channelCount === 0) {
return {};
}
// 使
const grids: GridComponentOption[] = [
{
left: "5%",
right: "5%",
top: "5%",
bottom: "15%",
},
];
// X
const xAxis: XAXisOption[] = [
{
type: "category",
boundaryGap: false,
data: analyzer.logicData.value.x.map((x) => x.toFixed(3)),
axisLabel: {
formatter: (value: string) =>
analyzer.logicData.value
? `${value}${analyzer.logicData.value.xUnit}`
: `${value}`,
},
},
];
// Y
const yAxis: YAXisOption[] = [
{
type: "value",
min: -0.5,
max: channelCount * channelSpacing - 0.5,
interval: channelSpacing,
axisLabel: {
formatter: (value: number) => {
const channelIndex = Math.round(value / channelSpacing);
return channelIndex < channelCount
? enabledChannels[channelIndex].name
: "";
},
},
splitLine: { show: false },
},
];
//
const series: LineSeriesOption[] = enabledChannelIndices.map(
(originalIndex: number, displayIndex: number) => ({
name: enabledChannels[displayIndex].name,
type: "line",
data: analyzer.logicData.value!.y[originalIndex].value.map(
(value: number) => value + displayIndex * channelSpacing + 0.2,
),
step: "end",
lineStyle: {
width: 2,
color: enabledChannels[displayIndex].color,
},
areaStyle: {
opacity: 0.3,
origin: displayIndex * channelSpacing,
color: enabledChannels[displayIndex].color,
},
symbol: "none",
//
sampling: "lttb",
//
animation: false,
}),
);
return {
//
animation: false,
tooltip: {
trigger: "axis",
axisPointer: {
type: "line",
label: {
backgroundColor: "#6a7985",
},
// axisPointer
animation: false,
},
formatter: (params: any) => {
if (Array.isArray(params) && params.length > 0) {
const timeValue = analyzer.logicData.value!.x[params[0].dataIndex];
const dataIndex = params[0].dataIndex;
let tooltip = `Time: ${timeValue.toFixed(3)}${analyzer.logicData.value!.xUnit}<br/>`;
// 01
enabledChannelIndices.forEach(
(originalIndex: number, displayIndex: number) => {
const channelName = enabledChannels[displayIndex].name;
const originalValue =
analyzer.logicData.value!.y[originalIndex].value[dataIndex];
tooltip += `${channelName}: ${originalValue}<br/>`;
},
);
return tooltip;
}
return "";
},
// tooltip
hideDelay: 100,
},
toolbox: {
feature: {
restore: {},
},
},
grid: grids,
xAxis: xAxis,
yAxis: yAxis,
dataZoom: [
{
show: true,
realtime: true,
start: 0,
end: 100,
},
{
type: "inside",
realtime: true,
start: 0,
end: 100,
},
],
series: series,
};
});
</script>

View File

@ -0,0 +1,73 @@
import { createInjectionState } from "@vueuse/core";
import { shallowRef } from "vue";
export type LogicDataType = {
x: number[];
y: {
enabled: boolean;
type: "logic" | "number";
name: string;
color: string;
value: number[];
base: "bin" | "dec" | "hex";
}[];
xUnit: "s" | "ms" | "us" | "ns";
};
// 生成4路测试数据的函数
export function generateTestData(): LogicDataType {
// 生成时间轴数据 (0-100ns每1ns一个采样点)
const timePoints = Array.from({ length: 101 }, (_, i) => i);
return {
x: timePoints,
y: [
{
enabled: true,
type: "logic",
name: "CLK",
color: "#ff0000",
value: timePoints.map((t) => t % 2), // 时钟信号每1ns翻转
base: "bin",
},
{
enabled: true,
type: "logic",
name: "RESET",
color: "#00ff00",
value: timePoints.map((t) => (t < 10 ? 1 : 0)), // 复位信号前10ns为高电平
base: "bin",
},
{
enabled: true,
type: "number",
name: "DATA",
color: "#0000ff",
value: timePoints.map((t) => Math.floor(t / 4) % 16), // 计数器每4ns递增
base: "hex",
},
{
enabled: true,
type: "logic",
name: "ENABLE",
color: "#ff8800",
value: timePoints.map((t) => (t >= 20 && t < 80 ? 1 : 0)), // 使能信号20-80ns为高电平
base: "bin",
},
],
xUnit: "ns",
};
}
const [useProvideWaveformManager, useWaveformManager] = createInjectionState(
() => {
const logicData = shallowRef<LogicDataType>();
return {
logicData,
generateTestData,
};
},
);
export { useProvideWaveformManager, useWaveformManager };

View File

@ -0,0 +1,5 @@
import WaveformDisplay from "./WaveformDisplay.vue";
export {
WaveformDisplay
};

View File

@ -1,6 +1,6 @@
<template>
<div class="h-full flex flex-col gap-7">
<div class="tabs tabs-box flex-shrink-0 shadow-xl">
<div class="tabs tabs-box flex-shrink-0 shadow-xl mx-5">
<label class="tab">
<input
type="radio"
@ -42,9 +42,20 @@
:checked="checkID === 4"
@change="handleTabChange"
/>
<Zap class="icon" />
<Binary class="icon" />
逻辑分析仪
</label>
<label class="tab">
<input
type="radio"
name="function-bar"
id="5"
:checked="checkID === 5"
@change="handleTabChange"
/>
<Hand class="icon" />
嵌入式逻辑分析仪
</label>
<!-- 全屏按钮 -->
<button
class="fullscreen-btn ml-auto btn btn-ghost btn-sm"
@ -67,6 +78,9 @@
<div v-else-if="checkID === 4" class="h-full overflow-y-auto">
<LogicAnalyzerView />
</div>
<div v-else-if="checkID === 5" class="h-full overflow-y-auto">
<Debugger />
</div>
</div>
</div>
</template>
@ -78,7 +92,8 @@ import {
TerminalIcon,
MaximizeIcon,
MinimizeIcon,
Zap,
Binary,
Hand,
} from "lucide-vue-next";
import { useLocalStorage } from "@vueuse/core";
import VideoStreamView from "@/views/Project/VideoStream.vue";
@ -86,8 +101,13 @@ import OscilloscopeView from "@/views/Project/Oscilloscope.vue";
import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue";
import { isNull, toNumber } from "lodash";
import { onMounted, ref, watch } from "vue";
import { im } from "mathjs";
import Debugger from "./Debugger.vue";
import { useProvideLogicAnalyzer } from "@/components/LogicAnalyzer";
import { useProvideWaveformManager } from "@/components/WaveformDisplay/WaveformManager";
const analyzer = useProvideLogicAnalyzer();
const waveformManager = useProvideWaveformManager();
waveformManager.logicData.value = waveformManager.generateTestData();
const checkID = useLocalStorage("checkID", 1);

View File

@ -0,0 +1,11 @@
<template>
<div>
<div class="card">
<WaveformDisplay />
</div>
</div>
</template>
<script setup lang="ts">
import WaveformDisplay from '@/components/WaveformDisplay/WaveformDisplay.vue';
</script>

View File

@ -78,7 +78,7 @@
id="splitter-group-v-panel-bar"
:default-size="isBottomBarFullscreen ? 100 : (100 - verticalSplitterSize)"
:min-size="isBottomBarFullscreen ? 100 : 15"
class="w-full overflow-hidden px-5 pt-3"
class="w-full overflow-hidden pt-3"
>
<BottomBar
:isFullscreen="isBottomBarFullscreen"

View File

@ -1,7 +1,7 @@
<template>
<div class="bg-base-100 flex flex-col gap-10 mb-5">
<!-- 逻辑信号展示 -->
<div class="card bg-base-200 shadow-xl">
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title flex justify-between items-center">
<div class="flex items-center gap-2">
@ -19,7 +19,7 @@
</div>
<!-- 触发设置 -->
<div class="card bg-base-200 shadow-xl">
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title">
<Settings class="w-5 h-5" />

View File

@ -1,7 +1,7 @@
<template>
<div class="bg-base-100 flex flex-col">
<!-- 波形展示 -->
<div class="card bg-base-200 shadow-xl">
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title">
<Activity class="w-5 h-5" />

View File

@ -1,7 +1,7 @@
<template>
<div class="bg-base-100 flex flex-col gap-7">
<!-- 控制面板 -->
<div class="card bg-base-200 shadow-xl">
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title text-primary">
<Settings class="w-6 h-6" />
@ -153,7 +153,7 @@
</div>
<!-- 视频预览区域 -->
<div class="card bg-base-200 shadow-xl">
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title text-primary">
<Video class="w-6 h-6" />
@ -299,7 +299,7 @@
</div>
<!-- 日志区域 -->
<div class="card bg-base-200 shadow-xl">
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title text-primary">
<FileText class="w-6 h-6" />