18 KiB
PCIe8586M 采集调试程序 — 开发文档
给本机 Claude Code 的工作指令。本程序是「项目接入前测试探针」,目标是摸清阿尔泰 PCIe8586M 高速数字化仪的驱动行为,为主项目(.NET 8 + WPF + MVVM)提供经过验证的接入方案与精确配置。
0. 给 Claude Code 的首要约定
- 函数名/结构体/常量的唯一权威是
Samples/目录下的 VC 示例和Samples/VC/Include/*.h头文件。 本文档刻意不写死任何 SDK 函数名。凡涉及具体 API,先grep/阅读Samples/VC/Include与Samples/VC/Simple/AD/Continue、Finite,以头文件为准。 - Samples 是 VC(C/C++)的。 C# 接入靠 P/Invoke:把
.h的extern "C"函数翻译成[DllImport],把 C 结构体翻译成[StructLayout(LayoutKind.Sequential)]。翻译方法论见 §6。 - 分层不可破坏。 只有
Hardware/Pcie8586Digitizer.cs一个文件允许出现[DllImport]和 SDK 类型。其余所有代码只依赖IDigitizer接口。这样主项目可原样复用Hardware+Acquisition两层。 - 先用模拟卡跑通全链路,再接真卡。 本机无硬件、无驱动,开发阶段用
SimulatedDigitizer验证 UI/数据流/存盘;真卡实现Pcie8586Digitizer时只改这一层。 - 平台必须 x64。 SDK 是 64 位非托管 DLL,工程
<PlatformTarget>x64</PlatformTarget>,否则运行时BadImageFormatException。
1. 硬件事实(已从手册固化,视为常量,不要再"识别")
设备:ACTS1000 PCIE8586 8CH 16Bit 100Msps High Speed Digitizer
硬件 ID:PCI\VEN_1E42&DEV_8586(厂商号 0x1E42 = 阿尔泰,设备号 0x8586)
驱动版本:1.6.2.0(2025/7/20)
| 项目 | 值 | 来源 |
|---|---|---|
| 通道数 | 8(单端) | 手册 2.3.2 |
| ADC 分辨率 | 16 bit(8586M) | 手册 2.2 |
| 最高采样率 | 100 MS/s | 手册 2.1 |
| 输入量程 | ±5V 或 ±1V(软件可选) | 手册 2.3.2 |
| 板载内存 | 2 GB(八通道共享) | 手册 2.3.6 |
| 数据传输 | DMA | 手册 2.3.6 |
| 总线 | PCIe 2.0 x8 | 手册 2.3.1 |
| 支持系统 | XP/Win7/Win8/Win10 | 手册 2.3.1 |
1.1 数据格式(关键,§4 解析的依据)
- 每个采样点 = 16 bit 无符号原始码值 = 2 字节,小端。
- 码值→电压换算公式(仅 8586M,手册 4.3),单位 mV:
- ±5V 量程:
Volt = (10000.0 / 65536) * (code & 0xFFFF) - 5000.0 - ±1V 量程:
Volt = (2000.0 / 65536) * (code & 0xFFFF) - 1000.0 - 注意 8582M(12bit)、8584M(14bit) 公式不同(分母 4096/16384),本卡是 16bit,分母固定 65536。
- ±5V 量程:
- 中间值(零点)码值 = 0x8000 = 32768,对应 0V。
1.2 多通道数据排列(解交错依据,手册 4.6)
- 使能通道数只能是 1 / 2 / 4 / 8,且必须从 CH0 起连续(4 通道 = CH0~3;单通道只能是 CH0)。
- 多通道时按采样点交错存放(每点 1 个字):
CH0[0] CH1[0] CH2[0] CH3[0] CH4[0] CH5[0] CH6[0] CH7[0] CH0[1] CH1[1] CH2[1] CH3[1] CH4[1] CH5[1] CH6[1] CH7[1] ... - 解交错:
result[ch][s] = raw[s * channelCount + ch]。
1.3 采样率是离散值(采集频率控制依据,手册 4.7)
采样率 = 扫描时基频率 / ADC时钟分频器- 时基 = 100 MHz;分频器 = 1, 2, 3, ... (2³²−1) 的正整数。
- 所以可设采样率 =
100MHz / N:100M、50M、33.33M、25M ... 1M(N=100) ... 1k(N=100000)。 - UI 不能让用户自由填任意采样率。 应让用户选分频器 N,或从预设档(100M/50M/10M/1M/100k...)选,并实时显示
100e6/N的实际采样率。
1.4 采集模式与触发(手册 4.8 / 4.9)
- 模式:连续采样、有限点采样。无硬件级"暂停/继续"。
- 触发源:软件触发、ATR(模拟)、DTR(数字 PFI)、同步信号。调试程序默认软件触发。
- 有限点支持:中间触发、后触发、预触发、硬件延时触发、重复触发。
- 定时录制(本程序核心功能)映射:见 §3.2。
2. 架构
三层,自底向上:
Hardware 层 IDigitizer 接口 + Pcie8586Digitizer(真卡, 唯一含 DllImport)
+ SimulatedDigitizer(模拟卡)
↓
Acquisition 层 后台采集线程 / 环形历史缓冲 / 解交错 / 码值换算 / 定时录制 / 存盘
↓
UI 层 (WPF+MVVM) 设备识别 / 实时曲线(ScottPlot) / 实时表格 / 录制控制 / 剪辑
复用边界:Hardware + Acquisition 两层将原样进主项目;UI 层主项目重写。因此这两层不得引用任何 WPF 类型(无 Dispatcher、无 ObservableCollection),保持纯 .NET。
2.1 建议目录结构
Pcie8586Probe/
├─ Pcie8586Probe.csproj # net8.0-windows, UseWPF, x64
├─ Samples/ # ← 你放上位机参考的地方(VC 示例 + Include 头文件)
├─ Hardware/
│ ├─ IDigitizer.cs # 抽象接口(见 §5.1 职责)
│ ├─ SimulatedDigitizer.cs # 模拟实现(生成交错原始码值)
│ └─ Pcie8586Digitizer.cs # 真卡实现(P/Invoke,照 Samples 填)
├─ Acquisition/
│ ├─ CodeConverter.cs # 码值→电压 + 解交错(§1.1/§1.2)
│ ├─ RingHistoryBuffer.cs # 录制/剪辑用的最近 N 秒环形缓冲
│ ├─ Recorder.cs # 定时定长录制状态机(§3.2)
│ └─ WaveformWriter.cs # 落盘(二进制 + JSON 头)
├─ Models/
│ ├─ AcquisitionConfig.cs # 通道数/量程/分频器/模式 等配置
│ ├─ DeviceInfo.cs
│ └─ SampleBlock.cs # 一块已解交错的电压数据
├─ ViewModels/
│ ├─ MainViewModel.cs
│ └─ ChannelRow.cs # 表格一行(通道实时统计)
├─ Views/
│ └─ MainWindow.xaml(.cs)
└─ Converters/ # XAML 值转换器(如枚举→显示文本)
2.2 NuGet 依赖
CommunityToolkit.Mvvm([ObservableProperty]/[RelayCommand]源生成器,减样板)。ScottPlot.WPF(高刷新实时曲线,远优于 WPF 原生绘图)。
3. 功能需求与验收点
3.1 设备识别(自动 + 手动)
- 自动:调用 SDK 枚举接口列出所有同型号板卡,填充下拉框(显示逻辑号 + 描述)。
- 多数阿尔泰 SDK 有
GetDeviceCount之类;若没有,可循环CreateDevice(i)试探,成功记录后ReleaseDevice。具体函数看 Samples/VC/Simple/AD/PhysicalID 或 Include。
- 多数阿尔泰 SDK 有
- 手动:用户直接填逻辑号(0/1/2...)打开。
- 验收:模拟卡返回 1 张虚拟卡可被选中打开;真卡能枚举到
VEN_1E42&DEV_8586。
3.2 定时定长录制 +剪辑(替代"暂停/继续")
这是本程序与普通示波器软件的核心差异点。语义:
- 连续采集常驻:打开设备并 Start 后,DMA 持续流入,曲线实时滚动显示。这一路不落盘,只进
RingHistoryBuffer(保留最近 N 秒,N 可配,受内存约束见 §7)。 - 定时录制按钮:1ms / 10ms / 100ms / 1s / 10s 五个预设。点击后:
- 计算目标点数
target = round(采样率 × 时长)。 - 进入"录制中"状态:从下一个数据块起,把块追加进录制缓冲,累计每通道点数 ≥ target 时自动停止录制,截断到精确 target 点,落盘。
- 录制中曲线继续显示、不影响连续监控。
- 计算目标点数
- 采集后剪辑:录制完(或从历史缓冲)得到一段数据后,提供"起点/终点"两个游标(UI 上拖动或填采样序号),裁出子区间另存。
- 验收:选 1MS/s、录 10ms → 落盘文件每通道恰好 10000 点(±0,因软件截断);裁剪 [2000,5000) → 3000 点。
两种实现取舍(文档建议默认前者):
- 连续采集 + 软件截取(默认):录制只是从连续流里数够点数。能边显示边录,长度精确(软件截断),两段录制无硬件重启间隙。
- 有限点 + 软件触发(备选):每次录制重新 init→start,硬件采满 target 点自停。最干净,但与实时显示互斥、两段间有重配置延时。 真卡阶段若连续采集 DMA 吞吐顶不住(§7),再退到方案 2。
3.3 实时显示
- 曲线:ScottPlot,每通道一条线,X 轴时间(ms 或样本序号),Y 轴电压(V),量程决定 Y 范围(±5 或 ±1)。只画最近一屏的点(降采样显示,见 §7)。
- 表格:每通道一行,列 = 通道名 / 最新值 / Min / Max / RMS,随采集刷新(节流到 ~10–20 Hz)。
3.4 采集参数控制
- 通道数下拉:1/2/4/8。
- 量程下拉:±5V / ±1V。
- 采样率:选分频器 N 或预设档,旁显实际
100e6/N。 - 模式:连续 / 有限点(有限点需填点数)。
3.5 存盘格式(见 §8,与未来 Python 分析对齐)
4. 数据解析(Acquisition/CodeConverter.cs)
实现两个纯函数(无副作用、无 SDK 依赖、可单测):
CodeToVolts(ushort code, InputRange range)→ 按 §1.1 公式返回 V。Deinterleave(ReadOnlySpan<ushort> raw, int channelCount, InputRange range)→double[][]([通道][点]),按 §1.2 排列解交错并换算。
单元测试验收:code=0x8000 → 0V;code=0xFFFF → +5V 量程下约 +4.99985V;code=0 → −5V。
5. 各层职责(不写函数名,描述 API 类别)
5.1 IDigitizer 接口应暴露的能力
- 枚举设备 → 返回
IReadOnlyList<DeviceInfo>。 - 按逻辑号打开 / 关闭。
- 用
AcquisitionConfig初始化采集(设量程、通道掩码、时钟分频、模式、软件触发)。 - 启动采集:以回调或 IAsyncEnumerable 的方式持续吐出
SampleBlock(已解交错、已换算),直到取消或有限点采满。 - 实现
IDisposable释放 SDK 资源。
5.2 Pcie8586Digitizer(真卡,唯一含 P/Invoke)
照 Samples/VC/Simple/AD/Continue 的调用顺序翻译。典型连续采集流程(具体函数名看 Samples):
- 创建设备:
CreateDevice(逻辑号)→ 返回设备句柄(HANDLE/IntPtr)。判断无效值(通常 0 或 -1/INVALID_HANDLE_VALUE)。 - 配置参数:填充 SDK 的"AD 参数结构体"(量程枚举、通道数/掩码、
ADC时钟分频器 = config.ClockDivider、采集模式、触发源=软件触发、是否主卡等),调用初始化函数(InitDeviceAD/SetADParam一类)。 - 启动:
StartDeviceAD(句柄)。 - 读 DMA:循环调用读取函数(
GetDeviceAD/ReadDeviceAD,参数通常是句柄, 缓冲区指针, 期望点数, 超时ms),返回实际读到的点数。把读到的ushort[]原始码值交给CodeConverter.Deinterleave→SampleBlock→ 回调。 - 软件触发:若初始化为软件触发,需在 Start 后调用一次软件触发函数(手册 4.8.2)。
- 停止/释放:
StopDeviceAD(句柄)→ReleaseDevice(句柄)。
读取循环必须在后台线程/Task,不可阻塞 UI 线程。缓冲区用 GCHandle.Alloc(buf, GCHandleType.Pinned) 固定后传 AddrOfPinnedObject(),用完 Free();或用 Marshal.AllocHGlobal + Marshal.Copy。
5.3 SimulatedDigitizer(模拟卡)
- 不碰 SDK。内部按 §1.2 布局生成交错的 ushort 原始码值(每通道不同频率正弦 + 1% 噪声),再走与真卡完全相同的
Deinterleave路径,确保两条路径解析逻辑一致。 - 按配置的采样率节流出块(建议每秒约 30 块,每块点数 = 采样率/30 上限封顶几千点),
await Task.Delay模拟 DMA 节奏。 - 支持连续与有限点(有限点采满点数后结束)。
5.4 MainViewModel(UI 线程安全是重点)
- 用
CommunityToolkit.Mvvm的[ObservableProperty]/[RelayCommand]。 - 高速数据进 UI 的关键:采集回调在后台线程触发,不要每块都
Dispatcher.Invoke更新绑定属性(会卡死 UI)。正确做法:- 后台把最新数据写入一个无锁/带锁的"最新帧"缓冲;
- 用一个
DispatcherTimer(~50ms,即 20Hz)在 UI 线程定时把最新帧刷到 ScottPlot 和表格。 - 即"采集高频、渲染定频",解耦两者速率。
- 命令:
Open/Close/StartMonitor/StopMonitor/Record(时长)/ClearScreen/SaveClip(起,止)。
6. P/Invoke 翻译方法论(VC 头文件 → C#)
Claude Code 对着 Include/*.h 按下表翻译。逐个核对头文件的真实类型,不要套用记忆。
| C / VC 头文件 | C# P/Invoke | 备注 |
|---|---|---|
HANDLE / void* / DEVICE_HANDLE |
IntPtr |
设备句柄一律 IntPtr |
int / LONG |
int |
4 字节 |
unsigned int / DWORD / ULONG |
uint |
时钟分频器常是 DWORD |
unsigned short / WORD / USHORT |
ushort |
原始码值类型 |
unsigned char / BYTE |
byte |
|
BOOL |
[return: MarshalAs(UnmanagedType.Bool)] ... bool 或直接 int |
BOOL 是 4 字节 int,别用 1 字节 bool |
char*(入参字符串) |
string + CharSet / 或 byte[] |
看是 ANSI 还是宽字符 |
WORD* pData / void* pBuffer(缓冲区) |
IntPtr + pinning,或 ushort[] |
大缓冲建议 IntPtr + Marshal.Copy |
struct { ... }(参数结构体) |
[StructLayout(LayoutKind.Sequential)] struct |
字段顺序、类型、对齐必须 1:1 |
enum |
C# enum : int 或直接用 int 常量 |
[DllImport] 模板:
[DllImport("PCIe8586M.dll", // ← DLL 名以实际为准(看驱动安装目录/Samples 链接的 .lib)
CallingConvention = CallingConvention.StdCall, // ← VC 常是 __stdcall;若 .h 里是 __cdecl 则改 Cdecl
CharSet = CharSet.Ansi,
SetLastError = true)]
private static extern IntPtr 函数名(参数);
6.1 已知坑
- 调用约定:阿尔泰 DLL 多为
__stdcall(=StdCall)。若崩溃在调用后栈不平衡(PInvokeStackImbalance调试警告),改CallingConvention。看.h里是否有WINAPI/__stdcall/__cdecl宏。 - 结构体对齐:若
.h里有#pragma pack(1),C# struct 要[StructLayout(LayoutKind.Sequential, Pack = 1)]。错了会导致字段错位、参数乱掉。 - BOOL ≠ bool:C 的
BOOL是 4 字节。直接用 C#bool(1字节) 编组会错位,用UnmanagedType.Bool或声明成int。 - 缓冲区生命周期:读 DMA 的缓冲必须 pinned 或非托管堆分配,传指针期间不能被 GC 移动。循环里复用同一块缓冲,别每块 new。
- 句柄无效判断:
CreateDevice失败可能返回0、-1或0xFFFFFFFF,看 Samples 里怎么判断的,照抄。 - x86/x64:DLL 是 64 位 → 工程必须 x64,否则
BadImageFormatException。 - DLL 找不到:运行时把 SDK 的 DLL 放 exe 同目录(或加 PATH)。开发期 DLL 不在本机属正常,模拟卡不需要它。
7. 性能与内存约束
- 8 通道 @ 100MS/s = 1600 MB/s 原始数据(手册 4.9)。调试程序不要全速 8 通道连续显示,会爆。测试时用降速(如 1MS/s)或少通道。
- 显示降采样:ScottPlot 一屏最多画几千个点。来一块几万点的数据,画之前做 min/max 抽稀(每像素列取最值),否则渲染卡。ScottPlot 的
Signal类型对密集点有内建优化,优先用。 - 录制/剪辑缓冲上限:
RingHistoryBuffer按"最近 N 秒 × 采样率 × 通道数 × 4字节(float)"预估,设硬上限(如 ≤ 500MB),超出丢最旧。UI 标明可剪辑的历史长度。 - 采集频率 vs 渲染频率解耦:见 §5.4,渲染固定 ~20Hz。
8. 存盘格式(与未来 Python 分析对齐)
每次录制/剪辑输出两个文件:
{name}.bin:float32小端,按通道交错:CH0[0]CH1[0]..CHn[0]CH0[1]..(电压,单位 V)。{name}.json:元数据头:{ "channel_count": 8, "sample_rate_hz": 1000000.0, "input_range": "5V", "samples_per_channel": 10000, "dtype": "float32", "layout": "interleaved", "unit": "volt", "recorded_at": "ISO8601", "duration_ms": 10.0 }
Python 读法(未来):
np.fromfile(bin, dtype='<f4').reshape(-1, channel_count).T即得 [通道][点]。本程序阶段不实现 Python。
9. 推荐开发顺序(里程碑)
- M1 骨架编译通过:建工程(x64/WPF) +
Models+IDigitizer+ 空SimulatedDigitizer,能dotnet build。 - M2 模拟卡出数据:
CodeConverter+SimulatedDigitizer生成交错码值并解析,控制台/单测打印验证电压正确(§4 验收点)。 - M3 UI 实时显示:MainWindow + ScottPlot 曲线 + 表格,跑模拟卡看到滚动波形,验证 §5.4 的"采集高频渲染定频"不卡。
- M4 录制与剪辑:
RingHistoryBuffer+Recorder+WaveformWriter,验证 §3.2 录制点数精确、剪辑正确。 - M5 设备识别 UI:自动枚举 + 手动逻辑号(仍走模拟卡)。
- M6 真卡接入:读
Samples/VC/Include与Continue示例,实现Pcie8586Digitizer,照 §5.2/§6 翻译 P/Invoke。换掉模拟卡,连真硬件联调。 - M7 抽离复用层:确认
Hardware+Acquisition无 WPF 依赖,整理成可被主项目引用的形态。
每个里程碑用模拟卡保证可独立验证,真硬件只在 M6 才需要。
10. 验收清单
- x64 编译,模拟卡下无硬件即可运行。
- 码值换算单测通过(0x8000→0V 等)。
- 解交错对 1/2/4/8 通道均正确。
- 实时曲线 + 表格在模拟卡 1MS/s 下流畅(UI 不卡)。
- 采样率 UI 只允许 100e6/N 离散值,旁显实际值。
- 定时录制 1ms/10ms/100ms/1s/10s,落盘点数精确。
- 剪辑任意 [起,止) 区间另存正确。
- 存盘 .bin/.json 格式符合 §8。
Hardware+Acquisition两层不含任何 WPF/UI 引用。- 真卡实现仅
Pcie8586Digitizer.cs含[DllImport]。 - 真卡 CreateDevice→Configure→Start→读DMA→Stop→Release 全流程联通。