Разбираемся как устроена память микроконтроллера на примере STM32F205VGT6.
Так же мы используем CubeMX для подготовки проекта.
Где хранятся данные , где стек, где код программы?
Генерим сначала пустой проект, оставляем только пустой main().
читаем ld файл
Linker script for STM32F205VG Device with ** 1024KByte FLASH, 128KByte RAM.
ld файл это основа, здесь CubeMx прописывает все исходные параметры памяти, стека, кода. Именно его надо спокойно/внимательно изучить, здесь все очевидно на самом деле.
/* Specify the memory areas */ MEMORY < RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K >
Стек в микроконтроллерах на ядрах ARM растёт сверху вниз.
_estack = 0x20020000; /* end of RAM */ // 0x20000=131072
RAM по даташиту Up to 128 4 Kbytes of SRAM. То есть есть еще какие-то 4Kb SRAM в конце.
Стек располагается отдельно от остальных блоков памяти, в конце ОЗУ. Конец ОЗУ по мнению CubeMX это 0x20020000=..131072 примерно (128 4Kb).
Программирование микроконтроллеров: Урок 2. Организация памяти данных (регистры)
Для контроля минимальных запасов стека и кучи в CubeMX есть такие установки
_Min_Heap_Size = 0x200; /* required amount of heap */ _Min_Stack_Size = 0x400; /* required amount of stack */
(_user_heap_stack = _Min_Heap_Size _Min_Stack_Size )
list полезный по информации файл на выходе сборки
.text — /* The program code and other data goes into FLASH */ , это скомпилированный машинный код — помещается во FLASH;
.data — /* Initialized data sections goes into RAM, load LMA copy after code */ — Переменные, это помещается в RAM; и зачем-то копия помещается еще во FLASH.
.rodata — /* Constant data goes into FLASH */ — аналог .data для неизменяемых данных, но помещается во FLASH;
.bss — /* Uninitialized data section */ , глобальные и статические переменные, которые при старте содержат нулевое значение — помещаются в RAM.
Момент истины
FLASH может содержать только неизменяемые данные , т.е. их нельзя в процессе исполнения перезаписать. (FLASH (rx) ) rx = read execute . Отсюда и идет весь этот винигред.
Sections: Idx Name Size VMA LMA File off Algn 0 .isr_vector 00000184 08000000 08000000 00010000 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .text 0000015c 08000184 08000184 00010184 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .rodata 00000000 080002e0 080002e8 000102e8 2**0 CONTENTS, ALLOC, LOAD, DATA 3 .init_array 00000004 080002e0 080002e0 000102e0 2**2 CONTENTS, ALLOC, LOAD, DATA 4 .fini_array 00000004 080002e4 080002e4 000102e4 2**2 CONTENTS, ALLOC, LOAD, DATA 5 .data 00000000 20000000 20000000 000102e8 2**0 CONTENTS, ALLOC, LOAD, DATA 6 .bss 00000020 20000000 080002e8 00020000 2**2 ALLOC 7 ._user_heap_stack 00000600 20000020 080002e8 00020020 2**0 ALLOC
ld файл прописывает где и куда какие данные сохранить
.bss : < /* This is used by the startup in order to initialize the .bss secion */ _sbss = .; /* define a global symbol at bss start */ __bss_start__ = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; > >RAM
>RAM — это помещается в RAM !
Микроконтроллеры это просто.#2 Память
файл startup_stm32f205xx.s
Этот файл генерируется CubeMX. Он на ассемблере.
Смысл его в том, чтобы начать выполнение программы: для этого надо сначала установить указатель стека на начало стека :
Reset_Handler: ldr sp, =_estack /* set stack pointer */
Теперь для закрепления понимания поэкспериментируем
char buf[10000]; main()
эффекта изменения распределения памяти не произойдет
сделаем так : char buf[10000]=; — ничего не меняется
теперь в main сделаем так :
for(int i=0; i < sizeof(buf); i ) buf[i]=1;
и .bss увеличивается с 00000020 до 00002730 (10032!) . То есть ушло в RAM!
теперь делаем так :
const char buf[10000]=0>; main < char ch=0; for(int i=0; i < sizeof(buf); i ) char ch=buf[i]; printf(«%c»,ch);
И .rodata увеличивается с 10 до 00002784 (10116) ! То есть ушло во FLASH (кстати только при условии явной инициализации=).
Выводы такие :
Побольше const
Если RAM памяти не хватает или не хватает стека (он всегда есть часть RAM), то можно попробовать все что не меняется обозвать const , инициализировать обязательно и тогда это попадет не в RAM , а в FLASH.
section `._user_heap_stack’ will not fit in region `RAM’
Вот такая бяка появляется, когда RAM не хватает, например добавили нового кода , сторонние библиотеки .
У меня (на самом деле) эта ошибки ушла после удаления файла startupxxx.s и перегенерации заново проекта(и файла startup) из CubeMx.
Файлы для скачивания
* STM32F205VGT6_MemoryUsed [zip]
пустышка для изучения распределения памяти в контроллере STM32
Источник: kkmspb.ru
Память контроллера
Практически любая программа затрагивает память: там хранятся переменные, состояние программы, даже сама программа. Давайте разберёмся, как устроена память в микроконтроллере, что где хранится и как используется.
Обзор
В целом, самое главное, что должно быть у любого уважающего себя контроллера — это память программ. То место, куда записаны инструкции, которые он должен исполнять и какие-то, может, дополнительные данные: таблицы, картинки, тексты. Память эта обычно записывается программатором и в процессе выполнения не меняется. В любой момент времени там всегда одни и те же данные.
Вторая, очень важная и обязательная часть — оперативная память. То место, где хранятся данные времени выполнения: данные с датчиков, сформированные тексты, картинки для экрана частично или полностью, промежуточные данные расчётов, стек и куча, всё, что угодно, что создаётся во время работы. Данные там хранятся только пока есть питание. После перезагрузки всё пропадёт.
Эта память обычно инициализируется нулями в процессе старта программы и глобальными переменными, если они есть. Это обычно скрыто от программиста.
У очень маленьких контроллеров (вроде attiny10) её может и не быть вовсе.
Иногда встречается память для хранения настроек (обычно типа EEPROM), очень небольшого размера. Все значения, которые туда записаны, сохраняются и без питания. Обычно запись может идти по одному байту, без необходимости стирания целого блока. В контроллерах серии AVR, например, она почти всегда есть, а в STM32 её нет. Ресурс перезаписей ограничен.
Иногда встречается другой тип памяти: встроенный в блок часов реального времени, которые питаются от отдельной батарейки. Обычно объём там тоже очень незначительный, десяток-другой байт, хотя бывает и больше. Пока батарейка есть, данные там будут присутствовать. Перезаписывать можно сколько угодно.
Это вся память, которая доступна ядру напрямую без каких-то особых ухищрений. В серьёзных контроллерах встречается ещё контроллер памяти, с помощью которого можно подключать внешние микросхемы разных типов: параллельный NOR, параллельный NAND, последовательный spi nand, QuadSPI, SRAM, SDRAM и дальше в зависимости от крутости контроллера.
Бывают ещё микросхемы памяти с неспециализированными под память интерфейсами: I2C, SPI и т.д., но к ним надо обращаться в общем порядке для используемого протокола. Программно передавать все команды. Впрочем, это часто используемый способ подключения дополнительной памяти для хранения настроек.
И ещё один момент: память MMC/SD, для которой, бывает, предусматривают специальный модуль с аппаратной реализацией протокола SDIO.
Архитектура контроллера
Первый важный момент, который стоит понимать, какая используется в контроллере архитектура:
1. Архитектура фон Неймана. Память программ и данных находятся в одном адресном пространстве и имеют идентичные методы доступа к ним. То есть прочитать какую-нибудь таблицу из памяти программ и из оперативной памяти можно одним и тем же кодом, просто поменяв там адрес. Это все контроллеры ARM, stm8, MSP430 и др.
2. Гарвардская архитектура. Память программ и память данных имеют свои адресные пространства и свои методы доступа к ним. Нельзя одним и тем же кодом прочитать данные и из памяти программ, и из оперативной памяти. Требуется их вычитывать с помощью специальных инструкций и механизмов ядра. Это контроллеры AVR, PIC, 8051 и др.
Второй важный момент: разрядность ядра и памяти. Восьмибитные контроллеры (AVR, PIC) для оперативной памяти имеют 8-битную шину. То есть чтобы вычитать 32-битное число, нужно четыре операции чтения. В том же арме на это уйдёт всего лишь одна операция.
Память программ
Память программ может быть внешней, может быть и внутренней. Внешняя — это или для мамонтов тридцати-сорокалетней давности, или для контроллеров и процессоров, где память должна быть огромной, какая-нибудь параллельная NOR/NAND на десятки-сотни мегабайт или даже гигабайты, как во всяких мобильных процессорах. Впрочем, процессоры могут запускаться и со всяких экзотических устройств типа MMC/SD-карт.
Внутренняя память более обычное дело. Объём небольшой: от какого-нибудь килобайта до пары мегабайт.
У современных контроллеров бывает встроенный механизм её программной перезаписи изнутри. Всякие армы, авры и прочие похожие могут обновлять свою программу или какие-то данные в ней самостоятельно. В таких контроллерах, бывает, предусматривают дополнительный блок памяти или регион памяти для специальной программы-бутлоадера, которая может запустится вместо основной и записать основную программу по командам снаружи, без необходимости использовать программатор. И это даже может быть выполнено через USB, если, конечно, этот протокол поддерживается =) Бутлоадер, в общем-то, от обычной программы ничем не отличается, кроме разве что специального назначения и возможности запуститься вместо обычной.
Но никто не запрещает обновлять программу и из основного кода, разве что требуются некоторые ухищрения. А то стирать программу, которую выполняешь — такое себе развлечение. Необходимо сначала куда-то записать новый код, а потом каким-то внешним кодом, который не будет перезаписан, перезаписать основную программу. Но это уже тонкости.
Это механизм можно использовать и для записи настроек, хотя это и не очень удобно: память программ обычно стирается большими блоками, размером в несколько килобайт, и чтобы поменять один байт надо сохранить и перезаписать весь блок. А ресурс перезаписей тратится у всего блока! Да ещё и может записаться не так. Всегда надо проверять, то ли записалось, что хотелось.
Скорость доступа к ней обычно невелика. Но у контроллеров с низкой тактовой частотой это незаметно, всё успевается. У контроллеров с частотой в сотню-другую мегагерц, чтение может тормозить выполнение программы.
Ресурс обычно 10-100 тысяч перезаписей, хотя в дешёвых сериях встречается и ресурс всего в сотню гарантированных перезаписей.
Для контроллеров с архитектурой фон Неймана чтение будет выглядеть так:
uint8_t from_pgm = *(uint8_t)pointer_to_program_data;
const char text[] = «Hello world»;
puts( // это сработает!
В фон-нейманоской архитектуре константа text будет записана, скорее всего, в память программ и прочитана оттуда же.
Для гарвардской архитектуры такой же код сработает, но это будет трюком компилятора. Он просто подгрузит при запуске этот текст из памяти программ в оперативную память. А если памяти мало и надо экономить. то тогда используем специальные функции, которые берут данные напрямую из памяти программ. Например, в AVR это будет выглядеть так:
#include
PROGMEM const char text[] = «Hello world»;
puts_P( // Это сработает!
puts( // А это нет!
Внутри же там, если делать самостоятельно, будет что-то вроде:
void puts_P(PROGMEM const char * text) char C = pgm_read_byte(text++); // специальная функция из avr/pgmspace.h, которая умеет читать память программ
while(C) putc(C);
C = pgm_read_byte(text++);
>
>
Это очень неудобно с точки зрения программиста, но приходится выкручиваться.
Запись в эту память происходит очень по-разному. В STM32 надо в специальном модуле настроить режим записи: по байту ли, по два ли байта, или по четыре сразу, скорость доступа и разрешить запись. а дальше запись идёт так же, как в оперативку, пока не будет снят флаг разрешения записи. А модуль будет перехватывать команды записи и менять содержимое памяти.
При этом, при записи, можно записывать нули (как если бы запись была такой: *olddata = *olddata // читаем байт из адреса 12 памяти настроек
eeprom_write_byte( (uint8_t*) 0x12, 0x55); // обновим
Внешняя память по интерфейсам памяти
Если требуется зачем-то хранить много данных, подключаем микросхему внешней памяти. И можем писать туда песни-пляски-логи-конфигурации, в общем, много чего.
Объём обычно велик, десятки и сотни мегабайт, иначе нет смысла заморачиваться с этим, скорость большая, но всё же до скорости ядра обычно не дотягивает. Выполнение программ из этой памяти будет не самым быстрым действом.
С точки зрения программы, такая оперативная память ничем не будет отличаться от оперативной, разве что адресом и скоростью доступа. Ну или память программ, в зависимости от типа памяти.
Она не доступна с момента запуска и требуется настраивать контроллер памяти, чтобы этот доступ всё же появился: какой тип памяти, ширина линий адреса/данных, отдельные ли они, как синхронизировать и т.д.
Работа с динамической памятью (SRAM/SDRAM) после подключения не отличается от работы с оперативной. Работа со статической памятью (NAND/NOR) не отличается на чтение от обычной памяти программ, но с записью надо соблюсти ритуал, прописанный в даташите на микросхему памяти. Например, запись в NOR (условно):
#define BASE_NOR_ADDRESS 0x60000000
// Перед этим память стёрли
// Сначала пляски с бубном
*( BASE_NOR_ADDRESS + 0xAAA) = 0xAA;
*(BASE_NOR_ADDRESS + 0x555) = 0x55;
*(BASE_NOR_ADDRESS + 0xAAA) = 0xA0;
// А потом пишем!
for(size_t i = 0; i < size; i++) *(BASE_NOR_ADDRESS + address + i) = data[i];
>
Внешняя память по стандартным интерфейсам
Обычно микросхемы памяти для настроек имеют интерфейс или SPI, или I2C. В микросхемах SPI общение выглядит как «команда-данные (чтение или запись)», протокол надо уточнять в даташите, то в I2C это просто чтение или запись штатными средствами протокола. То есть регистры памяти замаплены на регистры I2C. Из различий может быть только размер адреса, который записывается перед операцией чтения/записи: один, два или три байта.
Довольно часто используется как память настроек в контроллерах, где нет встроенной памяти.
Обычно такая память выполняется по технологии EEPROM, то есть имеет все те же недостатки: ресурс 100-1000 записей, низкую скорость записи, блочное стирание. В еепром-микросхемах с индивидуальной перезаписью байт, запись одного байта может занимать несколько миллисекунд. Жуть как долго.
В SPI это может быть и побыстрее, за это время можно успеть записать целый блок, но и стирать там надо тоже блоками, нельзя взять и перезаписать один байт. Хотя подробности лучше смотреть для каждой микросхемы отдельно. В общем, медленно и печально, но зато стоит копейки. То есть можно найти микросхемы за пару-тройку рублей.
Есть серия памяти по технологии FRAM, там ресурс в несколько миллиардов перезаписей, высокая надёжность, индивидуальная перезапись каждого байта и быстрота. Правда, объём и цена там такие себе.
Естественно, такая память ядру не видна, код из неё не может выполняться, да и указатель на такую память не получишь. Надо байт за байтом её копировать с помощью приёмопередатчика в память. Или обратно.
Пример записи данных в SPI-память (условно):
void write_memory(uint32_t address, const uint8_t * data, uint32_t length) MemCS.Low(); // Опустим линию выбора микросхемы
spi_write(MEM_CODE_WRITE);
spi_write(address // младший байт адреса
spi_write((address >> 8) // средний байт адреса
spi_write((address >> 16) // старший байт адреса
for(size_t i = 0; i < length; i++) < // а теперь данные
spi_write(*data++);
>
MemCS.High(); // Сеанс окончен, линию выбора микросхемы в высокий уровень
>
Регистры периферии
Раз уж затронули тему памяти, то рассмотрим и ещё один момент: обычно все модули, какие есть в процессоре или контроллере, имеют выход в память, через которую они и управляются. То есть каждый модуль может иметь несколько десятков байт в адресном пространстве, где можно прочитать его состояние и записать какие-то команды.
Например, запись в регистр данных приёмопередатчика инициирует процесс отправки этого байта по протоколу с заданными настройками. Или же данные появятся в приёмном регистре после получения посылки, откуда их можно прочитать.
Например, условный модуль контроля выводов микросхемы:
Регистр GPDIR, адрес 0x41000000, 16 бит, отвечает за направление работы выводов номерами с 0 до 15. Бит в состоянии 0 означает вход, состояние 1 означает выход.
Регистр GPSET, адрес 0x41000002, 16 бит, отвечает за работу выводов в режиме выхода. Состояние 0 означает низкий уровень на ножке, состояние 1 высокий. Для выводов в режиме входа значение игнорируется.
Регистр GPGET, адрес 0x41000004, 16 бит, отвечает за работу выводов в режиме входа, показывает логический уровень на ножке. Установлен в 0, если на ножке низкий уровень, установлен в 1, если высокий. Запись игнорирует. Для ножек в режиме вывода дублирует значение из регистра GPSET.
И примерный код для такого описания:
// Перевели в режим выхода вывод номер 7.
*((volatile uint16_t*)0x41000000) |= 1
// Установили его состояние в низкий уровень
*((volatile uint16_t*)0x41000002) (1
Обратите внимание на использование ключевого слова volatile при работе с регистрами периферии. Оно запрещает буферизацию значения, так как значение может поменяться в любой момент.
Модуль прямой работы с памятью
В контроллерах посерьёзнее часто включается специальный модуль, имеющий часто свой выделенный доступ к разным шинам памяти, который умеет память копировать. Из одного места в другое. Он может копировать тупо от забора и до обеда, может копировать по сигналу: пришёл байт, отсчитал таймер нужное время, и т.д.
И тем самым может освобождать ядро от кучи задач, особенно, если на них стоит реагировать сразу и выдерживать точное время. Например, хотим мы измерить напряжение в сети через делитель. Говорим модулю копирования: доставай данные из модуля АЦП и складывай их по сигналу таймера (с шагом 10 мкс) в участок памяти такой-то с такой-то длиной. И всё, что остаётся сделать после запуска модуля — дождаться завершения, занимаясь своими делами. Данные будут лежать и ждать обработки.
Вся работа программы заключается в работе с памятью. Надо хорошо понимать, где какая память используется, какие там есть сложности и подводные камни, и как их избежать.
Постарайтесь понять особенности работы с каждой доступной памятью вашего микроконтроллера, это недолго, и сбережёт вам много времени и нервных клеток в перспективе.
Источник: dzen.ru
Память
Коды программ микроконтроллера размещаются в энергонезависимом ПЗУ, выполненной по технологии FLASH. При нормальных условиях эксплуатации, FLASH-память позволяет сохранять свое содержимое в неизменном виде в течение 40 лет и допускает как минимум 10000 циклов стирания/записи.
Рис.5 Организация FLASH памяти программ ATmega8 Организация памяти программ, на примере ATmega8, приведена на рис.5. Размер FLASH-памяти этой модели составляет 8192 байт. Но, поскольку каждая команда занимает 2 или 4 байта, то по отношению к AVR точнее будет говорить об объеме в 4096 слов.
Такая размерность определяет максимально возможное число слов команд (кодов операций), доступное при написании программы. Для адресации памяти программ используется программный счетчик PC (Program Counter) (другое название: регистр или счетчик команд). Он представляет собой регистр, в котором находится текущей адрес команды во FLASH-памяти.
Таким образом, счетчик команд адресует 16-разрядные слова, а не байты, и имеет переменную разрядность, которая зависит от размера FLASH. Так у ATmega8 (4096 слов), PC имеет разрядность 12 бит, у ATmega16 (8192 слова) — 13 бит и т.д. В архитектуре AVR, PC является недоступным для программиста регистром.
Помимо своего основного назначения (хранение команд), FLASH-память AVR-микроконтроллеров также позволяет хранить пользовательские данные произвольного типа: константы, таблицы, постоянные коэффициенты и т.д. Минимальный адресуемый элемент в этом случае 1 байт. Доступ к таким данным из прикладной программы производится различными модификациями инструкции lpm.
Как правило, алгоритмы работы микроконтроллера, записанные в памяти программ, не должны изменяться во времени. Однако у AVR все-таки имеется возможность модифицировать содержимое FLASH посредством собственного программного обеспечения. Обсуждению этого вопроса посвящена глава «Самопрограммирование микроконтроллеров AVR».
Там же показан и другой способ разделения FLASH на секцию прикладной программы (Application Section) и секцию загрузчика (Boot Loader Section). Изменять содержимое памяти программ можно только из области загрузчика. В процессе программирования запись во FLASH-память происходит постранично. Размер страницы зависит от объема памяти конкретной модели и может быть равен 32, 64 или 128 слов. Перейти к следующей части: Память данных SRAM
Теги:
Котов Игорь Юрьевич
Опубликована: 2012 г.
0
Вознаградить Я собрал 0 0
Оценить статью
- Техническая грамотность
Источник: cxem.net