17 lipca 2018

Komunikacja przez SPI

Ten wpis będzie krótki - właściwie nie planowałem zajmować się SPI, to w końcu taki prosty interfejs. Z drugiej strony okazało się, że jego obsługa konieczna jest do uruchomienia LCD. Więc tym razem krótko i prosto - komunikacja przez SPI.

Na początek małe sprzątanie kodu. Do przetestowania SPI wygodnie będzie wysłać otrzymane dane przez UART, więc utworzyłem odpowiedni moduł. Plik nagłówkowy uart.h:

#ifndef __UART__
#define __UART__

#include <stdint.h>

void uart_init(void);

void usart_putc(char c);

void usart_print_hex(uint8_t value);

void usart_send(const char *s);

#endif // __UART__

Oraz sam kod uart.c:

#include "uart.h"
#include "pll.h"
#include "stm32f429xx.h"

void uart_init(void)
{
        GPIOA->MODER |= GPIO_MODER_MODE9_1|GPIO_MODER_MODE10_1;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL9_0 | GPIO_AFRH_AFSEL9_1 | GPIO_AFRH_AFSEL9_2;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL10_0 | GPIO_AFRH_AFSEL10_1 | GPIO_AFRH_AFSEL10_2;

        RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
        USART1->BRR = APB2_FREQ / 115200;
        USART1->CR3 = 0;
        USART1->CR2 = 0;
        USART1->CR1 = USART_CR1_TE | USART_CR1_RE;
        for (volatile uint32_t dly = 0; dly < 1000; dly++);
        USART1->CR1 |= USART_CR1_UE;
}

void usart_putc(char c)
{
        while ((USART1->SR & USART_SR_TXE) == 0);
        USART1->DR = (int)c;
}

void usart_print_hex(uint8_t value)
{
        const char hex2char[] = "0123456789abcdef";

        usart_putc(hex2char[value >> 4]);
        usart_putc(hex2char[value & 0x0f]);
}

void usart_send(const char *s)
{
        while (*s)
                usart_putc(*s++);
}

Do komunikacji przez SPI oczywiście przydałby się jakiś moduł. Na płytce discovery znajdziemy trzyosiowy żyroskop L3GD20.

 Jak widać jest podłączony do interfejsu SPI5 - tego samego co wyświetlacz.
Obsługa SPI jest bardzo prosta. Jak zwykle musimy najpierw uruchomić zegar modułu i  skonfigurować odpowiednie piny - podobnie jak poprzednio użyję funkcji pin_config, chociaż przyznam się do lenistwa i jej skopiowania. Kiedyś trzeba będzie to poprawić:

        RCC->APB2ENR |= RCC_APB2ENR_SPI5EN;
 

        config_pin(GPIOF, 7, 5);
        config_pin(GPIOF, 8, 5);
        config_pin(GPIOF, 9, 5);


Linią wyboru układu CS będziemy sterować programowo, więc jest to zwykły pin GPIO:

        GPIOC->MODER |= GPIO_MODER_MODE1_0;
        GPIOC->BSRR = GPIO_BSRR_BS1;


Na koniec sama konfiguracja i uruchomienie modułu SPI:

        SPI5->CR1 = SPI_CR1_BR_2 | SPI_CR1_BR_0 | SPI_CR1_MSTR |
                    SPI_CR1_SSM | SPI_CR1_SSI;
        SPI5->CR1 |= SPI_CR1_SPE;


BR2 i BR0 ustawiają dzielnik zegara dla SPI, pozostałe piny uruchamiają interfejs w trybie master. Plik nagłówkowy spi.h:


#ifndef __SPI__
#define __SPI__

#include <stdint.h>

void spi_init(void);

void spi_start(void);

void spi_stop(void);

uint8_t spi_sendrecv(uint8_t tx);

#endif // __SPI__

Implementacja spi.c:

#include "spi.h"
#include "stm32f429xx.h"

static void config_pin(GPIO_TypeDef* port, uint8_t pin, uint32_t func)
{
        if (pin < 8) {
                port->AFR[0] |=  func << (pin * 4);
                port->AFR[0] |=  func << (pin * 4);
        } else {
                port->AFR[1] |=  func << ((pin - 8) * 4);
                port->AFR[1] |=  func << ((pin - 8) * 4);
        }
        port->MODER |= 2 << (pin * 2);
        port->OSPEEDR |= 2 << (pin * 2);
}

void spi_init(void)
{
        RCC->APB2ENR |= RCC_APB2ENR_SPI5EN;

        config_pin(GPIOF, 7, 5);
        config_pin(GPIOF, 8, 5);
        config_pin(GPIOF, 9, 5);

        GPIOC->MODER |= GPIO_MODER_MODE1_0;
        GPIOC->BSRR = GPIO_BSRR_BS1;

        SPI5->CR1 = SPI_CR1_BR_2 | SPI_CR1_BR_0 | SPI_CR1_MSTR |
                    SPI_CR1_SSM | SPI_CR1_SSI;
        SPI5->CR1 |= SPI_CR1_SPE;
}

uint8_t spi_sendrecv(uint8_t tx)
{
        while ((SPI5->SR & SPI_SR_TXE) == 0);
        SPI5->DR = tx;
        while ((SPI5->SR & SPI_SR_RXNE) == 0);

        return SPI5->DR;
}

void spi_start(void)
{
        GPIOC->BSRR = GPIO_BSRR_BR1;
}

void spi_stop(void)
{
        while ((SPI5->SR & SPI_SR_TXE) == 0);
        while (SPI5->SR & SPI_SR_BSY);

        GPIOC->BSRR = GPIO_BSRR_BS1;
}

