20 lipca 2018

Pierwsze próby z wyświetlaczem

Jak wspominałem wcześniej moim celem było opanowanie wyświetlacza na płytce stm32f429-discovery. Wbrew pozorom to dość skomplikowane wyzwanie, ale teraz wszystkie niezbędne elementy: PLL, SPI oraz SDRAM są uruchomione, można zacząć przygodę z wyświetlaczem.
Na początek drobna poprawka modułu spi. Poprzednio testowałem go z żyroskopem, ale teraz chciałbym mieć możliwość używania zarówno do obsługi tego czujnika, jak i wyświetlacza. Zmieniam więc niec plik nagłówkowy spi.h:

#ifndef __SPI__
#define __SPI__

#include <stdint.h>

#define SPI_L3GD20              0
#define SPI_LCD                 1

void spi_init(void);

void spi_start(int spi);

void spi_stop(int spi);

uint8_t spi_sendrecv(uint8_t tx);

#endif // __SPI__

Oraz sam kod w 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 | GPIO_MODER_MODE2_0;
        GPIOC->BSRR = GPIO_BSRR_BS1 | GPIO_BSRR_BS2;

        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(int spi)
{
        if (spi == SPI_L3GD20)
                GPIOC->BSRR = GPIO_BSRR_BR1;
        else
                GPIOC->BSRR = GPIO_BSRR_BR2;
}

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

        if (spi == SPI_L3GD20)
                GPIOC->BSRR = GPIO_BSRR_BS1;
        else
                GPIOC->BSRR = GPIO_BSRR_BS2;
}

Zmiany są kosmetyczne - teraz można wybrać układ z którym chcemy się komunikować.
Czas uruchomić wyświetlacz. Pamięć SDRAM posłuży za bufor obrazu - w kolejnym wpisie, na początek niech wyświetli się samo tło. Interfejs SPI służy do konfiguracji kontrolera obrazu, same dane są przesyłane przez kontroler LCD.
Taka transmisja wymaga dość sporo pinów, używam więc funkcji config_pin do ich ustawienia:

        config_pin(GPIOA, 3, 14);
        config_pin(GPIOA, 4, 14);
        config_pin(GPIOA, 6, 14);
        config_pin(GPIOA, 11, 14);
        config_pin(GPIOA, 12, 14);

        config_pin(GPIOB, 0, 9);
        config_pin(GPIOB, 1, 9);
        config_pin(GPIOB, 8, 14);
        config_pin(GPIOB, 9, 14);
        config_pin(GPIOB, 10, 14);
        config_pin(GPIOB, 11, 14);

        config_pin(GPIOC, 6, 14);
        config_pin(GPIOC, 7, 14);
        config_pin(GPIOC, 10, 14);

        config_pin(GPIOD, 3, 14);
        config_pin(GPIOD, 6, 14);

        config_pin(GPIOF, 10, 14);

        config_pin(GPIOG, 6, 14);
        config_pin(GPIOG, 7, 14);
        config_pin(GPIOG, 10, 9);
        config_pin(GPIOG, 11, 14);
        config_pin(GPIOG, 12, 9);

To które piny są używane i jakie funkcje należy im przypisać opisane zostało w dokumentacji. Wystarczy poszukać i przepisać.
Jak zwykle moduł wyświetlacza jest domyślnie wyłączony, trzeba więc go uruchomić:

        RCC->APB2ENR |= RCC_APB2ENR_LTDCEN;

Wyświetlacz używa własnego sygnału zegarowego z dedykowanego PLL. Konfiguracja wygląda następująco:

        RCC->PLLSAICFGR = (192 << 6) | (7 << 24) | (4 << 28);
        RCC->DCKCFGR |= 0x00020000;
        RCC->CR |= RCC_CR_PLLSAION;
        while ((RCC->CR & RCC_CR_PLLSAIRDY) == 0);


Następnie należy ustawić timingi wyświetlacza. Jest z tym trochę liczenia, ale końcowy kod jest prosty:

        LTDC->SSCR = (9 << 16) | 1;
        LTDC->BPCR = (29 << 16) | 3;
        LTDC->AWCR |= (269 << 16) | 323;
        LTDC->TWCR |= (279 << 16) | 327;


W pierwszym przykładzie nie wyświetlam danych, jedynie tło. Jego kolor ustawiany jest w rejestrze BCCR:

        LTDC->BCCR |= 0xf0ff00;

Ostatni krok to uruchomienie wyświetlacza:

        LTDC->GCR |= LTDC_GCR_LTDCEN;

Okazuje się, że uruchomienie wyświetlacza nie jest aż takie trudne jak się wydaje. Co prawda jest jeszcze jeden ważny element - konfiguracja kontrolera w samym wyświetlaczu. Jak wspomniałem używane jest do tego SPI, a sam kod czyli funkcje LCD_PowerOn przeniosłem z przykładów - wymaga konfiguracji ogromnej liczby rejestrów, ale na szczęscie wykonuje się go tylko raz. Wypadałoby wszystko sprawdzić w dokumentacji, ale tym razem byłem trochę leniwy.
Poniżej efekt działania programu:


