Nhận hiệu suất nhanh từ MCU STM32


11

Tôi đang làm việc với bộ khám phá STM32F303VC và tôi hơi bối rối bởi hiệu suất của nó. Để làm quen với hệ thống, tôi đã viết một chương trình rất đơn giản chỉ để kiểm tra tốc độ đập của MCU này. Mã có thể được chia nhỏ như sau:

  1. Đồng hồ HSI (8 MHz) được bật;
  2. PLL được bắt đầu với bộ tổng hợp trước là 16 để đạt được HSI / 2 * 16 = 64 MHz;
  3. PLL được chỉ định là SYSCLK;
  4. SYSCLK được theo dõi trên chân MCO (PA8) và một trong các chân (PE10) liên tục được bật trong vòng lặp vô hạn.

Mã nguồn cho chương trình này được trình bày dưới đây:

#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;

      }
}

Mã được biên dịch với CoIDE V2 với GNU ARM Embedded Toolchain bằng cách sử dụng tối ưu hóa -O1. Các tín hiệu trên chân PA8 (MCO) và PE10, được kiểm tra bằng máy hiện sóng, trông như thế này: nhập mô tả hình ảnh ở đây

SYSCLK dường như được cấu hình đúng, vì MCO (đường cong màu cam) thể hiện dao động gần 64 MHz (xem xét biên độ lỗi của đồng hồ bên trong). Phần kỳ lạ đối với tôi là hành vi trên PE10 (đường cong màu xanh). Trong vòng lặp while (1) vô hạn, phải mất 4 + 4 + 5 = 13 chu kỳ xung nhịp để thực hiện thao tác 3 bước cơ bản (nghĩa là bit-set / bit-reset / return). Nó thậm chí còn tệ hơn ở các mức tối ưu hóa khác (ví dụ -O2, -O3, ar -Os): một số chu kỳ xung nhịp bổ sung được thêm vào phần THẤP của tín hiệu, tức là giữa các cạnh rơi và tăng của PE10 (cho phép LSI dường như bằng cách nào đó để khắc phục tình trạng này).

Là hành vi này được mong đợi từ MCU này? Tôi sẽ tưởng tượng một nhiệm vụ đơn giản như thiết lập và đặt lại một chút phải nhanh hơn 2-4 lần. Có cách nào để tăng tốc mọi thứ?


Bạn đã thử với một số MCU khác để so sánh chưa?
Marko Buršič

3
Những gì bạn đang cố gắng để đạt được? Nếu bạn muốn một đầu ra dao động nhanh, bạn nên sử dụng bộ định thời. Nếu bạn muốn giao diện với các giao thức nối tiếp nhanh, bạn nên sử dụng thiết bị ngoại vi phần cứng tương ứng.
Jonas Schäfer

2
Bắt đầu tuyệt vời với bộ !!
Scott Seidman

Bạn không được | = các thanh ghi BSRR hoặc BRR vì chúng chỉ được ghi.
P__J__

Câu trả lời:


25

Câu hỏi ở đây thực sự là: mã máy bạn đang tạo từ chương trình C là gì và nó khác với những gì bạn mong đợi.

Nếu bạn không có quyền truy cập vào mã gốc, thì đây sẽ là một bài tập về kỹ thuật đảo ngược (về cơ bản là một cái gì đó bắt đầu bằng radare2 -A arm image.bin; aaa; VV:), nhưng bạn đã có mã để việc này dễ dàng hơn.

Đầu tiên, biên dịch nó với -gcờ được thêm vào CFLAGS(cùng một nơi mà bạn cũng chỉ định -O1). Sau đó, nhìn vào hội đồng được tạo ra:

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

Tất nhiên lưu ý rằng cả tên của objdumptệp nhị phân cũng như tệp ELF trung gian của bạn có thể khác nhau.

Thông thường, bạn cũng có thể bỏ qua phần mà GCC gọi trình biên dịch chương trình và chỉ cần nhìn vào tệp lắp ráp. Chỉ cần thêm -Svào dòng lệnh GCC - nhưng điều đó thường sẽ phá vỡ bản dựng của bạn, vì vậy rất có thể bạn sẽ làm điều đó bên ngoài IDE của mình.

Tôi đã thực hiện việc lắp ráp một phiên bản vá lỗi của mã của bạn :

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

và nhận được những điều sau đây (đoạn trích, mã đầy đủ theo liên kết ở trên):

.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

Đó là một vòng lặp (chú ý bước nhảy vô điều kiện đến .L5 ở cuối và nhãn .L5 ở đầu).

Những gì chúng ta thấy ở đây là chúng ta

  • đầu tiên ldr(thanh ghi tải) thanh ghi r2với giá trị tại vị trí bộ nhớ được lưu trữ trong r3+ 24 byte. Quá lười biếng để tìm kiếm điều đó: rất có thể là vị trí của BSRR.
  • Sau đó, ORthanh r2ghi với hằng số 1024 == (1<<10), tương ứng với việc thiết lập bit thứ 10 trong thanh ghi đó và ghi kết quả vào r2chính nó.
  • Sau đó str(lưu trữ) kết quả ở vị trí bộ nhớ mà chúng ta đã đọc từ bước đầu tiên
  • và sau đó lặp lại tương tự cho một vị trí bộ nhớ khác, vì sự lười biếng: rất có thể BRRlà địa chỉ.
  • Cuối cùng b(nhánh) trở lại bước đầu tiên.

Vì vậy, chúng tôi có 7 hướng dẫn, không phải ba, để bắt đầu. Chỉ bxảy ra một lần và do đó rất có thể những gì đang diễn ra với số lượng chu kỳ kỳ lạ (chúng ta có tổng cộng 13 chu kỳ, do đó, một số chu kỳ lẻ phải xuất phát từ đâu đó). Vì tất cả các số lẻ dưới 13 là 1, 3, 5, 7, 9, 11 và chúng tôi có thể loại trừ bất kỳ số nào lớn hơn 13-6 (giả sử CPU không thể thực hiện một lệnh trong ít hơn một chu kỳ), chúng tôi biết rằng bphải mất 1, 3, 5, 7 hoặc chu kỳ CPU.

Chúng ta là ai, tôi đã xem tài liệu của ARM về hướng dẫn và họ đã thực hiện bao nhiêu chu kỳ cho M3:

  • ldr mất 2 chu kỳ (trong hầu hết các trường hợp)
  • orr mất 1 chu kỳ
  • str mất 2 chu kỳ
  • bmất 2 đến 4 chu kỳ. Chúng tôi biết nó phải là một số lẻ, vì vậy phải mất 3, ở đây.

Đó là tất cả các dòng với quan sát của bạn:

13= =2(ctôidr+corr+cStr)+cb= =2(2+1+2)+3= =25+3

Như tính toán trên cho thấy, hầu như không có cách nào làm cho vòng lặp của bạn nhanh hơn - các chân đầu ra trên bộ xử lý ARM thường được ánh xạ bộ nhớ , không phải là các thanh ghi lõi CPU, do đó bạn phải thực hiện quy trình lưu trữ tải sửa đổi thông thường nếu bạn muốn làm bất cứ điều gì với những người đó.

Tất nhiên những gì bạn có thể làm là không đọc ( |=hoàn toàn để đọc) giá trị của pin mỗi vòng lặp, nhưng chỉ cần viết giá trị của một biến cục bộ với nó, mà bạn chỉ cần chuyển đổi tất cả các vòng lặp.

Lưu ý rằng tôi cảm thấy như bạn có thể quen thuộc với micros 8 bit và sẽ cố gắng chỉ đọc các giá trị 8 bit, lưu trữ chúng trong các biến 8 bit cục bộ và viết chúng thành các khối 8 bit. Đừng. ARM là một kiến ​​trúc 32 bit và trích xuất 8 bit của từ 32 bit có thể cần thêm hướng dẫn. Nếu bạn có thể, chỉ cần đọc toàn bộ từ 32 bit, sửa đổi những gì bạn cần và viết lại toàn bộ. Tất nhiên điều đó có khả thi hay không phụ thuộc vào những gì bạn đang viết, tức là cách bố trí và chức năng của GPIO được ánh xạ bộ nhớ của bạn. Tham khảo hướng dẫn về biểu dữ liệu / hướng dẫn sử dụng STM32F3 để biết thông tin về những gì được lưu trữ trong 32 bit chứa bit bạn muốn chuyển đổi.


Bây giờ, tôi đã cố gắng tái tạo vấn đề của bạn với thời gian "thấp" ngày càng dài hơn, nhưng tôi đơn giản là không thể - vòng lặp trông giống hệt -O3như với -O1phiên bản trình biên dịch của tôi. Bạn sẽ phải tự làm điều đó! Có thể bạn đang sử dụng một số phiên bản GCC cổ với sự hỗ trợ ARM tối ưu.


4
Sẽ không chỉ lưu trữ ( =thay vì |=), như bạn nói, chính xác là tốc độ mà OP đang tìm kiếm? Lý do ARM có các thanh ghi BRR và BSRR riêng biệt là không yêu cầu đọc-sửa đổi-ghi. Trong trường hợp này, các hằng số có thể được lưu trữ trong các thanh ghi bên ngoài vòng lặp, vì vậy vòng lặp bên trong sẽ chỉ là 2 str và một nhánh, vì vậy 2 + 2 +3 = 7 chu kỳ cho cả vòng?
Timo