Teraz wystarczy napisać program główny i przetestować komunikację. Do testów najłatwiej będzie odczytać rejestr WHO_AM_I układu L3GD20:


Jak widać poprawna wartość to 0xd4. Przy okazji małe ułatwienie - dodałem przekierowanie printf() na uart. To trochę rozrzutność jak chodzi o kod, ale stm32f429 ma ogromną jak na mikrokontroler pamięć Flash, więc warto chociaż zapamiętać że taka opcja istnieje. Kod main.c:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "stm32f429xx.h"
#include "pll.h"
#include "uart.h"
#include "spi.h"

int _write(int fd, char *str, int len)
{
        int i;
        for (i = 0; i < len; i++)
                usart_putc(str[i]);
        return len;
}

int main(int argc, char *argv[])
{
        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOCEN | RCC_AHB1ENR_GPIOFEN |
                        RCC_AHB1ENR_GPIOGEN;

        pll_init();
        uart_init();
        spi_init();

        printf("Hello world!\r\n");

        spi_init();

        while (1) {
                printf("Starting SPI communication...\r\n");

                spi_start();

                uint8_t r = spi_sendrecv(0x8f);
                printf("Received: %02x\r\n", r);

                r = spi_sendrecv(0x00);
                printf("Received: %02x\r\n", r);

                spi_stop();

                printf("done.\r\n");
                for (volatile uint32_t dly = 0; dly < 10000000; dly++);
        }

        return 0;
}

Po uruchomieniu program wypisuje oczekiwaną wartość, czyli 0xd4 a analizator logiczny pokazuję piękny przebieg. Skoro SPI już działa, czas zająć się sterownikiem wyświetlacza.

14 lipca 2018

Pamięć SDRAM

Mikrokontroler STM32F429ZI jest wyposażony w 256KB pamięci RAM. To całkiem sporo jak na mikrokontroler - z drugiej strony pamięci zawsze brakuje, więc przydałoby się więcej. Na płytce ewaluacyjnej znajdziemy 8MB (czyli 64Mb) pamięci SDRAM IS42S16400J. Więcej o układzie jest w nocie katalogowej: http://www.issi.com/WW/pdf/42-45S16400J.pdf
Aby skorzystać z tej dodatkowej pamięci użyjemy modułu FMC, czyli Flexible Memory Controller.
Konfiguracja w pierwszej chwili może wydawać się skomplikowana, ale jak ją raz przejdziemy używanie pamięci jest bardzo proste - zaczynając od adresu 0xd0000000 mamy do dyspozycji 8 megabajtów pamięci. Dostęp do niej jest wolniejszy niż do wbudowanej pamięci RAM, ale poza tym działa identycznie.
Ponieważ kod konfiguracyjny FMC jest nieco zawiły, najlepiej wydzielić go do osobnego modułu. Jego interfejs będzie w pliku sdram.h:

#ifndef __SDRAM__
#define __SDRAM__

#define SDRAM_ADDR              0xd0000000

void sdram_init(void);

#endif // __SDRAM__

Mamy więc stałą z adresem pamięci oraz jedną funkcję która ją zainicjalizuje.
Plik sdram.c zawiera implementację i jest nieco bardziej złożony.
Po pierwsze plik nagłówkowy stm32f4xx.h okazuje się niewystarczający. Nie ma w nim definicji związanych z naszym modelem pamięci, ale brakuje również niektórych definicji dla rejestrów FMC. Ich definicje są częściowo przeniesione z przykładów dostarczonych do zestawu discovery.

#define FMC_SDCMR_MODE_NORMAL                   0
#define FMC_SDCMR_MODE_CLK_CFG_EN               1
#define FMC_SDCMR_MODE_PALL                     2
#define FMC_SDCMR_MODE_AUTO_REFRESH             3
#define FMC_SDCMR_MODE_LMR                      4
#define FMC_SDCMR_MODE_SELF_REFRESH             5
#define FMC_SDCMR_MODE_POWER_DOWN               6
#define FMC_SDCMR_NRFS_CYCLES(n)                (((n) - 1) << 5)
#define FMC_SDCMR_LMR_MRD(n)                    ((n) << 9)

#define FMC_SDRTR_COUNT_VALUE(n)                ((n) << 1)

#define FMC_SDCR_COLS_8b                        0x00000000
#define FMC_SDCR_COLS_9b                        0x00000001
#define FMC_SDCR_COLS_10b                       0x00000002
#define FMC_SDCR_COLS_11b                       0x00000003
#define FMC_SDCR_ROWS_11b                       0x00000000
#define FMC_SDCR_ROWS_12b                       0x00000004
#define FMC_SDCR_ROWS_13b                       0x00000008
#define FMC_SDCR_DATA_8b                        0x00000000
#define FMC_SDCR_DATA_16b                       0x00000010
#define FMC_SDCR_DATA_32b                       0x00000020
#define FMC_SDCR_BANKS_2                        0x00000000
#define FMC_SDCR_BANKS_4                        0x00000040
#define FMC_SDCR_CAS_1                          0x00000080
#define FMC_SDCR_CAS_2                          0x00000100
#define FMC_SDCR_CAS_3                          0x00000180
#define FMC_SDCR_WP                             0x00000200
#define FMC_SDCR_CLK_DIV2                       0x00000800
#define FMC_SDCR_CLK_DIV3                       0x00000c00

