4927 subscribers

Микроконтроллеры для начинающих. Часть 13. Практический взгляд на архитектуру памяти.

404 full reads
956 story viewsUnique page visitors
404 read the story to the endThat's 42% of the total page views
4 minutes — average reading time

В предыдущих статьях я описывал архитектуру памяти вычислительного ядра микроконтроллеров. Вот эти статьи:

Я не стал упоминать здесь десятую часть (внеплановую). Теперь мы знаем достаточно много подробностей о том, как организована память в микроконтроллерах. Однако, остались не затронутыми практические вопросы использования этих знаний. И нет ответа на вопрос "а какой же микроконтроллер лучше?".

Давайте немного поговорим о некоторых плюсах и минусах разных моделей микроконтроллеров, с точки зрения организации памяти. О том, как это все отражается на подходах к решению практических задач, в том числе, с использованием языков высокого уровня. О том, как жить без аппаратного стека данных в PIC. И немного о MCS-51.

Какое же семейство микроконтроллеров лучше?

Я сразу дам ответ на этот вопрос, а потом постараюсь его объяснить.

Нет лучшего или худшего семейства микроконтроллеров. Как нет и лучшей или худшей архитектуры. Каждый микроконтроллер находит свое применение. Каждая архитектура имеет своих почитателей и своих критиков.

Одни архитектуры давно канули в лету, другие продолжают здравствовать. Появляются новые разработки. И касается это не только микроконтроллеров, но и архитектуры ЭВМ в целом. И дело тут не только в удачности разработки. Так канула в лету прекрасная архитектура DEC PDP-11/VAX-11. При этом прекрасно себя чувствует архитектура MCS-51, одна из первых разработок для микроконтроллеров, которая стала, де-факто, промышленным стандартом.

Давайте попробуем разобраться в особенностях архитектуры памяти, которые не всегда видны с первого взгляда, но имеют важное значение с точки зрения практического применения микроконтроллеров.

Разрядность адреса

Я не буду делать разницы между адресами программ и данных. Речь пойдет об общих моментах.

На первый взгляд, самым лучшим выглядит подход STM8, линейное единое адресное пространство. Однако, давайте взглянем на это под несколько иным углом зрения. Но для этого нам потребуется вспомнить, что адреса в STM8 бывают короткие, длинные и расширенные (полные). Я об этом писал в девятой части.

Это дает большую гибкость и позволяет экономить память программ задавая, где это возможно, наиболее короткий адрес. И действительно, существует, например, три разных команды вызова подпрограмм

  • CALL - самый привычный и часто используемый формат вызова. Адрес задается в виде 2 байт, а значит, переход за пределы 64 Кбайт невозможен. Во многих случаях, особенно для новичков, это ограничение не представляет проблемы. Но ведь может использоваться и, например, микроконтроллер STM8S207C8, у которого 128 Кбайт памяти программ.
  • CALLF - самый универсальный формат вызова. Здесь может указываться и длинный (16 бит) и полный (24 бита) адрес подпрограммы. Причем длина кода команды будет одинакова в обоих случаях, так как при указании 16 битного адреса собственно команде предшествует префикс.
  • CALLR - для самых экономных. Код команды занимает меньше всего места, всего 2 байта. При этом в команде указывается смещение адреса перехода относительно текущего адреса команды. Но результирующий адрес перехода 16 битный. А значит, данная команда не позволяет выйти за пределы 64 Кбайт.

Кстати, говоря о пределе в 64 Кбайт я имею ввиду не абсолютный полный адрес, а лишь 16 младших разрядов адреса. Просто команды CALL и CALLR не затрагивают регистр PCE (и не сохраняют его в стеке в качестве составной части адреса возврата), но в формировании полного адреса PCE продолжает участвовать!

Не правда ли, очень похоже на то, что PCE задает номер страницы памяти программ, а пара PCH:PCL адрес внутри страницы? Не напоминает страничную организацию памяти программ в PIC?

Кстати, и в AVR, который многие хвалят за логичность тоже не так все просто при работе с памятью программ. Только в AVR команды называются немого по иному.

  • CALL - Дальний переход . Адрес перехода занимает 22 бита. Однако, тут все еще и от размера регистра PC (16 или 22 бита) все зависит. Доступна не для всех микроконтроллеров.
  • EICALL - Адрес перехода формируется из содержимого регистра Z и специального регистра EIND, который и хранит старшие 8 бит адреса. Доступна не для всех микроконтроллеров.
  • ICALL - Аналогична EICAL, за исключением того, что старшие разряды адреса всегда равны 0, так как EIND участия в ее выполнении не принимает. Доступна не для всех микроконтроллеров.
  • RCALL - относительный переход. Но смещение относительно текущего адреса команды состоит из 12 бит.

Не правда ли, и в данном случае все очень похоже на PIC с его страничной организацией памяти программ, только номер страницы тут хранится в регистре EIND.

Что же получается, страничная организация используется во всех микроконтроллерах? Да, это так, в самом общем случае. Просто размер страницы разный. И в случае STM8 или AVR вы просто столкнетесь с этим фактом в гораздо меньшем количестве случаев.

Программы состоящие из нескольких исходных файлов

Новички чаще всего пишут программы в виде одно исходного файла. Причем язык программирования роли не играет. Но когда проект разрастается становится трудно работать с одним большим текстом программы и его разбивают на несколько частей - отдельных файлов. Причем каждый отдельный файл может содержать как данные, так и процедуры. Причем процедур в файле может быть и больше одной.

И вот тут у любого компилятора возникают затруднения. Давайте посмотрим на ситуацию внимательнее.

Пример программы состоящий из нескольких исходных файлов. Иллюстрация моя
Пример программы состоящий из нескольких исходных файлов. Иллюстрация моя
Пример программы состоящий из нескольких исходных файлов. Иллюстрация моя

В данном случае я показ пример на С, но ничего не изменилось бы и при использовании ассемблера. Именно поэтому я указал расширение файла src (исходный), а не с. Каждый из этих исходных файлов будет компилироваться отдельно от остальных. А результат компиляции, объектные файлы, потом будут собраны воедино компоновщиком. Все просто и привычно, чего тут такого?

Но задумайтесь, откуда компилятору при работе с файлом file1.src знать адрес переменной var или функции func? Они будут размещены в памяти компоновщиком уже после компиляции. Поэтому компилятор будет вынужден сгенерировать команду с пустым полем адреса операнда и дополнительно сформировать специальную запись, которая сообщит компоновщику, что несколько байт в коде он должен будет изменить, поместив туда нужный адрес.

Это очень упрощенное описание процесса, но нам сейчас его достаточно. А теперь вспомним, что я говорил о разрядности адреса. Какую команду для вызова func должен сформировать компилятор? Будет ли достаточно CALL или потребуется использовать CALLF (RCALL или CALL/ICALL/EICALL)?

Аналогично и с обнулением переменной var, так как ее адрес неизвестен при трансляции, то компилятор будет затрудняться, какой формат команды CLR использовать (в случае STM8).

Для решения этой проблемы давно придумали понятие "модель памяти". Это уже немного подзабылось в мире программирования для ПК, но многие вспомнят про модели TINY, SMALL, COMPACT, LARGE. Указание модели памяти позволяло компилятору выбрать какие команды и в каком формате генерировать.

В данном случае ситуация аналогичная. Причем даже для PIC, с их относительно малыми страницами памяти программ можно задать формат адреса памяти (это влияет на необходимость переключения страниц при вызове или банков для доступа к данным).

Модель памяти можно не указывать для микроконтроллеров с объемом памяти до 64 Кбайт, просто все адреса будут 16 битными. В большинстве случаев это разумный размер. Если не указать формат адреса указателя для PIC, то он обычно считается 8 битным для данных и полным для кода.

Почему компоновщик не может разобраться автоматически? А он просто не имеет такого права. Он может подставить адрес, или смещение адреса, в нужное место кода, но не должен никоим образом изменять собственно коды команд, сформированные компилятором или написанные программистом.

Жизнь без аппаратного стека данных есть! И иногда это даже лучше

Вопреки ожиданиям речь пойдет далеко не только про PIC. Давайте, как всегда, посмотрим внимательнее

Типичное использование стека. Иллюстрация моя
Типичное использование стека. Иллюстрация моя
Типичное использование стека. Иллюстрация моя

Здесь я показал состояние стека после вызова процедуры, которой были переданы два параметра (arg1 и arg2) и которая имеет две локальные переменные (local1 и local2).

В случае аппаратного стека у нас есть регистр SP, который как раз соответствует указателю вершины стека. По своей сути это обычный индексный регистр, правда со специализированным поведением.

