Dapatkan kinerja cepat dari MCU STM32

11

Saya bekerja dengan kit penemuan STM32F303VC dan saya sedikit bingung dengan kinerjanya. Untuk berkenalan dengan sistem, saya telah menulis sebuah program yang sangat sederhana hanya untuk menguji kecepatan bit-banging dari MCU ini. Kode dapat diuraikan sebagai berikut:

  1. Jam HSI (8 MHz) dihidupkan;
  2. PLL dimulai dengan prescaler 16 untuk mencapai HSI / 2 * 16 = 64 MHz;
  3. PLL ditunjuk sebagai SYSCLK;
  4. SYSCLK dimonitor pada pin MCO (PA8), dan salah satu pin (PE10) terus-menerus diaktifkan di loop tak terbatas.

Kode sumber untuk program ini disajikan di bawah ini:

#include "stm32f3xx.h"

int main(void)
{
      // Initialize the HSI:
      RCC->CR |= RCC_CR_HSION;
      while(!(RCC->CR&RCC_CR_HSIRDY));

      // Initialize the LSI:
      // RCC->CSR |= RCC_CSR_LSION;
      // while(!(RCC->CSR & RCC_CSR_LSIRDY));

      // PLL configuration:
      RCC->CFGR &= ~RCC_CFGR_PLLSRC;     // HSI / 2 selected as the PLL input clock.
      RCC->CFGR |= RCC_CFGR_PLLMUL16;   // HSI / 2 * 16 = 64 MHz
      RCC->CR |= RCC_CR_PLLON;          // Enable PLL
      while(!(RCC->CR&RCC_CR_PLLRDY));  // Wait until PLL is ready

      // Flash configuration:
      FLASH->ACR |= FLASH_ACR_PRFTBE;
      FLASH->ACR |= FLASH_ACR_LATENCY_1;

      // Main clock output (MCO):
      RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
      GPIOA->MODER |= GPIO_MODER_MODER8_1;
      GPIOA->OTYPER &= ~GPIO_OTYPER_OT_8;
      GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR8;
      GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR8;
      GPIOA->AFR[0] &= ~GPIO_AFRL_AFRL0;

      // Output on the MCO pin:
      //RCC->CFGR |= RCC_CFGR_MCO_HSI;
      //RCC->CFGR |= RCC_CFGR_MCO_LSI;
      //RCC->CFGR |= RCC_CFGR_MCO_PLL;
      RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;

      // PLL as the system clock
      RCC->CFGR &= ~RCC_CFGR_SW;    // Clear the SW bits
      RCC->CFGR |= RCC_CFGR_SW_PLL; //Select PLL as the system clock
      while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL); //Wait until PLL is used

      // Bit-bang monitoring:
      RCC->AHBENR |= RCC_AHBENR_GPIOEEN;
      GPIOE->MODER |= GPIO_MODER_MODER10_0;
      GPIOE->OTYPER &= ~GPIO_OTYPER_OT_10;
      GPIOE->PUPDR &= ~GPIO_PUPDR_PUPDR10;
      GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR10;

      while(1)
      {
          GPIOE->BSRRL |= GPIO_BSRR_BS_10;
          GPIOE->BRR |= GPIO_BRR_BR_10;

      }
}

Kode ini dikompilasi dengan CoIDE V2 dengan GNU ARM Embedded Toolchain menggunakan optimasi -O1. Sinyal pada pin PA8 (MCO) dan PE10, diperiksa dengan osiloskop, terlihat seperti ini: masukkan deskripsi gambar di sini

SYSCLK tampaknya dikonfigurasikan dengan benar, karena MCO (kurva oranye) menunjukkan osilasi hampir 64 MHz (mempertimbangkan margin kesalahan jam internal). Bagian yang aneh bagi saya adalah perilaku pada PE10 (kurva biru). Dalam infinite while (1) loop dibutuhkan 4 + 4 + 5 = 13 siklus siklus untuk melakukan operasi 3-langkah dasar (yaitu bit-set / bit-reset / kembali). Ini menjadi lebih buruk pada level optimisasi lainnya (mis. -O2, -O3, ar -Os): beberapa siklus clock tambahan ditambahkan ke bagian RENDAH dari sinyal, yaitu antara tepi jatuh dan naik dari PE10 (memungkinkan entah bagaimana sepertinya LSI untuk memperbaiki situasi ini).

Apakah perilaku ini diharapkan dari MCU ini? Saya akan membayangkan tugas sesederhana mengatur dan mengatur ulang sedikit harus 2-4 kali lebih cepat. Apakah ada cara untuk mempercepat?

KR
sumber
Sudahkah Anda mencoba membandingkan beberapa MCU?
Marko Buršič
3
Apa yang ingin Anda capai? Jika Anda ingin output berosilasi cepat, Anda harus menggunakan timer. Jika Anda ingin berinteraksi dengan protokol serial cepat, Anda harus menggunakan perangkat keras yang sesuai.
Jonas Schäfer
2
Awal yang bagus dengan kit !!
Scott Seidman
Anda tidak boleh | = mendaftar BSRR atau BRR karena mereka hanya menulis.
P__J__

Jawaban:

25

Pertanyaannya di sini adalah: apa kode mesin yang Anda hasilkan dari program C, dan bagaimana perbedaannya dari apa yang Anda harapkan.

Jika Anda tidak memiliki akses ke kode asli, ini akan menjadi latihan dalam rekayasa terbalik (pada dasarnya sesuatu yang dimulai dengan:) radare2 -A arm image.bin; aaa; VV, tetapi Anda memiliki kode sehingga ini membuat semuanya lebih mudah.

Pertama, kompilasi dengan -gbendera yang ditambahkan ke CFLAGS(tempat yang sama di mana Anda juga menentukan -O1). Kemudian, lihat perakitan yang dihasilkan:

arm-none-eabi-objdump -S yourprog.elf

Perhatikan bahwa tentu saja nama objdumpbiner serta file ELF perantara Anda mungkin berbeda.

Biasanya, Anda juga bisa melewatkan bagian tempat GCC memanggil assembler dan cukup melihat file assembly. Tambahkan saja -Ske baris perintah GCC - tetapi itu biasanya akan merusak build Anda, jadi Anda kemungkinan besar akan melakukannya di luar IDE Anda.

Saya melakukan perakitan versi kode Anda yang sedikit ditambal :

arm-none-eabi-gcc 
    -O1 ## your optimization level
    -S  ## stop after generating assembly, i.e. don't run `as`
    -I/path/to/CMSIS/ST/STM32F3xx/ -I/path/to/CMSIS/include
     test.c

dan dapatkan yang berikut (kutipan, kode lengkap di bawah tautan di atas):

