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:
- Jam HSI (8 MHz) dihidupkan;
- PLL dimulai dengan prescaler 16 untuk mencapai HSI / 2 * 16 = 64 MHz;
- PLL ditunjuk sebagai SYSCLK;
- 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:
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?
Jawaban:
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
-g
bendera yang ditambahkan keCFLAGS
(tempat yang sama di mana Anda juga menentukan-O1
). Kemudian, lihat perakitan yang dihasilkan:Perhatikan bahwa tentu saja nama
objdump
biner serta file ELF perantara Anda mungkin berbeda.Biasanya, Anda juga bisa melewatkan bagian tempat GCC memanggil assembler dan cukup melihat file assembly. Tambahkan saja
-S
ke 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 :
dan dapatkan yang berikut (kutipan, kode lengkap di bawah tautan di atas):
Yang merupakan loop (perhatikan lompatan tanpa syarat ke .L5 di akhir dan label .L5 di awal).
Apa yang kita lihat di sini adalah kita
ldr
(muat register) registerr2
dengan nilai di lokasi memori disimpan dalamr3
+ 24 Bytes. Menjadi terlalu malas untuk melihat ke atas: sangat mungkin lokasiBSRR
.OR
yangr2
mendaftar dengan konstan1024 == (1<<10)
, yang akan sesuai dengan pengaturan bit-10 dalam register itu, dan menulis hasilnya ker2
itu sendiri.str
(simpan) hasil di lokasi memori yang telah kita baca di langkah pertamaBRR
alamat.b
(cabang) kembali ke langkah pertama.Jadi kita punya 7 instruksi, bukan tiga, untuk memulai. Hanya yang
b
terjadi 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 bahwab
dibutuhkan 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 siklusstr
membutuhkan 2 siklusb
membutuhkan 2 hingga 4 siklus. Kita tahu itu pasti angka ganjil, jadi harus 3, di sini.Itu semua sejalan dengan pengamatan Anda:
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
-O3
seperti-O1
dengan versi compiler saya. Anda harus melakukannya sendiri! Mungkin Anda menggunakan beberapa versi kuno GCC dengan dukungan ARM suboptimal.sumber
=
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?-O3
kesalahan 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
uxth
ada karenaGPIO->BSRRL
(salah) didefinisikan sebagai register 16 bit di header Anda. Gunakan versi header terbaru, dari perpustakaan STM32CubeF3 , di mana tidak ada BSRRL dan BSRRH, tetapiBSRR
register 32 bit tunggal . @Marcus tampaknya memiliki tajuk yang benar, sehingga kodenya melakukan akses penuh 32 bit alih-alih memuat setengah kata dan memperluasnya.LDRB
danSTRB
yang melakukan byte baca / tulis dalam satu instruksi, bukan?The
BSRR
danBRR
register adalah untuk menyiapkan dan ulang individual bit port:Seperti yang Anda lihat, membaca register ini selalu memberi 0, oleh karena itu apa kode Anda
tidak efektif adalah
GPIOE->BRR = 0 | GPIO_BRR_BR_10
, tetapi optimizer tidak tahu bahwa, sehingga menghasilkan urutanLDR
,ORR
,STR
petunjuk bukan dari sebuah toko.Anda dapat menghindari operasi baca-modifikasi-tulis yang mahal hanya dengan menulis
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 sebelumwhile(1)
loop.sumber
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:
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.
sumber
Ini adalah perilaku kode Anda.
Anda harus menulis ke register BRR / BSRR, bukan baca-modifikasi-tulis seperti yang Anda lakukan sekarang.
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.
sumber
|=
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
gcc -funroll-loops
) dapat melakukannya dengan sangat baik, dan bahwa ketika disalahgunakan (seperti di sini) memiliki efek kebalikan dari apa yang Anda inginkan.somePortLatch
mengontrol port yang 4 bit lebih rendahnya ditetapkan untuk output, dimungkinkan untuk membukawhile(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.