Мы можем организовать собственный стек выделив область памяти данных и использовать для доступа к ней один из доступных индексных регистров. При этом придется использовать не команды PUSH и POP, а обычную косвенную адресацию. И добавить команды увеличения и уменьшения содержимого индексного регистра, если отсутствуют режимы с автоинкрементом и автодекрементом.

Это вряд ли имеет смысл для STM8 или AVR, но для PIC такая замена аппаратного стека данных (правда там адрес возврата будет в аппаратном стеке) используется и называется программным (software) стеком.

Стек, безусловно, очень полезное изобретение. И он широко используется для передачи параметров в процедуры и размещения временных и локальных данных с ограниченным временем жизни. Я не буду подробно останавливаться на тонкостях использования стека, нам сейчас это не очень важно, просто сошлюсь на когда то написанную мной статью "Размещение переменных, передача параметров в процедуры".

Однако, кроме преимуществ стек имеет и недостатки. Во первых, со стеком работают посредством косвенной адресации, что может иногда привести к более медленной работе программы из-за дополнительных шагов для доступа к данным. Во вторых, размер стека нужно контролировать, иначе он может расшириться на область совершенно посторонних данных. Такое бывает при несбалансированном доступе, когда количество операций PUSH не равно количеству операций POP. Или при выделении стека недостаточного объема. И делать это нужно при выполнении программы, так как стек сущность динамическая.

Другими словами, стек может увеличить размер кода программы и замедлить ее выполнение. Конечно, такое происходит не всегда, но в общем случае такое возможно.

Однако, есть способ превратить динамическую сущность стека в статическую. Давайте посмотрим еще внимательнее

Состояние стека при вызове функций. Иллюстрация моя
Состояние стека при вызове функций. Иллюстрация моя
Состояние стека при вызове функций. Иллюстрация моя

Здесь я показал три исходных файла, хоть это и не столь важно, в данном случае. Важнее то, что функция main у нас вызывает последовательно функции func1 и func2. При этом func2 вызывает еще и func3. Чуть ниже показано условное состояние стека внутри функций. Цветные прямоугольники это стековые кадры соответствующей функции.

Что мы здесь видим? Раз функции func1 и func2 вызываются не одновременно, а друг за другом, то и стековые кадры этих функций не могут существовать одновременно. А вот стековые кадры func2 и func3 существуют одновременно, так как во время работы func3 работа func2 еще не завершилась.

А это означает, что мы можем распределять пространство стека не во время работы программы, динамически, а статически во время компиляции. И переменным в стеке мы можем присвоить статические адреса.

Распределение адресов "стека" статически во время компиляции. Иллюстрация моя
Распределение адресов "стека" статически во время компиляции. Иллюстрация моя
Распределение адресов "стека" статически во время компиляции. Иллюстрация моя

Здесь я показал, что переменные в стековых кадрах func1 и func2 занимают одну и туже область памяти. Причем мы вынуждены резервировать память исходя из наибольшего размера кадра. И только потом размещать кадр func3.

Красота да и только! Теперь у нас все переменные используют статические адреса и прямую адресацию, что может быть более быстрым. И контроль стека будет выполнен компилятором, что даст еще большую экономию и скорость. Хорошо?

Хорошо, да не очень. За все приходится платить, и данный случай не исключение. Такое распределение памяти на этапе компиляции исключает возможность рекурсивных вызовов и повторной входимости. Но если этого не требуется, а таких программ много на самом деле, то и статический "стек" становится интересным и целесообразным.

Такое распределение памяти для PIC называется компилируемым (compiled) стеком. Но применять этот метод имеет смысл для любых микроконтроллеров, если не нужна рекурсия и повторная входимость.

Страничная (банковая) память не всегда плохо, а иногда бывает и полезна

И опять я не только про PIC! Принято считать, что страничное деление памяти всегда плохо. Однако, это не так. Дело в том, что переключая страницы или банки памяти можно существенно экономить время. Как? А вот смотрите

Пусть у нас имеется несколько потоков, или модулей. Например, это может быть связано с несколькими датчиками или управляемыми системами. С каждым потоком связан один из блоков данных, причем их структура одинакова, так как управляемые системы одинаковы. Это довольно распространенный случай.

Как в этом случае можно организовать работу с данными, что бы модуль-обработчик не догадывался, что систем несколько? Самое очевидное, и наиболее часто используемое, решение это передавать обработчику базовый адрес блока данных. Но ведь базовым адресом блока данных может быть и собственно номер банка (страницы) памяти. Если конечно количество доступных банков не меньше числа блоков данных.