.L5:
    ldr r2, [r3, #24]
    orr r2, r2, #1024
    str r2, [r3, #24]
    ldr r2, [r3, #40]
    orr r2, r2, #1024
    str r2, [r3, #40]
    b   .L5

Yang merupakan loop (perhatikan lompatan tanpa syarat ke .L5 di akhir dan label .L5 di awal).

Apa yang kita lihat di sini adalah kita

  • pertama ldr(muat register) register r2dengan nilai di lokasi memori disimpan dalam r3+ 24 Bytes. Menjadi terlalu malas untuk melihat ke atas: sangat mungkin lokasi BSRR.
  • Kemudian ORyang r2mendaftar dengan konstan 1024 == (1<<10), yang akan sesuai dengan pengaturan bit-10 dalam register itu, dan menulis hasilnya ke r2itu sendiri.
  • Kemudian str(simpan) hasil di lokasi memori yang telah kita baca di langkah pertama
  • dan kemudian ulangi hal yang sama untuk lokasi memori yang berbeda, karena malas: kemungkinan besar BRRalamat.
  • Akhirnya b(cabang) kembali ke langkah pertama.

Jadi kita punya 7 instruksi, bukan tiga, untuk memulai. Hanya yang bterjadi sekali, dan dengan demikian sangat mungkin apa yang mengambil jumlah siklus ganjil (kita memiliki total 13 siklus, jadi dari suatu tempat hitungan siklus ganjil harus berasal). Karena semua angka ganjil di bawah 13 adalah 1, 3, 5, 7, 9, 11, dan kita dapat mengesampingkan angka yang lebih besar dari 13-6 (dengan asumsi CPU tidak dapat menjalankan instruksi dalam waktu kurang dari satu siklus), kita tahu bahwa bdibutuhkan 1, 3, 5, atau 7 siklus CPU.

Menjadi siapa kita, saya melihat dokumentasi ARM tentang instruksi dan berapa banyak siklus yang mereka ambil untuk M3:

  • ldr membutuhkan 2 siklus (dalam banyak kasus)
  • orr membutuhkan 1 siklus
  • str membutuhkan 2 siklus
  • bmembutuhkan 2 hingga 4 siklus. Kita tahu itu pasti angka ganjil, jadi harus 3, di sini.

Itu semua sejalan dengan pengamatan Anda:

13=2(cldr+corr+cstr)+cb=2(2+1+2)+3=25+3

Seperti yang ditunjukkan oleh perhitungan di atas, tidak akan ada cara untuk membuat loop Anda lebih cepat - pin output pada prosesor ARM biasanya dipetakan dengan memori , bukan register inti CPU, jadi Anda harus melalui rutinitas penyimpanan - modifikasi - store jika Anda ingin melakukan apa pun dengan itu.

Apa yang tentu saja bisa Anda lakukan adalah tidak membaca ( |=secara implisit harus membaca) nilai pin setiap iterasi loop, tetapi hanya menulis nilai variabel lokal untuk itu, yang Anda hanya beralih setiap iterasi loop.

Perhatikan bahwa saya merasa Anda mungkin terbiasa dengan mikro 8bit, dan akan mencoba membaca hanya nilai 8 bit, menyimpannya dalam variabel 8 bit lokal, dan menulisnya dalam potongan 8 bit. Jangan. ARM adalah arsitektur 32bit, dan mengekstraksi 8 bit kata 32bit mungkin memerlukan instruksi tambahan. Jika Anda bisa, cukup baca seluruh kata 32bit, modifikasi apa yang Anda butuhkan, dan tulis kembali secara keseluruhan. Apakah itu mungkin tentu saja tergantung pada apa yang Anda tulis, yaitu tata letak dan fungsionalitas GPIO yang dipetakan oleh memori Anda. Bacalah lembar data / pengguna STM32F3 untuk info tentang apa yang disimpan dalam 32bit yang mengandung bit yang ingin Anda toggle.


Sekarang, saya mencoba untuk mereproduksi masalah Anda dengan "rendah" periode semakin panjang, tapi aku hanya tidak bisa - loop terlihat persis sama dengan -O3seperti -O1dengan versi compiler saya. Anda harus melakukannya sendiri! Mungkin Anda menggunakan beberapa versi kuno GCC dengan dukungan ARM suboptimal.

Marcus Müller
sumber
4
Tidakkah hanya menyimpan ( =bukan |=), seperti yang Anda katakan, persis seperti percepatan yang dicari OP? Alasan ARM memiliki register BRR dan BSRR secara terpisah adalah untuk tidak memerlukan baca-modifikasi-tulis. Dalam hal ini, konstanta dapat disimpan dalam register di luar loop, sehingga loop dalam hanya 2 str dan cabang, jadi 2 + 2 +3 = 7 siklus untuk seluruh putaran?
Timo
Terima kasih. Itu benar-benar beres sedikit. Itu agak terburu-buru berpikir untuk bersikeras bahwa hanya 3 siklus clock akan diperlukan - 6 sampai 7 siklus adalah sesuatu yang sebenarnya saya harapkan. The -O3kesalahan tampaknya telah menghilang setelah membersihkan dan membangun kembali solusi. Meskipun demikian, kode assembly saya tampaknya memiliki instruksi UTXH tambahan di dalamnya: .L5: ldrh r3, [r2, #24] uxth r3, r3 orr r3, r3, #1024 strh r3, [r2, #24] @ movhi ldr r3, [r2, #40] orr r3, r3, #1024 str r3, [r2, #40] b .L5
KR
1
uxthada karena GPIO->BSRRL(salah) didefinisikan sebagai register 16 bit di header Anda. Gunakan versi header terbaru, dari perpustakaan STM32CubeF3 , di mana tidak ada BSRRL dan BSRRH, tetapi BSRRregister 32 bit tunggal . @Marcus tampaknya memiliki tajuk yang benar, sehingga kodenya melakukan akses penuh 32 bit alih-alih memuat setengah kata dan memperluasnya.
berendi - memprotes
Mengapa memuat satu byte membutuhkan instruksi tambahan? Arsitektur ARM telah LDRBdan STRByang melakukan byte baca / tulis dalam satu instruksi, bukan?
psmears
1
Inti M3 dapat mendukung bit-banding (tidak yakin apakah implementasi khusus ini dilakukan), di mana ruang memori periferal 1 MB alias ke wilayah 32 MB. Setiap bit memiliki alamat kata diskrit (bit 0 hanya digunakan). Agaknya masih lebih lambat dari sekedar memuat / menyimpan.
Sean Houlihane
8

The BSRRdan BRRregister adalah untuk menyiapkan dan ulang individual bit port:

Bit set port GPIO / reset register (GPIOx_BSRR)

...

(x = A..H) Bit 15: 0

BSy: Port x set bit y (y = 0..15)

Bit-bit ini hanya untuk penulisan. Membaca ke bit ini mengembalikan nilai 0x0000.

0: Tidak ada tindakan pada bit ODRx yang sesuai

1: Mengatur bit ODRx yang sesuai

Seperti yang Anda lihat, membaca register ini selalu memberi 0, oleh karena itu apa kode Anda

GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;

tidak efektif adalah GPIOE->BRR = 0 | GPIO_BRR_BR_10, tetapi optimizer tidak tahu bahwa, sehingga menghasilkan urutan LDR, ORR, STRpetunjuk bukan dari sebuah toko.

Anda dapat menghindari operasi baca-modifikasi-tulis yang mahal hanya dengan menulis

GPIOE->BSRRL = GPIO_BSRR_BS_10;
GPIOE->BRR = GPIO_BRR_BR_10;

Anda mungkin mendapatkan beberapa peningkatan lebih lanjut dengan menyelaraskan loop ke alamat yang dibagi rata dengan 8. Cobalah menempatkan satu atau mode asm("nop");instruksi sebelum while(1)loop.

berendi - protes
sumber
1

Untuk menambah apa yang telah dikatakan di sini: Tentu saja dengan Cortex-M, tetapi hampir semua prosesor (dengan pipeline, cache, prediksi cabang atau fitur lainnya), itu sepele untuk mengambil bahkan loop paling sederhana:

top:
   subs r0,#1
   bne top

Jalankan sebanyak jutaan kali seperti yang Anda inginkan, tetapi dapat memiliki kinerja loop yang sangat bervariasi, hanya dua instruksi itu, tambahkan beberapa nops di tengah jika Anda mau; itu tidak masalah.

Mengubah penyelarasan loop dapat memvariasikan kinerja secara dramatis, terutama dengan loop kecil seperti itu jika dibutuhkan dua garis pengambilan alih-alih satu, Anda memakan biaya tambahan, pada mikrokontroler seperti ini di mana flash lebih lambat daripada CPU dengan 2 atau 3 dan kemudian dengan menaikkan jam rasionya menjadi lebih buruk 3 atau 4 atau 5 daripada menambahkan penjemputan ekstra.

Anda mungkin tidak memiliki cache, tetapi jika Anda punya itu membantu dalam beberapa kasus, tetapi sakit pada orang lain dan / atau tidak membuat perbedaan. Prediksi cabang yang Anda mungkin atau mungkin tidak miliki di sini (mungkin tidak) hanya dapat melihat sejauh yang dirancang dalam pipa, jadi bahkan jika Anda mengubah loop menjadi cabang dan memiliki cabang tanpa syarat di ujungnya (lebih mudah bagi prediktor cabang untuk gunakan) semua yang dilakukan adalah menyelamatkan Anda dari banyak jam (ukuran pipa dari tempat biasanya mengambil sampai seberapa dalam prediktor dapat melihat) pada pengambilan berikutnya dan / atau tidak melakukan prefetch untuk berjaga-jaga.

Dengan mengubah perataan sehubungan dengan mengambil dan cache baris Anda dapat mempengaruhi apakah atau tidak prediktor cabang membantu Anda atau tidak, dan itu dapat dilihat dalam kinerja keseluruhan, bahkan jika Anda hanya menguji dua instruksi atau dua dengan beberapa nops .

Agak sepele untuk melakukan ini, dan begitu Anda memahami itu, kemudian mengambil kode yang dikompilasi, atau bahkan perakitan tulisan tangan, Anda dapat melihat bahwa kinerjanya dapat sangat bervariasi karena faktor-faktor ini, menambah atau menyimpan beberapa hingga beberapa ratus persen, satu baris kode C, satu nop ditempatkan dengan buruk.

Setelah belajar menggunakan register BSRR, coba jalankan kode Anda dari RAM (salin dan lompat) alih-alih flash yang seharusnya memberi Anda peningkatan kinerja instan 2 hingga 3 kali dalam eksekusi tanpa melakukan hal lain.

old_timer
sumber
0

Apakah perilaku ini diharapkan dari MCU ini?

Ini adalah perilaku kode Anda.

  1. Anda harus menulis ke register BRR / BSRR, bukan baca-modifikasi-tulis seperti yang Anda lakukan sekarang.

  2. Anda juga dikenakan overhead loop. Untuk kinerja maksimum, ulangi operasi BRR / BSRR berulang-ulang → salin dan tempel mereka dalam loop berulang kali sehingga Anda melalui banyak siklus set / reset sebelum overhead satu loop.

sunting: beberapa tes cepat di bawah IAR.

flip through writing ke BRR / BSRR membutuhkan 6 instruksi di bawah optimisasi sedang dan 3 instruksi di bawah level optimasi tertinggi; flip melalui RMW'ng membutuhkan 10 instruksi / 6 instruksi.

loop overhead tambahan.

dannyf
sumber
Dengan mengubah |=ke =fase set / reset bit tunggal mengkonsumsi 9 siklus clock ( tautan ). Kode perakitan terdiri dari 3 instruksi:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR
1
Jangan membuka gulungan secara manual. Itu praktis bukan ide yang bagus. Dalam kasus khusus ini, ini sangat berbahaya: itu membuat bentuk gelombang non-periodik. Juga, memiliki kode yang sama berkali-kali dalam flash tidak selalu lebih cepat. Ini mungkin tidak berlaku di sini (mungkin!), Tetapi membuka gulungan adalah sesuatu yang banyak orang pikir dapat membantu, bahwa kompiler ( gcc -funroll-loops) dapat melakukannya dengan sangat baik, dan bahwa ketika disalahgunakan (seperti di sini) memiliki efek kebalikan dari apa yang Anda inginkan.
Marcus Müller
Infinite loop tidak pernah dapat dibuka secara efektif untuk mempertahankan perilaku pengaturan waktu yang konsisten.
Marcus Müller
1
@ MarcusMüller: Loop tak terbatas kadang-kadang dapat dibuka dengan berguna sambil mempertahankan waktu yang konsisten jika ada poin dalam beberapa pengulangan loop di mana instruksi tidak akan memiliki efek yang terlihat. Sebagai contoh, jika somePortLatchmengontrol port yang 4 bit lebih rendahnya ditetapkan untuk output, dimungkinkan untuk membuka while(1) { SomePortLatch ^= (ctr++); }kode yang menghasilkan 15 nilai dan kemudian loop kembali untuk memulai pada saat ketika itu akan menampilkan nilai yang sama dua kali berturut-turut.
supercat
Supercat, benar. Juga, efek seperti waktu antarmuka memori dll mungkin membuatnya masuk akal untuk "membuka sebagian". Pernyataan saya terlalu umum, tetapi saya merasa nasihat Danny bahkan lebih menggeneralisasi, dan bahkan berbahaya
Marcus Müller