如果使用過PCI采集卡的盆友應該對“研華”這個品牌不陌生,可以說研華還是很強大的。最近因為工作需要,使用一塊研華的PCI1714UL型號的采集卡,去高速采集電壓信號,學習了幾天后分享給各位。
采集卡
首先介紹一下這塊采集卡。品牌:研華Advantech,型號:PCI1714UL,基本參數:4通道,12位精度,采樣頻率:10MS/s,采樣:8192/通道,可采集電壓范圍有:±5、±2.5、±1、±0.5,每個范圍對應的絕對精度不同,所以視情況來決定需要什么樣的采樣范圍。支持PCI總線控制DMA數據傳輸。
工作方式
這塊采集卡有兩種工作模式:BufferedAI、InstantAI。
BufferedAI:使用PC的一個存儲塊,可以實現總線控制數據采集,不需要占用CPU;
InstantAI:瞬時值,采樣率相對沒那么高,適合一些對數據要求不算太高的場合。
資料下載
驅動安裝
研華提供了一款DAQNavi軟件(見“資料下載”去官網下載),有一個Navigator,可以直接打開可以看到所有支持的卡,選中你需要的卡右鍵就可以安裝驅動了。並且這款軟件也提供數據采集展示。


軟件二次開發:接口函數---Automation.BDaq4(見“資料下載”去官網下載)
一、使用研華提供的控件進行二次開發
研華提供了相關控件:

WaveformAiCtrl用於BufferedAI讀取,InstantAiCtr用於InstantAI讀取。
二、自己從底層開發
瞬時電壓讀取比較簡單,直接創建InstantAiCtrl,傳遞一個設備名稱:instantAiCtrl.SelectedDevice = new DeviceInformation(deviceCode);初始化:instantAiCtrl.Initialized,就可以開始讀取數據了:instantAiCtrl.Read(channelIndex,out data[channelIndex])。
BufferedAI代碼量多一點:首先需要創建一個線程,用於將采集卡數據讀取到內存中,再開一個線程專門見內存中的數據往本地寫入(因為數據量太大不能一下處理),最后開一個線程用於將本地的數據讀取出來,進行數據處理、分析、展示。
代碼展示
1. 創建一個AnalogCard父類,利於再添加采集卡型號的時候進行擴展
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.IO; namespace AdvantechPCIDemo { /// <summary> /// 數據讀取策略: /// 采集卡讀取速度極快(10MS/s、30MS/s),因此現將采集卡數據保存至指定文件,多線程同步將數據讀取 /// 再對數據進行處理、顯示 /// </summary> public class AnalogCard { protected int channelCount;//通道數 protected uint readLength;//數據一次讀取長度 protected uint readCount; protected ManualResetEventSlim readEvent = new ManualResetEventSlim(false); protected ManualResetEventSlim writeEvent = new ManualResetEventSlim(false); protected ManualResetEventSlim readADEvent = new ManualResetEventSlim(false); protected Task writeTask;//任務:將內存Queue數據寫入指定文件 protected Task readTask;//任務:將指定文件數據讀入內存Queue protected Task readADTask;//任務:將采集卡數據讀取進內存Queue protected Queue<ushort[]> dataQueue = new Queue<ushort[]>(1000);//先進先出隊列,定義初始長度,不足自動增長 protected CancellationTokenSource source = new CancellationTokenSource();//線程是否取消 protected string fileName = string.Format("Data\\Data-{0}.txt", DateTime.Now.ToString("yy-MM-dd-HH-mm-ss"));//定義數據保存與讀取文件名 protected Action<ushort[][]> ChannelDataAction { get; set; } public AnalogCard() { //將內存Queue數據寫入指定文件 writeTask = Task.Factory.StartNew(() => { using (FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read)) { while (true) { writeEvent.Wait(source.Token);//等待開啟寫入任務 if (source.IsCancellationRequested) return; if (dataQueue.Count == 0) continue; ; ushort[] us = dataQueue.Dequeue();//將第一個數據去除並移除 if (dataQueue.Count > 1000) { dataQueue.Clear(); } byte[] bt = new byte[readLength * 2]; for (int i = 0, j = 0; i < us.Length; i++) { ushort temp = us[i]; bt[j++] = (byte)(temp >> 8);//取高八位 bt[j++] = (byte)temp;//取低八位 } fs.Write(bt, 0, bt.Length); } } }, source.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); Thread.Sleep(500); //將指定文件的數據流寫入內存隊列Queue readTask = Task.Factory.StartNew(() => { using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Write)) { int currentLength = 0; byte[] bt=null; while (true) { readEvent.Wait(source.Token);//等待開啟讀取 if (source.IsCancellationRequested) break;//任務被取消 if (bt == null) { bt = new byte[readLength * 2];//byte數組為需要讀取數據的兩倍:兩個字節轉換為一個字符 } if (currentLength < bt.Length) { currentLength = fs.Read(bt, currentLength, bt.Length - currentLength); } else { currentLength = 0; if (ChannelDataAction == null) throw new Exception("無通道數據接收方法!"); ushort[][] us; if (!GetChannelData(bt, out us)) throw new Exception(""); ChannelDataAction(us);//將讀取到的數據向上傳遞 } } } }); } protected virtual void StartWriteReadTxt() { writeEvent.Set(); readEvent.Set(); readADEvent.Set(); dataQueue.Clear(); } protected virtual void StopWriteReadTxt() { writeEvent.Reset(); readEvent.Reset(); readADEvent.Reset(); } public virtual void CancelWriteReadTxt() { source.Cancel(); } /// <summary> /// 將讀取的數據分到各通道 /// 數據順序:temp[0]+temp[1]=ch1va1;temp[2]+temp[3]=ch2va1... /// </summary> /// <param name="temp"></param> /// <returns></returns> private bool GetChannelData(byte[] temp, out ushort[][] us) { us = new ushort[channelCount][]; int index = 0; for (int i = 0; i < temp.Length; i+=2*channelCount) { for (int j = 0; j < channelCount; j++) { if (us[j] == null) { us[j] = new ushort[readLength / channelCount]; } us[j][index] = (ushort)((temp[i + j * 2] << 8) + temp[i + j * 2 + 1]); } index++; } return true; } } }
在父類設置卡的一些基本字段,並創建了兩個線程:writeTask、readTask,分別是將內存Queue中數據寫入本地txt,將本地txt文件讀取進內存Queue。用ManualResetEventSlim對象現將該線程堵塞,等待開啟。
2. 創建一個PCI1714UL子類
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Automation.BDaq; using System.Threading; using System.Threading.Tasks; namespace AdvantechPCIDemo { public class PCI1714UL:AnalogCard { private string deviceCode = ""; private const int bufferLength = 2048; private int channelMax=0; private BufferedAiCtrl bufferedCtrl = null; private InstantAiCtrl instantAiCtrl = null; private ManualResetEventSlim readADInternal = new ManualResetEventSlim(false);//有數據再開啟 private ushort[] data; private object locker = new object(); public PCI1714UL(string deviceCode):base() { this.deviceCode = deviceCode; } public void StartBufferedAI(int channelCount,Action<ushort[][]> channelDataAction) { bufferedCtrl = new BufferedAiCtrl(); bufferedCtrl.SelectedDevice = new DeviceInformation(deviceCode); this.channelCount = channelCount; this.readLength = (uint)(bufferLength * channelCount); this.readCount = bufferLength; this.data = new ushort[readLength]; this.ChannelDataAction = channelDataAction; ScanChannel channel = bufferedCtrl.ScanChannel; channel.ChannelCount = channelCount; channel.ChannelStart = 0; channel.IntervalCount = bufferLength; // each channel 觸發DataReady事件的采樣個數,即到了指定個數之后便觸發DataReady事件,可以跟windows的定時器控件類似 channel.Samples = bufferLength;//采樣的個數。例如,我設定采樣個數為1024個,Rate是1024/s,那么也就是說采樣經過了1秒 bufferedCtrl.DataReady += new EventHandler<BfdAiEventArgs>(BufferedReady); this.channelMax = bufferedCtrl.Features.ChannelCountMax; bufferedCtrl.Streaming = false; ErrorCode ret = bufferedCtrl.Prepare();//初始化所選設備,准備開始 if (ret != ErrorCode.Success) throw new InvalidOperationException("Failed to prepare AD!"); //將采集卡數據讀取到Queue readADTask = Task.Factory.StartNew(() => { while (true) { readADEvent.Wait(source.Token);//等待開啟 if (source.IsCancellationRequested) return; if (!StartDevice()) return;//查看設備是否已運行 readADInternal.Wait(source.Token);//先等待,有數據傳遞觸發BufferedReady后再開啟 if (dataQueue.Count > 1000) dataQueue.Clear(); dataQueue.Enqueue(data); } }, source.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); StartWriteReadTxt(); } public void StopBufferedAI() { bufferedCtrl.Stop(); //bufferedCtrl.Dispose(); StopWriteReadTxt(); } public double[] StartInstantAI() { instantAiCtrl = new InstantAiCtrl(); instantAiCtrl.SelectedDevice = new DeviceInformation(deviceCode); if (!instantAiCtrl.Initialized) { throw new Exception("No device be selected or device open failed!"); } double[] data=new double[4] ; ErrorCode er0 = instantAiCtrl.Read(0,out data[0]); ErrorCode er1 = instantAiCtrl.Read(1, out data[1]); ErrorCode er2 = instantAiCtrl.Read(2, out data[2]); ErrorCode er3 = instantAiCtrl.Read(3, out data[3]); return data; } public void StopInstantAI() { if (instantAiCtrl.State == ControlState.Running) { instantAiCtrl.Dispose(); ; } } private bool StartDevice() { ErrorCode ret; if (bufferedCtrl.State != ControlState.Running) { readADInternal.Reset(); ret = bufferedCtrl.Start(); //Thread.Sleep(500); if (ret != ErrorCode.Success) { //log.ErrorFormat("Failed to start PCI1714 first time! ErrorCode is {0}.", ret); ret = bufferedCtrl.Stop(); if (ret != ErrorCode.Success) { //log.ErrorFormat("Failed to stop PCI1714! ErrorCode is {0}.", ret); } Thread.Sleep(500); ret = bufferedCtrl.Start(); if (ret != ErrorCode.Success) { //log.ErrorFormat("Failed to start PCI1714 second time! ErrorCode is {0}.", ret); return false; } } } return true; } public void BufferedReady(object e,BfdAiEventArgs b) { short[] data = new short[b.Count]; ErrorCode ret = bufferedCtrl.GetData(b.Count, data);//data存放的是從采集卡得到的數據 if (ret != ErrorCode.Success) { lock (locker) { this.data = null; readADInternal.Set(); } } lock (locker) { this.data = data.Select(o => (ushort)o).ToArray(); readADInternal.Set(); } } } }
在這個類里,先繼承上面的AnalogCard,再設置一些板卡信息。並提供兩種方式的數據讀取:StartBufferedAI,StartInstantAI。
主要說明StartBufferedAI:
在這里需要再創建一個線程readADTask,用於將采集卡數據讀取進內存Queue,同樣現將該線程堵塞等待開啟。定義一個BufferedReady事件,並進行綁定:bufferedCtrl.DataReady += new EventHandler<BfdAiEventArgs>(BufferedReady);這樣采集卡有數據后會自動觸發該事件,在該事件中將采集卡數據讀取出來。
3. 桌面數據顯示
參考研華的數據采集樣式,根據數據實時畫出數據曲線圖。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Windows.Forms.DataVisualization.Charting; using Automation.BDaq; namespace AdvantechPCIDemo { public partial class FrmMain : Form { private ushort[][] us;//接受PCI發生來的數據 private string deviceName = "PCI-1714UL,BID#13"; public FrmMain() { InitializeComponent(); OperateCustomBtn(false); DrawGrid(this.pbBuffCh1); DrawGrid(this.pbBuffCh2); DrawGrid(this.pbBuffCh3); DrawGrid(this.pbBuffCh4); DrawGrid(this.pbIstantCh1); DrawGrid(this.pbIstantCh2); DrawGrid(this.pbIstantCh3); DrawGrid(this.pbIstantCh4); } #region 數據列表定義 private List<Point[]> pointFromBuffAI = new List<Point[]>(); private List<double[]> dataFromBuffAI = new List<double[]>(); private List<Point[]> pointFromInstantAI = new List<Point[]>(); private List<double[]> dataFromInstantAI = new List<double[]>(); #endregion #region 畫網格和曲線 /// <summary> /// 畫網格 /// </summary> /// <param name="pb"></param> private void DrawGrid(PictureBox pb) { int width = pb.Width, height = pb.Height; int cellWidth = height / 4; Color color = Color.Green;//綠色線條 Bitmap bp = new Bitmap(width, height); Graphics objG = Graphics.FromImage(bp); objG.FillRectangle(new SolidBrush(Color.Black), 0, 0, width, height);//黑色背景 //網格列 for (int i = 0; i < width; i += cellWidth) { objG.DrawLine(new Pen(new SolidBrush(color)), i + cellWidth, 0, i + cellWidth, height); } //網格行 for (int i = 0; i < width; i += cellWidth) { objG.DrawLine(new Pen(new SolidBrush(color)), 0, i + cellWidth, width, i + cellWidth); } pb.Image = bp; } /// <summary> /// 畫動態曲線 /// </summary> /// <param name="channelIndex"></param> /// <param name="pb"></param> private void DrawBufferedAICurve() { for (int channel = 0; channel < 4; channel++) { PictureBox pbTemp = null; switch (channel) { case 0: pbTemp = this.pbBuffCh1; break; case 1: pbTemp = this.pbBuffCh2; break; case 2: pbTemp = this.pbBuffCh3; break; case 3: pbTemp = this.pbBuffCh4; break; } int width = pbTemp.Width, height = pbTemp.Height; if (pointFromBuffAI.Count == 0) { for (int i = 0; i < 4; i++) { pointFromBuffAI.Add(new Point[width]); } } lock (locker) { while (dataFromBuffAI.Count >= width + 2) { dataFromBuffAI.RemoveAt(0); } } int count = dataFromBuffAI.Count; if (count != 0) { pointFromBuffAI[channel] = new Point[count]; } for (int i = 0; i < width + 1; i++) { if (i >= count) break; if (dataFromBuffAI[i] == null) continue; pointFromBuffAI[channel][i] = new Point(i, (int)(height - ((dataFromBuffAI[i][channel] + 5) * height) / 10));//從負到正 //prData[channelIndex][i] = new Point(i, (int)(((Math.Abs(showData[i][channelIndex])) * height) / 10));//全負數 } pbTemp.Refresh(); } } private void ClearBufferedAICurve() { Graphics g = Graphics.FromImage(this.pbBuffCh1.Image); g.Clear(this.pbBuffCh1.BackColor); //DrawGrid(this.pbBuffCh1); //DrawGrid(this.pbBuffCh2); //DrawGrid(this.pbBuffCh3); //DrawGrid(this.pbBuffCh4); } private void DrawInstantAICurve() { for (int channel = 0; channel < 4; channel++) { PictureBox pbTemp = null; switch (channel) { case 0: pbTemp = this.pbIstantCh1; break; case 1: pbTemp = this.pbIstantCh2; break; case 2: pbTemp = this.pbIstantCh3; break; case 3: pbTemp = this.pbIstantCh4; break; } int width = pbTemp.Width, height = pbTemp.Height; if (pointFromInstantAI.Count == 0) { for (int i = 0; i < 4; i++) { pointFromInstantAI.Add(new Point[width]); } } lock (locker) { while (dataFromInstantAI.Count >= width + 2) { dataFromInstantAI.RemoveAt(0); } } int count = dataFromInstantAI.Count; if (count != 0) { pointFromInstantAI[channel] = new Point[count]; } for (int i = 0; i < width + 1; i++) { if (i >= count) break; if (dataFromInstantAI[i] == null) continue; pointFromInstantAI[channel][i] = new Point(i, (int)(height - ((dataFromInstantAI[i][channel] + 5) * height) / 10));//從負到正 //prData[channelIndex][i] = new Point(i, (int)(((Math.Abs(showData[i][channelIndex])) * height) / 10));//全負數 } pbTemp.Refresh(); } } private List<int> data = new List<int>(); private Pen redPen = new Pen(Color.Red, 1); private void pBCh_Paint(object sender, PaintEventArgs e) { PictureBox pb = (PictureBox)sender; string pbName = pb.Name; if (pbName.Contains("pbBuffCh")) { if (pointFromBuffAI.Count == 4) { switch (pbName) { case "pbBuffCh1": if (pointFromBuffAI[0].Count() < 3) break;//畫曲線至少需要三個點 e.Graphics.DrawCurve(redPen, pointFromBuffAI[0]); break; case "pbBuffCh2": if (pointFromBuffAI[1].Count() < 3) break; e.Graphics.DrawCurve(redPen, pointFromBuffAI[1]); break; case "pbBuffCh3": if (pointFromBuffAI[2].Count() < 3) break; e.Graphics.DrawCurve(redPen, pointFromBuffAI[2]); break; case "pbBuffCh4": if (pointFromBuffAI[3].Count() < 3) break; e.Graphics.DrawCurve(redPen, pointFromBuffAI[3]); break; } } } else if (pbName.Contains("pbIstantCh")) { if (pointFromInstantAI.Count == 4) { switch (pbName) { case "pbIstantCh1": if (pointFromInstantAI[0].Count() < 3) break;//畫曲線至少需要三個點 e.Graphics.DrawCurve(redPen, pointFromInstantAI[0]); break; case "pbIstantCh2": if (pointFromInstantAI[1].Count() < 3) break; e.Graphics.DrawCurve(redPen, pointFromInstantAI[1]); break; case "pbIstantCh3": if (pointFromInstantAI[2].Count() < 3) break; e.Graphics.DrawCurve(redPen, pointFromInstantAI[2]); break; case "pbIstantCh4": if (pointFromInstantAI[3].Count() < 3) break; e.Graphics.DrawCurve(redPen, pointFromInstantAI[3]); break; } } } } #endregion #region 界面按鈕控制 private void OperateCustomBtn(bool b) { this.btnCustomBufferedAI.Enabled = b; this.btnCustomInstantAI.Enabled = b; } private void OperateCtrBtn(bool b) { this.btnControlInstantAI.Enabled = b; this.btnControlBufferedAI.Enabled = b; } #endregion #region 自定義BufferedAI PCI1714UL objPCI1714 = null; private void btnConfirm_Click(object sender, EventArgs e) { try { objPCI1714 = new PCI1714UL(deviceName); OperateCustomBtn(true); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnStartRead_Click(object sender, EventArgs e) { try { //ClearBufferedAICurve(); objPCI1714.StartBufferedAI(4, ChannelDataAction); OperateCtrBtn(false); OperateCustomBtn(false); bufferedAITimer.Start(); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnStopCusBuffAI_Click(object sender, EventArgs e) { try { //if (objPCI1714 != null) //{ // objPCI1714.StopBufferedAI(); // //objPCI1714 = null; // bufferedAITimer.Stop(); // OperateCustomBtn(true); // OperateCtrBtn(true); //} } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void ChannelDataAction(ushort[][] us)//動作方法:接受與處理數據,並交給數據展示列表 { this.us = us; if (us == null || us[0] == null) return; dataFromBuffAI.Add(this.us.Select(u1 => { return GetVoltage(u1.Select(u => (double)u).ToList().Average()); }).ToArray()); } private double GetVoltage(double data) { return data * 10 / 4095 - 5.0190516943459; } private void customBufferedAI_Tick(object sender, EventArgs e) { DrawBufferedAICurve(); } #endregion #region 自定義InstantAI private void btnCustomInstantAI_Click(object sender, EventArgs e) { try { customInstantAI.Start(); OperateCustomBtn(false); OperateCtrBtn(false); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnStopCusInstantAI_Click(object sender, EventArgs e) { try { //if (objPCI1714 != null) //{ // objPCI1714.StopInstantAI(); // objPCI1714 = null; // customInstantAI.Stop(); //} } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void customInstantAI_Tick(object sender, EventArgs e) { dataFromInstantAI.Add(objPCI1714.StartInstantAI()); DrawInstantAICurve(); } #endregion #region 控件BufferedAI //private double[] d; private void btnControlBufferedAI_Click(object sender, EventArgs e) { try { waveformAiCtrl1.SelectedDevice = new DeviceInformation(deviceName); if (!waveformAiCtrl1.Initialized) { MessageBox.Show("No device be selected or device open failed!"); return; } int channelCount = waveformAiCtrl1.Conversion.ChannelCount; int sectionLength = waveformAiCtrl1.Record.SectionLength; //d = new double[channelCount*sectionLength]; waveformAiCtrl1.Prepare(); ErrorCode er=ErrorCode.Success; if (waveformAiCtrl1.State != ControlState.Running) { er = waveformAiCtrl1.Start(); } if (er != ErrorCode.Success) { MessageBox.Show("設備打開失敗!"); return; } OperateCtrBtn(false); OperateCustomBtn(false); bufferedAITimer.Start(); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnStopCtrBuffAI_Click(object sender, EventArgs e) { //if (waveformAiCtrl1.State == ControlState.Idle) //{ // return;//has been disposed //} //waveformAiCtrl1.Stop(); //waveformAiCtrl1.Dispose(); } private object locker = new object(); private void waveformAiCtrl1_DataReady(object sender, BfdAiEventArgs e) { double[] data = new double[e.Count]; ErrorCode er = waveformAiCtrl1.GetData(e.Count, data); if (er != ErrorCode.Success) { return; } lock (locker) { for (int i = 0; i < data.Length; i += 4) { double[] temp = new double[4]; for (int j = 0; j < 4; j++) { temp[j] = data[i + j]; } dataFromBuffAI.Add(temp); } } } #endregion #region 控件InstantAI private void btnControlInstantAI_Click(object sender, EventArgs e) { try { instantAiCtrl1.SelectedDevice = new DeviceInformation(deviceName); if (!instantAiCtrl1.Initialized) { MessageBox.Show("No device be selected or device open failed!"); return; } OperateCtrBtn(false); OperateCustomBtn(false); ctrInstant.Start(); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnStopCtrInstantAI_Click(object sender, EventArgs e) { try { //ctrInstant.Stop(); } catch (Exception ex) { MessageBox.Show( ex.Message); } } private void ctrInstant_Tick(object sender, EventArgs e) { double[] data = new double[4]; ; ErrorCode er0 = instantAiCtrl1.Read(0, out data[0]); ErrorCode er1 = instantAiCtrl1.Read(1, out data[1]); ErrorCode er2 = instantAiCtrl1.Read(2, out data[2]); ErrorCode er3 = instantAiCtrl1.Read(3, out data[3]); dataFromInstantAI.Add(data); DrawInstantAICurve(); } #endregion } }
樣式如下圖所示:

歡迎各位留言討論!