#define FMC_SDTR_TMRD_VALUE(n)                  (((n) - 1) << 0)
#define FMC_SDTR_TXSR_VALUE(n)                  (((n) - 1) << 4)
#define FMC_SDTR_TRAS_VALUE(n)                  (((n) - 1) << 8)
#define FMC_SDTR_TRC_VALUE(n)                   (((n) - 1) << 12)
#define FMC_SDTR_TWR_VALUE(n)                   (((n) - 1) << 16)
#define FMC_SDTR_TRP_VALUE(n)                   (((n) - 1) << 20)
#define FMC_SDTR_TRCD_VALUE(n)                  (((n) - 1) << 24)

#define SDRAM_LMR_BURST_1                       0x0000
#define SDRAM_LMR_BURST_2                       0x0001
#define SDRAM_LMR_BURST_4                       0x0002
#define SDRAM_LMR_BURST_8                       0x0003
#define SDRAM_LMR_BURST_PAGE                    0x0007
#define SDRAM_LMR_BURST_INTERLEAVED             0x0008
#define SDRAM_LMR_CAS_2                         0x0020
#define SDRAM_LMR_CAS_3                         0x0030
#define SDRAM_LMR_WR_BURST_SINGLE               0x0200

Kolejna sprawa to ustawienie funkcji dla pinów. Pamięć wykorzystuje całkiem sporo pinów, aby ułatwić sobie zadanie przygotowałem funkcję konfigurującą pin:

static void pin_config(GPIO_TypeDef* port, uint8_t pin, uint32_t func)
{
        if (pin < 8) {
                port->AFR[0] |=  func << (pin * 4);
                port->AFR[0] |=  func << (pin * 4);
        } else {
                port->AFR[1] |=  func << ((pin - 8) * 4);
                port->AFR[1] |=  func << ((pin - 8) * 4);
        }
        port->MODER |= 2 << (pin * 2);
        port->OSPEEDR |= 2 << (pin * 2);
}

Teraz mogę skonfigurować wszystkie potrzebne piny - są opisane w dokumentacji płytki discovery oraz nocie katalogowej mikrokontrolera:

static void init_gpio(void)
{
        pin_config(GPIOG, 8, 12);       // SDCLK
        pin_config(GPIOG, 15, 12);      // SDNCAS
        pin_config(GPIOF, 11, 12);      // SDNRAS
        pin_config(GPIOB, 5, 12);       // SDCKE1
        pin_config(GPIOB, 6, 12);       // SDNE1
        pin_config(GPIOC, 0, 12);       // SDNWE

        pin_config(GPIOE, 0, 12);       // NBL0
        pin_config(GPIOE, 1, 12);       // NBL1

        pin_config(GPIOF, 0, 12);       // A0
        pin_config(GPIOF, 1, 12);       // A1
        pin_config(GPIOF, 2, 12);       // A2
        pin_config(GPIOF, 3, 12);       // A3
        pin_config(GPIOF, 4, 12);       // A4
        pin_config(GPIOF, 5, 12);       // A5
        pin_config(GPIOF, 12, 12);      // A6
        pin_config(GPIOF, 13, 12);      // A7
        pin_config(GPIOF, 14, 12);      // A8
        pin_config(GPIOF, 15, 12);      // A9
        pin_config(GPIOG, 0, 12);       // A10
        pin_config(GPIOG, 1, 12);       // A11

        pin_config(GPIOG, 4, 12);       // BA0
        pin_config(GPIOG, 5, 12);       // BA1

        pin_config(GPIOD, 14, 12);      // D0
        pin_config(GPIOD, 15, 12);      // D1
        pin_config(GPIOD, 0, 12);       // D2
        pin_config(GPIOD, 1, 12);       // D3
        pin_config(GPIOE, 7, 12);       // D4
        pin_config(GPIOE, 8, 12);       // D5
        pin_config(GPIOE, 9, 12);       // D6
        pin_config(GPIOE, 10, 12);      // D7
        pin_config(GPIOE, 11, 12);      // D8
        pin_config(GPIOE, 12, 12);      // D9
        pin_config(GPIOE, 13, 12);      // D10
        pin_config(GPIOE, 14, 12);      // D11
        pin_config(GPIOE, 15, 12);      // D12
        pin_config(GPIOD, 8, 12);       // D13
        pin_config(GPIOD, 9, 12);       // D14
        pin_config(GPIOD, 10, 12);      // D15
}

Na koniec to co najważniejsze, czyli sama inicjalizacja pamięci:


void sdram_init(void)
{
        init_gpio();

        RCC->AHB3ENR |= RCC_AHB3ENR_FMCEN;

        // SDCLK        84 MHz => 11.9 ns
        // TMRD:        2 clk
        // TXSR:        70 ns => 6
        // TRAS:        42 ns => 4
        // TRC:         63 ns => 6
        // TWR:         2 clk
        // TRP:         15 ns => 2
        // TRCD:        15 ns => 2

        FMC_Bank5_6->SDCR[0] = FMC_SDCR_CLK_DIV2;
        FMC_Bank5_6->SDCR[1] = FMC_SDCR_CLK_DIV2 | FMC_SDCR_CAS_2 |
                FMC_SDCR_BANKS_4 | FMC_SDCR_ROWS_12b | FMC_SDCR_COLS_8b | FMC_SDCR_DATA_16b;

        FMC_Bank5_6->SDTR[0] = FMC_SDTR_TRC_VALUE(6) | FMC_SDTR_TRP_VALUE(2);

        FMC_Bank5_6->SDTR[1] = FMC_SDTR_TMRD_VALUE(2) | FMC_SDTR_TXSR_VALUE(6) |
                FMC_SDTR_TRAS_VALUE(4) | FMC_SDTR_TWR_VALUE(2) | FMC_SDTR_TRCD_VALUE(2);

        /* Configure a clock configuration enable command */
        while (FMC_Bank5_6->SDSR & FMC_SDSR_BUSY);
        FMC_Bank5_6->SDCMR = FMC_SDCMR_MODE_CLK_CFG_EN | FMC_SDCMR_CTB2;

        /* Insert 100 us delay */
        for (volatile uint32_t dly = 0; dly < 16800; dly++)
                ;

        /* Configure a PALL (precharge all) command */
        while (FMC_Bank5_6->SDSR & FMC_SDSR_BUSY);
        FMC_Bank5_6->SDCMR = FMC_SDCMR_MODE_PALL | FMC_SDCMR_CTB2;

        /* Configure a Auto-Refresh command, 4 auto-refresh cycles */
        while (FMC_Bank5_6->SDSR & FMC_SDSR_BUSY);
        FMC_Bank5_6->SDCMR = FMC_SDCMR_MODE_AUTO_REFRESH | FMC_SDCMR_CTB2 |
                FMC_SDCMR_NRFS_CYCLES(4);

        while (FMC_Bank5_6->SDSR & FMC_SDSR_BUSY);
        FMC_Bank5_6->SDCMR = FMC_SDCMR_MODE_AUTO_REFRESH | FMC_SDCMR_CTB2 |
                FMC_SDCMR_NRFS_CYCLES(4);

        /* Program the external memory mode register */
        while (FMC_Bank5_6->SDSR & FMC_SDSR_BUSY);
        uint32_t lmr = SDRAM_LMR_BURST_2 | SDRAM_LMR_CAS_2 | SDRAM_LMR_WR_BURST_SINGLE;
        FMC_Bank5_6->SDCMR = FMC_SDCMR_MODE_LMR | FMC_SDCMR_CTB2 | FMC_SDCMR_LMR_MRD(lmr);

        /* Set the refresh rate counter */
        FMC_Bank5_6->SDRTR |= FMC_SDRTR_COUNT_VALUE(1292);

        while (FMC_Bank5_6->SDSR & FMC_SDSR_BUSY);
}

Opis wymaganych kroków do inicjalizacji FMC znajdziemy w dokumentacji mikrokontrolera. Natomiast dokumentacja pamięci dostarcza informacji o timingach.
Użyte przeze mnie są nieco inne niż w przypadku kodu przykładowego dla płytki ewaluacyjnej - wydaje mi się, że policzyłem je dobrze, program wydaje się działać poprwanie. Ale zastrzegam, że każdy się może mylić i nie daję gwarancji na powyższy kod.

Skoro pamięć już działa, można napisać program główny - który przy okazji przetestuje zapis i odczyt z pamięci.


#include <stdint.h>
#include <stdbool.h>
#include "stm32f429xx.h"
#include "pll.h"
#include "sdram.h"

int main(int argc, char *argv[])
{
        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOCEN | RCC_AHB1ENR_GPIODEN |
                        RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_GPIOFEN | RCC_AHB1ENR_GPIOGEN;

        pll_init();
        sdram_init();

        GPIOG->MODER |= GPIO_MODER_MODE13_0|GPIO_MODER_MODE14_0;

        GPIOG->BSRR = GPIO_BSRR_BS13;
        GPIOG->BSRR = GPIO_BSRR_BR14;

        for (int i = 0; i < 1024; i++)
                *(uint32_t*) (SDRAM_ADDR + 4 * i) = i;

        for (int i = 0; i < 1024; i++) {
                uint32_t x = *(uint32_t*)(SDRAM_ADDR + 4 * i);
                if (x != i)
                        while (1);
        }

        GPIOG->BSRR = GPIO_BSRR_BS14;
        while (1) {
        }

        return 0;
}

Skoro pamięc SDRAM już działa można uruchamiać kolejne moduły. Na celowniku wyświetlacz, miejsce na bufor obrazu już przygotowane.


11 lipca 2018

Reorganizacja kodu

W tym wpisie planowałem omówić pamięć SDRAM, ale zanim do niej przejdę jeszcze jeden, krótki wpis.
Okazuje się, że pisanie wszystkiego w jednym pliku main.c całkiem dobrze się sprawdza dla krótkiego programu. Ale im kod jest dłuższy, tym trudniej sobie poradzić i pojawia się konieczność podzielenia programu na moduły.
Co więcej okazuje się, że te moduły zaczynają tworzyć pewną bibliotekę - co niestety sprowadza się do ponownego wynajdowania koła. Użycie rejestrów miało wyeliminować bibliotekę Cube HAL, czy StdPeriph, jednak po pewnym czasie kod zmienia się we własną bibliotekę...

Nie mam jednak wyjścia, muszę coś z kodem w main.c zrobić. Na początek wydzielę ostatnio napisany kod inicjalizujący pętlę PLL.
W tym celu kopiuję początek funkcji main() do nowego pliku pll.c:

#include "pll.h"
#include "stm32f429xx.h"

#define PLL_M           8uL             // 8 MHz / 8 = 1 MHz
#define PLL_N           336uL           // 336 MHz => hclk = 168 MHz
#define PLL_Q           7               // USB PLL => 336/7 = 48 MHz

