feat: 逻辑分析仪深度可用户输入自定义数字

This commit is contained in:
alivender 2025-08-04 13:27:35 +08:00
parent 51b39cee07
commit 5c87204ef6
5 changed files with 355 additions and 107 deletions

View File

@ -259,7 +259,8 @@ public class LogicAnalyzerController : ControllerBase
{ {
try try
{ {
if (capture_length < 0 || capture_length > 2048*32) //DDR深度为 32'h01000000 - 32'h0FFFFFFF
if (capture_length < 0 || capture_length > 0x10000000 - 0x01000000)
return BadRequest("采样深度设置错误"); return BadRequest("采样深度设置错误");
if (pre_capture_length < 0 || pre_capture_length >= capture_length) if (pre_capture_length < 0 || pre_capture_length >= capture_length)
return BadRequest("预采样深度必须小于捕获深度"); return BadRequest("预采样深度必须小于捕获深度");

View File

@ -70,7 +70,7 @@ static class AnalyzerAddr
public const UInt32 DMA1_START_WRITE_ADDR = DMA1_BASE + 0x0000_0012; public const UInt32 DMA1_START_WRITE_ADDR = DMA1_BASE + 0x0000_0012;
public const UInt32 DMA1_END_WRITE_ADDR = DMA1_BASE + 0x0000_0013; public const UInt32 DMA1_END_WRITE_ADDR = DMA1_BASE + 0x0000_0013;
public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014; public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014;
public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0010_0000; public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0100_0000;
/// <summary> /// <summary>
/// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储得到的32位数据中低八位最先捕获高八位最后捕获。<br/> /// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储得到的32位数据中低八位最先捕获高八位最后捕获。<br/>

View File

@ -76,28 +76,12 @@ const channelDivOptions = [
{ value: 32, label: "32通道", description: "启用32个通道 (CH0-CH31)" }, { value: 32, label: "32通道", description: "启用32个通道 (CH0-CH31)" },
]; ];
// 捕获深度选项 // 捕获深度限制常量
const captureLengthOptions = [ const CAPTURE_LENGTH_MIN = 1024; // 最小捕获深度 1024
{ value: 256, label: "256" }, const CAPTURE_LENGTH_MAX = 0x10000000 - 0x01000000; // 最大捕获深度
{ value: 512, label: "512" },
{ value: 1024, label: "1K" },
{ value: 2048, label: "2K" },
{ value: 4096, label: "4K" },
{ value: 8192, label: "8K" },
{ value: 16384, label: "16K" },
{ value: 32768, label: "32K" },
];
// 预捕获深度选项 // 预捕获深度限制常量
const preCaptureLengthOptions = [ const PRE_CAPTURE_LENGTH_MIN = 0; // 最小预捕获深度 0
{ value: 0, label: "0" },
{ value: 16, label: "16" },
{ value: 32, label: "32" },
{ value: 64, label: "64" },
{ value: 128, label: "128" },
{ value: 256, label: "256" },
{ value: 512, label: "512" },
];
// 默认颜色数组 // 默认颜色数组
const defaultColors = [ const defaultColors = [
@ -126,8 +110,8 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 触发设置相关状态 // 触发设置相关状态
const currentGlobalMode = ref<GlobalCaptureMode>(GlobalCaptureMode.AND); const currentGlobalMode = ref<GlobalCaptureMode>(GlobalCaptureMode.AND);
const currentChannelDiv = ref<number>(8); // 默认启用8个通道 const currentChannelDiv = ref<number>(8); // 默认启用8个通道
const captureLength = ref<number>(1024); // 捕获深度默认1024 const captureLength = ref<number>(CAPTURE_LENGTH_MIN); // 捕获深度,默认为最小值
const preCaptureLength = ref<number>(0); // 预捕获深度默认0 const preCaptureLength = ref<number>(PRE_CAPTURE_LENGTH_MIN); // 预捕获深度默认0
const isApplying = ref(false); const isApplying = ref(false);
const isCapturing = ref(false); // 添加捕获状态标识 const isCapturing = ref(false); // 添加捕获状态标识
@ -181,6 +165,64 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
} }
}; };
// 验证捕获深度
const validateCaptureLength = (value: number): { valid: boolean; message?: string } => {
if (!Number.isInteger(value)) {
return { valid: false, message: "捕获深度必须是整数" };
}
if (value < CAPTURE_LENGTH_MIN) {
return { valid: false, message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}` };
}
if (value > CAPTURE_LENGTH_MAX) {
return { valid: false, message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}` };
}
return { valid: true };
};
// 验证预捕获深度
const validatePreCaptureLength = (value: number, currentCaptureLength: number): { valid: boolean; message?: string } => {
if (!Number.isInteger(value)) {
return { valid: false, message: "预捕获深度必须是整数" };
}
if (value < PRE_CAPTURE_LENGTH_MIN) {
return { valid: false, message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}` };
}
if (value >= currentCaptureLength) {
return { valid: false, message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})` };
}
return { valid: true };
};
// 设置捕获深度
const setCaptureLength = (value: number) => {
const validation = validateCaptureLength(value);
if (!validation.valid) {
alert?.error(validation.message!, 3000);
return false;
}
// 检查预捕获深度是否仍然有效
if (preCaptureLength.value >= value) {
preCaptureLength.value = Math.max(0, value - 1);
alert?.warn(`预捕获深度已自动调整为 ${preCaptureLength.value}`, 3000);
}
captureLength.value = value;
return true;
};
// 设置预捕获深度
const setPreCaptureLength = (value: number) => {
const validation = validatePreCaptureLength(value, captureLength.value);
if (!validation.valid) {
alert?.error(validation.message!, 3000);
return false;
}
preCaptureLength.value = value;
return true;
};
// 设置通道组 // 设置通道组
const setChannelDiv = (channelCount: number) => { const setChannelDiv = (channelCount: number) => {
// 验证通道数量是否有效 // 验证通道数量是否有效
@ -717,8 +759,15 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
operators, operators,
signalValues, signalValues,
channelDivOptions, // 导出通道组选项 channelDivOptions, // 导出通道组选项
captureLengthOptions, // 导出捕获深度选项
preCaptureLengthOptions, // 导出预捕获深度选项 // 捕获深度常量和验证
CAPTURE_LENGTH_MIN,
CAPTURE_LENGTH_MAX,
PRE_CAPTURE_LENGTH_MIN,
validateCaptureLength,
validatePreCaptureLength,
setCaptureLength,
setPreCaptureLength,
// 触发设置方法 // 触发设置方法
setChannelDiv, // 导出设置通道组方法 setChannelDiv, // 导出设置通道组方法

View File

@ -3,89 +3,182 @@
<!-- 通道配置 --> <!-- 通道配置 -->
<div class="form-control"> <div class="form-control">
<!-- 全局触发模式选择和通道组配置 --> <!-- 全局触发模式选择和通道组配置 -->
<div class="flex flex-col lg:flex-row justify-between gap-4 my-4 mx-2"> <div class="flex flex-col gap-6 my-4 mx-2">
<!-- 左侧全局触发模式和通道组选择 --> <div class="flex flex-col lg:flex-row gap-6">
<div class="flex flex-col lg:flex-row gap-4"> <div class="flex flex-col gap-2">
<div class="flex flex-row gap-2 items-center"> <label class="block text-sm font-semibold antialiased">
<label class="label"> 全局触发逻辑
<span class="label-text text-sm">全局触发逻辑</span>
</label> </label>
<select <div class="relative w-[200px]">
v-model="currentGlobalMode" <button
@change="setGlobalMode(currentGlobalMode)" tabindex="0"
class="select select-sm select-bordered" type="button"
class="flex items-center gap-4 justify-between h-max outline-none focus:outline-none bg-transparent ring-transparent border border-slate-200 transition-all duration-300 ease-in disabled:opacity-50 disabled:pointer-events-none select-none text-start text-sm rounded-md py-2 px-2.5 ring shadow-sm hover:border-slate-800 hover:ring-slate-800/10 focus:border-slate-800 focus:ring-slate-800/10 w-full"
@click="toggleGlobalModeDropdown"
:aria-expanded="showGlobalModeDropdown"
aria-haspopup="listbox"
role="combobox"
> >
<option <span>{{ currentGlobalModeLabel }}</span>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-[1em] w-[1em] translate-x-0.5 stroke-[1.5]">
<path d="M17 8L12 3L7 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M17 16L12 21L7 16" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
<input readonly style="display:none" :value="currentGlobalMode" />
<!-- 下拉菜单 -->
<div v-if="showGlobalModeDropdown" class="absolute top-full left-0 right-0 mt-1 bg-white border border-slate-200 rounded-md shadow-lg z-50">
<div
v-for="mode in globalModes" v-for="mode in globalModes"
:key="mode.value" :key="mode.value"
:value="mode.value" @click="selectGlobalMode(mode.value)"
class="px-3 py-2 text-sm text-slate-700 hover:bg-slate-100 cursor-pointer"
:class="{ 'bg-slate-100': mode.value === currentGlobalMode }"
> >
{{ mode.label }} - {{ mode.description }} {{ mode.label }}
</option>
</select>
</div> </div>
</div>
<div class="flex flex-row gap-2 items-center"> </div>
<label class="label"> <p class="flex items-center text-xs text-slate-400">
<span class="label-text text-sm">通道组</span> {{ currentGlobalModeDescription }}
</p>
</div>
<div class="flex flex-col gap-2">
<label class="block text-sm font-semibold antialiased">
通道组
</label> </label>
<select <div class="relative w-[200px]">
v-model="currentChannelDiv" <button
@change="setChannelDiv(currentChannelDiv)" tabindex="0"
class="select select-sm select-bordered" type="button"
class="flex items-center gap-4 justify-between h-max outline-none focus:outline-none bg-transparent ring-transparent border border-slate-200 transition-all duration-300 ease-in disabled:opacity-50 disabled:pointer-events-none select-none text-start text-sm rounded-md py-2 px-2.5 ring shadow-sm hover:border-slate-800 hover:ring-slate-800/10 focus:border-slate-800 focus:ring-slate-800/10 w-full"
@click="toggleChannelDivDropdown"
:aria-expanded="showChannelDivDropdown"
aria-haspopup="listbox"
role="combobox"
> >
<option <span>{{ currentChannelDivLabel }}</span>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-[1em] w-[1em] translate-x-0.5 stroke-[1.5]">
<path d="M17 8L12 3L7 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M17 16L12 21L7 16" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
<input readonly style="display:none" :value="currentChannelDiv" />
<!-- 下拉菜单 -->
<div v-if="showChannelDivDropdown" class="absolute top-full left-0 right-0 mt-1 bg-white border border-slate-200 rounded-md shadow-lg z-50">
<div
v-for="option in channelDivOptions" v-for="option in channelDivOptions"
:key="option.value" :key="option.value"
:value="option.value" @click="selectChannelDiv(option.value)"
class="px-3 py-2 text-sm text-slate-700 hover:bg-slate-100 cursor-pointer"
:class="{ 'bg-slate-100': option.value === currentChannelDiv }"
> >
{{ option.label }} {{ option.label }}
</option>
</select>
</div> </div>
</div>
<div class="flex flex-row gap-2 items-center"> </div>
<label class="label"> <p class="flex items-center text-xs text-slate-400">
<span class="label-text text-sm">捕获深度</span> {{ currentChannelDivDescription }}
</p>
</div>
<div class="flex flex-col gap-2">
<label class="block text-sm font-semibold antialiased">
捕获深度
</label> </label>
<select <div class="relative w-[200px]">
v-model="captureLength" <button
class="select select-sm select-bordered" @click="decreaseCaptureLength"
class="absolute right-9 top-1 rounded-md border border-transparent p-1.5 text-center text-sm transition-all hover:bg-slate-100 focus:bg-slate-100 active:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
type="button"
:disabled="captureLength <= CAPTURE_LENGTH_MIN"
> >
<option <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
v-for="option in captureLengthOptions" <path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
:key="option.value" </svg>
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="flex flex-row gap-2 items-center">
<label class="label">
<span class="label-text text-sm">预捕获深度</span>
</label>
<select
v-model="preCaptureLength"
class="select select-sm select-bordered"
>
<option
v-for="option in preCaptureLengthOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</div>
<!-- 右侧操作按钮 -->
<div class="flex flex-row gap-2">
<button @click="resetConfiguration" class="btn btn-outline btn-sm">
重置配置
</button> </button>
<input
v-model.number="captureLength"
@change="handleCaptureLengthChange"
type="number"
:min="CAPTURE_LENGTH_MIN"
:max="CAPTURE_LENGTH_MAX"
class="w-full bg-transparent placeholder:text-sm border border-slate-200 rounded-md pl-3 pr-20 py-2 transition duration-300 ease focus:outline-none focus:border-slate-400 ring ring-transparent hover:ring-slate-800/10 focus:ring-slate-800/10 hover:border-slate-800 shadow-sm focus:shadow appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
:placeholder="CAPTURE_LENGTH_MIN.toString()"
/>
<button
@click="increaseCaptureLength"
class="absolute right-1 top-1 rounded-md border border-transparent p-1.5 text-center text-sm transition-all hover:bg-slate-100 focus:bg-slate-100 active:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
type="button"
:disabled="captureLength >= CAPTURE_LENGTH_MAX"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
</svg>
</button>
</div>
<p class="flex items-center text-xs text-slate-400">
范围: {{ CAPTURE_LENGTH_MIN.toLocaleString() }} - {{ CAPTURE_LENGTH_MAX.toLocaleString() }}
</p>
</div>
<div class="flex flex-col gap-2">
<label class="block text-sm font-semibold antialiased">
预捕获深度
</label>
<div class="relative w-[200px]">
<button
@click="decreasePreCaptureLength"
class="absolute right-9 top-1 rounded-md border border-transparent p-1.5 text-center text-sm transition-all hover:bg-slate-100 focus:bg-slate-100 active:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
type="button"
:disabled="preCaptureLength <= PRE_CAPTURE_LENGTH_MIN"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</button>
<input
v-model.number="preCaptureLength"
@change="handlePreCaptureLengthChange"
type="number"
:min="PRE_CAPTURE_LENGTH_MIN"
:max="Math.max(0, captureLength - 1)"
class="w-full bg-transparent placeholder:text-sm border border-slate-200 rounded-md pl-3 pr-20 py-2 transition duration-300 ease focus:outline-none focus:border-slate-400 ring ring-transparent hover:ring-slate-800/10 focus:ring-slate-800/10 hover:border-slate-800 shadow-sm focus:shadow appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
:placeholder="PRE_CAPTURE_LENGTH_MIN.toString()"
/>
<button
@click="increasePreCaptureLength"
class="absolute right-1 top-1 rounded-md border border-transparent p-1.5 text-center text-sm transition-all hover:bg-slate-100 focus:bg-slate-100 active:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
type="button"
:disabled="preCaptureLength >= Math.max(0, captureLength - 1)"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
</svg>
</button>
</div>
<p class="flex items-center text-xs text-slate-400">
范围: {{ PRE_CAPTURE_LENGTH_MIN }} - {{ Math.max(0, captureLength - 1) }}
</p>
</div>
<div class="flex flex-col gap-2">
<label class="block text-sm font-semibold antialiased">
重置配置
</label>
<div class="relative w-[200px]">
<button
@click="resetConfiguration"
class="w-10 h-10 bg-transparent text-red-600 text-sm border border-red-200 rounded-md py-2 px-2.5 transition duration-300 ease ring ring-transparent hover:ring-red-600/10 focus:ring-red-600/10 hover:border-red-600 shadow-sm focus:shadow flex items-center justify-center"
type="button"
title="重置配置"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
<p class="flex items-center text-xs text-slate-400">
恢复所有设置到默认值
</p>
</div>
</div> </div>
</div> </div>
<!-- 通道列表 --> <!-- 通道列表 -->
@ -177,6 +270,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from "vue";
import { useRequiredInjection } from "@/utils/Common"; import { useRequiredInjection } from "@/utils/Common";
import { useLogicAnalyzerState } from "./LogicAnalyzerManager"; import { useLogicAnalyzerState } from "./LogicAnalyzerManager";
@ -193,10 +287,121 @@ const {
operators, operators,
signalValues, signalValues,
channelDivOptions, channelDivOptions,
captureLengthOptions, CAPTURE_LENGTH_MIN,
preCaptureLengthOptions, CAPTURE_LENGTH_MAX,
PRE_CAPTURE_LENGTH_MIN,
validateCaptureLength,
validatePreCaptureLength,
setCaptureLength,
setPreCaptureLength,
setChannelDiv, setChannelDiv,
setGlobalMode, setGlobalMode,
resetConfiguration, resetConfiguration,
} = useRequiredInjection(useLogicAnalyzerState); } = useRequiredInjection(useLogicAnalyzerState);
//
const showGlobalModeDropdown = ref(false);
const showChannelDivDropdown = ref(false);
//
const handleCaptureLengthChange = () => {
setCaptureLength(captureLength.value);
};
//
const handlePreCaptureLengthChange = () => {
setPreCaptureLength(preCaptureLength.value);
};
//
const increaseCaptureLength = () => {
const newValue = Math.min(captureLength.value + 1024, CAPTURE_LENGTH_MAX);
setCaptureLength(newValue);
};
//
const decreaseCaptureLength = () => {
const newValue = Math.max(captureLength.value - 1024, CAPTURE_LENGTH_MIN);
setCaptureLength(newValue);
};
//
const increasePreCaptureLength = () => {
const maxValue = Math.max(0, captureLength.value - 1);
const newValue = Math.min(preCaptureLength.value + 64, maxValue);
setPreCaptureLength(newValue);
};
//
const decreasePreCaptureLength = () => {
const newValue = Math.max(preCaptureLength.value - 64, PRE_CAPTURE_LENGTH_MIN);
setPreCaptureLength(newValue);
};
//
const currentGlobalModeLabel = computed(() => {
const mode = globalModes.find(m => m.value === currentGlobalMode.value);
return mode ? mode.label : '';
});
//
const currentGlobalModeDescription = computed(() => {
const mode = globalModes.find(m => m.value === currentGlobalMode.value);
return mode ? mode.description : '';
});
//
const currentChannelDivLabel = computed(() => {
const option = channelDivOptions.find(opt => opt.value === currentChannelDiv.value);
return option ? option.label : '';
});
//
const currentChannelDivDescription = computed(() => {
const option = channelDivOptions.find(opt => opt.value === currentChannelDiv.value);
return option ? option.description : '';
});
//
const toggleGlobalModeDropdown = () => {
showGlobalModeDropdown.value = !showGlobalModeDropdown.value;
if (showGlobalModeDropdown.value) {
showChannelDivDropdown.value = false;
}
};
const selectGlobalMode = (mode: any) => {
setGlobalMode(mode);
showGlobalModeDropdown.value = false;
};
//
const toggleChannelDivDropdown = () => {
showChannelDivDropdown.value = !showChannelDivDropdown.value;
if (showChannelDivDropdown.value) {
showGlobalModeDropdown.value = false;
}
};
const selectChannelDiv = (value: number) => {
setChannelDiv(value);
showChannelDivDropdown.value = false;
};
//
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.relative')) {
showGlobalModeDropdown.value = false;
showChannelDivDropdown.value = false;
}
};
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
</script> </script>

View File

@ -61,13 +61,6 @@
<Settings class="w-5 h-5" /> <Settings class="w-5 h-5" />
触发设置 触发设置
</div> </div>
<!-- 配置摘要 -->
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>{{ analyzer.enabledChannelCount.value }}/32 通道</span>
<span>捕获: {{ analyzer.captureLength.value }}</span>
<span>预捕获: {{ analyzer.preCaptureLength.value }}</span>
<span>{{ analyzer.globalModes.find(m => m.value === analyzer.currentGlobalMode.value)?.label || '未知' }}</span>
</div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<!-- 状态指示 --> <!-- 状态指示 -->