Program główny w main.c:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "stm32f429xx.h"
#include "pll.h"
#include "sdram.h"
#include "uart.h"
#include "spi.h"
#include "lcd.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_GPIOBEN | RCC_AHB1ENR_GPIOCEN |
                        RCC_AHB1ENR_GPIODEN | RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_GPIOFEN |
                        RCC_AHB1ENR_GPIOGEN;

        pll_init();
        uart_init();
        sdram_init();
        spi_init();
        lcd_init();

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

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

                spi_start(SPI_L3GD20);

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

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

                spi_stop(SPI_L3GD20);

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

        return 0;
}

Oraz to co najważniejsze, czyli moduł lcd.c:

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

#define LCD_SLEEP_OUT            0x11   /* Sleep out register */
#define LCD_GAMMA                0x26   /* Gamma register */
#define LCD_DISPLAY_OFF          0x28   /* Display off register */
#define LCD_DISPLAY_ON           0x29   /* Display on register */
#define LCD_COLUMN_ADDR          0x2A   /* Colomn address register */
#define LCD_PAGE_ADDR            0x2B   /* Page address register */
#define LCD_GRAM                 0x2C   /* GRAM register */
#define LCD_MAC                  0x36   /* Memory Access Control register*/
#define LCD_PIXEL_FORMAT         0x3A   /* Pixel Format register */
#define LCD_WDB                  0x51   /* Write Brightness Display register */
#define LCD_WCD                  0x53   /* Write Control Display register*/
#define LCD_RGB_INTERFACE        0xB0   /* RGB Interface Signal Control */
#define LCD_FRC                  0xB1   /* Frame Rate Control register */
#define LCD_BPC                  0xB5   /* Blanking Porch Control register*/
#define LCD_DFC                  0xB6   /* Display Function Control register*/
#define LCD_POWER1               0xC0   /* Power Control 1 register */
#define LCD_POWER2               0xC1   /* Power Control 2 register */
#define LCD_VCOM1                0xC5   /* VCOM Control 1 register */
#define LCD_VCOM2                0xC7   /* VCOM Control 2 register */
#define LCD_POWERA               0xCB   /* Power control A register */
#define LCD_POWERB               0xCF   /* Power control B register */
#define LCD_PGAMMA               0xE0   /* Positive Gamma Correction register*/
#define LCD_NGAMMA               0xE1   /* Negative Gamma Correction register*/
#define LCD_DTCA                 0xE8   /* Driver timing control A */
#define LCD_DTCB                 0xEA   /* Driver timing control B */
#define LCD_POWER_SEQ            0xED   /* Power on sequence register */
#define LCD_3GAMMA_EN            0xF2   /* 3 Gamma enable register */
#define LCD_INTERFACE            0xF6   /* Interface control register */
#define LCD_PRC                  0xF7   /* Pump ratio control register */

static void LCD_WriteCommand(uint8_t LCD_Reg)
{
        GPIOD->BSRR = GPIO_BSRR_BR13;

        spi_start(SPI_LCD);
        spi_sendrecv(LCD_Reg);
        spi_stop(SPI_LCD);
}

static void LCD_WriteData(uint8_t value)
{
        GPIOD->BSRR = GPIO_BSRR_BS13;

        spi_start(SPI_LCD);
        spi_sendrecv(value);
        spi_stop(SPI_LCD);
}

