PID控制器的數字實現及C語法講解

来源:http://www.cnblogs.com/cjq0301/archive/2016/02/10/5184808.html
-Advertisement-
Play Games

PID控制器的數字實現及C語法講解 概述 為方便學習與交流,根據自己的理解與經驗寫了這份教程,有錯誤之處請各位讀者予以指出,具體包含以下三部分內容: (1) PID數字化的推導過程(實質:微積分的近似計算); (2) 程式風格介紹(程式風格來源於TI官方案例); (3) C有關語法簡述(語法會結合實


PID控制器的數字實現及C語法講解

  • 概述  

  為方便學習與交流,根據自己的理解與經驗寫了這份教程,有錯誤之處請各位讀者予以指出,具體包含以下三部分內容:

  (1)  PID數字化的推導過程(實質:微積分的近似計算);

  (2)  程式風格介紹(程式風格來源於TI官方案例);

  (3)  C有關語法簡述(語法會結合實例進行講解)。


  • PID控制器的數字化

  PID控制器是工業過程式控制制中廣泛採用的一種控制器,其中,P、I、D分別為比例(Proportion)、積分(Integral)、微分(Differential)的簡寫;將偏差的比例、積分和微分通過線性組合構成控制量,用該控制量對受控對象進行控制,稱為PID演算法

  為了用軟體實現PID演算法,需將PID控制器離散化。

  1. 整體思路

        

  2. 方框圖

  PID控制器的方框圖如圖所示:

           

  3. 拉氏域的表達式

  根據方框圖,可寫出PID控制器對應的傳遞函數:

              

         其中,Kp為比例繫數,ki為積分繫數,Kd為微分繫數

 

  4. 時域的表達式

  在分析時,通常藉助於拉氏空間,例如判斷系統的穩定性與相對穩定性;而現在我們關心的是時域里的問題,因此對上式進行拉普拉斯逆變換,得到時域里的表達式:

             

       其對應的結構框圖如圖所示:

              

  5. 差分方程

  該時域里的表達式不便於編程處理,因此需對該式進行離散化處理,從而得到可編程實現的差分方程,分析過程如下:

  (說明:PID離散化的實質為微積分的離散化(數值化處理),由於這個推導過程很多教材上都有介紹,因而略去推導過程,只給出最終表達式,程式的演算法就是基於此表達式而寫的

  數字PID控制器的增量式演算法:

      

      其中,T為步長,即採樣周期(由微控制器的定時器確定

  u(kT)=u(k),便得到PID控制器增量式演算法的差分方程:

      

  這樣就可編程實現了或許有人會問,為什麼差分方程就可編程實現呢?這是因為解差分方程的一般解法就是迭代法,而迭代法只需初值跟通項公式,這在電腦編程中很容易實現 

  為使編程方便,可引入中間變數,定義如下:

             

  則,PID控制器增量式演算法的差分方程變為:

      

  說明:

   (1)在PID增量式演算法中只需對輸出u(t)作限幅處理

   (2)當微分繫數 Kd=0 時,PID控制器就成了PI控制器在編寫PID程式時預設使其為PI調節器

       當積分繫數 Ki=0 時,PID控制器就成了PD控制器。

 


  •  基於微控制器的演算法實現

  我寫的數字PID程式如圖所示(在最後的附件部分),有兩套代碼,一套是直接函數調用C/C++通用,另一套是使用函數指針進行函數調用僅適用於C,現從兩個方面對該程式做講解:

(一)程式風格

  程式採用了模塊化編程的思想,這樣做的目的增強代碼的可移植性及程式的可讀性

  程式被拆分成三個模塊:

  一個是PID的頭文件’PID.h’:主要是定義演算法實現有關的數據類型;

  一個是PID的源文件’PID.c’:主要是定義演算法實現的函數;

  一個是主函數文件’amain.c’:PID程式的使用方法,即在主程式中做相應的初始化工作,在中斷服務程式中進行PID的計算。

說明:讀這個程式時可能有點困難,不過這屬情理之中的事,畢竟剛接觸這種風格的童鞋不太能理解這種風格的產生(為什麼這麼做)及用意(這麼做的好處);我的建議是:在理解演算法的原理後,根據自己的編程風格嘗試著寫一下,然後再跟這套程式對比著來理解,推敲一下別人為什麼要這麼做;當熟悉了整個流程後,你才能體會這種程式風格的優勢,再將這種編程風格慢慢轉化為自己的編程風格。

(二)程式中涉及的C語法講解

  這裡,我只講述為什麼要採用這些語法以及採用這些語法所帶來的好處,至於細枝末節的問題,就請各位童鞋自行查閱有關資料,順帶給大家推薦一本不錯的C語言教材:C Primer Plus,畢竟學習的興趣濃度跟書籍的編排也有關。

  1. 條件編譯指令

  第一處:#ifndef PID_H語句

  使用該語句的目的是避免造成把重覆定義語句(如,結構體類型定義)添加到工程中,而使得編譯出錯

  說明:其實也可不用#ifndef語句,因為每個定義的變數都具有特定的物理含義,不會造成重覆定義現象。

  第二處:#if (PID_DEBUG) 語句

  使用該語句的目的實現功能切換註意了:是在校正PID參數後手動切換,通過改變巨集定義語句#define PID_DEBUG 1中的巨集體實現),具體請看程式清單。

  2. 結構體及結構體指針

  使用結構體類型的好處:可為實現某一功能的各變數進行“打包”處理

  使用結構體指針的好處:通過傳址調用,對方便對結構體變數本身進行操作

  3. typedef數據類型定義

  使用typedef數據類型定義的好處是方便跨平臺進行代碼移植操作;但由於教材的緣故,造成很多童鞋都停留在錶面層次上的理解(typedef 數據類型 別名),因而此處作重點講解。

       我的理解:任何一個typedef聲明中的標識符不再是一個變數,而是代表一個數據類型,其表示的數據類型為正常變數聲明(去掉typedef)的那個標識符的數據類型。


  理解起來可能有點困難,現結合實例來講解:

       [例1]

typedef int Myint;

  分析:

  第一步:整體分析

  該語句表示定義了一個數據類型Myint(這裡Myint為數據類型標識符);

  (至於其具體表示什麼類型,請看下步分析)

  第二步:正常變數聲明(去掉typedef)

  int Myint;

  其所表示的類型為變數Myint(這裡Myint為變數標識符)的數據類型,即整型類型。

  應用:

  Myint a; //聲明整型變數a

       [例2]    

typedef struct
{
    //省略成員
}PID;

  分析:

  第一步:整體分析

  該語句表示定義了一個數據類型PID(這裡PID為類型標識符)

  (至於其具體表示什麼類型,請看下步分析)

  第二步:正常變數聲明(去掉typedef)

    struct
    {
        //省略成員
    }PID;    

  其所表示的類型為結構體變數PID(這裡PID為變數標識符)的數據類型,即結構體類型,且其具有的成員同結構體變數PID。

  應用:

    PID ASR; //定義結構體變數ASR

       [例3]

typedef void (*PFun)(int );

  分析:

  第一步:整體分析

  該語句表示定義了一個數據類型PFun(這裡PFun為類型標識符)

  (至於其具體表示什麼類型,請看下步分析)

  第二步:正常變數聲明(去掉typedef)

    void (*PFun)(int );

  其所表示的類型為函數指針PFun(這裡PFun為變數標識符)的數據類型,即函數指針類型,且指針所指向的函數類型:形參為整型,無返回值的一類函數。

  應用:

  PFun  pf; //定義函數指針pf

  說明:typedef的用法與巨集定義#define的用法類似,但又有區別,體現在以下兩點:

  (a)  typedef是對數據類型的定義,而#define是對數值的定義;

  (b)  typedef由編譯器解釋,而#define由預處理器執行。

  4. 空形參函數和形參帶(void)函數

  這是在C/C++中相當容易混淆的地方,因此這裡重點介紹一下,若是這個知識點沒搞懂,那麼這個程式你就無法看懂為什麼會如此定義函數指針及利用函數指針來進行函數調用。

  void本身就是一種數據類型(空類型),把void作為形參時,表示這個函數不需要參數。

  在C++中,空形參表與新參為void是等價的,這是C++中明確規定的;但在C中則是兩回事:C中的空形參表僅表示函數的形參個數和類型不確定,並非沒有參數,這會暫時掛起編譯器的類型檢查機制,從而造成類型安全隱患,所以在C中欲表示函數無形參時,最好用void,此時編譯器將進行函數參數類型驗證。

  [例]

void pid_calc(int); //函數聲明
void (*calc_1)(int); //函數指針聲明
void (*calc_2)(); //函數指針聲明

void main()
{
    //將函數的入口地址賦給函數指針
    calc_1=pid_calc; //C編譯通過;C++編譯通過
    calc_2=pid_calc; //C編譯通過;C++編譯失敗
}

  5. 函數指針及其函數調用

  函數調用,除了直接調用”函數名(實參)”這種語法外,還可通過函數指針來實現,兩者並無區別,但為了代碼的緊湊性及美觀性,建議大家使用函數指針來進行函數調用。

  在我放出的兩套代碼中,一套是直接函數調用C/C++通用),另一套是使用函數指針進行函數調用僅適用於C),大家可體會這兩種用法的區別。

  6. 數據類型轉換

  C語言中的數據類型分為自動類型轉換與強制類型轉換

  (1) 自動類型轉換(由編譯器完成)

  自動轉換的適用場合及其轉換規則,請讀者查閱有關資料

  (2) 強制類型轉換通過類型轉換運算實現

  在本程式中,即可對函數名`pid_calc`(函數名代表對應函數的入口地址)使用強制類型轉換(轉換為函數指針類型),也可不用,我都調試驗證過;現把程式截取出來,方便大家理解:

void pid_calc(PID *p); //函數聲明
void (*calc)(); //函數指針:指向PID計算函數

void main()
{
    //將函數的入口地址賦給指針變數
    calc=(void (*)(unsigned long))pid_calc; //編譯通過(強制類型轉換)
    calc=pid_calc; //編譯通過
}

  7. 代碼換行問題

  為了代碼的美觀調試方便,需涉及到代碼換行問題

  在本程式的巨集定義語句中使用了”\”,這是巨集定義中連接上下行的連接符,表示該巨集定義還未結束。

//定義PID控制器的初始值
#define PID_DEFAULTS {0,0, \
                      0,0,0, \
                      0.0002, \
                      0,0,0, \
                      0,0,0, \
                      0,0,0,0, \
                      (void (*)(unsigned long))pid_calc}

 


附件一:直接函數調用C/C++通用

PID.h文件

//===================================================
//PID.h文件
//===================================================
#ifndef PID_H
#define PID_H

//定義PID計算用到的結構體類型
typedef struct
{
    float Ref; //輸入:系統待調節量的給定值
    float Fdb; //輸入:系統待調節量的反饋值
    
    //PID控制器部分
    float Kp; //參數:比例繫數
    float Ki; //參數:積分繫數
    float Kd; //參數:微分繫數
    
    float T; //參數:離散化系統的採樣周期
    
    float a0; //變數:a0
    float a1; //變數: a1
    float a2; //變數: a2
    
    float Err; //變數:當前的偏差e(k)
    float Err_1; //歷史:前一步的偏差e(k-1)
    float Err_2;    //歷史:前前一步的偏差e(k-2)
    
    float Out; //輸出:PID控制器的輸出u(k)
    float Out_1;    //歷史:PID控制器前一步的輸出u(k-1)
    float OutMax; //參數:PID控制器的最大輸出
    float OutMin; //參數:PID控制器的最小輸出
    
}PID;

//定義PID控制器的初始值
#define PID_DEFAULTS {0,0, \
                      0,0,0, \
                      0.0002, \
                      0,0,0, \
                      0,0,0, \
                      0,0,0,0}

