41б. FSMC и FMC: дисплей 8080, внешняя SRAM и SDRAM
Раздел 41 назвал FSMC одной строкой таблицы: параллельная шина для SRAM, NOR, NAND и SDRAM. Раздел 22а подключает к ней дисплей со своим контроллером: команда и пиксель — запись по одному из двух адресов. Здесь — как это устроено: откуда берутся эти адреса, как считать тайминги в тактах шины, чем внешняя SRAM отличается от SDRAM и почему SDRAM надо запускать по шагам и регенерировать.
Опоры: справочное руководство RM0090 (F405/F407 и F427/F429, главы FSMC и FMC), руководство по ядру Cortex-M7 (DUI 0646B) и RM0385 для F7. Числа плат — из пакета поддержки плат (BSP) в паках Keil: STM32412G-Discovery (дисплей), STM324xG-EVAL (SRAM), STM32F429I-Discovery и STM32746G-Discovery (SDRAM). Код собран armclang для F407 и F429; расчёты — скриптом на ПК. Внешних шин QEMU не моделирует — это проверено сверкой с документами, а не прогоном.
Как это называется в документации
Оригинал Перевод FSMC (flexible static memory controller) контроллер статической памяти: SRAM, NOR, NAND, PC Card FMC (flexible memory controller) то же плюс контроллер SDRAM NE1…NE4 выбор микросхемы в банке 1: четыре области по 64 МБ HADDR внутренний адрес шины AHB ADDSET, DATAST фаза адреса и фаза данных, в тактах HCLK BUSTURN пауза между обращениями, в тактах HCLK extended mode, mode A раздельные тайминги чтения и записи SDRAM refresh регенерация: подзарядка строк динамической памяти CAS latency задержка выдачи данных после команды чтения столбца
41б.1 Где есть FSMC и FMC
FSMC — у F103 высокой плотности (раздел 41) и у F405/F407; у BluePill его нет. FMC — у F427/F429, F7 и H7: к тем же банкам статической памяти добавлен контроллер SDRAM. Для программы оба выглядят одинаково: внешняя память — это область адресов, и чтение или запись по ней становится циклом на выводах. Контроллер сам выставляет адрес, выбор микросхемы и стробы чтения и записи, выжидает заданные такты и забирает данные. Ни одной строки кода на цикл шины — только настройка таймингов.
Банки у RM0090 (таблица 218 и соседние):
| Адреса | Банк | Что подключают |
|---|---|---|
| 0x6000 0000–0x6FFF FFFF | банк 1: NE1…NE4 по 64 МБ | SRAM, PSRAM, NOR, дисплей 8080 |
| 0x7000 0000, 0x8000 0000 | банки 2 и 3 | NAND |
| 0x9000 0000 | банк 4 | PC Card |
| 0xC000 0000, 0xD000 0000 | банки SDRAM 1 и 2 (только FMC) | SDRAM |
41б.2 Адреса: банк, NE и сдвиг на 16-битной шине
Биты адреса HADDR[27:26] выбирают одну из четырёх микросхем банка 1 (RM0090, таблица 216): NE1 — 0x6000 0000, NE2 — 0x6400 0000, NE3 — 0x6800 0000, NE4 — 0x6C00 0000. Каждой достаётся 64 МБ адресов.
Тонкость — в ширине шины (таблица 217). HADDR адресует байты, а 16-битная память — слова. Поэтому на 16-битной шине контроллер выдаёт на линии FSMC_A[24:0] биты HADDR[25:1]: линия A0 — это бит 1 адреса, A16 — бит 17. Отсюда адреса дисплея, у которого вход RS («команда или данные») посажен на одну адресную линию:
| Выбор | RS на линии | Команда | Данные |
|---|---|---|---|
| NE1 | A0 | 0x6000 0000 | 0x6000 0002 |
| NE1 | A16 | 0x6000 0000 | 0x6002 0000 |
| NE1 | A18 | 0x6000 0000 | 0x6008 0000 |
| NE4 | A6 | 0x6C00 0000 | 0x6C00 0080 |
| NE4 | A10 | 0x6C00 0000 | 0x6C00 0800 |
Для A16 наивный расчёт 1 << 16 даёт 0x6001 0000 — это всё ещё команда:
на линии A16 ноль. Сдвиг на единицу забывают чаще всего.
В примерах к недорогим платам встречается база 0x6C00 007E. Это тот же
расчёт: структура из двух 16-битных полей {REG; RAM}, положенная с
0x6C00 007E, даёт REG по 0x6C00 007E (A6 = 0) и RAM по 0x6C00 0080
(A6 = 1) — RS на линии A6.
41б.3 Дисплей 8080 через FSMC
У STM32412G-Discovery дисплей висит на NE1, RS — на линии A0 (вывод PF0). BSP описывает его структурой из двух полей — и тогда команда и данные пишутся обычным присваиванием:
typedef struct {
volatile uint16_t REG; /* 0x60000000: A0 = 0 — команда */
volatile uint16_t RAM; /* 0x60000002: A0 = 1 — данные */
} lcd_t;
#define LCD ((lcd_t *)0x60000000u)
_Static_assert(offsetof(lcd_t, RAM) == 2, "на 16-битной шине A0 = HADDR[1]: данные на 2 байта выше");
void lcd_cmd(uint8_t c) { LCD->REG = c; }
void lcd_data(uint16_t d) { LCD->RAM = d; }
armclang для F407 превращает каждую функцию в одну 16-битную запись:
lcd_cmd: MOV r1,#0x60000000
STRH r0,[r1,#0]
lcd_data: MOVS r1,#2
MOVT r1,#0x6000
STRH r0,[r1,#0]
volatile здесь обязателен (раздел 124): без него две записи подряд по
адресу RAM компилятор вправе слить в одну, а для дисплея это два пикселя.
Настройка банка по регистрам — в функции fsmc_lcd_init файла примера:
тайминги в BTR1 и BWTR1 (41б.4) и BCR1 с шириной 16 бит, разрешением
записи и раздельными таймингами (EXTMOD). Выводы — альтернативная функция
AF12; их проще отдать CubeMX.
Запись по шине не ждёт процессор дольше, чем идёт цикл, — и её можно отдать DMA в режиме «память → память» (раздел 9): источник — буфер строки в ОЗУ, приёмник — адрес RAM без приращения.
41б.4 Тайминги: режим A в тактах HCLK
Асинхронный цикл состоит из фаз, каждая — целое число тактов HCLK. В режиме A (раздельные тайминги чтения и записи, EXTMOD = 1) RM0090 даёт (таблицы 229–230):
- чтение: ADDSET + DATAST тактов, строб NOE опущен на DATAST;
- запись: ADDSET + (DATAST + 1) тактов, строб NWE опущен на DATAST + 1;
- BUSTURN — пауза между обращениями.
Тайминги чтения лежат в BTR1, записи — в BWTR1. У дисплея чтение обычно намного медленнее записи, и именно поэтому режим A: одни и те же числа на оба направления либо замедлят запись, либо сорвут чтение.
BSP STM32412G-Discovery ставит (такт 10 нс при HCLK 100 МГц — предельной частоте F412):
| ADDSET | DATAST | Тактов | Время цикла | |
|---|---|---|---|---|
| чтение | 9 | 36 | 45 | 450 нс |
| запись | 1 | 7 | 9 | 90 нс |
Как из этого получить числа под свой дисплей: из его документации берут минимальную длительность цикла записи и минимальную длительность низкого уровня на WR, делят на такт HCLK с округлением вверх: DATAST + 1 — не меньше низкого уровня WR, ADDSET + DATAST + 1 — не меньше цикла. Для чтения — так же по циклу чтения. Запас по такту — нормально; нехватка даёт ошибки, которые видны не сразу, а на части точек.
Предел скорости картинки — число записей на такт шины. При 90 нс на запись (16 бит на точку, одна запись на точку):
| Экран | Записей | Время кадра | Не больше кадров/с |
|---|---|---|---|
| 240×240 | 57 600 | 5,184 мс | 192 |
| 320×240 | 76 800 | 6,912 мс | 144 |
| 480×320 | 153 600 | 13,824 мс | 72 |
Это потолок шины; цикл программы или DMA добавляют своё.
41б.5 Внешняя SRAM
Статическая память подключается так же, как дисплей, только адресных линий много. У STM324xG-EVAL SRAM IS61WV102416BLL на 2 МБ (16 бит) стоит на NE2 — адрес 0x6400 0000. BSP ставит ADDSET 2, DATAST 2, BUSTURN 1 в режиме A; при HCLK 168 МГц это 4 такта (23,8 нс) на чтение и 5 тактов (29,8 нс) на запись.
Такая память видна как обычный массив: в неё можно положить кучу, буфер кадра или большие таблицы — через секцию компоновщика (раздел 11) или просто указателем. Она медленнее встроенной SRAM: каждое 32-битное обращение к 16-битной памяти контроллер делит на два цикла шины (RM0090, правила транзакций в 36.3.1).
41б.6 SDRAM на FMC: запуск и регенерация
SDRAM дешевле SRAM того же объёма, но она динамическая: ячейки разряжаются, и каждую строку надо регенерировать не реже, чем за период регенерации (обычно 64 мс на все строки). Её адрес мультиплексирован: строка, потом столбец. Поэтому FMC держит отдельный контроллер SDRAM со своими регистрами SDCR, SDTR, SDCMR и SDRTR.
У STM32F429I-Discovery SDRAM IS42S16400J на банке 2 — адрес 0xD000 0000:
12 бит строки, 8 бит столбца, 4 банка, шина 16 бит. Объём —
4096 × 256 × 4 × 2 = 8 388 608 байт = 0x80 0000, как SDRAM_DEVICE_SIZE
в BSP. SDCLK — HCLK / 2.
Запуск по RM0090 — восемь шагов:
- SDCR: частота SDCLK, чтение пачками RBURST, задержка RPIPE, строки, столбцы, ширина, банки, CAS.
- SDTR: тайминги памяти.
- Команда «Clock Configuration Enable» — на память пошёл SDCLK.
- Пауза: обычно около 100 мкс (по документации памяти).
- Команда «Precharge All» — закрыть все строки.
- Команда «Auto-refresh» — обычно 8 раз подряд (поле NRFS).
- Команда «Load Mode Register»: длина пачки 1 и CAS, как в SDCR.
- Счётчик регенерации в SDRTR.
По регистрам это функция sdram_init файла примера. Две вещи в ней легко
пропустить. Частоту SDCLK, RBURST и RPIPE пишут только в SDCR1, а TRC и TRP —
только в SDTR1, даже когда память стоит на банке 2: у SDCR2 и SDTR2 эти поля
не действуют. И в поля таймингов пишут «тактов минус один»: TRC 7 тактов —
это 6 в поле (так же делает HAL).
Тайминги BSP F429I-Discovery при SDCLK 90 МГц (11,1 нс):
| Параметр | Тактов | нс |
|---|---|---|
| TMRD (после Load Mode) | 2 | 22,2 |
| TXSR (выход из саморегенерации) | 7 | 77,8 |
| TRAS (строка открыта) | 4 | 44,4 |
| TRC (от строки до строки) | 7 | 77,8 |
| TWR (после записи) | 2 | 22,2 |
| TRP (закрытие строки) | 2 | 22,2 |
| TRCD (строка → столбец) | 2 | 22,2 |
Счётчик регенерации (регистр FMC_SDRTR):
COUNT = (период регенерации / число строк) × SDCLK − 20
Двадцать тактов — запас на случай, если запрос регенерации придёт во время принятого чтения. COUNT не меньше 41; сумма TWR + TRP + TRC + TRCD + 4 (здесь 17) запрещена.
| Плата | SDCLK | Строк | Интервал | Расчёт COUNT | В BSP |
|---|---|---|---|---|---|
| F429I-Discovery | 90 МГц | 4096 | 15,625 мкс | 1386 | 1386 |
| 746G-Discovery | 100 МГц | 4096 | 15,625 мкс | 1542 | 1539 |
| пример RM0090 | 60 МГц | 8192 | 7,812 мкс | 448 | 448 |
У 746G-Discovery в BSP на три такта меньше расчёта: регенерация чуть чаще нужного — это безопасная сторона. В примере RM0090 строк напечатано 8196 — опечатка, на результат 448 она не влияет.
41б.7 Грабли
- Сдвиг адреса на 16-битной шине. RS на A16 — это 0x6002 0000, а не 0x6001 0000: FSMC_A[n] = HADDR[n+1]. Дисплей молчит или рисует мусор, потому что данные уходят как команды.
- Банк 1 после сброса — NOR с мультиплексированной шиной. Сброс BCR1 —
0x0000 30DB: MUXEN = 1, тип NOR, WAITEN = 1, и MBKEN уже стоит. Этого мало:
нужно снять MUXEN, выбрать тип SRAM и ширину — как в
fsmc_lcd_init. - Счётчик регенерации под чужую частоту. Пример emWin для
F429I-Discovery из пака Keil запускает ядро на 168 МГц и зовёт
BSP_SDRAM_Initс COUNT 1386 — числом для SDCLK 90 МГц. На 84 МГц регенерация идёт раз в 16,74 мкс вместо 15,625: все 4096 строк обходятся за 68,6 мс вместо 64, реже нормы на 7,1 %. На столе это работает — запас у памяти есть, — но нарушение нормы проявится на жаре. Для 84 МГц нужен COUNT 1292. Меняли HCLK или делитель SDCLK — пересчитайте COUNT. - Банк 2 SDRAM и поля SDCR1/SDTR1. SDCLK, RBURST, RPIPE, TRC и TRP пишутся в регистры банка 1, даже если памяти на банке 1 нет.
- Внешняя память на Cortex-M7 без MPU. По карте памяти ядра по умолчанию (DUI 0646B, таблицы 2-11 и 2-12) область 0x6000 0000–0x7FFF FFFF — External RAM: Normal, кэш с обратной записью. Дисплей на NE1 у F7 без настройки MPU (раздел 11а) получит запись в кэш вместо шины и упреждающее чтение регистров. Область 0xC000 0000–0xDFFF FFFF — External device: Device и XN, код из SDRAM там не исполнить. RM0385 даёт биты SWP_FMC в SYSCFG_MEMRMP: они меняют местами банки SDRAM и NOR/PSRAM, и SDRAM переезжает на 0x6000 0000 — «чтобы исполнять код из SDRAM, не меняя атрибутов MPU по умолчанию».
- Слишком короткий DATAST. Цикл, короче требуемого памятью или дисплеем, чаще ломает часть обращений, а не все: картинка «почти правильная». Сначала ставят с запасом, потом ужимают.
41б.8 Итоги
- FSMC — у F103 высокой плотности и F405/F407; FMC — у F427/F429, F7 и H7, с контроллером SDRAM. Внешняя память для программы — область адресов.
- Банк 1: NE1…NE4 с 0x6000 0000, по 64 МБ; SDRAM — 0xC000 0000 и 0xD000 0000.
- На 16-битной шине FSMC_A[n] = HADDR[n+1]: RS на A0 — данные по 0x6000 0002, на A16 — по 0x6002 0000.
- Режим A: чтение ADDSET + DATAST, запись ADDSET + DATAST + 1 тактов HCLK; у 412G-Discovery — 450 и 90 нс; 240×240 за 5,184 мс по шине.
- SDRAM запускают восемью шагами; COUNT = интервал × SDCLK − 20: 1386 для 90 МГц, 1292 для 84 МГц.
- На Cortex-M7 область 0x6000 0000 кэшируется, а 0xC000 0000 — Device без исполнения: нужен MPU или SWP_FMC.
Полные исходники
fmc41b.c — FSMC/FMC по регистрам: дисплей 8080 на F407 и SDRAM на F429.
/* 41б: FSMC/FMC по регистрам — дисплей 8080 на F407 и SDRAM на F429.
Собирается armclang: -DSTM32F407xx (дисплей) и -DSTM32F429xx (SDRAM). */
#include <stddef.h>
#include <stdint.h>
/* ── дисплей 8080 на NE1, линия RS на A0 (как у STM32412G-Discovery) ── */
typedef struct {
volatile uint16_t REG; /* 0x60000000: A0 = 0 — команда */
volatile uint16_t RAM; /* 0x60000002: A0 = 1 — данные */
} lcd_t;
#define LCD ((lcd_t *)0x60000000u)
_Static_assert(offsetof(lcd_t, RAM) == 2, "на 16-битной шине A0 = HADDR[1]: данные на 2 байта выше");
void lcd_cmd(uint8_t c) { LCD->REG = c; }
void lcd_data(uint16_t d) { LCD->RAM = d; }
void lcd_fill(uint16_t color, uint32_t n)
{
while (n--)
LCD->RAM = color; /* одна запись по шине на точку */
}
#if defined(STM32F407xx)
/* поля BTR/BWTR — RM0090, таблицы 229–230; ACCMOD = 0 — режим A */
#define BTR(addset, datast, busturn) \
(((uint32_t)(addset) << FSMC_BTR1_ADDSET_Pos) | ((uint32_t)(datast) << FSMC_BTR1_DATAST_Pos) | \
((uint32_t)(busturn) << FSMC_BTR1_BUSTURN_Pos))
_Static_assert(FSMC_Bank1_R_BASE == 0xA0000000u && FSMC_Bank1E_R_BASE == 0xA0000104u, "регистры FSMC");
_Static_assert(BTR(9, 36, 1) == 0x00012409u && BTR(1, 7, 0) == 0x00000701u, "BTR1 и BWTR1");
void fsmc_lcd_init(void) /* выводы PD, PE, PF — AF12, их настраивает CubeMX */
{
RCC->AHB3ENR |= RCC_AHB3ENR_FSMCEN;
FSMC_Bank1->BTCR[1] = BTR(9, 36, 1); /* BTR1, чтение: 9 + 36 = 45 HCLK */
FSMC_Bank1E->BWTR[0] = BTR(1, 7, 0); /* BWTR1, запись: 1 + 7 + 1 = 9 HCLK */
/* после сброса BCR1 = 0x30DB: NOR, адрес и данные на одних линиях (MUXEN), WAITEN */
FSMC_Bank1->BTCR[0] = (FSMC_Bank1->BTCR[0]
& ~(FSMC_BCR1_MUXEN | FSMC_BCR1_MTYP | FSMC_BCR1_MWID | FSMC_BCR1_WAITEN))
| FSMC_BCR1_MWID_0 | FSMC_BCR1_WREN | FSMC_BCR1_EXTMOD | FSMC_BCR1_MBKEN;
}
#elif defined(STM32F429xx)
/* ── SDRAM IS42S16400J на банке 2 (0xD0000000), как у STM32F429I-Discovery ── */
_Static_assert(FMC_Bank5_6_R_BASE == 0xA0000140u, "регистры SDRAM");
enum { CMD_CLK = 1, CMD_PALL = 2, CMD_AUTOREFRESH = 3, CMD_LOAD_MODE = 4 };
static void sdram_cmd(uint32_t mode, uint32_t nrfs, uint32_t mrd)
{
FMC_Bank5_6->SDCMR = (mode << FMC_SDCMR_MODE_Pos) | FMC_SDCMR_CTB2 |
((nrfs - 1u) << FMC_SDCMR_NRFS_Pos) | (mrd << FMC_SDCMR_MRD_Pos);
while (FMC_Bank5_6->SDSR & FMC_SDSR_BUSY) {
}
}
extern void delay_us(uint32_t us);
void sdram_init(uint32_t count) /* 1386 при SDCLK 90 МГц, 1292 при 84 МГц */
{
RCC->AHB3ENR |= RCC_AHB3ENR_FMCEN;
/* SDCLK, RBURST и RPIPE — только в SDCR1, даже когда память на банке 2 */
FMC_Bank5_6->SDCR[0] = (2u << FMC_SDCR1_SDCLK_Pos) | (1u << FMC_SDCR1_RPIPE_Pos);
FMC_Bank5_6->SDCR[1] = (0u << FMC_SDCR1_NC_Pos) | (1u << FMC_SDCR1_NR_Pos) | (1u << FMC_SDCR1_MWID_Pos)
| (1u << FMC_SDCR1_NB_Pos) | (3u << FMC_SDCR1_CAS_Pos);
/* TRC и TRP — только в SDTR1; в поля пишут «тактов минус один» */
FMC_Bank5_6->SDTR[0] = ((7u - 1u) << FMC_SDTR1_TRC_Pos) | ((2u - 1u) << FMC_SDTR1_TRP_Pos);
FMC_Bank5_6->SDTR[1] = ((2u - 1u) << FMC_SDTR1_TMRD_Pos) | ((7u - 1u) << FMC_SDTR1_TXSR_Pos)
| ((4u - 1u) << FMC_SDTR1_TRAS_Pos) | ((2u - 1u) << FMC_SDTR1_TWR_Pos)
| ((2u - 1u) << FMC_SDTR1_TRCD_Pos);
sdram_cmd(CMD_CLK, 1, 0); /* 3: подать SDCLK */
delay_us(100); /* 4: около 100 мкс */
sdram_cmd(CMD_PALL, 1, 0); /* 5: закрыть все строки */
sdram_cmd(CMD_AUTOREFRESH, 8, 0); /* 6: восемь регенераций подряд */
sdram_cmd(CMD_LOAD_MODE, 1, 0x0230); /* 7: BL 1, CAS 3, запись одиночная */
FMC_Bank5_6->SDRTR = count << FMC_SDRTR_COUNT_Pos; /* 8: счётчик регенерации */
}
#endif
Дальше: раздел 41в «Quad-SPI: внешняя флеш как память и код из неё» — последовательная внешняя флеш, которую тоже видно как область адресов.