static void LCD_PowerOn(void)
{
        LCD_WriteCommand(0xCA);
        LCD_WriteData(0xC3);
        LCD_WriteData(0x08);
        LCD_WriteData(0x50);
        LCD_WriteCommand(LCD_POWERB);
        LCD_WriteData(0x00);
        LCD_WriteData(0xC1);
        LCD_WriteData(0x30);
        LCD_WriteCommand(LCD_POWER_SEQ);
        LCD_WriteData(0x64);
        LCD_WriteData(0x03);
        LCD_WriteData(0x12);
        LCD_WriteData(0x81);
        LCD_WriteCommand(LCD_DTCA);
        LCD_WriteData(0x85);
        LCD_WriteData(0x00);
        LCD_WriteData(0x78);
        LCD_WriteCommand(LCD_POWERA);
        LCD_WriteData(0x39);
        LCD_WriteData(0x2C);
        LCD_WriteData(0x00);
        LCD_WriteData(0x34);
        LCD_WriteData(0x02);
        LCD_WriteCommand(LCD_PRC);
        LCD_WriteData(0x20);
        LCD_WriteCommand(LCD_DTCB);
        LCD_WriteData(0x00);
        LCD_WriteData(0x00);
        LCD_WriteCommand(LCD_FRC);
        LCD_WriteData(0x00);
        LCD_WriteData(0x1B);
        LCD_WriteCommand(LCD_DFC);
        LCD_WriteData(0x0A);
        LCD_WriteData(0xA2);
        LCD_WriteCommand(LCD_POWER1);
        LCD_WriteData(0x10);
        LCD_WriteCommand(LCD_POWER2);
        LCD_WriteData(0x10);
        LCD_WriteCommand(LCD_VCOM1);
        LCD_WriteData(0x45);
        LCD_WriteData(0x15);
        LCD_WriteCommand(LCD_VCOM2);
        LCD_WriteData(0x90);
        LCD_WriteCommand(LCD_MAC);
        LCD_WriteData(0xC8);
        LCD_WriteCommand(LCD_3GAMMA_EN);
        LCD_WriteData(0x00);
        LCD_WriteCommand(LCD_RGB_INTERFACE);
        LCD_WriteData(0xC2);
        LCD_WriteCommand(LCD_DFC);
        LCD_WriteData(0x0A);
        LCD_WriteData(0xA7);
        LCD_WriteData(0x27);
        LCD_WriteData(0x04);

        /* colomn address set */
        LCD_WriteCommand(LCD_COLUMN_ADDR);
        LCD_WriteData(0x00);
        LCD_WriteData(0x00);
        LCD_WriteData(0x00);
        LCD_WriteData(0xEF);
        /* Page Address Set */
        LCD_WriteCommand(LCD_PAGE_ADDR);
        LCD_WriteData(0x00);
        LCD_WriteData(0x00);
        LCD_WriteData(0x01);
        LCD_WriteData(0x3F);
        LCD_WriteCommand(LCD_INTERFACE);
        LCD_WriteData(0x01);
        LCD_WriteData(0x00);
        LCD_WriteData(0x06);

        LCD_WriteCommand(LCD_GRAM);

        LCD_WriteCommand(LCD_GAMMA);
        LCD_WriteData(0x01);

        LCD_WriteCommand(LCD_PGAMMA);
        LCD_WriteData(0x0F);
        LCD_WriteData(0x29);
        LCD_WriteData(0x24);
        LCD_WriteData(0x0C);
        LCD_WriteData(0x0E);
        LCD_WriteData(0x09);
        LCD_WriteData(0x4E);
        LCD_WriteData(0x78);
        LCD_WriteData(0x3C);
        LCD_WriteData(0x09);
        LCD_WriteData(0x13);
        LCD_WriteData(0x05);
        LCD_WriteData(0x17);
        LCD_WriteData(0x11);
        LCD_WriteData(0x00);
        LCD_WriteCommand(LCD_NGAMMA);
        LCD_WriteData(0x00);
        LCD_WriteData(0x16);
        LCD_WriteData(0x1B);
        LCD_WriteData(0x04);
        LCD_WriteData(0x11);
        LCD_WriteData(0x07);
        LCD_WriteData(0x31);
        LCD_WriteData(0x33);
        LCD_WriteData(0x42);
        LCD_WriteData(0x05);
        LCD_WriteData(0x0C);
        LCD_WriteData(0x0A);
        LCD_WriteData(0x28);
        LCD_WriteData(0x2F);
        LCD_WriteData(0x0F);

        LCD_WriteCommand(LCD_SLEEP_OUT);
        LCD_WriteCommand(LCD_DISPLAY_ON);
        /* GRAM start writing */
        LCD_WriteCommand(LCD_GRAM);
}

void lcd_init(void)
{
        GPIOD->MODER |= GPIO_MODER_MODE13_0;

        LCD_PowerOn();

        RCC->APB2ENR |= RCC_APB2ENR_LTDCEN;

        config_pin(GPIOA, 3, 14);
        config_pin(GPIOA, 4, 14);
        config_pin(GPIOA, 6, 14);
        config_pin(GPIOA, 11, 14);
        config_pin(GPIOA, 12, 14);

        config_pin(GPIOB, 0, 9);
        config_pin(GPIOB, 1, 9);
        config_pin(GPIOB, 8, 14);
        config_pin(GPIOB, 9, 14);
        config_pin(GPIOB, 10, 14);
        config_pin(GPIOB, 11, 14);

        config_pin(GPIOC, 6, 14);
        config_pin(GPIOC, 7, 14);
        config_pin(GPIOC, 10, 14);

        config_pin(GPIOD, 3, 14);
        config_pin(GPIOD, 6, 14);

        config_pin(GPIOF, 10, 14);

        config_pin(GPIOG, 6, 14);
        config_pin(GPIOG, 7, 14);
        config_pin(GPIOG, 10, 9);
        config_pin(GPIOG, 11, 14);
        config_pin(GPIOG, 12, 9);

        RCC->PLLSAICFGR = (192 << 6) | (7 << 24) | (4 << 28);
        RCC->DCKCFGR |= 0x00020000;
        RCC->CR |= RCC_CR_PLLSAION;
        while ((RCC->CR & RCC_CR_PLLSAIRDY) == 0);

        LTDC->SSCR = (9 << 16) | 1;
        LTDC->BPCR = (29 << 16) | 3;
        LTDC->AWCR |= (269 << 16) | 323;
        LTDC->TWCR |= (279 << 16) | 327;
        LTDC->BCCR |= 0xf0ff00;

        LTDC->GCR |= LTDC_GCR_LTDCEN;
}

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

W kolejnych wpisach dodam drobne modyfikacje do modułu lcd. Wbrew pozorom teraz będzie już tylko łatwiej (i ładniej).

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/*