題:
如何估算此微控制器的此代碼段的速度?
ty_1917
2020-01-09 17:00:38 UTC
view on stackexchange narkive permalink

我正在使用 ATmega328P來讀取數字輸入的狀態,方法是使用以下用C語言編寫的代碼節(可以有其他方法,但這是一個示例)。 val uint8_t 變量類型,它存儲數字輸入引腳的狀態:

這是代碼的一部分:

  if((PIND &(1 << PIND6))==(1 << PIND6)){
    val = 1;
}其他{
    val = 0;
}
 

我將時鐘設置為:

  #define F_CPU 16000000UL
 

想像一下,數字輸入是佔空比為50%的ON / OFF脈衝序列,並且我們逐漸增加其頻率。上面的代碼有時在某個頻率上不能正確捕獲數字輸入狀態。

  1. 我們如何大致估計上述代碼可以讀取以正確讀取狀態的最大脈衝頻率?
  2. 我們應該找出它使用了多少個時鐘週期並乘以時鐘頻率嗎?
  3. 如果是的話,我該怎麼做呢?

      int main(void){
    
        DDRD = B0100000;
        DDRD | = 1<<5;
    
        而(1){
    
            無符號長數據= 0;
            uint8_t val;
    
            為(int i = 0; i<25; i ++){
                數據<< = 1;
                PORTD & =〜(1 << 5);
                _delay_us(2);
                PORTD | =(1 << 5);
                _delay_us(2);
    
                如果((PIND &(1 << PIND6))==((1 << PIND6)){
                    val = 1;
                }
                其他{
                    val = 0;
                }
    
                數據| = val;
            }
    
            //其餘代碼
    
        }
    }
     
  4. ol>
我希望這將在每次運行時以任何頻率正確捕獲數字輸入狀態。問題是,您多久可以運行一次此代碼?
您可以將代碼編寫為`val =((PIND&(1 << PIND6))==(1 << PIND6));`或`val =((PIND >> PIND6)&1);`(以及其他多種方式)。您應該查看編譯器的輸出,以查看在每種情況下是否獲得了不同的彙編代碼以及最快的彙編代碼。
您是否嘗試過使用硬件分析器來“測量”而不是“估計”?
您的新代碼與舊代碼存在相同的問題:-不使用`val`和`data`,因此將對其進行優化。
五 答案:
Swedgin
2020-01-09 17:08:36 UTC
view on stackexchange narkive permalink
  1. 由於您感興趣的代碼段不大,因此您可以反彙編已編譯的代碼,查看所有彙編指令併計算它們需要多少個週期。您可以在數據表中找到每條指令的周期數。

  2. 如果您有示波器,則可以在 if 語句之前打開一個引腳,並在代碼段之後將其關閉。 (使用直接端口操縱 PORTB ,而不是Arduino庫函數)通過作用域,您可以看到運行代碼需要多長時間。

  3. 使用Arduino庫中的 micros()函數。在代碼段之前和之後放置一個。但是,由於必須同時運行“ micros()”,因此您將需要幾微秒的時間。

  4. 使用可以計算週期的調試器或硬件模擬器。在代碼段的第一個語句上放置一個斷點,在代碼段後的語句上放置一個斷點。 delta_t =週期/ clock_freq (與Oldfart的回答一致)

  5. ol>
打開和關閉引腳時也有開銷。遍歷Arduino庫時,開銷特別大。我想我會堅持使用micros()。
@Dampmaskin,因為OP使用了“ PIND6”,所以我認為他不會使用Arduino庫,而只會使用“ PORTB6”。使用它,開銷比`micros()`小得多。
我正在使用Atmel Studio
@ty_1917不管使用哪個編輯器,都是對其他評論的答复。事實是,Arduino庫函數有很多開銷。因此,當使用位設置/清除“ PORTB”進行切換時,它將比使用Arduino庫函數進行切換快得多。我比使用`micros()`函數更信任我的範圍。
我認為關鍵是開銷不僅是庫,而且總體上來說,I / O(尤其是atmel / microchip部分非常快)需要花費一些時鐘來執行某項操作,因為它們沒有理由在核心cpu率。因此,儘管您可以縮短cpu指令的數量,減少分支等,但您可能受到I / O約束。但這可以通過實驗確定。還了解到,可能不是針對該內核的,但是代碼和其他因素的對齊(不是在談論中斷)可能會使同一機器代碼每次都不會在同一MCU上執行相同的操作。
@Dampmaskin,因此您無需測試代碼即可測量引腳更換之間的時間,以獲取開銷,然後從結果中減去開銷。
需要注意的一件事是編譯器優化了您的代碼。在給出的示例中,未使用'val',因此不會生成任何代碼來對其進行設置!
請看我的編輯。我現在可以覆蓋大部分代碼。您將在何處以及如何聲明val和數據以提高效率?在while循環之外並不穩定嗎?
@old_timer在AVR上沒有這樣的問題-端口寫入需要一個週期(如果您需要讀取,屏蔽和寫入三個週期)並立即命中。
yar
2020-01-10 06:16:27 UTC
view on stackexchange narkive permalink

開始吧!

說我們有代碼

  int main(void)
{
    易失性uint8_t val = 0;

    而(1)
    {
        如果((PIND &(1 << PIND6))==((1 << PIND6)){
            val = 1;
        }其他{
            val = 0;
        }
    }
}
 

假設我們使用帶有優化標記-O1的AVR GCC,那麼相關部分的反彙編如下:

  val = 1;
00000046 LDI R24,0x01立即加載,1個時鐘週期
    如果((PIND &(1 << PIND6))==((1 << PIND6)){
00000047 SBIS 0x09,6如果I / O寄存器中的位置1,則跳過1/2個時鐘週期
00000048 RJMP PC + 0x0003相對跳轉,2個時鐘週期
        val = 1;
00000049 STD Y + 1,R24帶位移間接存儲,2個時鐘週期
0000004A RJMP PC-0x0003相對跳轉,2個時鐘週期
        val = 0;
0000004B STD Y + 1,R1位移間接存儲,2個時鐘週期
0000004C RJMP PC-0x0005相對跳轉,2個時鐘週期
 

我根據數據表第281ff頁添加了帶有時鐘週期數的註釋。請注意, SBIS 可能需要1或2個週期,具體取決於是否跳過下一條指令。

所以我們看到,if分支(行0x47、0x49、0x4A)花費6個時鐘週期,而else分支(行0x47、0x48、0x4B,0x4C)花費7個時鐘週期。

現在讓我們考慮更長的時間。如果使用16MHz,則需要(7 / 16e6)秒,即您以16e6 / 7 Hz的頻率進行採樣。由於您始終希望至少有一個低/高采樣點,因此需要以> 2倍的信號頻率進行採樣,即信號頻率必須為<16e6 /(7 * 2)Hz,約為1Mhz。

現在註意,這是一個純虛擬的示例,因為val設置正確,但是您無法測試它。您需要以某種方式輸出val,這將增加額外的時鐘週期。

像您一樣在while循環之外將volatile聲明為volatile uint8_t是更好還是任何好處?在我的代碼中,它被聲明為uint8_t val = 0;在while循環中。因此,我應該像您一樣聲明為volatile uint8_t並在while循環之外嗎?
請看我的編輯。我添加了大部分代碼。您將在何處以及如何聲明val和數據以提高效率?在while循環之外並不穩定嗎?
@ty_1917:不應有任何區別,yar只是將其聲明為volatile以防止編譯器對其進行優化(因為在最小示例中未讀取該變量)。您始終可以查看彙編代碼(或對代碼進行基準測試)來確保。
@Michael我在這裡打開了一個更詳細的問題:https://electronics.stackexchange.com/questions/475476/stuck-with-deciding-the-location-and-the-type-of-variable-declarations-for-this
這是一個很好的練習,也是您應該怎麼做-始終查看生成的機器代碼。但是,我認為在此示例中使用“ volatile”結果變量可能會使優化工作有些混亂,唯一需要更改的部分是寄存器本身。看看這個[示例](https://godbolt.org/z/VhmVUh)。-O2開啟時有效地是4條指令。
在這種情況下,@ty_1917的“ volatile”僅是使編譯器在啟用優化的情況下完全生成任何代碼的一種人工方法。如果不是“ volatile”,則整個代碼將被優化器刪除。但是,正如我上面的評論所指出的那樣,“ volatile”對性能也有輕微的影響。最好像我在上面的Godbolt鏈接中那樣做,並分解一個隔離的函數-然後編譯器被迫生成代碼,因為它不知道如何/是否調用該函數。
還要注意的是,在用C(而不是asm)編寫代碼時,代碼片段的性能取決於*環繞*代碼,具體取決於編譯器如何將其優化為早期/之後的代碼。(即使您確實保留了相同的類型)。當然,這取決於優化級別,大多數答案甚至都沒有提到。
請注意,除了彼得·科德斯(Peter Cordes)指出的問題之外,一旦優化器掌握了代碼並內聯了東西,生成的程序集就很可能會發生變化,這種週期計數方法僅適用於為確定性週期計數提供確定性的微控制器。說明。AVR很可能就是這種情況,但是對於許多ARM內核來說並不是這樣,對於x86而言肯定不是正確的。除了顯而易見的(例如流水線和調度)之外,您還必須考慮內存延遲,指令預取和其他因素。不過,它將給您一個大概的估計。
@Lundin如果使用volatile,則編譯器被迫將其寫入SRAM,而在您的示例中,變量保留在寄存器中。在這個人為的示例中,它實際上並沒有什麼不同,但是我使用volatile使它更接近於現實。
Oldfart
2020-01-10 07:44:50 UTC
view on stackexchange narkive permalink

我通常使用Atmel Studio的內置模擬器,該模擬器在處理器狀態窗口中具有一個循環計數器。 這是通過瀏覽代碼獲得的組合屏幕截圖:

Enter image description here]

如您所見,單步執行兩個語句之前,循環計數器為18,之後為22。因此,根據模擬器,它需要4個週期。

您可以使用它來遍歷整個循環。

時鐘頻率是16MHz還是1MHz?我將其設置為16MHz,但如您的屏幕截圖所示,在“處理器狀態”下顯示為1MHz。它需要4個週期,但是每個週期的頻率是多少?
執行指令的周期數與頻率無關。
是的,但是我想知道每個週期需要多長時間。因為這決定了我要問的最大脈衝序列輸入頻率。
如果((PIND&(1 << PIND6))==(1 << PIND6)){ val = 1; }其他{ val = 0; }這將讀取狀態,並且如果輸入脈衝序列為1GHz,則它無法讀取例如正確的狀態,因為mcu時鐘比1GHz慢得多。我試圖弄清楚它可以正確讀取引腳狀態的最大頻率。
Yar已經提供了示例計算方法,如何從CPU週期和時鐘頻率獲得採樣率。還要注意她/他關於輸出值的警告。
Curd
2020-01-09 17:35:27 UTC
view on stackexchange narkive permalink

如果速度對於這段代碼很重要,則可能需要注意以下幾點:

你可以寫

val =(PIND &(1 << PIND6))= 0;

val = 1 &(PIND >> PIND6);

我想最後一個短/快。

關於速度/時間估計: 要么

  • 讓您的編譯器生成一個列出文件(* .lst)的彙編程序,或
  • 看看的反彙編代碼

然後查找並添加指令的執行時間(時鐘週期)。

您的代碼可以“處理”的頻率當然取決於調用頻率,即取決於周圍/調用代碼的速度(即訪問代碼片段的頻率),而不僅取決於代碼摘要本身。

_“我想最後一個是更短/更快。” _-我在AVR GCC 5.4.0中使用優化O嘗試了所有3個變體,並且它們生成了相同的代碼(4個指令/週期)。
這表明編譯器做得很好。
因此,編寫最易讀的代碼,可能是`bool val = PIND&(1u << PIND6);`。
@Lundin:,但該行給出不同的結果:如果輸入為高,則val將為1 << PIND6而不是1。
@Curd編號。使用stdbool.h中的標準C`bool`,擴展為`_Bool`。
是的,但這需要更改val類型。最初,OP尚未發布代碼來顯示如何使用`val`,因此該選項未啟用,現在OP仍希望將其用作`int`。
我的代碼中的val必須是單個狀態。data是要由for循環中的每個val狀態填充的32位變量。
當您僅發佈設置了val但尚未使用過val的前三行代碼時,一開始並不清楚。可能是後來也使用了val中的更高位。
Bruce Abbott
2020-01-12 15:35:23 UTC
view on stackexchange narkive permalink

我們如何才能大致估算上述代碼的最大脈衝頻率 能正確讀取狀態嗎?

它將始終正確讀取狀態。我想您想問的問題是,它可以“測量”而不丟失任何高點或低點的最大頻率是什麼。

我們應該找出它使用了多少個時鐘週期,然後乘以 時鐘頻率?

基本上是。重要的因素是每次讀取端口之間的時間。請注意,根據機器代碼的不同,這可能並不總是相同的,因此應在兩次讀取之間使用最長時間。

如果是的話,我該怎麼做呢?

您可以反彙編代碼,計算出每條指令要花費多少時間,或者在模擬器中逐步執行,或者在實際的ATmega328p中運行代碼並監視物理輸出(例如,切換輸出引腳或在LCD屏幕上顯示頻率)。

請注意,結果在很大程度上取決於編譯器生成的機器代碼。通過對不影響輸出的任何變量進行優化,可以將其優化,其他看似微不足道的更改可能會對所生成的代碼量產生很大影響。因此,唯一可以保證測試代碼準確的方法是全部。運行隔離代碼的小片段可能會給最終應用程序帶來非常誤導的性能提示。

例如,這是您的問題代碼清單:-

  int main(void){
  86:89 e1 ldi r24,0x19; 25
  88:90 e0 ldi r25,0x00; 0

        uint8_t val;

        對於(int i = 0; i<25; i ++){
            數據<< = 1;
            PORTD & =〜(1 << 5);
  8a:5d 98 cbi 0x0b,5; 11
 // _delay_us(2);
            PORTD | =(1 << 5);
8c:5d 9a sbi 0x0b,5; 11
 // _delay_us(2);

            如果((PIND &(1 << PIND6))==((1 << PIND6)){
  8e:r18中的29 b1,0x09; 9
  90:01 97旋轉r24,0x01; 1個

    而(1){

        uint8_t val;

        對於(int i = 0; i<25; i ++){
  92:d9 f7 brne .-10; 0x8a <main + 0xa>
  94:f8 cf rjmp .-16; 0x86 <main + 0x6>
 

不會為 val data 生成任何代碼,並且內部循環只有5條指令佔用9個週期。使用16 MHz時鐘時,內部環路時間為62.5 ns * 9 = 562.5 ns,這應該能夠滿足〜888 kHz的輸入頻率。

接下來,我將 data 輸出到PORTD,這將迫使編譯器為其生成代碼:-

 而(1){

        uint8_t val;

        對於(int i = 0; i<25; i ++){
            數據<< = 1;
  90:88 0f添加r24,r24
  92:99 1f adc r25,r25
  94:aa 1f adc r26,r26
  96:bb 1f adc r27,r27
            PORTD & =〜(1 << 5);
  98:5d 98 cbi 0x0b,5; 11
 // _delay_us(2);
            PORTD | =(1 << 5);
  9a:5d 9a sbi 0x0b,5; 11
 // _delay_us(2);

            如果((PIND &(1 << PIND6))==((1 << PIND6)){
  9c:r20中的49 b1,0x09; 9
            }
            其他{
                val = 0;
            }

            數據| = val;
  9e:46 fb bst r20、6
  a0:44 27 eor r20,r20
  a2:40 f9 bld r20、0
  a4:84 2b或r24,r20
  a6:21 50 subi r18,0x01; 1個
  a8:31 09 sbc r19,r1

    而(1){

        uint8_t val;

        對於(int i = 0; i<25; i ++){
aa:91 f7 brne .-28; 0x90 <main + 0x10>
            }

            數據| = val;
        }

    PORTD =(uint8_t)數據;
  ac:8b b9 out 0x0b,r24; 11

        //其餘代碼

    }
  ae:ee cf rjmp .-36; 0x8c <main + 0xc>
 

內部循環現在有14條指令需要17個週期,並且它可以精確跟踪的最大頻率幾乎減半。

最後,我將 data 設為靜態,以強制編譯器將其存儲在內存中(這對於更複雜的程序可能是必需的):-

 而(1){

        uint8_t val;

        對於(int i = 0; i<25; i ++){
            數據<< = 1;
  9a:40 91 00 01 lds r20,0x0100; 0x800100 <_edata>
  9e:50 91 01 01 lds r21,0x0101; 0x800101 <_edata + 0x1>
  a2:60 91 02 01 lds r22,0x0102; 0x800102 <_edata + 0x2>
  a6:70 91 03 01 lds r23,0x0103; 0x800103 <_edata + 0x3>
  aa:44 0f加r20,r20
  交流電:55 1f adc r21,r21
  AE:66 1F ADC R22,R22
  b0:77 1f adc r23,r23
  b2:40 93 00 01 sts 0x0100,r20; 0x800100 <_edata>
  b6:50 93 01 01 sts 0x0101,r21; 0x800101 <_edata + 0x1>
  ba:60 93 02 01 sts 0x0102,r22; 0x800102 <_edata + 0x2>
  是:70 93 03 01 sts 0x0103,r23; 0x800103 <_edata + 0x3>
            PORTD & =〜(1 << 5);
  c2:5d 98 cbi 0x0b,5; 11
 // _delay_us(2);
            PORTD | =(1 << 5);
  c4:5d 9a sbi 0x0b,5; 11
 // _delay_us(2);

            如果((PIND &(1 << PIND6))==((1 << PIND6)){
  c6:r18中的29 b1,0x09; 9
            }
其他{
                val = 0;
            }

            數據| = val;
  c8:26 fb bst r18、6
  ca:22 27 eor r18,r18
  抄送:20 f9 bld r18,0
  ce:40 91 00 01 lds r20,0x0100; 0x800100 <_edata>
  d2:50 91 01 01 lds r21,0x0101; 0x800101 <_edata + 0x1>
  d6:60 91 02 01 lds r22,0x0102; 0x800102 <_edata + 0x2>
  da:70 91 03 01 lds r23,0x0103; 0x800103 <_edata + 0x3>
  de:42 2b或r20,r18
  e0:40 93 00 01 sts 0x0100,r20; 0x800100 <_edata>
  e4:50 93 01 01 sts 0x0101,r21; 0x800101 <_edata + 0x1>
  e8:60 93 02 01 sts 0x0102,r22; 0x800102 <_edata + 0x2>
  ec:70 93 03 01 sts 0x0103,r23; 0x800103 <_edata + 0x3>
  f0:01 97旋轉r24,0x01; 1個

    而(1){

        uint8_t val;

        對於(int i = 0; i<25; i ++){
  f2:99 f6 brne .-90; 0x9a <main + 0xa>
  f4:d0 cf rjmp .-96; 0x96 <main + 0x6>
 

內部循環代碼現在已膨脹為29條指令,耗時49個週期,從而將最大可測量頻率降低至〜163 kHz。只需添加 static 關鍵字就足以使其慢5倍。但是,這是在較大的應用程序中使用代碼時可能期望的實際速度。

如果您需要盡可能快的速度而不受編譯器怪癖的影響,則可以使用3個選項:-

  1. 編寫精巧的彙編代碼,以最有效的方式使用每條指令(其他非關鍵代碼仍可以用C編寫)。

  2. 使用外圍硬件,如定時器/計數器或SPI。

  3. 添加一個外部芯片,例如預分頻器以分頻,或者添加一個移位寄存器(例如 CD4031)以捕獲波形。

  4. ol>


該問答將自動從英語翻譯而來。原始內容可在stackexchange上找到,我們感謝它分發的cc by-sa 4.0許可。
Loading...