//條件編譯的判別條件
#define PID_DEBUG 1                     
    
//函數聲明
void pid_calc(PID *p);

#endif

//===================================================
//End of File
//===================================================

 

PID.c文件 

//===================================================
//PID.c文件
//===================================================
#include "PID.h"
//===================函數定義========================
/****************************************************
*說    明:
*    (1)PID控制器預設為PI調節器
*    (2)使用了條件編譯進行功能切換:節省計算時間
*        在校正PID參數時,使用巨集定義將PID_DEBUG設為1;
*        當參數校正完成後,使用巨集定義將PID_DEBUG設為0,同時,在初始化時
*    直接為p->a0、p->a1、p->a2賦值
****************************************************/
void pid_calc(PID *p)
{
    //使用條件編譯進行功能切換
    #if (PID_DEBUG)
    float a0,a1,a2;
    //計算中間變數a0、a1、a2
    a0=p->Kp+p->Ki*p->T+p->Kd/p->T;
    a1=p->Kp+2*p->Kd/p->T;
    a2=p->Kd/p->T;
    //計算PID控制器的輸出
    p->Out=p->Out_1+a0*p->Err-a1*p->Err_1+a2*p->Err_2;
    #else
    //計算PID控制器的輸出
    p->Out=p->Out_1+p->a0*p->Err-p->a1*p->Err_1+p->a2*p->Err_2;
    #endif
    
    //輸出限幅
    if(p->Out>p->OutMax)
        p->Out=p->OutMax;
    if(p->Out<p->OutMin)
        p->Out=p->OutMin;
    
    //為下步計算做準備
    p->Out_1=p->Out;
    p->Err_2=p->Err_1;
    p->Err_1=p->Err;
    
}