void pll_init(void)
{
        RCC->CR |= RCC_CR_HSEON;
        while ((RCC->CR & RCC_CR_HSERDY) == 0) ;

        RCC->PLLCFGR = PLL_M | (PLL_N << 6) | RCC_PLLCFGR_PLLSRC_HSE | (PLL_Q << 24);
        RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV2 | RCC_CFGR_PPRE1_DIV4;

        RCC->CR |= RCC_CR_PLLON;
        while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

        RCC->APB1ENR |= RCC_APB1ENR_PWREN;

        PWR->CR |= PWR_CR_ODEN;
        while ((PWR->CSR & PWR_CSR_ODRDY) == 0) ;

        PWR->CR |= PWR_CR_ODSWEN;
        while ((PWR->CSR & PWR_CSR_ODSWRDY) == 0) ;

        FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN |
                     FLASH_ACR_LATENCY_5WS;

        uint32_t cfgr = RCC->CFGR;
        cfgr &= ~RCC_CFGR_SW;
        cfgr |= RCC_CFGR_SW_PLL;
        RCC->CFGR = cfgr;
        while ((RCC->CFGR & RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL) ;
}

W pliku nagłówkowym pll.h umieszczam odpowiednie deklaracje:

#ifndef __PLL__
#define __PLL__

#define HCLK_FREQ       168000000uL
#define APB1_FREQ       (HCLK_FREQ/4)
#define APB2_FREQ       (HCLK_FREQ/2)

void pll_init(void);

#endif // __PLL__

Teraz program głowny po prostu wywołuje pll_init. Kod jest czytelniejszy, ale co ważniejsze w kolejnych programach mogę wykorzystać ten sam moduł pll.
Plik main.c:

#include <stdint.h>
#include <stdbool.h>
#include "stm32f429xx.h"
#include "pll.h"

volatile uint32_t ms_counter = 0;

void SysTick_Handler(void)
{
        if (ms_counter)
                ms_counter--;
}

void delay_ms(uint32_t ms)
{
        ms_counter = ms;
        while (ms_counter) ;
}

void usart_send(const char *s)
{
        while (*s) {
                while ((USART1->SR & USART_SR_TXE) == 0);
                USART1->DR = *s++;
        }
}

int main(int argc, char *argv[])
{
        pll_init();

        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOGEN;
        RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

        GPIOG->MODER |= GPIO_MODER_MODE13_0|GPIO_MODER_MODE14_0;

        GPIOA->MODER |= GPIO_MODER_MODE9_1|GPIO_MODER_MODE10_1;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL9_0 | GPIO_AFRH_AFSEL9_1 | GPIO_AFRH_AFSEL9_2;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL10_0 | GPIO_AFRH_AFSEL10_1 | GPIO_AFRH_AFSEL10_2;

        SysTick->LOAD = HCLK_FREQ / 1000 - 1;
        SysTick->VAL = 0;
        SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                        SysTick_CTRL_TICKINT_Msk |
                        SysTick_CTRL_ENABLE_Msk;

        USART1->BRR = APB2_FREQ / 115200;
        USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;

        usart_send("Hello world!\r\n");

        while (1) {
                GPIOG->BSRR = GPIO_BSRR_BS13;
                GPIOG->BSRR = GPIO_BSRR_BR14;
                delay_ms(500);

                GPIOG->BSRR = GPIO_BSRR_BS14;
                GPIOG->BSRR = GPIO_BSRR_BR13;
                delay_ms(500);

                usart_send("stm32f429\r\n");
        }

        return 0;
}

Jednak głównym powodem powstania tego wpisu jest nowy plik Makefile. Może nie nowy, ale zmieniony - tym razem będzie kompilowanych więcej plików źródłowych. Dodałem zmienną SOURCES, która przechowuje listę z plikami do skompilowania. Dzięki temu można łatwo dodać moduł pll, a w przyszłości również kolejne.
Plik Makefile wygląda teraz następująco:

CROSS_COMPILE = arm-none-eabi-

AS = $(CROSS_COMPILE)as
CC = $(CROSS_COMPILE)gcc
SIZE = $(CROSS_COMPILE)size

TARGET = bin/test07

SOURCES = src/main.c \
          src/pll.c

OBJECTS = $(SOURCES:src/%.c=obj/%.o)

CFLAGS=-mthumb -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -std=gnu11 -O0 \
        -ffunction-sections -fdata-sections -g -fstack-usage -Wall -specs=nano.specs

LDFLAGS=$(CFLAGS) -Wl,--gc-sections -specs=nosys.specs -Wl,-cref,-u,Reset_Handler \
        -Wl,-Map=$(TARGET).map -Tsrc/stm32f4_flash.ld

all: $(TARGET).elf
        $(SIZE) $<

$(TARGET).elf: $(OBJECTS) obj/startup_stm32f429xx.o
        $(CC) $(LDFLAGS) -o $@ $^

obj/%.o: src/%.c
        $(CC) $(CFLAGS) -o $@ -c $<

obj/startup_stm32f429xx.o: src/startup_stm32f429xx.s
        $(CC) $(CFLAGS) -o $@ -c $<

debug: $(TARGET).elf
        openocd -f board/stm32f429discovery.cfg -f interface/stlink-v2-1.cfg \
                -c "init; sleep 200; reset halt; wait_halt; \
                flash write_image erase $(TARGET).elf; \
                reset run; sleep 10; shutdown"

clean:
        rm -f bin/* obj/*

08 lipca 2018

Pętla PLL

Dotychczas wszystkie programy działały na domyślnym źródle sygnału zegarowego, czyli wbudowanym generatorze RC o częstotliwości 16 MHz (w dokumentacji nazywanym HSI).
Niby nie było w tym nic złego, ale nasz mikrokontroler może pracować z maksymalną częstotliwością 180 MHz, więc używaliśmy mniej niż 10% jego możliwości. Druga sprawa to niska stabilność generatora RC - faktyczna częstotwliość pewnie znacznie odbiegała od 16 MHz, co więcej mogła się zmieniać pod wpływem temperatury, zmian napięcia itd.
Na szczęście na płytce stm32f429-disc znajdziemy rezonator kwarcowy 8 MHz, który pozwoli na wygenerowanie sygnału zegarowego o znacznie większej precyzji.
Na początek jak zwykle odniesienie do dokumentacji:


Na pierwszy rzut oka ten diagram może nie wydawać się zbyt łatwy, więc najlepiej podejść do niego małymi krokami - wtedy zobaczymy że to nic trudnego.
Aby ułatwić opis pozwoliłem sobie bardzo amatorsko pokolorować powyższy diagram. Piękne to nie jest, ale może będzie łatwiej opisać o co chodzi.


Na początek fragment zaznaczony na zielono, to tutaj widzimy podłączenie kwarcu 8MHz oraz generator sygnału zegarowego (HSE).
Generator uruchamiamy zapalając flagę HSEON w rejestrze RCC->CR. Następnie musimy poczekać na włączenie generatora:

        RCC->CR |= RCC_CR_HSEON;
        while ((RCC->CR & RCC_CR_HSERDY) == 0) ;


Gdy generator już działa, czas skonfigurować pętlę PLL, którą zaznaczyłem na żółto.
Na pierwszy rzut oka konfiguracja pętli PLL może wydawać się nieco skomplikowana, ale nie ma co się poddawać. Na początek wystarczy że wybierzemy trzy wartości:
  • M - czyli dzielnik częstotliwości sygnału wejściowego
  • N - mnożnik częstotliwości
  • P - dzielnik częstotliwości dla sygnału wyjściowego
M określa dzielnik sygnału wejściowego, czyli 8MHz. Typowe rozwiązanie to podzielenie przez 8, dzięki temu dostaniemy wygodną wartość 1 MHz.
P dla odmiany to dzielnik wartości wyjściowej PLL. Dostępne są wartości 2, 4, 6 oraz 8. Wybieramy 2 jako najniższą dostępną.
Teraz wystaczy zająć się wartością N, czyli mnożnikiem częstotliwości. Na wejściu mamy sygnał o częstotliwości 1 MHz (bo podzieliliśmy przez 8). Wartość N musi się mieścić w przedziale od 2 do 432, ale częstotliwości prawidłowe dla PLL to 100 do 432, więc lepiej unikać niższych wartości.
Zacznijmy więc niejako od końca - interesuje nas częstotliwość taktowania procesora, czyli sygnal HCLK. Maksymalna częstotliwość to 180 MHz, jednak wygodniej jest użyć wartości 168 MHz - daje ona możliwość łatwego wygenerowania zegara dla USB.
Przyjmijmy więc, że chcemy uzyskać częstotliwość wyjściową HCLK o wartości 168MHz. Między PLL a sygnałem HCLK jest jeszcze dzielnik P, który ustawimy na wartość 2, czyli nasza pętla PLL powinna dawać częstotliwość wyjściową 336MHz.
Mamy więc nasz współczynnik N, jest to po prostu 336.
Ponieważ współczynniki możemy kiedyś zmienić, warto je zdefiniować jako stałe:

#define PLL_M           8uL         // 8 MHz / 8 = 1 MHz
#define PLL_N           336uL       // 336 MHz => hclk = 168 MHz
#define PLL_Q           7           // USB PLL => 336/7 = 48 MHz


Pojawił się tutaj jeszcze współczynnik Q - na razie nie używamy USB, ale od razu ustawimy odpowiedni zegar. Powinien mieć częstotliwość 48 MHz, co jak widać idealnie wpasowuje się w dzielnik Q = 7.

Mając ustalone odpowiednie współczynniki dla pętli PLL wystarczy wpisać je do rejestru RCC->PLLCFGR:

RCC->PLLCFGR = PLL_M | (PLL_N << 6) | RCC_PLLCFGR_PLLSRC_HSE | (PLL_Q << 24);

Teraz dwa słowa o częstotliwości taktowania - sam mikrokontroler, a właściwie jego rdzeń może być taktowany częstotliwością 180MHz. Wybraliśmy 168MHz, aby łatwo uzyskać zegar dla USB. Okazuje się jednak, że nie wszystkie moduły peryferyjne naszego mikrokontrolera mogą pracować z pełną prędkością. Ma to pewne uzasadnienie - po co przykładowo moduł UART o prędkości transmisji 115kb/s taktować z 168MHz. Można znacznie zmiejszyć zużycie prądu (oraz bramek w układzie) wydzielając dla wolniejszych peryferiów oddzielne domeny zegara.
Tak właśnie zbudowany jest mikrokontroler stm32f429 - posiada wolniejsze magistrale  peryferyjne: APB1 oraz APB2, które nie pracują z pełną prędkością. APB1 może maksymalnie być taktowana z 45 MHz, natomiast APB2 z 90 MHz. Z pomocą przyjdą nam kolejne dzielniki częstotliwości. Sygnał HCLK podzilimy przez 2 dla APB2 oraz przez 4 dla APB1:

RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV2 | RCC_CFGR_PPRE1_DIV4;

W programie wartości zegarów odpowiednich domen mogą się nam jeszcze przydać, więc warto dodać odpowiednie definicje:

#define HCLK_FREQ       168000000uL
#define APB1_FREQ       (HCLK_FREQ/4)
#define APB2_FREQ       (HCLK_FREQ/2)


Teraz jesteśmy gotowi na uruchomienie pętli PLL:

RCC->CR |= RCC_CR_PLLON;
while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

Zanim przejdziemy dalej jeszcze jedna drobnostka. Okazuje się, że aby stabilnie pracować z pełną prędkością mikrokontroler wymaga podwyższenia napięcia zasilania rdzenia. Wykonujemy to następującym kodem:

RCC->APB1ENR |= RCC_APB1ENR_PWREN;
 

PWR->CR |= PWR_CR_ODEN;
while ((PWR->CSR & PWR_CSR_ODRDY) == 0) ;
 

PWR->CR |= PWR_CR_ODSWEN;
while ((PWR->CSR & PWR_CSR_ODSWRDY) == 0) ;

Teraz kolejny problem - pamięć Flash w której znajduje się nasz program może pracować maksymalnie z częstotliwością ~30MHz, czyli 168MHz to znacznie za dużo. Dodając 5 cykli oczekiwania, będzimy pewni że pamięć flash nadąży za naszym programem:

 FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_LATENCY_5WS;

Teraz jesteśmy już gotowi - pętla PLL działa, pamięć flash jest przygotowana na szybsze taktowanie procesora, zmieniamy więc źródło sygnału na PLL:

        uint32_t cfgr = RCC->CFGR;
        cfgr &= ~RCC_CFGR_SW;
        cfgr |= RCC_CFGR_SW_PLL;
        RCC->CFGR = cfgr;
        while ((RCC->CFGR & RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL) ;


Od tej chwili nasz mikrokontroler działa na częstotliwości 168 MHz. Jak to sprawdzić? Mamy co najmniej dwa sposoby. Po pierwsze timer SysTick - skonfigurujemy go z nową wartością dzielnika i zobaczymy czy diody migają z ta samą częstotliwością.
Drugi moduł to UART - jeśli wszystko działa jak chcieliśmy uzyskamy ponownie poprawną komunikację przez port szeregowy.
Cały program wygląda następująco:

#include <stdint.h>
#include <stdbool.h>
#include "stm32f429xx.h"

#define PLL_M           8uL             // 8 MHz / 8 = 1 MHz
#define PLL_N           336uL           // 336 MHz => hclk = 168 MHz
#define PLL_Q           7               // USB PLL => 336/7 = 48 MHz

#define HCLK_FREQ       168000000uL
#define APB1_FREQ       (HCLK_FREQ/4)
#define APB2_FREQ       (HCLK_FREQ/2)

volatile uint32_t ms_counter = 0;

void SysTick_Handler(void)
{
        if (ms_counter)
                ms_counter--;
}

void delay_ms(uint32_t ms)
{
        ms_counter = ms;
        while (ms_counter) ;
}

void usart_send(const char *s)
{
        while (*s) {
                while ((USART1->SR & USART_SR_TXE) == 0);
                USART1->DR = *s++;
        }
}

int main(int argc, char *argv[])
{
        RCC->CR |= RCC_CR_HSEON;
        while ((RCC->CR & RCC_CR_HSERDY) == 0) ;

        RCC->PLLCFGR = PLL_M | (PLL_N << 6) | RCC_PLLCFGR_PLLSRC_HSE | (PLL_Q << 24);
        RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV2 | RCC_CFGR_PPRE1_DIV4;

        RCC->CR |= RCC_CR_PLLON;
        while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

        RCC->APB1ENR |= RCC_APB1ENR_PWREN;

        PWR->CR |= PWR_CR_ODEN;
        while ((PWR->CSR & PWR_CSR_ODRDY) == 0) ;

        PWR->CR |= PWR_CR_ODSWEN;
        while ((PWR->CSR & PWR_CSR_ODSWRDY) == 0) ;

        FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN |
                FLASH_ACR_LATENCY_5WS;

        uint32_t cfgr = RCC->CFGR;
        cfgr &= ~RCC_CFGR_SW;
        cfgr |= RCC_CFGR_SW_PLL;
        RCC->CFGR = cfgr;
        while ((RCC->CFGR & RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL) ;

        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOGEN;
        RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

        GPIOG->MODER |= GPIO_MODER_MODE13_0|GPIO_MODER_MODE14_0;

        GPIOA->MODER |= GPIO_MODER_MODE9_1|GPIO_MODER_MODE10_1;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL9_0 | GPIO_AFRH_AFSEL9_1 | GPIO_AFRH_AFSEL9_2;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL10_0 | GPIO_AFRH_AFSEL10_1 | GPIO_AFRH_AFSEL10_2;

        SysTick->LOAD = HCLK_FREQ / 1000 - 1;
        SysTick->VAL = 0;
        SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                        SysTick_CTRL_TICKINT_Msk |
                        SysTick_CTRL_ENABLE_Msk;

        USART1->BRR = APB2_FREQ / 115200;
        USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;

        usart_send("Hello world!\r\n");

        while (1) {
                GPIOG->BSRR = GPIO_BSRR_BS13;
                GPIOG->BSRR = GPIO_BSRR_BR14;
                delay_ms(500);

                GPIOG->BSRR = GPIO_BSRR_BS14;
                GPIOG->BSRR = GPIO_BSRR_BR13;
                delay_ms(500);

                usart_send("stm32f429\r\n");
        }

        return 0;
}

Port szeregowy

Dawno, dawno temu każdy szanujący się komputer PC był wyposażony z złącze RS-232C. Teraz to już historia, złącze szeregowe zostało zastąpione przez USB, ale w przypadku mikrokontrolerów starsze rozwiązanie nadal ma się całkiem dobrze. Powód jest oczywisty - prostota.
Można używać USB do komunikacji, ale nawet podstawowe przesłanie prostego komunikatu wymaga napisania znacznej ilości kodu. Ewentualnie użycia biblioteki, która na ogół jest droga, albo pełna błędów (czasem jedno i drugie).
Użycie portu szeregowego jest banalnie proste - chociaż komputery, czy laptopy go nie obsługują, ale od czego są konwertery UART-USB.
W przypadku płytki stm32f429-disc konieczny był właśnie taki konwerter, nowsza wersja stm32f429-disc1 ma już odpowiedni konwerter wbudowany.

Niezależnie od wersji trzeba odpowiednio skonfigurować piny przeznaczone do transmisji.


Jak widać chodzi o linie PA9 oraz PA10. Tym razem obie linie mają być połączone z modułem peryferyjnym. Oznacza to ustawienie trybu "10" w rejestrze MODER oraz wybór układu peryferyjnego za pomocą AFR.

W rejestrze MODER wystarczy zapalić wyższe bity dla odpowiednich linii:

GPIOA->MODER |= GPIO_MODER_MODE9_1|GPIO_MODER_MODE10_1;

Z rejestrzem AFR jest nieco trudniej. Piny mogą mieć przypisaną jedną z maksymalnie 16 funkcji - oznacza to 4 bity na każdy pin. Ponieważ pinów jest 16 w każdym porcie, do konfiguracji niezbędne są 64 bity. Oznacza to że jeden rejestr AFR nie wystarczy. Dlatego w dokumentacji znajdziemy rejestry AFRL oraz AFRH. Co ciekawe w pliku stm32f429xx.h te rejestry są zdefinowane jako tablica AFR[2].
Numery dostępnych modułów, które można wybierać za pomocą AFR znajdziemy w dokumentacji mikrokontrolera:


Jak widać chcemy użyć funkcji AF7 zarówno dla PA9 i PA10. Można więc napisać:
GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL9_0 | GPIO_AFRH_AFSEL9_1 | GPIO_AFRH_AFSEL9_2;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL10_0 | GPIO_AFRH_AFSEL10_1 | GPIO_AFRH_AFSEL10_2;


Dalej jest już tylko łatwiej. Jak zwykle musimy pamiętać o uruchomieniu taktowania modułu UART1:

RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

Konfiguracja sprowadza się do ustawienia prędkości komunikacji:

USART1->BRR = 16000000uL / 115200;

A następnie włączenia nadajnika i odbiornika:

USART1->CR1 |= USART_CR1_TE | USART_CR1_RE;

Oraz samego modułu uart:

USART1->CR1 |= USART_CR1_UE;

Tutaj natrafiłem na pewną niespodziankę - jeśli zapalenie bitu UE następuje razem z konfiguracją pozostałych bitów, przed pierwszym wysłanym znakiem pojawiają się "śmieci". Konieczne okazało się dodanie pewnego opóźnienia - niestety nie wiem dlaczego.

Wysyłanie polega na wpisaniu wartości do rejestru UART1->DR, czyli:

USART1->DR = value;

Oczywiście przed zapisem należy upewnić się, że rejestr jest wolny. Rejestr statusu UART1->SR zawiera flagę TXE, która jest ustawiana gdy rejestr nadawania jest pusty. Należy więc przed wysłaniem poczekać na zapalenie tej flagi.
Cały program wysyłający komunikaty przez port szeregowy wygląda następująco:


#include <stdint.h>
#include <stdbool.h>
#include "stm32f429xx.h"

volatile uint32_t ms_counter = 0;

void SysTick_Handler(void)
{
        if (ms_counter)
                ms_counter--;
}

void delay_ms(uint32_t ms)
{
        ms_counter = ms;
        while (ms_counter) ;
}

void usart_send(const char *s)
{
        while (*s) {
                while ((USART1->SR & USART_SR_TXE) == 0);
                USART1->DR = *s++;
        }
}

int main(int argc, char *argv[])
{
        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOGEN;
        RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

        GPIOG->MODER |= GPIO_MODER_MODE13_0|GPIO_MODER_MODE14_0;

        GPIOA->MODER |= GPIO_MODER_MODE9_1|GPIO_MODER_MODE10_1;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL9_0 | GPIO_AFRH_AFSEL9_1 | GPIO_AFRH_AFSEL9_2;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL10_0 | GPIO_AFRH_AFSEL10_1 | GPIO_AFRH_AFSEL10_2;

        SysTick->LOAD = 16000-1;
        SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                        SysTick_CTRL_TICKINT_Msk |
                        SysTick_CTRL_ENABLE_Msk;

        USART1->BRR = 16000000uL / 115200;
        USART1->CR1 |= USART_CR1_TE | USART_CR1_RE;

        delay_ms(1);

        USART1->CR1 |= USART_CR1_UE;

        usart_send("Hello world!\r\n");

        while (1) {
                GPIOG->BSRR = GPIO_BSRR_BS13;
                GPIOG->BSRR = GPIO_BSRR_BR14;
                delay_ms(500);

                GPIOG->BSRR = GPIO_BSRR_BS14;
                GPIOG->BSRR = GPIO_BSRR_BR13;
                delay_ms(500);

                usart_send("stm32f429\r\n");
        }

        return 0;
}