声明一下,写下这些编程技巧,即不是什么祖传秘籍,也不是什么必杀招或绝招,在此只为方便同仁们在编程控制软件时,对此可以进行适当的斟酌。以下展现的编程思想及奉上的源代码都非常简易,但并不是随手写写,可都是经过实践的。若没有成功经验作后盾,我也就没有必要在此打字练五笔了。
事实上,正如一个编程大师所言(Michael Abrash),当你的软件正常而且有效率的运行起来时,好像一切都是那么显而易见。故,在此,我仍坚持那句编程口号,将事情变得越简单越好,越简单就越有效率,越稳定。
在以下的介绍中,我将尽可能的展示本人的编程思想,最大可能的给出知其然也知其所然的解释。若你有更好的见解,希望能得到你的指正。人长大了明显标志就是变得不太负责,而且不敢承认自己还需要努力,害怕面对自己的错误。若是这样,放心,我还没长大。因为我无法保证我能面面俱到。
关于源代码的阅读,需要读者有一定的C++编程基础,至少对以下表示形式不会产生误解:
const char *pString; //指定pString邦定的数据不能被修改
char * const pString; //指定pString的地址不能被修改
const char * const pString; //含上面两种指定功能
当然,随便提醒一下,这些源代码若需要加入你的软件工程当中,还需要作一些调整和修改,因此,这些源代码实质上称为伪代码也可以,之所以展现它们,是让程序员们有个可视化的快感,特别是那些认为源代码就是一切的程序员。
同时,为了提高针对性,大部分控制卡调用的函数会明确指出是邦定哪些卡的,实际应用时,程序员可自行选择,以体现一下自己的智商是可以写写软件的。
一、 控制卡类的单一实例实现
把控制卡类作一个类来处理,几乎所有C++程序员都为举双手表示赞同,故第一个什么都没有的伪代码就此产生,如下表现:
class CCtrlCard
{
public:
…Function
public:
…attrib
}
于是,用这个CctrlCard可以产生n多个控制卡实例,只要内存足够。然而,针对现实世界,情况并不那么美好。通常情况下,PC机内只插同种类型的控制卡1到2张,在通过调用d1000_board_init或d3000_board_init函数时,它们会负责返回有效卡数nCards,然后从0-nCards*4 - 1自行按排好轴数。初始化函数就是C++的new或malloc的操作,取得系统的资源,但是控制卡的资源与内存不一样,取得资源后必需要释放才可以再次获取,即控制卡资源是唯一的。
既然控制卡资源是唯一的,那么最好Cctrlcard产生的实例也是唯一的,这样,我们可以方便的需要定义一个全局变量即可:
CctrlCard g_Dmcard;
在其它需要调用的地方,进行外部呼叫:
extern CctrlCard g_DmcCard;
以上方法实在太简单了,很多人都会开心起来。实质上,方法还有很多,即然可以产生n多对实例,我们的核心是只要保证调用board_init函数一次即可,故也可以单独定义一个InitBoard函数:
class CctrlCard
{
public:
static int InitBoard(); //定义一个静态函数,以表警示
}
int CctrlCard::InitBoard()
{
return d1000_board_init();
}
还有一种方法,情况稍加复杂,但表达的功能也要强一些,以下展现可以稍微安慰一下代码狂。
Class CctrlCard
{
public:
CctrlCard(); //请注意这个构造函数的定义
}
CctrlCard::CctrlCard()
{ //呵呵,也很明了
static int n(0); //注意,是个静态变量
n++; //每次调用CctrlCard生成实例时,都会计数一次
assert( n == 1 ); //在DEBUG版本下,只有n==1的情况下可以通过
//否则,会出现致命错误,还好,它会告诉你错在哪个文件,
//哪一行,呵呵,是个好东东啊。
}
通过强行报警处理,当你有g_DmcCard这个实例时,其它的所有控制卡的定义都只能是以引用或指针的方式进行了,不会再产生新有效的实例了,对于由小组编程的项目软件,而你又恰好负责编程控制卡这一块的话,以上的显性报警,会让其它人心领神会。当然,你也可以将上面的方法加入到InitBoard当中去,可以避你的无意识的多次调用了。
附:无意识的多次调用经常发生,特别是那些对MFC机制不明确的程序员,在多文档框架下,不知道这个CctrlCard::InitBoard函数到底是应该放在CmainFrame的OnCreate里面,还是应该放在CchildFrame的OnCreate,或者是Cview的OnInitUpdate里面进行调用。
在一言难尽MFC的情况下,我建议两个小方法:
No.1 将CctrlCard的函数置于Cmainframe的OnCreate或者Capp::Initstance内调用
No.2 将InitBoard函数稍加改造成这样:
Int CctrlCard::InitBoard()
{
static int n(-1000);//注意,-1000是控制卡函数不可能返回的值
if( n == -1000 )
n = d1000_board_init();
return n;//这样,即使多次调用也不样怕了,呵呵,雕虫小技也可以除虫啊
}
必须额外声明一下,我们不是不重视资源的释放,而是作为一个C++程序员写下这些代码是基本的义务(这也是我为什么要交待读者必须要有一定的C++基础):
class CctrlCard
{
public:
~CctrlCard()
{ //定义析构函数,在此释放资源,对此,我不想再转到读者的眼球了
d1000_board_close();
}
}
二、 数据结构及数据类型的定义,部分相关声明
调用控制卡驱动函数时,经常会有如下形式:
单轴相对运动 d1000_start_t_move( axis, pulse, start, speed, accel );
单轴绝对运动 d1000_start_ta_move( axis, pulse, start, speed, accel );
两轴相对插补 d1000_start_t_line( axisArray, distArray, start, speed, accel );
两轴绝对插补 d1000_start_ta_line( axisArray, distArray, start, speed, accel );
圆弧相对插补 d3000_start_t_arc( axisArray, C1, C2, E1,E2, dir, start, speed, accel );
圆弧绝对插补 d3000_start_ta_arc( axisArray, C1, C2, E1,E2, dir, start, speed, accel );
以上的调用,很多重复枯燥,又不直观,难于理解,并且在面向客户时,常常是指每分多少米,或者每秒多少毫米,很少有人问每秒多少脉冲,移动多少脉冲作距离,故需要单位之间的换算。显然,对于这些问题,我想,C++程序员应该找到用武之地了,所以我们一步一步来,慢慢统一各个问题。实质上,在以下的几个技巧,也需要在此澄清一些概念。
我们先来几个宏定义提高一下情绪:
# define MAX_AXIS 4 //最多轴数
# define XCH 0 //定义X轴的值
# define YCH 1
# define ZCH 2
# define UCH 3
…..(其它以次类推)
# define M_ABS 0x01 //定义一个绝对标志位
# define M_INP 0x02 //定义一个插补位
接下来深入一点点,再来几个结构定义:
typedef struct tag_ARC
{
tag_ARC( double ox=0.0, double oy=0.0, double ex=0.0, double ey=0.0, int dir=0 ):
ox(ox), oy(oy),
ex(ex), ey(ey),
dir(dir)//定义这样一个构造函数需要勇气,看似不合理,但是好用麻
{
}
double ox,oy;
double ex,ey;
int dir;
}ARC;
typedef struct tag_SPEED
{
tag_SPEED( double start=0.0, double speed=0.0, double accel=0.0, double decel=0.0,
double scc=0.0 ) :
start(start),
speed(speed),
accel(accel),
decel(decel),
scc(scc)
{
}
double start;
double speed;
double accel;
double decel;
double scc;
}SPEED;
以上两个ARC和SPEED的结构定义,把几个参数变成一个参数。比如要实现的单轴驱动函数,就变得非常明了:
void Move( int nAxis, double fMM, const SPEED &speed, int nFlag = M_ABS );//往后我们再具体完善其实现。
以上的结构具有类的特性,但是由于其每个成员都可以给外部直接使用,故就不需要什么类的public及其析构函数的定义了。之所以全都采用double的数据类型,是面向客户习惯及单位计算方便的。
接下来是对控制卡常用的单位计算及部分常用变量的声明:
class Cctrlcard
{
public:
…(其它略去)
public:
//属性
mutable int ORGIN; //指定原点状态位
mutable int LIMIT_A, LIMIT_B; //指定左右限位状态位
private:
//以下的属性不给外部访问的
struct tag_AXIS{//单轴属性
double fUnitPM; //脉冲当量
long nRP; //每转脉冲数
double fJourey; //行程
};
tag_AXIS m_axis[MAX_AXIS];
定义ORGIN,LIMIT_A, LIMIT_B为变量,是有两个意义:
No.1 当你访问它们的状态时,不需要每次调用d1000_get_axis_status函数,你可以这样:
Int nStatus = d1000_get_axis( XCH );
If( nStatus & g_DmcCard.ORGIN == g_dmcCard.ORGIN )
If( nStatus & g_DmcCard.LIMIT_A == g_DmcCard.LIMIT_A )
If( nStatus & g_DmcCard.LIMIT_B == g_DmcCard.LIMIT_B );
No.2 你可以扩展不同的卡,当外部调用的程序逻辑已被确定时,当你需要从DMC1000控制卡升级到DMC3000控制卡时,只需要给ORGIN等状态位指定不同的值即可。指定状态位的值也有一个小小的技巧,以ORGIN为例,在DMC1000控制卡,其位值在2位,则可以这样:
ORGIN = 1<<2;
在DMC3000控制卡,其值在第9位,则这样:
ORGIN = 1<<9;
方法都很简单,关键是要想得到。
对于tag_AXIS定义,引出几个函数的声明,专门为其服务:
void SetUP( nit nAxis, double fMM, double nPulse, double fMax );//设定当量
double P2M ( int nAxis, long nPulse ); //脉冲转成毫米 pulse to metric
long M2P( int nAxis, double fMM ); //毫米转成脉冲 mitric to pulse
现在,我们再回过头来完成Move函数的实现,以便获得一点点成就感,同时也展示一下以上的大堆表述是有其意义的。
void Move( int nAxis, double fMM, const SPEED &speed, int nFlag = M_ABS )
{
( nFlag & M_ABS == M_ABS ) ?
d1000_start_ta_move( nAxis, //绝对
M2P( nAxis, fMM),
M2P( nAxis, speed.start ),
M2P( nAxis, speed.speed),
Speed.accel ): //注意是冒号,?:是一个表达式
d1000_start_t_move( nAxis, //相对
M2P( nAxis, fMM),
M2P( nAxis, speed.start ),
M2P( nAxis, speed.speed),
Speed.accel );
}
是不是很简单呢,当外部调用时,客户的观念就直接面对Metric即可,如:
Move( XCH, 10.0, SPEED(5,10,0.1), M_ABS );//达到绝对位置10.0毫米处。
三、 插补和联动函数
当程序员决定需要几轴进行插补时,尽量选择最大插补轴数,如在雕铣系统时,有时会用到两轴插补,有时会进行三轴插补,在这个基础上,为简化编程,我的理论只使用三轴插补,当需要进行两轴插补或联动时,根据相对或绝对的坐标关系,将不运动轴填入0偏移或绝对位置即可。
以下为XYZ三轴联动和插补的函数,由nFlag的M_INP位决定是否进行插补:
void MoveXYZ( double fX, double fY, double fZ, const tag_SPEED &speed,
int nFlag = M_ABS )
{
short axisArray[]={ XCH, YCH, ZCH };
if( nFlag & M_INP == M_INP )
{//插补
long distArray[]={ M2P(XCH, fX), M2P(YCH,fY), M2P(ZCH,fZ) };
long nStart, nSpeed;//计算新的矢量速度,参见DMC1000矢量速度的计算
(…矢量速度计算在此略去)
( nFlag & M_ABS == M_ABS ) ?
d1000_start_ta_line( 3, axisArray, nStart, nSpeed, speed,accel )://绝对
d1000_start_t_line(3, axisArray, nStart, nSpeed, accel );//相对
}
else
{ //联动
double fpos[]={ fX, fY, fZ};
for( int I(0); I<3; I++)//发三次单轴移动命令
Move( axisArray[I], fpos[I], speed, nFlag );
}
}
在我给出的DMC3000控制卡类完整源代码一文中,有其更完善的版本。通过以下的函数封装,将插补和联动,绝对位置,相对位置等等都很好的整合在一起,用户在使用起来具体更准确的目标。
四、 驱动轴状态、位置读取和设定
对于驱动轴的状态,分为两种:1、指脉冲输出状态;2、指专用输入信号电平状态
检测脉冲输出是否完成,可以写成如下函数,假设软件总共只用到XYZ三轴:
int IsRunning( int nAxis = -1 )//默认为-1是有目的的
{
if( nAxis != -1 )
return d1000_check_done( nAxis ) == 0 ;
//当nAxis == -1时,检测三个轴是否有一个在运行,这种检测在加工时常用
return d1000_check_done( XCH ) == 0 ||
d1000_check_done( YCH ) == 0 ||
d1000_check_done( ZCH ) == 0;
}
当用户等待YCH脉冲发完,则用一个循环检测即可:
while( g_DmcCard.IsRuning( YCH ) ) ::DoEvents();
别忘了,IsRuning是CctrlCard的成员函数,而DoEvents函数在DMC1000不能响应系统消息的文章中有详细实现和功能描述。
在实际加工时,作插补时,常需要等待上次所有运动结束才开始新的运动。故有如下表现:
for( int I(0),step(0); I
{
DoEvents();
switch( m_nworkStatus ){
case Pause:
continue;
case Continue: m_nWorkStatus = Running;
case Running:
{
switch( step ){
case 0:
if( IsRunning() ) break;//检测所有运动结束,否则继续检测
MoveXYZ( data[I].x, data[I].y, data[I].z …… );
Step ++;
Break;
case 1:
if( IsRunning() ) break;//同上
i++; //准备下一段数据,之所以放在此处,是需要考虑在运行过程中,有外部的暂停和继续操作。
step = 0;//准备运行新的数据
break;
}
} break;
}
以上程序框架,有着非常广阔的应用前景,非常简单,可以让程序员随意控制,故而它又非常稳定,比起线程的操作,它具体非常透明的可操作性。 此框架在雕刻,焊接,切割等许多场合都将成为经典,当然,若你不曾深入了解它,则不会发现它的可爱之处。
对于专用输入信号状态的检测,几乎没有什么特别之处:
int GetStatus( int nAxis )
{
return d1000_get_axis_status( nAxis );
}
位置的读取和设定,对于DMC1000比较容易,故在此我将写出DMC3000控制卡的这两个函数,当然用于DMC1000也是没问题的。
DMC3000控制卡的位置分为指令位置和物理位置(编码器反馈的),所以函数需要有一个小小的选择,先看看位置获取函数:
Double GetPosition( int nAxis, BOOL bCmd = true )// bCmd == true时,读取指令位置,否则为物理位置
{
long pulse = (bCmd == true ) ?
d3000_get_command_pos( nAxis ):
d3000_get_encoder_pos(nAxis);
return P2M( nAxis, pulse );//脉冲转成毫米然后返回
}
位置设定函数多了一点点动作:
double SetPosition( int nAxis, double fMM, BOOL bCmd = true )
{
double pos = GetPosition( nAxis, bCmd );//先取得原来的位置
( bCmd == true )?
D3000_set_command_pos( nAxis, M2P(nAxis, fMM )):
D3000_set_encoder_pos( nAxis, M2P(nAxis, fMM) );
Return pos;//返回旧的位置
}
为什么这样设计?当你用过CPen *pOldPen= pDC->SelectObject( &newPen );时,或者除了复位之外,你真正需要调用这个SetPosition函数时,你会发现这个设计,真是人情味实足。
五、 复位,相对与绝对,
在如今PC机开发控制卡软件时代,设备上电不复位的几乎没有,在此谈到复位这个问题确实有必要,实现上,复位动作因不同设备的工艺要求而定,故一般而言,控制卡提供的那个复位函数太过简单,有点力不从心,所以,本人自己写了个复位函数,但是代码写起来将会占用很大的面版,故有此需要者,可以来电或E_mail索取。
其基本思路是采用两次找原点,第一次高速找,停止后退出,再次以较低的速度找原点。并且在执行第二次复位时,会在离原点5毫米处减速(第一次执行做不到)。
提供相对和绝对位置的概念是很有必要的,众所周知,现在控制卡能作到最小单位为1个脉冲,当然,作为数字脉冲,到此已不能再小了,故为了提高精度,通常情况下要提高计算当量,即增加每转脉冲数,或减少每转毫米数。
不论怎么,我们将问题放大并明朗化,可以看看以下片段:
for( int I(0); I<10000; I++)// 走10000次
move( 0.5 );//走相对0.5个脉冲的距离
结果是:1个脉冲也发不出,造成很大的累积误差。
若换成绝对方式:
for( int I(0); I<10000; I++)
goto( I*0.5 );
最后的误差,最大也就是1个脉冲以内。虽然还是有误差,但总算达到可容忍的程序,再加上适当的复位操作,让客户至少不必再担心这个巨大的累积误差了。
实质上,在整个软件设计时最好采用绝对坐标系,即使要处理加工原点或工面起点等这些参数,也要把它换算成绝对位置,唯手动移动设备可以例外。另外,在CNC系统中,除了有循环用到相对坐标系,其余都是用绝对坐标系为上策,实际上,在实现编程算法上,为统一起见,最好将相对的坐标关系全部转成绝对的坐标关系,这样也便于外部进行暂停或继续的处理。
相信,到此为止,若你的设备在加工时有一定的误差漂移,你会意识到自己应该是不是要检查一下采用了什么坐标系了吧。
六、 输出输入及软限位
对于通用的I/O操作,没有什么特别要说明的,只有两点需要注意的,先给出两个小函数,以作参考:
int ReadBit( int nIO ); //读指定通用输入口的电平状态,返回1 或 0
int WriteBit( int nIO, int nStatus ); // 输出电平到指定输出端口
两点注意:
No.1 对于ReadBit若需要加入抗干扰处理,则写一个函数:
Int RealInput( int nIO, int nStatus, int di=50 )
{
if( ReadBit( nIO ) != nStatus )
return 0;
while( di -- );//耗上几个CPU的周期时间,再读一次
return ReadBit( nIO ) == nStatus;
}
No.2 增加一个变量及函数扩展一下输出功能:
Long m_nOutStatus= 0x00000000;
再次改造一下WirteBit
void WriteBit( int nIO, int nStatus )
{
if( nStatus ){
m_nOutStatus |= (1<
}
else{
m_nOutStatus &= (~(1<
}
d1000_out_bit( nIO, nStatus );
}
添加的访问输出状态函数:
int ReadOutbit( int nIO )
{
static int a;
a = 1<<(nIO-1);
a &= m_nOutStatus;
return a!=0;
}
软限位的思想原本是用于为客户节省正负限位的光电开关成本而产生的,致使使用软件限位正常的话,设备每个驱动轴只需要一个原点开关即可。当然,软限位能正确运作是非常重要的,否则很容易撞坏设备。而其正确运行,就必须依赖正确的复位动作,以找到可靠的机械原点位置。
软件限位的基本算法非常简单,特别是在一个绝对坐标系当中。其原理如下:
if( pos < minPos ) pos = minPos;
if( pos > maxPos ) pos = maxPos;
实在没有必要再详说下去了。