//===================================================
//End of File
//===================================================

 

amain.c主函數文件

//===================================================
//amain.c文件
//===================================================

//將用戶定義的頭文件包含進來
#include "PID.h"

//=============巨集定義=====================
#define T0 0.0002 //離散化採樣周期,單位s

//============全局變數========================
//定義PID控制器對應的結構體變數
PID ASR=PID_DEFAULTS; //速度PI調節器ASR

//定義PID控制器的參數及輸出限幅值
float SpeedKp=2,SpeedKi=1,SpeedLimit=10; //速度PI調節器ASR

//===============主程式=======================
void main()
{
    //初始化PID控制器
    ASR.Kp=SpeedKp;
    ASR.Ki=SpeedKi;
    ASR.T=T0;
    ASR.OutMax=SpeedLimit;
    ASR.OutMin=-SpeedLimit;
    
}

//============中斷服務程式====================
interrupt void T1UFINT_ISR(void)
{
    //轉速調節ASR
    ASR.Ref=2; //速度給定
    ASR.Fdb=1; //速度反饋
    ASR.Err=ASR.Ref-ASR.Fdb; //偏差
    pid_calc(&ASR); //PI計算(直接函數調用)
    
}

//===================================================
//End of File
//===================================================

 

附件二:使用函數指針進行函數調用僅適用於C

PID.h文件

//===================================================
//PID.h文件
//===================================================
#ifndef PID_H
#define PID_H

//定義PID計算用到的結構體類型
typedef struct
{
    float Ref; //輸入:系統待調節量的給定值
    float Fdb; //輸入:系統待調節量的反饋值
    
    //PID控制器部分
    float Kp; //參數:比例繫數
    float Ki; //參數:積分繫數
    float Kd; //參數:微分繫數
    
    float T; //參數:離散化系統的採樣周期
    
    float a0; //變數:a0
    float a1; //變數: a1
    float a2; //變數: a2
    
    float Err; //變數:當前的偏差e(k)
    float Err_1; //歷史:前一步的偏差e(k-1)
    float Err_2;    //歷史:前前一步的偏差e(k-2)
    
    float Out; //輸出:PID控制器的輸出u(k)
    float Out_1;    //歷史:PID控制器前一步的輸出u(k-1)
    float OutMax; //參數:PID控制器的最大輸出
    float OutMin; //參數:PID控制器的最小輸出
    
    void (*calc)(); //函數指針:指向PID計算函數
    
}PID;

//定義PID控制器的初始值
#define PID_DEFAULTS {0,0, \
                      0,0,0, \
                      0.0002, \
                      0,0,0, \
                      0,0,0, \
                      0,0,0,0, \
                      (void (*)(unsigned long))pid_calc} //加與不加強制類型轉換都沒影響

//條件編譯的判別條件
#define PID_DEBUG 1                     
    
//函數聲明
void pid_calc(PID *p);

#endif

//===================================================
//End of File
//===================================================

 

PID.c文件

//===================================================
//PID.c文件
//===================================================
#include "PID.h"
//===================函數定義========================
/****************************************************
*說    明:
*    (1)PID控制器預設為PI調節器
*    (2)使用了條件編譯進行功能切換:節省計算時間
*        在校正PID參數時,使用巨集定義將PID_DEBUG設為1;
*        當參數校正完成後,使用巨集定義將PID_DEBUG設為0,同時,在初始化時
*    直接為p->a0、p->a1、p->a2賦值
****************************************************/
void pid_calc(PID *p)
{
    //使用條件編譯進行功能切換
    #if (PID_DEBUG)
    float a0,a1,a2;
    //計算中間變數a0、a1、a2
    a0=p->Kp+p->Ki*p->T+p->Kd/p->T;
    a1=p->Kp+2*p->Kd/p->T;
    a2=p->Kd/p->T;
    //計算PID控制器的輸出
    p->Out=p->Out_1+a0*p->Err-a1*p->Err_1+a2*p->Err_2;
    #else
    //計算PID控制器的輸出
    p->Out=p->Out_1+p->a0*p->Err-p->a1*p->Err_1+p->a2*p->Err_2;
    #endif
    
    //輸出限幅
    if(p->Out>p->OutMax)
        p->Out=p->OutMax;
    if(p->Out<p->OutMin)
        p->Out=p->OutMin;
    
    //為下步計算做準備
    p->Out_1=p->Out;
    p->Err_2=p->Err_1;
    p->Err_1=p->Err;
    
}

