SSPC-Tester/SSPCTester.Devices/Drivers/Real/Pcie8586Daq.cs

383 lines
17 KiB
C#
Raw Permalink 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.

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Pcie8586Probe.Hardware;
using Pcie8586Probe.Models;
using SSPCTester.Devices.Config;
using SSPCTester.Devices.Interfaces;
using DriverInputRange = Pcie8586Probe.Models.InputRange;
namespace SSPCTester.Devices.Drivers.Real;
/// <summary>PCIe8586 高速采集卡适配器。原生句柄仅由 IDigitizer 持有。</summary>
public sealed class Pcie8586Daq : IDaq, IDisposable
{
private readonly DaqOptions _options;
private readonly ILogger<Pcie8586Daq> _logger;
private readonly IDigitizer _digitizer;
private readonly SemaphoreSlim _operationLock = new(1, 1);
private bool _connected;
public Pcie8586Daq(IOptions<DeviceOptions> options, ILogger<Pcie8586Daq> logger, IDigitizer digitizer)
: this(options.Value.Daq, logger, digitizer)
{
}
internal Pcie8586Daq(DaqOptions options, ILogger<Pcie8586Daq> logger, IDigitizer digitizer)
{
_options = options;
_logger = logger;
_digitizer = digitizer;
}
public bool IsConnected => _connected && _digitizer.IsOpen;
public string DisplayName => "PCIe8586 高速采集卡";
public event EventHandler<bool>? ConnectionChanged;
public event EventHandler<CurveSample>? SampleReceived;
public async Task<IReadOnlyList<DaqDeviceDescriptor>> EnumerateDevicesAsync(CancellationToken ct = default)
{
try
{
var devices = await _digitizer.EnumerateDevicesAsync(ct);
return devices.Select(x => new DaqDeviceDescriptor(
x.LogicalId, x.PhysicalId, x.Description, x.IsSimulated)).ToArray();
}
catch (DllNotFoundException ex)
{
throw new InvalidOperationException("找不到 ACTS1000_64.dll。请安装厂商驱动并将 x64 运行库放到程序目录。", ex);
}
catch (BadImageFormatException ex)
{
throw new InvalidOperationException("ACTS1000_64.dll 位数不匹配,程序和运行库必须都是 x64。", ex);
}
}
public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
await _operationLock.WaitAsync(ct);
try
{
if (IsConnected) return true;
var devices = await EnumerateDevicesAsync(ct);
var selected = devices.SingleOrDefault(x => x.LogicalId == _options.LogicalDeviceId)
?? throw new InvalidOperationException($"未发现逻辑 ID {_options.LogicalDeviceId} 的 PCIe8586。");
await _digitizer.OpenAsync(
new DeviceInfo(selected.LogicalId, selected.PhysicalId, selected.Description, selected.IsSimulated), ct);
_connected = true;
ConnectionChanged?.Invoke(this, true);
return true;
}
finally
{
_operationLock.Release();
}
}
public async Task DisconnectAsync()
{
await _operationLock.WaitAsync();
try
{
await _digitizer.StopAsync();
await _digitizer.CloseAsync();
if (_connected)
{
_connected = false;
ConnectionChanged?.Invoke(this, false);
}
}
finally
{
_operationLock.Release();
}
}
public async Task<CurveData> CaptureAroundActionAsync(
DaqActionCaptureRequest request,
Func<CancellationToken, Task> action,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(action);
if (request.PreTriggerMs <= 0 || request.PostTriggerMs <= 0)
throw new ArgumentOutOfRangeException(nameof(request), "Pre/post trigger windows must be positive.");
ValidateOptions(requireOutputVoltage: true);
await _operationLock.WaitAsync(ct);
try
{
if (!IsConnected) await ConnectWithoutLockAsync(ct);
var config = CreateDriverConfig();
await _digitizer.ConfigureAsync(config, ct);
int preSamples = Math.Max(1, (int)Math.Round(config.SampleRateHz * request.PreTriggerMs / 1_000.0));
int postSamples = Math.Max(1, (int)Math.Round(config.SampleRateHz * request.PostTriggerMs / 1_000.0));
int targetSamples = checked(preSamples + postSamples);
var collected = Enumerable.Range(0, config.ChannelCount)
.Select(_ => new List<double>(targetSamples)).ToArray();
bool actionInvoked = false;
int triggerIndex = 0;
var preTriggerTimeout = TimeSpan.FromMilliseconds(Math.Max(3_000, request.PreTriggerMs * 5));
using var acquisitionCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
acquisitionCts.CancelAfter(preTriggerTimeout);
try
{
await foreach (var block in _digitizer.StartAcquisitionAsync(acquisitionCts.Token))
{
if (!actionInvoked)
{
int take = Math.Min(preSamples - collected[0].Count, block.SamplesPerChannel);
for (int channel = 0; channel < config.ChannelCount; channel++)
collected[channel].AddRange(block.Channels[channel].AsSpan(0, take).ToArray());
if (collected[0].Count >= preSamples)
{
triggerIndex = collected[0].Count;
actionInvoked = true;
acquisitionCts.CancelAfter(Timeout.InfiniteTimeSpan);
await action(ct);
}
// 动作发生在本次 DMA 读取完成之后;当前块未使用余量仍属于动作前数据,必须丢弃。
continue;
}
int remaining = targetSamples - collected[0].Count;
int postTake = Math.Min(remaining, block.SamplesPerChannel);
for (int channel = 0; channel < config.ChannelCount; channel++)
collected[channel].AddRange(block.Channels[channel].AsSpan(0, postTake).ToArray());
if (collected[0].Count >= targetSamples) break;
}
}
catch (OperationCanceledException ex) when (!ct.IsCancellationRequested && !actionInvoked)
{
string message =
$"PCIe8586 did not provide enough pre-trigger samples within {preTriggerTimeout.TotalMilliseconds:F0}ms; control action was not executed. " +
$"sampleRate={config.SampleRateHz:F0}Hz, channels={config.ChannelCount}, " +
$"preTrigger={request.PreTriggerMs:F1}ms, postTrigger={request.PostTriggerMs:F1}ms, " +
$"samples={collected[0].Count}/{preSamples}.";
_logger.LogWarning(message);
throw new InvalidOperationException(message, ex);
}
finally
{
await _digitizer.StopAsync();
}
if (!actionInvoked)
throw new InvalidOperationException(
$"PCIe8586 acquisition ended before enough pre-trigger samples were collected; control action was not executed. " +
$"sampleRate={config.SampleRateHz:F0}Hz, channels={config.ChannelCount}, " +
$"preTrigger={request.PreTriggerMs:F1}ms, postTrigger={request.PostTriggerMs:F1}ms, " +
$"samples={collected[0].Count}/{preSamples}.");
if (collected[0].Count < targetSamples)
throw new InvalidOperationException("PCIe8586 采集提前结束,后置波形不完整。");
return BuildCurve(
collected.Select(x => x.ToArray()).ToArray(),
config.SampleRateHz,
triggerIndex,
requireOutputVoltage: true);
}
finally
{
_operationLock.Release();
}
}
public async Task<CurveData> CaptureAsync(
int[] channels, int sampleRateHz, double durationSec, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(channels);
if (channels.Length == 0)
throw new ArgumentException("At least one channel must be requested.", nameof(channels));
if (durationSec <= 0 || !double.IsFinite(durationSec))
throw new ArgumentOutOfRangeException(nameof(durationSec), "Capture duration must be positive.");
ValidateOptions(requireOutputVoltage: false);
await _operationLock.WaitAsync(ct);
try
{
if (!IsConnected) await ConnectWithoutLockAsync(ct);
var config = CreateDriverConfig();
var requestedChannels = channels.Distinct().ToArray();
foreach (int channel in requestedChannels)
{
if (channel < 0 || channel >= config.ChannelCount)
throw new ArgumentOutOfRangeException(
nameof(channels),
$"Requested channel {channel} is outside the configured PCIe8586 channel count {config.ChannelCount}.");
}
await _digitizer.ConfigureAsync(config, ct);
int targetSamples = Math.Max(1, (int)Math.Round(config.SampleRateHz * durationSec));
var collected = Enumerable.Range(0, config.ChannelCount)
.Select(_ => new List<double>(targetSamples)).ToArray();
var acquisitionTimeout = TimeSpan.FromMilliseconds(Math.Max(3_000, durationSec * 5_000));
using var acquisitionCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
acquisitionCts.CancelAfter(acquisitionTimeout);
try
{
await foreach (var block in _digitizer.StartAcquisitionAsync(acquisitionCts.Token))
{
int remaining = targetSamples - collected[0].Count;
int take = Math.Min(remaining, block.SamplesPerChannel);
for (int channel = 0; channel < config.ChannelCount; channel++)
collected[channel].AddRange(block.Channels[channel].AsSpan(0, take).ToArray());
if (collected[0].Count >= targetSamples) break;
}
}
catch (OperationCanceledException ex) when (!ct.IsCancellationRequested)
{
string message =
$"PCIe8586 did not provide enough samples within {acquisitionTimeout.TotalMilliseconds:F0}ms. " +
$"sampleRate={config.SampleRateHz:F0}Hz, channels={config.ChannelCount}, " +
$"duration={durationSec:F3}s, samples={collected[0].Count}/{targetSamples}.";
_logger.LogWarning(message);
throw new InvalidOperationException(message, ex);
}
finally
{
await _digitizer.StopAsync();
}
if (collected[0].Count < targetSamples)
throw new InvalidOperationException(
$"PCIe8586 acquisition ended before the requested sample window completed; " +
$"samples={collected[0].Count}/{targetSamples}.");
return BuildCurve(
collected.Select(x => x.ToArray()).ToArray(),
config.SampleRateHz,
triggerIndex: 0,
requireOutputVoltage: false,
primaryPhysicalChannel: requestedChannels[0]);
}
finally
{
_operationLock.Release();
}
}
public async Task<double[]> ReadInstantAsync(int[] channels, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(channels);
if (channels.Length == 0) return Array.Empty<double>();
var curve = await CaptureAsync(channels, _options.SampleRateHz, durationSec: 0.003, ct);
return channels.Select(channel =>
{
var data = curve.EngineeringChannels.SingleOrDefault(x => x.PhysicalChannel == channel);
return data?.Samples.Average() ?? double.NaN;
}).ToArray();
}
public void ValidateOptions(bool requireOutputVoltage)
{
if (_options.ChannelCount is not (1 or 2 or 4 or 8))
throw new InvalidOperationException("PCIe8586 启用通道数只能为 1、2、4 或 8。");
if (_options.ClockDivider < 1)
throw new InvalidOperationException("PCIe8586 时钟分频必须大于 0。");
if (_options.PreTriggerMs <= 0 || _options.PostTriggerMs <= 0)
throw new InvalidOperationException("前置和后置采集窗口必须大于 0。");
var enabled = _options.Channels.Where(x => x.PhysicalChannel < _options.ChannelCount).ToArray();
foreach (var channel in enabled.Where(x => requireOutputVoltage && ParseRole(x.Role) != DaqChannelRole.Unconfigured))
{
if (!channel.IsCalibrated)
throw new InvalidOperationException($"PCIe8586 CH{channel.PhysicalChannel}{channel.Name})尚未标定。");
if (!double.IsFinite(channel.Scale) || channel.Scale == 0 || !double.IsFinite(channel.Offset))
throw new InvalidOperationException($"PCIe8586 CH{channel.PhysicalChannel} 标定比例或偏置无效。");
}
if (requireOutputVoltage &&
enabled.Count(x => ParseRole(x.Role) == DaqChannelRole.OutputVoltage && x.IsCalibrated) != 1)
throw new InvalidOperationException("曲线测试必须配置且只能配置一个已标定的“输出电压”通道。");
}
private async Task ConnectWithoutLockAsync(CancellationToken ct)
{
var devices = await EnumerateDevicesAsync(ct);
var selected = devices.SingleOrDefault(x => x.LogicalId == _options.LogicalDeviceId)
?? throw new InvalidOperationException($"未发现逻辑 ID {_options.LogicalDeviceId} 的 PCIe8586。");
await _digitizer.OpenAsync(
new DeviceInfo(selected.LogicalId, selected.PhysicalId, selected.Description, selected.IsSimulated), ct);
_connected = true;
ConnectionChanged?.Invoke(this, true);
}
private AcquisitionConfig CreateDriverConfig() => new(
_options.ChannelCount,
string.Equals(_options.InputRange, "PlusMinus1V", StringComparison.OrdinalIgnoreCase)
? DriverInputRange.PlusMinus1V
: DriverInputRange.PlusMinus5V,
_options.ClockDivider,
AcquisitionMode.Continuous,
10_000);
private CurveData BuildCurve(
double[][] cardVolts,
double sampleRateHz,
int triggerIndex,
bool requireOutputVoltage,
int? primaryPhysicalChannel = null)
{
var engineering = new List<CurveChannelData>();
for (int channel = 0; channel < cardVolts.Length; channel++)
{
var cfg = _options.Channels.FirstOrDefault(x => x.PhysicalChannel == channel)
?? new DaqChannelOptions { PhysicalChannel = channel, Name = $"CH{channel}" };
var role = ParseRole(cfg.Role);
var values = cardVolts[channel].Select(x => x * cfg.Scale + cfg.Offset).ToArray();
engineering.Add(new CurveChannelData(
channel, role, cfg.Name, UnitFor(role), cfg.Scale, cfg.Offset, values));
}
var outputVoltage = engineering.Where(x => x.Role == DaqChannelRole.OutputVoltage).ToArray();
if (requireOutputVoltage && outputVoltage.Length != 1)
throw new InvalidOperationException("Curve capture requires exactly one configured OutputVoltage channel.");
var primary = !requireOutputVoltage && primaryPhysicalChannel.HasValue
? engineering.FirstOrDefault(x => x.PhysicalChannel == primaryPhysicalChannel) ?? engineering.FirstOrDefault()
: outputVoltage.Length == 1
? outputVoltage[0]
: engineering.FirstOrDefault();
var voltage = primary?.Samples ?? Array.Empty<double>();
var current = engineering.FirstOrDefault(x => x.Role == DaqChannelRole.OutputCurrent)?.Samples;
if (outputVoltage.Length == 1 && voltage.Length > 0)
{
int sample = Math.Clamp(triggerIndex, 0, voltage.Length - 1);
SampleReceived?.Invoke(this, new CurveSample(
sample / sampleRateHz,
outputVoltage[0].Samples[sample],
current is { Length: > 0 } ? current[Math.Min(sample, current.Length - 1)] : 0));
}
return new CurveData
{
SampleRateHz = (int)Math.Round(sampleRateHz),
TriggerSampleIndex = triggerIndex,
TriggerTimeSec = triggerIndex / sampleRateHz,
Voltage = voltage,
Current = current,
EngineeringChannels = engineering
};
}
internal static DaqChannelRole ParseRole(string? role) =>
Enum.TryParse<DaqChannelRole>(role, true, out var value) ? value : DaqChannelRole.Unconfigured;
internal static string UnitFor(DaqChannelRole role) => role switch
{
DaqChannelRole.OutputVoltage or DaqChannelRole.InputVoltage => "V",
DaqChannelRole.OutputCurrent or DaqChannelRole.InputCurrent => "A",
_ => "V"
};
public void Dispose()
{
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
_digitizer.Dispose();
_operationLock.Dispose();
}
}