Cảm ơn. Điều đó thực sự đã xóa mọi thứ lên một chút. Đó là một chút suy nghĩ vội vàng khi nhấn mạnh rằng chỉ cần 3 chu kỳ đồng hồ - 6 đến 7 chu kỳ là điều mà tôi thực sự hy vọng. Các -O3lỗi dường như đã biến mất sau khi làm sạch và xây dựng lại các giải pháp. Tuy nhiên, mã lắp ráp của tôi dường như có một hướng dẫn UTXH bổ sung trong đó: .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
uxthcó bởi vì GPIO->BSRRL(không chính xác) được định nghĩa là một thanh ghi 16 bit trong các tiêu đề của bạn. Sử dụng một phiên bản gần đây của các tiêu đề, từ các thư viện STM32CubeF3 , nơi không có BSRRL và BSRRH, nhưng chỉ có một BSRRthanh ghi 32 bit . @Marcus rõ ràng có các tiêu đề chính xác, vì vậy mã của anh ta truy cập đầy đủ 32 bit thay vì tải một nửa câu và mở rộng nó.
berendi - phản đối

Tại sao tải một byte sẽ có thêm hướng dẫn? Kiến trúc ARM có LDRBSTRBthực hiện đọc / ghi byte trong một lệnh đơn, không?
psmears

1
Lõi M3 có thể hỗ trợ dải bit (không chắc việc triển khai cụ thể này có thực hiện không), trong đó vùng không gian bộ nhớ ngoại vi 1 MB được đặt bí danh cho vùng 32 MB. Mỗi bit có một địa chỉ từ riêng biệt (chỉ sử dụng bit 0). Có lẽ vẫn còn chậm hơn chỉ là một tải / cửa hàng.
Sean Houlihane

8

Các BSRRvà các BRRthanh ghi là để thiết lập và đặt lại các bit cổng riêng lẻ:

Thanh ghi thiết lập / thiết lập lại cổng GPIO (GPIOx_BSRR)

...

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

BSy: Cổng x đặt bit y (y = 0..15)

Các bit này chỉ ghi. Việc đọc các bit này trả về giá trị 0x0000.

0: Không có hành động nào đối với bit ODRx tương ứng

1: Đặt bit ODRx tương ứng

Như bạn có thể thấy, đọc các thanh ghi này luôn cho 0, do đó mã của bạn là gì

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

thực hiện có hiệu quả là GPIOE->BRR = 0 | GPIO_BRR_BR_10, nhưng tôi ưu hoa không biết rằng, vì vậy nó tạo ra một chuỗi các LDR, ORR, STRhướng dẫn thay vì một cửa hàng duy nhất.

Bạn có thể tránh thao tác đọc-sửa-ghi đắt tiền bằng cách viết đơn giản

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

Bạn có thể nhận được một số cải tiến hơn nữa bằng cách căn chỉnh vòng lặp thành một địa chỉ chia đều cho 8. Hãy thử đặt một hoặc các asm("nop");hướng dẫn chế độ trước while(1)vòng lặp.


1

Để thêm vào những gì đã được nói ở đây: Chắc chắn với Cortex-M, nhưng hầu như bất kỳ bộ xử lý nào (với một đường ống, bộ đệm, dự đoán nhánh hoặc các tính năng khác), thật đơn giản để thực hiện ngay cả vòng lặp đơn giản nhất:

top:
   subs r0,#1
   bne top

Chạy nó nhiều triệu lần như bạn muốn, nhưng có thể có hiệu suất của vòng lặp đó rất khác nhau, chỉ cần hai hướng dẫn đó, thêm một số bước ở giữa nếu bạn muốn; nó không thành vấn đề

Thay đổi căn chỉnh của vòng lặp có thể thay đổi hiệu suất đáng kể, đặc biệt là với một vòng lặp nhỏ như vậy nếu phải mất hai dòng tìm nạp thay vì một dòng, bạn sẽ phải trả thêm chi phí đó, trên một vi điều khiển như thế này khi flash chậm hơn CPU 2 lần hoặc 3 và sau đó bằng cách tăng đồng hồ, tỷ lệ thậm chí còn tệ hơn 3 hoặc 4 hoặc 5 so với việc thêm tìm nạp.

Bạn có thể không có bộ đệm, nhưng nếu bạn có nó sẽ giúp ích trong một số trường hợp, nhưng nó gây tổn thương cho những người khác và / hoặc không tạo ra sự khác biệt. Dự đoán chi nhánh mà bạn có thể có hoặc không có ở đây (có thể không) chỉ có thể nhìn xa như được thiết kế trong đường ống, vì vậy ngay cả khi bạn thay đổi vòng lặp để phân nhánh ra và có một nhánh vô điều kiện ở cuối (công cụ dự đoán nhánh dễ dàng hơn sử dụng) tất cả những gì giúp bạn tiết kiệm được nhiều đồng hồ (kích thước của đường ống từ nơi nó thường tìm nạp đến mức độ sâu của người dự đoán có thể nhìn thấy) trong lần tìm nạp tiếp theo và / hoặc nó không thực hiện tìm nạp trước trong trường hợp.