Вы можете сказать, что это не польза, а всего лишь уменьшение вреда и способ уменьшить количество минусов преобразовав их в плюсы умелым использованием. И будете правы. Но давайте рассмотрим еще один вариант.

Микроконтроллеры предназначены для использования в составе систем управления, а это иногда требует очень быстрой реакции на происходящее. Обычно, для этого используются прерывания или иные механизмы. При этом всегда требуется сохранять используемые регистры и другую чувствительную информацию о состоянии прерываемого процесса. А это может потребовать относительно много времени.

В микроконтроллерах MCS-51 для решения этой проблемы имеется четыре блока регистров. В эти блоки отображаются регистры R0-R7. переключение этих регистровых банков осуществляется через биты RS0 и RS1 в регистре PSW. Если основная программа использует регистры R0-R7 в нулевом банке, а обработчик прерываний в первом, то мы можем сэкономить довольно много времени не сохраняя регистры в стеке или временной области памяти, а просто переключив регистровый банк.

Неравноправные регистры

С этой особенностью обычно сталкиваются те, кто пишет на ассемблере. Наиболее ярким примером может служить ограничение на использование регистров R0-R15 в микроконтроллерах AVR. Например, в команде CPI (сравнение с константой) можно указать только регистры R16-R31. На этом часто попадаются новички.

Другим примером может служить увеличение длины кода команды при использовании вместо индексного регистра X индексного регистра Y в микроконтроллерах STM8. В этом случае команде предшествует байт префикса замены регистра. Это конечно гораздо более мягкий случай не равноправия, по сравнению с AVR, но определенную злую шутку иногда сыграть может.

Еще одним примером неравноправия может служить нехватка пространства адресов ввода-вывода в AVR и связанное с этим вынесение части регистров оборудования в область памяти данных. Хотя тут неравноправие только логическое.

Адреса разные, а ячейка памяти одна

Тут опять можно привести в пример AVR с его наследием в виде адресного пространства вода-вывода. В 12 части я приводил пример с обращением к регистру SREG по адресу 3F в пространстве ввода-вывода и по адресу 5F в пространстве данных.

В этом смысле пространство ввода-вывода в AVR выглядит излишним и нелогичным. Его семантическое назначение понятно, но и новичков оно путает.

Менее очевидным примером может служить способ организации линейного пространства данных в Enhanced Mid-range. Напомню, что при этом банковая организация памяти данных сохраняется, но память доступна и в виде непрерывной линейной области причем с исключением SFR.

Казалось бы, что тут плохого? На самом деле плохое тут есть. Можно непреднамеренно исказить содержимое ячейки памяти получив к ней доступ по другому адресу. И найти подобные ошибки бывает очень трудно. А возникнуть они могут, например, когда вы беретесь за доработку кем то ранее написанной программы. Или когда переносите программу на другой микроконтроллер того же семейства но обладающий большими возможностями. Да и простая ошибка не исключается.

Адрес один, а ячейки памяти разные

Такое бывает при страничной (банковой) организации памяти. Например, в случае PIC команда CLR 0x06 какой регистр обнуляет? Заглянув в документацию можно сказать, что регистр PORTB и сильно ошибиться если упустить из виду выбранный банк памяти. Так как регистры PORTB и TRISB имеют один и тот же адрес байта внутри банка. А ведь переключение банка может быть сделано где то далеко в тексте программы, а не обязательно прямо перед нашей командой.

Другим примером может послужить переключение регистровых банков в MCS-51. Дело в том, что это не исключает возможности доступа к ячейкам памяти регистровых банков по прямым адресам в памяти данных. Кстати, тоже самое можно сказать об области прямо адресуемых бит (каждый бит имеет собственный адрес) и возможности доступа к этой области в в побайтовом режиме.

В этих случаях тоже возможны ошибки.

Заключение

В сегодняшней статье я попробовал показать, как изученная ранее теория и тонкости организации памяти в конкретных микроконтроллерах оказывают влияние на практическую, прикладную, сторону их применения.

Не смотря на кажущееся принципиальное различие между разными микроконтроллерами гораздо больше общего, чем может показаться с первого взгляда. Почти все, о чем я сегодня говорил, находит свое отражение во всех рассматриваемых микроконтроллерах.

Почему этого не было заметно раньше? Просто сегодня я рассматривал вопрос немного на более высоком уровне, скорее на логическом, чем на аппаратном.

До новых встреч!