//===================================================
//End of File
//===================================================

 

amain.c主函數文件

//===================================================
//amain.c文件
//===================================================

//將用戶定義的頭文件包含進來
#include "PID.h"

//=============巨集定義=====================
#define T0 0.0002 //離散化採樣周期,單位s

//============全局變數========================
//定義PID控制器對應的結構體變數
PID ASR=PID_DEFAULTS; //速度PI調節器ASR

//定義PID控制器的參數及輸出限幅值
float SpeedKp=2,SpeedKi=1,SpeedLimit=10; //速度PI調節器ASR

//===============主程式=======================
void main()
{
    //初始化PID控制器
    ASR.Kp=SpeedKp;
    ASR.Ki=SpeedKi;
    ASR.T=T0;
    ASR.OutMax=SpeedLimit;
    ASR.OutMin=-SpeedLimit;
    
}

//============中斷服務程式====================
interrupt void T1UFINT_ISR(void)
{
    //轉速調節ASR
    ASR.Ref=2; //速度給定
    ASR.Fdb=1; //速度反饋
    ASR.Err=ASR.Ref-ASR.Fdb; //偏差
    ASR.calc(&ASR); //PI計算_調用PID計算函數
    
}

//===================================================
//End of File
//===================================================

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前一段時間們需要對一個List<Model>集合去重,情況是該集合中會出現多個Name屬性值相同的,但是其他屬性值不同的數據。 在這種情況下,需求要只保留其中一個就好。 我覺得遍歷和HashSet都不是我想要的,便採用了一下方式 定義Compare類,繼承IEqualityComparer介面 pu
  • 看了《CLR via C#》的17章委托後,為自己做一點淺顯的總結,也分享給需要的人。 .NET通過委托來提供一種回調函數機制,.NET委托提供了很多功能,例如確保回調方法是類型安全的(CLR重要目標)。委托好允許順序調用多個方法(委托鏈),並且支持調用靜態方法和實例方法。 委托的基本語法就不多說了
  • 分類:C#、Android、VS2015; 創建日期:2016-02-07 一、簡介 圖庫(也叫畫廊)是一個佈局小部件,用於在可水平滾動的列表中顯示每一副圖片,當前所選的圖片將置於視圖的中心。 註意:Android已經棄用了這個小部件,棄用的原因是用Galery實現的效率比較低,官方的建議是改為用H
  • 分類:C#、Android、VS2015; 創建日期:2016-02-07 一、簡介 1、CheckBox 覆選 【Checked】屬性:是否選中。 2、RadioButton 單選 【Checked】屬性:是否選中。 【RadioGroup】屬性:RadioButton的分組容器。註意必須將Rad
  • 分類:C#、Android、VS2015; 創建日期:2016-02-07 一、簡介 1、Button 常規按鈕。 2、TextView 文本視圖,其功能和WPF的TextBlock控制項類似,【工具箱】中提供的3個組件實際上是同一個TextView控制項用不同的屬性來區分的,這3個不同的屬性在【工具箱
  • 分類:C#、Android、VS2015; 創建日期:2016-02-06 這一章主要介紹Android簡單控制項的基本用法。本章源程式共有9個示例,這些示例都在同一個項目中。 項目名:ch05demos,項目模板:Blank App(Android) 運行主界面截圖如下: 點擊每行的示例項,即進入對
  • 分類:C#、Android、VS2015;創建日期:2016-02-06 開發人員可以用以下兩種方式聲明UI:一是通過.xml文件(不帶預覽界面)或者.axml文件(帶預覽界面)來描述;二是用C#代碼實現。 用.axml文件描述用戶界面(UI)時,設計器分為【設計】視圖和【源】視圖。這種方式的優點是
  • 問題背景:八皇後問題是一個以國際象棋為背景的問題:如何能夠在 8×8 的國際象棋棋盤上放置八個皇後,使得任何一個皇後都無法直接吃掉其他的皇後。為了達到這個目的, 任兩個皇後都不能處於同一條橫行、縱行或斜線上。 以下的代碼給出的解法應該是最容易理解的一種方法,本問題還可以用回溯法和遞歸解決,理論上效率
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...