Bằng cách thay đổi căn chỉnh liên quan đến tìm nạp và dòng bộ đệm, bạn có thể ảnh hưởng đến việc công cụ dự đoán nhánh có giúp bạn hay không, và điều đó có thể được nhìn thấy trong hiệu suất tổng thể, ngay cả khi bạn chỉ kiểm tra hai hướng dẫn hoặc hai lệnh đó với một số bước .

Làm điều này hơi tầm thường và một khi bạn hiểu rằng, sau đó lấy mã được biên dịch, hoặc thậm chí lắp ráp bằng tay, bạn có thể thấy rằng hiệu suất của nó có thể thay đổi lớn do các yếu tố này, thêm hoặc tiết kiệm vài đến vài trăm phần trăm, một dòng mã C, một nop được đặt kém.

Sau khi học cách sử dụng thanh ghi BSRR, hãy thử chạy mã của bạn từ RAM (sao chép và nhảy) thay vì flash sẽ giúp bạn tăng hiệu suất ngay lập tức từ 2 đến 3 lần trong khi thực hiện mà không phải làm gì khác.


0

Là hành vi này được mong đợi từ MCU này?

Đó là một hành vi của mã của bạn.

  1. Bạn nên ghi vào các thanh ghi BRR / BSRR, không đọc-sửa-ghi như bạn làm bây giờ.

  2. Bạn cũng phải chịu chi phí vòng lặp. Để có hiệu suất tối đa, hãy lặp đi lặp lại các thao tác BRR / BSRR → sao chép và dán chúng trong vòng lặp nhiều lần để bạn trải qua nhiều chu kỳ thiết lập / đặt lại trước một vòng lặp.

chỉnh sửa: một số bài kiểm tra nhanh theo IAR.

lật qua viết cho BRR / BSRR có 6 hướng dẫn dưới mức tối ưu hóa vừa phải và 3 hướng dẫn dưới mức tối ưu hóa cao nhất; lướt qua RMW'ng mất 10 hướng dẫn / 6 hướng dẫn.

vòng lặp phụ thêm.


Bằng cách thay đổi |=sang =giai đoạn thiết lập / thiết lập bit đơn sẽ tiêu tốn 9 chu kỳ xung nhịp ( liên kết ). Mã lắp ráp dài 3 hướng dẫn:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR

1
Đừng bỏ qua các vòng lặp thủ công. Đó thực tế không bao giờ là một ý tưởng tốt. Trong trường hợp cụ thể này, nó đặc biệt đáng sợ: nó làm cho dạng sóng không theo chu kỳ. Ngoài ra, có cùng mã nhiều lần trong flash không nhất thiết phải nhanh hơn. Điều này có thể không áp dụng ở đây (có thể!), Nhưng việc không kiểm soát vòng lặp là điều mà nhiều người nghĩ là có ích, trình biên dịch ( gcc -funroll-loops) có thể làm rất tốt và khi bị lạm dụng (như ở đây) có tác dụng ngược với những gì bạn muốn.
Marcus Müller

Một vòng lặp vô hạn không bao giờ có thể được kiểm soát một cách hiệu quả để duy trì một hành vi thời gian nhất quán.
Marcus Müller

1
@ MarcusMüller: Các vòng lặp vô hạn đôi khi có thể được kiểm soát một cách hữu ích trong khi duy trì thời gian nhất quán nếu có bất kỳ điểm nào trong một số lần lặp lại của vòng lặp trong đó một lệnh sẽ không có hiệu lực rõ ràng. Ví dụ: nếu somePortLatchđiều khiển một cổng có 4 bit thấp hơn được đặt cho đầu ra, có thể hủy đăng ký while(1) { SomePortLatch ^= (ctr++); }thành mã tạo ra 15 giá trị và sau đó lặp lại để bắt đầu tại thời điểm khi nó xuất ra cùng một giá trị hai lần liên tiếp.
supercat

Siêu xe, đúng. Ngoài ra, các hiệu ứng như thời gian của giao diện bộ nhớ, v.v. có thể làm cho nó trở nên nhạy cảm khi "không kiểm soát" một phần. Tuyên bố của tôi quá chung chung, nhưng tôi cảm thấy lời khuyên của Danny thậm chí còn khái quát hơn, và thậm chí còn nguy hiểm như vậy
Marcus Müller
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.