最近要用一个采集卡,有的阿尔泰PCI2300。看它的软件使用说明时有一个疑惑,使用说明如下:
与ISA、USB设备同理,使用子线程跟踪AD转换进度,并进行数据采集是保持高速数据采集与处理的最佳方案。但是与ISA总线设备不同的是,PCI设备在这里不使用动态指针去同步AD转换进度,因为ISA设备环形内存池的动态指针操作是一种软件化的同步,而PCI设备不再有软件化的同步,而完全由硬件和驱动程序自动完成。这样一来,用户要用程序方式实现大容量数据采集,其软件实现就显得极为容易。每次用ReadDevBulkAD函数读取AD数据时,那么设备驱动程序会按照AD转换进度将AD数据一一放进用户数据缓冲区,当完成该次所指定的点数时,它便会返回,当您再次用这个函数读取数据时,它会接着上一次的新的通道位置传递数据到用户数据缓冲区。
但是由于我们的设备是通常工作在一个单CPU多任务的环境中,由于任务之间的调度切换非常平凡,特别是当用户移动窗口、或弹出对话框等,则会使当前线程猛地花掉大量的时间去处理这些图形操作,因此如果处理不当,则将无法实现高速采集,那么如何更好的克服这些问题呢?用子线程则是必须的(在这里我们称之为数据采集线程),但这还不够,必须要求这个线程是绝对的工作者线程,即这个线程在正常采集中不能有任何窗口等图形操作。只有这样,当用户进行任何窗口操作时,这个线程才不会被堵塞,因此可以保证正常高速的数据采集。但是用户可能要问,不能进行任何窗口操作,那么我如何将采集的数据显示在屏幕上呢?其实很简单,再开辟一个子线程,我们称之数据处理线程,也叫用户界面线程。最初,数据处理线程不做任何工作,而是在Win32 API函数WaitForSingleObject的作用下进入睡眠状态,此时它不消耗CPU任何时间,即可保证其他线程代码有充分的运行机会(这里当然主要指数据采集线程),当数据采集线程取得指定长度的数据到用户空间时,则再用Win32 API函数SetEvent将指定事件消息发送给数据处理线程,则数据处理线程即刻恢复运行状态,迅速对这批数据进行处理,如计算、在窗口绘制波形、存盘等操作。
可能用户还要问,既然数据处理线程是非工作者线程,那么如果用户移动窗口等操作堵塞了该线程,而数据采集线程则在不停地采集数据,那数据处理线程难道不会因此而丢失数据采集线程发来的某一段数据吗?如果不另加处理,这个情况肯定有发生的可能。但是,我们采用了一级缓冲队列和二级缓冲队列的设计方案,足以避免这个问题。即假设数据采集线程每一次从设备上取出8K数据,那么我们就创建一个缓冲队列,在用户程序中最简单的办法就是开辟一个两维数组如InUserRegion[Count][DataLen], 我们将DataLen视为数据采集线程每次采集的数据长度,Count则为缓冲队列的成员个数。您应根据您的计算机物理内存大小和总体使用情况来设定这个数。假如我们设成32,则这个缓冲队列实际上就是数组InUserRegion[32][8192]的形式。那么如何使用这个缓冲队列呢?方法很简单,它跟 一个普通的缓冲区如一维数组差不多,唯一不同是,两个线程首先要通过改变Count字段的值,即这个下标Index的值来填充和引用由Index下标指向某一段DataLen长度的数据缓冲区。需要注意的两个线程不共用一个Index下标变量。具体情况是当数据采集线程在AD部件被InitDeviceProAD或InitDeviceIntAD初始化之后,首次采集数据时,则将自己的ReadIndex下标置为0,即用第一个缓冲区采集AD数据。当采集完后,则向数据处理线程发送消息,且两个线程的公共变量SegmentCounts加1,(注意SegmentCounts变量是用于记录当前时刻缓冲队列中有多少个已被数据采集线程使用了,但是却没被数据处理线程处理掉的缓冲区数量。)然后再接着将ReadIndex偏移至1,再用第二个缓冲区采集数据。再将SegmentCounts加1,至到ReadIndex等于31为止,然后再回到0位置,重新开始。而数据处理线程则在每次接受到消息时判断有多少由于自已被堵塞而没有被处理的缓冲区个数,然后逐一进行处理,最后再从SegmentCounts变量中减去在所接受到的当前事件下所处理的缓冲区个数,具体处理哪个缓冲区由CurrentIndex指向。因此,即便应用程序突然很忙,使数据处理线程没有时间处理已到来的数据,但是由于缓冲区队列的缓冲作用,可以让数据采集线程先将数据连续缓存在这个区域中,由于这个缓冲区可以设计得比较大,因此可以缓冲很大的时间,这样即便是数据处理线程由于系统的偶而繁忙面被堵塞,也很难使数据丢失。而且通过这种方案,用户还可以在数据采集线程中对SegmentCounts加以判断,观察其值是否大小了32,如果大于,则缓冲区队列肯定因数据处理采集的过度繁忙而被溢出,如果溢出即可报警。因此具有强大的容错处理。
这是VC代码
PPCI2300_PARA_AD Para;
HANDLE hDevice;
CwinThread *m_ReadThread;
ULONG DiskFreeWords;
#define MAX_SEGMENT 32
WORD InUserRegion[MAX_SEGMENT][8192]; // 缓冲队列
BOOL bDeviceRun;
int SegmentCounts=0; // 记录未处理缓冲区个数
// 该函数创建子线程对象,初始化文件对象,创建设备对象,并启动线程
void UserFunction(void)
{
DWORD dwErrorCode;
m_ReadThread=AfxBeginThread( // 创建子线程
ReadThread, // 子线程函数
&m_hWnd,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
m_ReadThread->m_bAutoDelete=FALSE; // 不让子线程结束后自动删除
DiskFreeWords=GetDiskFreeBytes("D:\\")/2; // 获取目标盘D盘剩余容量
hDevice = CreateDevice(0);
// 手工设置硬件参数
Para.FirstChannel=0;
Para.LastChannel=31;
if(!CreateFileObject(hDevice, "data.dat", 0, modeCreate | modeWrite)) // 初始化设备文件对象0
{ // 创建新文件,且指定为只写文件方式
MessageBox("初始化文件对象失败...");
return;
}
bDeviceRun=TRUE; // 线程可以运行
m_ReadThread->ResumeThread(); // 启动子线程
}
//=========================================================
UINT ReadThread(PVOID hWnd) // 读数据线程
{
CsysApp *pApp=(CsysApp *)AfxGetApp();
CshowDataDoc* pDoc=pApp->pShowDataDoc;
BOOL bFirst=TRUE;
ULONG Wrote8KWCounter=0;
ULONG WroteMB=0;
Pdoc->m_Wrote8KWCounter=0;
Pdoc->m_WroteMB=0;
DeviceID = pApp->m_CurrentDeviceID; // 取得当前应用程序使用的设备ID号
HDevice =PCI2300_CreateDevice(DeviceID);
If(hDevice==INVALID_HANDLE_VALUE)
{
AfxMessageBox("创建设备对象失败...",MB_ICONSTOP,0);
return 0;
}
bCreateDevice = TRUE;
// 复位段索引号
ReadIndex = 0;
SegmentCounts = 0;
While(bDeviceRun) // 循环采集AD数据
{
if(bBunch!=FALSE) continue; // 如果串道,则不进行数据采集
// 从设备上读取8192个字的数据
if(!PCI2300_ReadDevBulk(hDevice, InUserRegion[ReadIndex], 8192)) {
AfxMessageBox("读数据出错...",MB_ICONSTOP);
return FALSE;
}
// 建议:不要在数据采集线程中操作窗口,因为窗口操作(如放大、
// 缩小、重绘)会花掉CPU很多时间,处理不当,会引起采集线程
// 的严重堵塞,使数据被丢掉。详细情况请参考
// 软件说明书,在那里会告诉您很多解决方案。
// 发送事件,告诉绘制窗口线程,该批数据已采集完毕
SetEvent(hEvent); // 发送消息给数据处理线程
ReadIndex++; // 使下标指向下一个缓冲区
if(ReadIndex==MAX_SEGMENT) ReadIndex=0;
SegmentCounts++; // 每采集一个片段数据,则增加计数
// 如果您需要容错处理,防止缓冲区溢出,数据丢失,请加入下列代码
/*
if(SegmentCounts>MAX_SEGMENT) // 如果溢出
{
AfxMessageBox("系统很忙,整个缓冲区已满,\
建议您在数据采集时,\
关闭所有不必要的应用程序...",MB_ICONSTOP);
goto ExitReadThread;
}*/
}// 线程循环取样
goto ExitReadThread;
ExitReadThread:
if(!PCI2300_ReleaseDevice(hDevice)) // 关闭设备
{
AfxMessageBox("关闭设备失败...",MB_ICONSTOP,0);
}
bCreateDevice = FALSE;
return TRUE;
}
UINT DrawWindowProc(PVOID hWnd) // 绘制数据线程
{
CSysApp *pApp=(CSysApp *)AfxGetApp();
CShowDataDoc* pDoc=pApp->pShowDataDoc;
BOOL bFirst=TRUE;
ULONG Wrote8KWCounter=0;
ULONG WroteMB=0;
pDoc->m_Wrote8KWCounter=0;
pDoc->m_WroteMB=0;
int i, j;
CurrentIndex=0;
while(bDeviceRun) // 循环采集AD数据
{
WaitForSingleObject(hEvent, INFINITE);
j=SegmentCounts;
for(i=0; i<j; i++) // 处理采集线程累计采集的未处理的数据段
{
switch(nProcessMode) // 数据处理
{
case 1: // 数字显示
if(!m_FirstScreenStop || bFirst) // 如果不停止首屏显示
{
// 传递1,要求重绘数字视图
pDoc->UpdateAllViews(pDigitView, 1, NULL);
m_bProgress=TRUE; // 使OnDraw函数能更新进度条
bFirst=FALSE; // 置不是第一次采集的标志
}
break;
case 2: // 波形显示
if(!m_FirstScreenStop|| bFirst)
{
// 传递2,要求重绘波形视图
pDoc->UpdateAllViews(pWaveView, 2, NULL);
m_bProgress=TRUE; // 使OnDraw函数能更新进度条
bFirst=FALSE;
}
break;
case 3: // 数据存盘
// 关于ID号为0的文件对象的初始化请见菜单
// 《新建数据文件》的代码
// 将8192个字的数据存放在硬盘上
PCI2300_WriteDataFile(hDevice, InUserRegion[CurrentIndex], \
16384, 0);
if(WroteMB>=(pDoc->m_RemainMB-4))
{
AfxMessageBox("对不起,当前磁盘已满,\
不能再存盘",MB_ICONSTOP,0);
return FALSE;
}
// 递加写入8K数据的计数器,以便存盘视图推进存盘进度条
pDoc->m_Wrote8KWCounter++;
// 传递3,要求重绘存盘视图子控件
pDoc->UpdateAllViews(pSaveView, 3, NULL);
break;
}
CurrentIndex++;
if(CurrentIndex==MAX_SEGMENT) CurrentIndex=0;
}
// 从未处理缓冲区计数中减去刚被处理的缓冲区个数
SegmentCounts=SegmentCounts-j;
if(SegmentCounts<0) SegmentCounts=0;
}// 线程循环取样
return TRUE;
}
这是VB代码
Visual Basic:
Public bDeviceRun As Boolean
Public hDevice As Long
Public hEvent As Long
Public Const MAX_SEGMENT = 32
Public InUserRegion(65536, MAX_SEGMENT) As Integer '建立二维数组(即缓冲级链),用于循环交替接受AD
'数据,以避免内存的重叠和重复拷贝,从而提高数据
'采集和处理效率
Public hCollectDataThread As Long
Public Status As Boolean
Public DeviceID As Long
Public bCreateDevice As Boolean
Public Para As PCI2300_PARA_AD
Public ProcessMode As Long '数据采集方式 1:数字方式 2:图形方式
Public Const DigitMode = 1
Public Const WaveMode = 2
Public Flag(32) As Boolean '置32通道标志,用此标志确定该通道是否被刷新
Public SegmentCounts As Integer '用于统计当前段总数
Public CurrentIndex As Integer '数据处理时使用的当前缓冲索引
Public Sub DrawWaveProc()
Dim Status As Boolean
Para.FirstChannel = 0 ' 置首通道为0
Para.LastChannel = 31 ' 置末通道为31
Original = PerHeight / 2
Picture1.Cls
For Channel = 0 To ChannelCount - 1 ' 绘制所有有效通道数据
X = 0 '开始绘制每通道数据时,将屏幕X坐标定位在窗口的0位置
' 求出该通道第一个点的Y轴原始坐标OldY
OldY = Original - ((((InUserRegion(Channel, CurrentIndex) And &HFFF)) - 2048)) / middle1
For Index = 0 To 2048 Step ChannelCount '绘制该通道的所有数据
' 求出相对于OldY的第二个点的Y轴新坐标
NewY = Original - ((((InUserRegion(Channel + Index, CurrentIndex) And &HFFF)) - 2048)) / middle1 ' 根据Y轴新旧坐标绘制曲线
Picture1.Line (X, OldY)-(X + 1, NewY), RGB(255, 0, 0)
X = X + 1 '绘制完一个点后,将X轴向的坐标往右偏移一个象素位置
OldY = NewY ' 保存新坐标,以便做为下一个点的Y轴始坐标
Next Index ' 绘制完某个通道的数据
Original = Original + PerHeight ' 确定下一个通道的原点Y轴坐标
Next Channel
CurrentIndex=CurrentIndex + 1 '当该缓冲区的数据被处理完后,将索引指针下一个位置
If CurrentIndex = MAX_SEGMENT Then
CurrentIndex = 0 ' 如果已处理整个级链缓冲区的的末端,则将索引指针恢复至0,再重新开始
End If
End Sub
Public Sub ShowDigitProc()
Dim DigitString As String
Dim Channel As Integer
Dim PerLsbVolt As Single ' 根据设备的Bit位数,用于确定每个Lsb分配的电压值
PerLsbVolt = 10000# / 4096 ' 求出单位Lsb分配的电压值
For Channel = 0 To 31
If Flag(Channel) = True Then
DigitString = (Str$((InUserRegion(Channel, CurrentIndex) And &HFFF) * PerLsbVolt - 5000))
DstChannelText.Item(Channel).Text = DigitString
End If
Next Channel
CurrentIndex = CurrentIndex + 1 '当该缓冲区的数据被处理完后,将索引指针下一个位置
If CurrentIndex =MAX_SEGMENT Then
CurrentIndex = 0 ' 如果已处理整个级链缓冲区的的末端,则将索引指针恢复至0,再重新开始
End If
End Sub
Function CollectDataFunction() As Long '数据采集
Dim i As Long
Dim bStatus As Boolean
Dim ReadIndex As Integer ' 级链缓冲区的索引号
hEvent = INVALID_HANDLE_VALUE
hEvent = PCI2300_CreateSystemEvent() '创建系统内核事件对象,用于线程同步
If hEvent = INVALID_HANDLE_VALUE Then
MsgBox "创建设备对象失败..."
Exit Function
End If
DeviceID = 0 ' 设当前被操作的PCI设备只有一个
hDevice = PCI2300_CreateDevice(DeviceID) '创建设备对象
If hDevice = INVALID_HANDLE_VALUE Then
MsgBox "创建设备对象失败..."
Exit Function
End If
'初始化设备对象
Para.FirstChannel = 0
Para.LastChannel = 31
ReadIndex = 0 '使级链缓冲的指针指向第一个缓冲区(即0缓冲区)
SegmentCounts = 0
Do While (bDeviceRun) '循环采集AD数据
If Not PCI2300_ReadDeviBulkAD(hDevice, InUserRegion(0, ReadIndex), 8192, Para) Then
MsgBox "读数据出错..."
Exit Function
End If
SetEvent (hEvent)
ReadIndex = ReadIndex + 1 ' 当采集完一段指定长度的数据后,将缓冲区索引号下移一个位置
If ReadIndex = MAX_SEGMENT Then
ReadIndex = 0 ' 如果采集线程已完成了
End If
'SegmentCounts = SegmentCounts + 1
Loop
If Not PCI2300_ReleaseDevice(hDevice) Then '关闭设备
MsgBox "关闭设备失败..."
End If
CollectDataFuncion = 1
End Function
Function ProcessDataFunction() As Long '数据处理
Dim Status As Boolean
Dim i As Long
CurrentIndex = 0
Do While (bDeviceRun)
Status = WaitForSingleObject(hEvent, INFINITE)
Select Case ProcessMode
Case DigitMode
Call AD_Form.ShowDigitProc '=1:数字显示
Case WaveMode
Call AD_Form.DrawWaveProc '=2:图形显示
End Select
Loop
End Function
我的疑惑是VC里这样定义数组
#define MAX_SEGMENT 32
WORD InUserRegion[MAX_SEGMENT][8192]; // 缓冲队列
VB里却
Public Const MAX_SEGMENT = 32
Public InUserRegion(65536, MAX_SEGMENT) As Integer '建立二维数组(即缓冲级链),用于循环交替接受AD
'数据,以避免内存的重叠和重复拷贝,从而提高数据
'采集和处理效率
数组结构在内存分配不是都一样的吗?
PCI2300_ReadDeviBulkAD(hDevice, InUserRegion(0, ReadIndex), 8192, Para)
ReadIndex = ReadIndex + 1 ' 当采集完一段指定长度的数据后,将缓冲区索引号下移一个位置
这样能下移65536个字吗?我感觉怎只移一个字啊。是不是要这样定义数组啊
Public InUserRegion( MAX_SEGMENT,65536) As Integer