控制卡應用編程技巧幾招

 聲明一下,寫下這些編程技巧,即不是什麼祖傳祕籍,也不是什麼必殺招或絕招,在此只爲方便同仁們在編程控制軟件時,對此可以進行適當的斟酌。以下展現的編程思想及奉上的源代碼都非常簡易,但並不是隨手寫寫,可都是經過實踐的。若沒有成功經驗作後盾,我也就沒有必要在此打字練五筆了。
    事實上,正如一個編程大師所言(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;
   
    實在沒有必要再詳說下去了。 
   

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章