ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#与VC调用NI采集卡:从驱动选择到回环自检全解析

C#与VC调用NI采集卡:从驱动选择到回环自检全解析 简介面向 Visual C 开发者的 NI-DAQ 采集卡调用程序模板主要解决 VC 环境下通过 NI-DAQmx 驱动控制 NI 采集卡、以 1000Hz 采样率定时采集数据的问题也适合需要在 MFC 对话框中集成采集逻辑的 C/C# 开发者参考。压缩包共 35 个文件约 2.43MB核心内容为 VC 工程源代码与工程配置包括 h/cpp 源码、NIDAQmx.h 头文件与 NIDAQmx.lib 导入库、MFC 对话框资源、HighResolutionTimer 定时器实现以及 exe/pdb/ilk 等编译输出文件另有 originaldata.txt 采样数据和 ReadMe 说明。目前已有 848 人学习下载。按包内目录和 Debug 输出结构可快速定位工程入口对照源码理解驱动函数调用、定时器设置、通道配置、连续读取与停止清理等关键环节同时包内保留 dsp/dsw/clw/rc 等 MFC 工程辅助文件可在 Visual C 工程环境中直接打开编译结合 originaldata.txt 核对采集数据格式便于把模板迁移到自己的采集项目适合作为学习 NI-DAQ 采集编程的入门模板。1. 从 DAQandVC.zip 解出来的不只是代码还有整套采集链路要重新对齐DAQandVC.zip 这个压缩包在工控和测试领域很常见解压后通常是 VC 写的采集程序源文件配一张 NI 采集卡卡上可能印着 FRM11 之类的型号标签旁边还散落着 C# 重写的界面工程或测试脚本。打包的人把它们放一起原意是“既有 VC 老代码也有 C# 新思路”但真正接手时麻烦点不在这两种语言而在驱动层的选择老工程用的是传统 NI-DAQ新 C# 工程却必须走 NI-DAQmx两者 API 不兼容设备名、通道字符串、采样时钟配置全都不一样。理顺这条链路的关键是先搞清楚 NI-DAQ 与 NI-DAQmx 在 C# 和 VC 里分别怎么被调用再把数据从采集线程搬到界面线程而不卡顿最后用回环自检确认板卡接线没配对。对刚接触 NI 采集卡上位机开发的人这几步能省掉大量试探性修改。2. 驱动层先定调NI-DAQ 与 NI-DAQmx 对 C# 和 VC 的 API 差异2.1 传统 NI-DAQ 与 NI-DAQmx 的核心区别DAQandVC.zip 里的老 VC 工程很可能写在 2005 年前后那时调用 NI 采集卡常用传统 NI-DAQ 驱动头文件叫 nidaq.h设备名写成 Device 1配置采样时钟要自己操作寄存器或调用老式配置函数。现在的 NI-DAQmx 是完全不同的驱动架构C 接口头文件换成 NIDAQmx.h设备名由 NI MAX 统一分配成 Dev1、Dev2C# 侧则有官方 .NET 程序集 NationalInstruments.DAQmx.dll。两者不能混用一个工程里只能选其中一套驱动来链接。选哪套取决于硬件。现在在售的 NI 采集卡基本只提供 NI-DAQmx 驱动老卡如 PCI-6024E 虽然还能在较新的 DAQmx 下工作但部分老函数的兼容层已经不再主动维护。如果 zip 包里的 VC 工程链接的是传统 NI-DAQ新机器上大概率会报缺少 nidaq32.dll这时候与其在兼容模式里挣扎不如把调用代码改写成 DAQmx C 接口。如果只是跑通旧功能且硬件和系统都很老那保留传统 NI-DAQ 也未尝不可但新的 C# 项目不要选这条路。对比项传统 NI-DAQNI-DAQmxC 头文件nidaq.h / nidaqcns.hNIDAQmx.h设备命名Device 1Dev1NI MAX 分配C# 官方支持基本没有NationalInstruments.DAQmx.dll采样循环缓冲手动维护内建缓冲读取接口直接拿数据错误反馈简单错误码完整错误描述可打印出具体原因VC 兼容性老 VC 6.0 工程常见VC 2008 以后均可常见做法是老 VC 工程只做维护性修改保留原驱动所有新写的采集代码一律用 NI-DAQmx。C# 上位机开发中这种“新旧并存”的情况很多先确认驱动版本再动手比先改代码更重要。2.2 C# 工程里的引用与设备名确认在 C# 里调 NI 采集卡需要安装 NI-DAQmx 运行时安装时勾选 .NET 支持之后在工程里添加引用 NationalInstruments.DAQmx。这个程序集跟 Visual Studio 版本没有关系.NET Framework 4.x 和 .NET Core 3.1 以上都能用但要注意目标平台位数要跟驱动一致建议 x64 工程配 64 位驱动AnyCPU 在某些环境下会加载失败。设备名以 NI MAX 里看到的为准。打开 NI MAX在设备与接口下能看到类似 Dev1 的名称右键可以执行自检和测试面板。FRM11 这类印在板卡或接线端子上的型号只是物理标识不会出现在设备管理器或 NI MAX 里代码里的通道字符串要写 Dev1/ai0 这种格式而不是 FRM11/ai0。提示接线前先看 FRM11 的引脚定义确认 ai0、ai1、ao0、地线分别在哪个端子。物理型号决定接线驱动设备名决定代码两套信息不要混在一起记。2.3 用最小 C# 代码读一帧电压数据设备名确认后先写一个最小工程验证采集链路不画界面不做统计只把数据打到控制台using System; using NationalInstruments.DAQmx; class Program { static void Main() { using (var task new Task()) { task.AIChannels.CreateVoltageChannel( Dev1/ai0, 电压输入, AITerminalConfiguration.Rse, -10, 10, AIVoltageUnits.Volts); task.Timing.ConfigureSampleClock( , 1000, SampleClockActiveEdge.Rising, SampleQuantityMode.FiniteSamples, 100); var reader new AnalogSingleChannelReader(task.Stream); double[] data reader.ReadMultiSample(100); for (int i 0; i data.Length; i) Console.WriteLine(data[i].ToString(F4)); } } }这段代码做了三件事创建采集任务、配置电压通道和采样时钟、读取 100 个样本。CreateVoltageChannel 里的 Dev1/ai0 是物理通道Rse 表示参考单端模式适合信号地线与采集卡地线比较接近的场景如果信号源有独立参考地或需要抗共模干扰应改用 Differential 并接好对应通道。ConfigureSampleClock 第一个参数传空字符串表示使用板载时钟第二个参数 1000 是采样率最后两个参数表示总共采 100 点。ReadMultiSample 是阻塞读取会等 100 点全部采完才返回。跑通这段说明驱动、设备名、模拟输入链路都正常接下来可以继续做连续采集。3. C# 上位机连续采集不卡顿后台任务、队列与节流刷新3.1 阻塞读取为什么会拖垮 UI连续采集的代码不能直接写在按钮点击事件里。ReadMultiSample 和 ReadAnalogF64 这一类调用在没有采够指定点数之前不会返回如果放在 UI 线程里执行界面会一直处于未响应状态拖拽窗口、点击按钮全都没反应。常见做法是把采集循环放到后台线程用队列把数据传给界面线程界面线程只做绘图和显示。这样设计还有一个额外好处采集循环不会因为界面重绘而暂停采样率更稳定。很多 C# 上位机新手在连续采集时看到波形卡顿第一反应是去调采样率其实问题往往出在界面刷新频率太高。每收到一个数据块就刷新一次图形假设采样率 1000 S/s一次刷 50 点每秒要刷 20 次再加上曲线控件重绘的开销CPU 占用立刻飙高。正确的思路是降低界面刷新频率把多个数据块合并到一次刷新里画出来。3.2 后台采集加定时节流的工程写法用一个后台任务跑采集循环采集结果放进 ConcurrentQueue界面定时器按固定间隔从队列取数据。ConcurrentQueue 是线程安全的多线程读写同一队列不需要再加锁适合单个生产者单个消费者的场景。以下就是比较稳固的结构private readonly ConcurrentQueuedouble[] _queue new ConcurrentQueuedouble[](); private volatile bool _running; private void AcquisitionLoop(string channel, double sampleRate) { using (var task new Task()) { task.AIChannels.CreateVoltageChannel(channel, , AITerminalConfiguration.Rse, -10, 10, AIVoltageUnits.Volts); task.Timing.ConfigureSampleClock(, sampleRate, SampleClockActiveEdge.Rising, SampleQuantityMode.ContinuousSamples, (int)sampleRate / 2); var reader new AnalogSingleChannelReader(task.Stream); task.Start(); while (_running) { double[] block reader.ReadMultiSample(500); _queue.Enqueue(block); } } }连续采样模式下DAQmx 内部有缓冲采样时钟在后台持续运行ReadMultiSample 每次取 500 点。缓冲区大小设置为采样率的一半比如采样率 1000 S/s 时就缓冲 500 点这样即使界面线程卡顿几十毫秒缓冲也还能兜住一部分数据。界面上用一个 Timer 每隔 33 到 66 毫秒取一次队列取到数据后合并成一段再画private void Timer_Tick(object sender, EventArgs e) { var merged new Listdouble(); while (_queue.TryDequeue(out var block)) merged.AddRange(block); if (merged.Count 0) { labelValue.Text merged.Average().ToString(F3); chart.Series[0].Points.Clear(); for (int i 0; i merged.Count; i) chart.Series[0].Points.Add(merged[i]); } }采集线程负责保证不丢数界面线程负责限频刷新两者通过队列解耦这正是 C# 循环数据采集和 UI 刷新卡顿问题最直接的解决方案。3.3 采样率、块大小与界面刷新频率的匹配参数实际工程里这四个参数需要一起调单一参数调优很难见效。采样率由被测信号决定比如测市电 50 Hz 信号采样率至少 1000 S/s 才有余量块大小决定单次读取的样本数块越大越省 CPU但拿到的曲线滞后越明显界面刷新频率控制画面的流畅度15 到 30 FPS 足够观测实时波形。参数建议取值作用与影响sampleRate信号最高频率的 5 到 10 倍决定时间分辨率blockSize500 到 1000 点单次读取量影响 CPU 占用刷新间隔33 到 66 毫秒控制 UI 刷新频率防止重绘卡顿队列容量不设上限或按数据量设上限避免后台线程被 UI 慢速拖住队列如果不设上限高频采集中界面一旦卡住内存占用会持续上涨累积大量历史数据。可以在入队前判断队列长度超过一定阈值就丢弃最旧的数据块保证显示的是最近一段时间的波形。这个取舍在实时监测场景里比保存所有原始数据更重要。注意ReadMultiSample 的 timeOut 参数不要传 -1否则设备出错时采集线程会一直卡住无法退出。传 10 秒以内的超时值退出前用 DAQmxStopTask 或 task.Stop() 停止任务。4. VC 调用 NI 采集卡的两条路径C 接口直调与 DLL 封装4.1 在 VC 源码里直接调用 NI-DAQmx C 接口VC 工程调用 NI 采集卡最常见的方式是直接包含 NIDAQmx.h链接 NIDAQmx.lib。函数名很直白DAQmxCreateTask 建任务DAQmxCreateAIVoltageChan 配通道DAQmxCfgSampClkTiming 配采样时钟。下面这段 C 函数能从指定通道读取一段有限长度的电压数据#include NIDAQmx.h #include stdio.h int32_t ReadVoltageBlock(const char* channel, double* buffer, int32_t points) { int32_t error 0; TaskHandle taskHandle NULL; error DAQmxCreateTask(, taskHandle); if (DAQmxFailed(error)) goto cleanup; error DAQmxCreateAIVoltageChan(taskHandle, channel, , DAQmx_Val_Cfg_Default, -10.0, 10.0, DAQmx_Val_Volts, NULL); if (DAQmxFailed(error)) goto cleanup; error DAQmxCfgSampClkTiming(taskHandle, , 1000.0, DAQmx_Val_Rising, DAQmx_Val_FiniteSamps, points); if (DAQmxFailed(error)) goto cleanup; error DAQmxStartTask(taskHandle); if (DAQmxFailed(error)) goto cleanup; int32_t pointsRead 0; error DAQmxReadAnalogF64(taskHandle, points, 10.0, DAQmx_Val_GroupByChannel, buffer, points, pointsRead, NULL); if (DAQmxFailed(error)) goto cleanup; if (pointsRead ! points) error -1; cleanup: if (DAQmxFailed(error)) { char errInfo[2048] {0}; DAQmxGetExtendedErrorInfo(errInfo, sizeof(errInfo)); fprintf(stderr, DAQmx error: %s\n, errInfo); } if (taskHandle ! NULL) DAQmxClearTask(taskHandle); return error; }这段代码里最关键的不是配置函数本身而是 cleanup 分支的 DAQmxGetExtendedErrorInfo。DAQmx 的错误码是负数单看 -200077 这类数字很难定位问题调用这个函数能拿到完整中文错误描述。每次任务用完后 DAQmxClearTask 必须执行否则多次调用会累积句柄最终导致设备被占用报错。4.2 把 VC 采集逻辑封成 DLL 交给 C# 调用如果 zip 包里已经有 VC 写的采集代码且功能和硬件都能用没必要用 C# 重写这一层直接把 VC 代码封装成一个 DLL 导出 C 接口给 C# 调。这样既能保留原有采集逻辑又能在新写的 C# 上位机里复用。VC 侧导出函数时加上 extern C 和 __declspec(dllexport)extern C __declspec(dllexport) int32_t ReadVoltageBlock(const char* channel, double* buffer, int32_t points);C# 侧用 P/Invoke 声明同样签名的函数注意调用约定和字符集[DllImport(DAQCtrl.dll, CallingConvention CallingConvention.Cdecl, CharSet CharSet.Ansi)] private static extern int ReadVoltageBlock( string channel, double[] buffer, int points);这里的 channel 是 C 语言 char*对应 C# string必须用 CharSet.Ansi如果 VC 工程编译时使用 Unicode 字符集char* 和 wchar_t* 会出现不匹配最常见的就是 CSV 参数传进去变成乱码或访问冲突。调用约定 VC 侧默认 C 风格是 cdeclC# 侧也要写成 Cdecl否则进程直接崩溃。buffer 是 double[]传的就是 double*这一点是兼容的不需要额外处理。DLL 的位数必须与 C# 工程一致。VC 编译 32 位 DLLC# 工程就要设成 x86VC 编译 64 位C# 设 x64。AnyCPU 在 64 位系统上会以 64 位进程运行加载 32 位 DLL 会抛 BadImageFormatException这条错误信息在联调时非常常见。4.3 从老工程迁移到 NI-DAQmx 的典型坑老 VC 工程里写 Device 1/ai0 的地方在 NI-DAQmx 下改成 Dev1/ai0。如果 DAQandVC.zip 里装过传统 NI-DAQ 的老驱动卸载后 NI MAX 里设备可能还残留旧的 Device 名称需要重新启动系统让驱动重新枚举。通道字符串写错时 DAQmx 会在创建通道或启动任务时直接报错不会等你读出数据才提醒。另一个常见问题是老代码把寄存器基地址和中断号写死在配置里这类代码在 DAQmx 下已经完全失去意义需要删掉重写。老 VC 数据采集程序里普遍存在大量 while 循环加 Sleep 的轮询写法迁移到 DAQmx 后应改为事件回调或连续读取。VC 里用 DAQmxRegisterEveryNSamplesEvent 注册回调每采够指定点数就触发一次比轮询更稳定。与之配套的是在回调内部只负责取数据不要做文件写入或控件刷新把耗时操作丢给另一个线程否则回调阻塞会导致缓冲区溢出DAQmx 会停止采集并报错。5. AO 回环自检不拆仪器也能验证 FRM11 板卡的接线与链路拿到不熟悉的板卡最有效的验证方式是用它自己的 AO 通道输出一个已知电压再用 AI 通道读回来。这样采样链路、通道命名、接线和量程全程覆盖。回环自检不依赖外部信号源只需要一根导线把 FRM11 板卡的 ao0 接到 ai0再把两端的参考地接在一起。用 C# 写一个简短的自检函数输出直流电平后读取判断偏差private static double LoopbackTest(string aoChannel, string aiChannel, double level) { using (var aoTask new Task()) using (var aiTask new Task()) { aoTask.AOChannels.CreateVoltageChannel( aoChannel, , -10, 10, AOVoltageUnits.Volts); aiTask.AIChannels.CreateVoltageChannel( aiChannel, , AITerminalConfiguration.Differential, -10, 10, AIVoltageUnits.Volts); aiTask.Timing.ConfigureSampleClock( , 1000, SampleClockActiveEdge.Rising, SampleQuantityMode.FiniteSamples, 100); aoTask.Start(); aoTask.WriteSingleSample(true, level); aiTask.Start(); var reader new AnalogSingleChannelReader(aiTask.Stream); double[] data reader.ReadMultiSample(100); double avg 0; for (int i 0; i data.Length; i) avg data[i] / data.Length; return avg; } }分别用 0 V 和 2.5 V 两个电平各跑一轮。0 V 时读回值应该在 ±0.02 V 以内2.5 V 时读回值应该在 2.48 到 2.52 V 之间。如果 0 V 偏差很大多半是地线没接好或通道悬空如果两个电平都有固定偏置说明接线回路里有共模电压或 AO 与 AI 的参考地不一致如果读数跳动超过 0.1 V优先检查 FRM11 端子上的接线是否松动其次再查驱动版本。这个自检函数可以直接写进 C# 上位机里做成菜单项运行前点一下链路是否正常立刻知道。本文还有配套的精品资源点击获取
返回列表