24 października 2018

GPIO raz jeszcze

Obsługa linii wejścia-wyjścia, czyli GPIO to pozornie prosty temat. Niestety jak zwykle pozory mylą.
Tym razem zajmę się prędkością działania wyjść. Za ustawianie prędkości portu odpowiedzialny jestr rejestr GPIO_OSPEEDR:

Jak łatwo zauważyć, każdy pin jest konfigurowany za pomocą dwóch bitów. Mamy możliwość ustawienia trzech prędkości przełączania wyprowadzenia: niskiej, średniej i wysokiej.
Co znaczą te określenia odnajdziemy w nocie katalogowej naszego mikrokontrolera:
Postanowiłem sprawdzić jak działanie wygląda w praktyce. Napisałem więc prosty program, który jednocześnie przełącza trzy piny - z których każdy ma inne ustawienia prędkości:

#include "stm32f0xx.h"

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

int main(void)
{
 clock_init();

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER9_0 | GPIO_MODER_MODER5_0;

 GPIOC->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR9_0;
 GPIOC->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR5_0|GPIO_OSPEEDER_OSPEEDR5_1;

 while (1)
 {
  GPIOC->BSRR = GPIO_BSRR_BS_8 | GPIO_BSRR_BS_9 | GPIO_BSRR_BS_5;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);

  GPIOC->BSRR = GPIO_BSRR_BR_8 | GPIO_BSRR_BR_9 | GPIO_BSRR_BR_5;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);
 }
}

W ramach testów podłączyłem oscyloskop do wspomnianych wyprowadzeń. Wolnozmienny sygnał nie pokazuje różnic między ustawieniami pinów:
Jednak po zmianie podstawy czasu widać różnice w czasie narastania sygnału. Kanał 1 to wysoka prędkość działania wyjścia, drugi - średnia i trzeci - niska:


Podobnie sytuacja wygląda dla zbocza opadającego:


Właściwie na tym mógłbym zakończyć testowanie prędkości wyprowadzeń GPIO, ale nie byłbym sobą gdybym nie spróbował napisać programu, który wygeneruje możliwie wysoką częstotliwość na wyjściu.
Usunięcie opóźnień z prezentowanego wcześniej programu niewiele pomogło. Kolejnym krokiem było więc ustawienie maksymalnej optymalizacji kodu. Niestety nadal uzyskana częstotliwość niewiele przekraczała 2 MHz.
Postanowiłem więc zmienić C na stary dobry asembler i przygotowałem taką funkcję testową:

 .syntax unified
   .thumb

 .global test_gpio

 .equ GPIOC, 0x48000800
 .equ BSRR, 0x18
 .equ BRR, 0x28

 .section .text
 .type test_gpio, %function
test_gpio:
 ldr  r0, =0x0100 | 0x0200 | 0x0020
 ldr  r1, =GPIOC
 ldr  r2, =GPIOC
main_loop:
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]

 b main_loop

Powtarzanie instrukcji str zamiast prostej pętli miało na celu wyeliminowanie opóźnienia instrukcji skoku.
Efekt był dużo lepszy niż w przypadku języka C:

Nie dawało mi jednak spokoju skąd się bierze asymetria sygnału. Postanowiłem więc przenieść program testowy z pamięci Flash do SRAM. Efekt był znacznie lepszy:

Wykonanie instrukcji LDR oraz STR zajmuje 2 cykle maszynowe. Przy częstotliwości taktowania 48 MHz utrzymujemy więc 12 MHz na wyjściu.

Wniosek jest taki, że STM32F030 nie jest demonem prędkości. No i warto pamiętać, że opóźnienia dostępów do pamięci flash mogą mieć wpływ na działanie programu.

1 komentarz: