Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 4. Регистры, старт и режимы работы контроллеров STM32
Понимаю, что статья уже получилась довольно длинной и всем хочется поскорее написать какую-нибудь программулину, делающую хоть что-то полезное, и залить её в контроллер, но так уж вышло, что контроллеры STM32 несколько сложнее простейших восьмибитных железяк, поэтому сейчас речь снова пойдёт, как пишут некоторые нетерпеливые читатели, «не о том».
В этой части мы поговорим о том, с чем нам чаще всего придётся оперировать при работе с контроллером — о рабочих регистрах ядра Cortex-M3, о режимах его работы и о том, как контроллер включается.
Итак, в ядре Cortex-M3 имеется 13 регистров общего назначения — R0..R12, регистр, используемый для хранения указателя стека, — R13, регистр связи — R14, счётчик команд — R15 и 5 регистров специального назначения.
Регистры общего назначения разделяются на младшие регистры — R0..R7 и старшие регистры — R8..R12. Разница между ними в том, что некоторые 16-тибитные команды набора thumb-2 умеют работать только с младшими регистрами, а со старшими — не умеют.
Регистров R13 вообще-то говоря два, а не один. Первый называется MSP — указатель основного стека, а второй PSP — указатель стека процесса. Однако в каждый момент доступен только один из этих регистров. Какой именно — определяется в одном из регистров специального назначения. Зачем такое надо? Это сделано для возможности организации защиты операционной системы (ага, на этот контроллер можно поставить ОС, если хочется) от кривых прикладных программ. MSP используется обработчиками исключительных ситуаций и всеми программами, использующими привилегированный уровень выполнения (например ядро ОС), а PSP — используется программами, не требующими привилегированного уровня выполнения (например, прикладными программами от которых мы хотим защитить ядро ОС). Указатели стека всегда должны быть выровнены на границу 32-хбитного слова, т.е. два их младших бита всегда должны быть сброшены в ноль.
Регистр R14 называется LR (link register) — регистр связи и используется для запоминания адреса возврата при вызове подпрограмм.
Регистр R15 называется PC (program counter) — счётчик команд и используется для хранения адреса текущей выполняемой команды.
Теперь о специальных регистрах.
Регистр xPSR содержит флаги результатов выполнения арифметических и логических операций, состояние выполнения программы и номер обрабатываемого в данный момент прерывания. Иногда об этом регистре пишут во множественном числе. Это сделано потому, что к трём его частям можно обращаться как к отдельным регистрам. Эти «подрегистры» называются: APSR — регистр состояния приложения (тут как раз хранятся флаги), IPSR — регистр состояния прерывания (содержит номер обрабатываемого прерывания) и EPSR — регистр состояния выполнения. Полностью структура регистра xPSR приведена на рисунке ниже.
Флаги в регистре APSR стандартные:
В регистре PRIORITY MASK используется только нулевой бит (PRIMASK), который будучи установлен в единицу запрещает все прерывания с конфигурируемым приоритетом. После включения бит PRIMASK сброшен в ноль — все прерывания разрешены.
В регистре FAULT MASK также использует только нулевой бит (FAULTMASK), который будучи установлен в единицу запрещает все прерывания и исключения, кроме немаскируемого прерывания (NMI). После включения бит FAULTMASK сброшен в ноль — все прерывания разрешены.
Регистр BASEPRI используется для запрещения всех прерываний, значение приоритета которых больше или равно, чем записано в этом регистре. Тут надо сказать, что чем меньше значение — тем выше уровень приоритета. В регистре BASEPRI используются только младшие 8 бит.
Регистр CONTROL используется для управления одним из режимов процессора — режимом потока. Нулевой бит этого регистра (nPRIV) определяет уровень выполнения (привилегированный — Privilegied, или непривилегированный — Unprivilegied), а первый бит (SPSEL) — используемый указатель стека (MSP или PSP). Разница между привилегированным и непривилегированным уровнями выполнения состоит в том, что для привилегированного уровня доступны все области памяти и все команды процессора, а для непривилегированного уровня некоторые области памяти закрыты (например, регистры специального назначения, кроме APSR, системная область) и, соответственно, команды для доступа в эти обасти — запрещены. Попытка выполнения запрещённых команд, пытающихся обратиться в закрытые области памяти вызывает генерацию исключения отказа.
Теперь о режимах работы процессора.
Процессор Cortex-M3 имеет два режима работы: режим потока (Thread) и режим обработчика (Handle). Режим Handle используется для обработки исключительных ситуаций, а режим Thread — для выполнения всего остального кода. Переключение из одного режима в другой происходит автоматически. Как мы уже говорили, когда разбирались с регистром CONTROL, в режиме Thread процессор может использовать как привилегированный уровень выполнения, так и непривилегированный, в режиме Handle — только привилегированный. Аналогично, в режиме Thread может использоваться как основной стек (MSP), так и стек процесса (PSP), а в режиме Handle — только основной стек.
Важно понимать, что, например, переключившись в режиме Thread с привилегированного уровня в непривилегированный, мы потеряем доступ в регистр CONTROL и обратно сможем переключиться только в режиме Handle. В режиме Handle бит nPRIV регистра CONTROL доступен для чтения/записи, но не влияет на текущий режим выполнения. Это позволяет изменить уровень выполнения, который будет у программы, когда процессор выйдет из режима обработчика в режим потока. Бит SPSEL в режиме Handle для записи недоступен и всегда читается как ноль, а при выходе из режима обработчика в режим потока восстанавливается автоматически. Все варианты переходов между различными режимами и уровнями выполнения иллюстрирует ориентированный граф, представленный на рисунке ниже:
Далее поговорим о том, как контроллер стартует.
Стартует контроллер всегда на внутреннем генераторе, на частоте 8 Мгц. Откуда брать тактовый сигнал в дальнейшем, на сколько его умножать или делить — настраивается в программе. Если в программе этого не сделать, то хоть десять внешних кварцев повесьте, контроллер так и будет работать от внутреннего генератора 8 МГц.
При старте контроллер анализирует сочетание уровней на двух своих ногах — BOOT0, BOOT1, и, в зависимости от этого сочетания, начинает загрузку либо из flash-памяти, либо из ОЗУ, либо из системной области памяти. Это делается с помощью уже описанного нами ранее механизма псевдонимизации. По идее загрузка всегда начинается с нулевого адреса, просто в зависимости от
сочетания на ногах BOOT0, BOOT1 начальные адреса памяти назначаются псевдонимами одной из трёх областей: flash, встроенного ОЗУ или системной области. Справа приведена табличка, в которой указано, какая именно область проецируется в начальные адреса памяти в зависимости от сочетания ног BOOT0, BOOT1.
При этом в системной области производителем зашита специальная программа (bootloader), которая позволяет запрограммировать flash-память. Но об этом позже.
Первым делом контроллер считывает 32-х битное слово по адресу 0x00000000 и помещает его в регистр R13 (указатель стека). Далее он считывает 32-х битное слово по адресу 0x00000004 и помещает его в регистр R15 (счётчик команд). Последнее действие вызывает переход на начало программного кода и дальше начинается выполнение программы.
Слово по адресу 0x00000004 (адрес начала основной программы) называется вектор сброса. Вообще в памяти контроллера после указателя стека по адресу 0x00000000, начиная с адреса 0x00000004 должна лежать таблица векторов исключений и прерываний, первый вектор в которой — это вектор сброса, а остальные вектора — адреса процедур обработчиков различных исключений и прерываний. В простейших программах, если вы не собираетесь обрабатывать исключения и прерывания, все остальные вектора, кроме вектора сброса, могут отсутствовать. Хотелось бы обратить внимание, что в таблице векторов указываются именно адреса начала обработчиков, а не команды перехода на эти обработчики, как, например, в 8-ми битных пиках или атмелах.
Надеюсь понятно, что если ногами BOOT0, BOOT1 начальная область памяти установлена псевдонимом, например, flash-памяти, то считывание по адресу 0x00000000 реально приведёт к считыванию адреса 0x08000000 (начало flash-памяти), а считывание адреса 0x00000004 — к считыванию адреса 0x08000004 и так далее.
Ну и в завершение скажу, что стартует контроллер всегда в режиме потока с привилегированным уровнем выполнения.
Микроконтроллеры семейств AVR, MSP430, STM32 и мои субъективные впечатления
Микроконтроллеры семейств AVR, MSP430, STM32 и мои субъективные впечатления
Здравствуйте, обитатели Хабра. В этой статье хочу поделится своими впечатлениями об опыте программирования микроконтроллеров семейств AVR, MSP430, STM32.
Введение
Семейство микроконтроллеров AVR
Предмет «Микропроцессорные контроллеры» как раз и был посвящен программированию микроконтроллеров на примере семейства AVR Atmega фирмы Atmel. Лабораторные работы по данному предмету заключались в программировании отладочных плат с Atmega16 на ассемблере данного семейства в программной среде AVR Studio 4.18.
Программа отлаживалась при помощи симулятора и зашивалась в микроконтроллер посредством встроенного в отладочную плату LPT-программатора на логике через программу ponyprog2000. На этих лабораторных работах я и ознакомился с волшебным миром микроконтроллеров, что включало в себя «помигать светодиодом», обработать нажатие на кнопку, настроить аппаратный таймер на работу и обрабатывать генерируемые им прерывания, настроить UART и передать данные по нему и т.д.
Прекрасный новый мир открылся мне. Но потом все это немного заглохло до следующего курса, на котором программирование тех же самых плат происходило, но уже не на ассемблере, а на языке Pascal в среде E-LAB. Об этой среде мало кто знает, а зря. Ведь задолго до всяких там arduino, данная среда включала в себя много библиотек для внешних устройств простых в использовании. Не верите?
Посмотрите сами тут. Тут вам для E-LAB и JTAG-отладчики есть.
Но во времена написания лабораторных работ JTAG-отладка доступна не была. Поэтому мы пользовались встроенным в E-LAB симулятором. Как и тогда, библиотеки E-LAB позволяют создавать проекты с ОСРВ, работающей по принципу Round-robin.
Последние версии E-LAB поддерживают и кооперативную многозадачность.
В принципе с этих двух циклов лабораторных работ я и начал свое знакомство с микроконтроллерами и, в частности, семейством AVR. Что можно сказать теперь?
AVR — самое популярное семейство микроконтроллеров в мире, я думаю.
Arduino-мания только укрепила это. Эти простые в освоении микроконтроллеры и сейчас остаются лучшим решением для первого знакомства. Позволяют получить опыт создания простых приложений с использование интерфейсов SPI, I2C, UART, позволяют понять работу портов ввода/вывода, подсистемы прерываний. По сути, на данном семействе можно научится основам и делать малые и средние проекты. В последних версиях AVR Studio можно делать проекты на С.
А если взять в руки паяльник, то можно себя обеспечить и программатором, и JTAG-отладчиком.
Есть желание начать? Тут тоже много всего.
Фирменные отладочные платы и программаторы от Atmel крайне дороги.
Главным недостатком AVR является слабое вычислительное ядро без вспомогательных математических блоков, причем восьмиразрядность усугубляет ситуацию. Т.е. на сложные математические вычисления может уйти много времени. Микроконтроллер может не успевать обрабатывать собранную или принятую информацию. Последний проект для Atmega16 я делал на С в среде разработки IAR Embedded Workbench for Atmel AVR.
Семейство микроконтроллеров MSP430
После семейства AVR микроконтроллерый мир уже открыл мне часть своих тайн.
А тут подоспел и новый предмет, посвященный также программированию микроконтроллеров, но это было уже семейство MSP430 фирмы Texas Instruments, а именно микроконтроллер msp430f169 на отладочной плате с ziff-панелью и минимальной обвязкой.
Разработка программы для него и отладка проходили в среде IAR Embedded Workbench for MSP430 при помощи JTAG-отладчика MSP-FET430UIF.
В первую очередь в этом семействе понравились примеры программ работы с внутренней периферией от производителя. Ну и с него берет моя подсадка на JTAG и IAR. Однажды попробовав JTAG-отладку не захочется возвращаться к разработке с просто программированием. Ведь под JTAG-отладкой можно по шагам видеть, что происходит в регистрах в памяти и где сейчас идет выполнение кода, ставить точки останова. С этого времени я и на IAR подсел. Ведь это кроссплатформенный компилятор выпущенный под множество микроконтроллерных семейств. Стоит один раз запомнить интерфейс и не надо каждый раз, при переходе на новое микроконтроллерное семейство, переучиваться. Это ли не чудо? Но затягивает.
Минус только в стоимости полной версии. Вообщем на этом семействе я и начал свою работу, как программист микроконтроллеров. И связка язык C (по сути кроссплатформенный ассемблер), кроссплатформенная среда разработки IAR и JTAG-отладка всегда были вместе со мной.
Семейство MSP430 в отличие от AVR шестнадцатиразрядное и более производительное за счет применения встроенного аппаратного умножителя.
Возможность использования режимов пониженного энергопотребления обеспечивает увеличение срока службы элементов питания при применении в мобильных портативных устройствах. А микроконтроллеры MSP430F5419 и MSP430F5438, с которыми я работал, на частоте 25 МГц в плотную подтягиваются к ARM. Так что они такие мощные середнячки. Если иметь фирменный JTAG-отладчик, IAR for MSP-430, нормальную отладочную плату, то работать с ними одно удовольствие.
Семейство микроконтроллеров STM32
Последним я познакомился с семейством STM32 фирмы STMicroelectronics.
Архитектура ARM сама по себе является дверью ко многим семействам.
Т.к. для множества этих микроконтроллеров разных фирм потребуется только один JTAG-отлдачик J-Link или его клон. А также если есть в наличии среда разработки IAR Embedded Workbench for ARM, то двери открыты.
Плюсом в сторону семейства STM32 является наличие библиотеки встроенной периферии, которая позволяет быстро писать свои пользовательские библиотеки с минимальными трудозатратами, а также 32-разрядность ядра в отличии от AVR и MSP430. Линейка микроконтроллеров STM32 включает много вариантов их внутреннего наполнения встроенной периферией от чего варьируется и стоимость. Например, микроконтроллер STM32L152VBT6 на ядре Cortex-M3, как микроконтроллеры семейства MSP430, нацелен на низкое энергопотребление и работает на 32 МГц.
Другой микроконтроллер STM32F107VCT6 также на ядре Cortex-M3 подходит для большинства задач, возлагаемый на данный класс устройств, и имеет частоту 72 МГц. Тут сразу видно, что для «тяжелой» математики и обработки микроконтроллеры на ядре Cortex-M3 куда больше подходят, чем MSP430 и AVR. Я работал и с «тяжеловесом» данного семейства STM32F407VGT6 на ядре Cortex-M4, частота которого доходит до 168 МГц. «Большой брат» идеально подошел для решения сложных математических задач. Кроме того, он имеет аппаратный FPU для математики с плавающей точкой. Для семейства STM32 разработана линейка плат DISCOVERY, которая позволяет получить плату со встроенным JTAG-отладчиком ST-Link, причем его можно использовать, чтобы программировать платы собственной разработки.
Результат их маркетинговой политики позволяет влиться в разработку микроконтроллеров с минимальными затратами, при этом имея фирменные платы и JTAG-отладчики от производителя.
Надежное хранение и обновление данных во флэш памяти микроконтроллеров STM32 и MSP430
Как работает флэш память
В основе флэш памяти лежит особая модификация транзистора с изолированным затвором (МОП-транзистора). Классический МОП-транзистор формируется на кремниевой пластине, покрытой слоем окисла, который играет роль изолятора. Поверх окисла напыляется электрод, называемый затвором. Подачей напряжения на этот электрод, можно управлять током, текущим между двумя электродами на кремниевой пластине — стоком и истоком. Происходит это потому, что положительный заряд затвора притягивает электроны и под затвором образуется проводящий канал из электронов. Если убрать напряжение с затвора, проводящий канал пропадает.
Во флэш памяти используются транзисторы с плавающим затвором. Они имеют изолированный от всего островок кремния в толще окисла между затвором и каналом. Если островок не заряжен, транзистор работает так же, как и обычный. Однако, если мы поселим на островке некоторое количество электронов, то они скомпенсируют положительный заряд затвора и проводящий канал пропадет.
Электроны попадают на плавающий затвор в процессе записи данных, туннелируя через изолятор. Этот процесс наглядно показан в фильме Чародеи — главное хорошо разогнаться, видеть цель и не замечать препятствий. Разгоняются электроны при пропускании тока в канале.
Со стиранием сложнее — ведь нам нужно не поселить электроны на затворе, а убрать их оттуда, значит разогнать их никак не получится. Поэтому мы просто формируем положительный потенциал в канале и ждем, когда электроны притянутся и протуннелируют в канал. Вот почему стирание занимает на несколько порядков большее время, чем запись. Для нашего STM32 это время от долей секунд до секунд. Более сложные устройства вроде SSD дисков поддерживают некоторый запас стертых транзисторов, но если они заканчиваются, время выполнения операций записи радикально увеличивается.
Чтобы сэкономить время, стирают память большими блоками — секторами. В случае STM32 минимальный размер — 16 килобайт — имеют 4 сектора, расположенные по младшим адресам. Записывать наш STM32 умеет по одному байту, по два, или четыре. Стертый транзистор читается как логическая единица. Соответственно, при записи мы поселяем электроны на затворы тех транзисторов, которые соответствуют логическим нулям в записываемых данных. Отсюда интересное наблюдение — мы можем выставлять в ноль биты в одном и том же байте один за другим, а не все сразу. Обратная операция — выставить нулевой бит в единицу — невозможна без стирания. При записи единичного бита содержимое памяти не изменяется.
Проблема стабильности чтений
Что же произойдет, если мы выключим питание в момент записи данных? Понятно, что часть данных окажется незаписанной. А что призойдет с тем байтом или словом, которое мы записывали в момент выключения питания? При записи на плавающий затвор может попасть разное количество электронов. Много электронов читается как 0, мало — как 1, значит есть и некоторое пограничное количество электронов. Если до выключения питания на затвор попадет количество электронов, близкое к пограничному, то при чтении мы можем получать как 0, так и 1 в зависимости от совершенно случайных факторов. Со временем заряд будет стекать с затвора, так что вероятность прочитать 1 будет расти. Эта крайне неприятная особенность делает ненадежной даже схему с использованием контрольной суммы. Если питание отключилось в момент записи последнего слова нашего пакета с данными, которые мы можем сопроводить любым количеством проверочной информации, то мы можем сегодня прочитать наши данные, а завтра нет, или наоборот. Более того, нас ждут неприятности и при записи в область, которую мы считаем стертой, потому что она сегодня читается как все единицы — ведь завтра там могут проступить нули и испортить наши данные.
Аналогичные проблемы возникают и при выключении питания в момент стирания. При этом мы получаем совершенно непредсказуемое содержимое памяти с непредсказуемым поведением в будущем. Значит, такую ситуацию нужно уметь детектировать и проводить повторное стирание. Вот почему код, имеющий дело с записью во флэш память, должен писаться в состоянии обостренной паранойи, причем никогда нельзя сказать, достаточна ли степень этой паранойи или нет.
Реализация с гарантией целостности данных
Теперь мы готовы рассмотреть схему хранения данных, которая гарантирует целостность данных в смысле, обсуждавшемся выше. Поскольку STM не скупится на размер флэша, было решено упростить конструкцию, отказавшись от экономии, и использовать модель, где все данные объединены в единственную структуру фиксированного размера. При обновлении данных мы записываем всю структуру целиком. Разные версии данных записываются последовательно в предварительно стертую область флэш памяти. Актуальными считаются данные, записанные последними.
Система разбита на 2 уровня, предоставляющих различные гарантии относительно целостности данных. На нижнем уровне находится пул данных, позволяющий записывать данные последовательно в предварительно стертый сектор. Ниже показан формат пакета с данными на этом уровне.
После собственно данных следует выравнивание до 32 битного слова, после которого записывается контрольная сумма. После контрольной суммы следует проверочный байт, куда мы просто записываем нулевые биты. Эту часть пакета с данными мы записываем байт за байтом, поэтому, если при чтении мы видим хотя бы один нулевой бит в проверочном байте, мы можем быть уверены в том, что контрольная сумма записана правильно и ее содержимое не будет меняться со временем. Следующий байт после проверочного — статусный. Здесь есть нулевой бит, который маркирует пакет, как завершенный. Если при чтении мы обнаружили этот нулевой бит, это означает, что проверочный байт тоже был записан правильно и его содержимое не будет меняться со временем. То есть, мы можем считать данные полностью записанными и наше мнение не изменится со временем. Если при чтении мы не обнаружили флаг завершенности, но проверочный байт имеет нулевые биты, мы просто перезаписываем последние 2 байта. Если же в проверочном байте читаются все единичные биты, мы считаем, что данные не были записаны правильно независимо от контрольной суммы.
Итак, мы можем гарантировать, что будучи однажды прочитанными, данные будут читаться и дальше. Однако, с прочими свойствами надежного хранилища данных все не так радужно. Очевидна проблема с необходимостью периодически стирать сектор, когда там заканчивается место. Если при этом выключится питание, мы не только не запишем новые данные, но и потеряем старые. Несколько менее очевидна проблема с неправильно записанными данными (в результате отключения питания во время записи). Мы не можем гарантировать, что со временем там не проступят отсутствующие биты и мы не начнем читать эти данные как правильные. Может показаться, что дополнительные статусные биты, маркирующие запись как неправильную, могут спасти положение, однако это не так. Ведь питание может пропасть и при записи этих дополнительных бит, и в итоге проблем станет только больше. Схема, описанная выше, успешно использует корректирующие записи только потому, что они записывают ровно те же данные, что и первоначальная запись, поэтому при любой последовательности отключений питания последняя успешная запись переводит флэш в стабильное состояние. Конечно, и в таком виде описанное хранилище может использоваться в приложениях не предъявляющих повышенные требования к надежности хранения. Но оказывается, что на базе двух хранилищ описанного типа можно создать более надежный вариант, лишенный описанных недостатков. Схема такого хранилища показана на следующем рисунке.
Два пула данных вышеописанного типа хранят пользовательские данные (в 2-х различных секторах флэша), дополненные служебным байтом. В нем хранится номер эпохи и флаг невалидности данных (называемый часто ‘могильным камнем’). Если в текущем пуле заканчивается место, мы увеличиваем номер эпохи на единицу и начинаем писать в следующий. Отключение питания уже не грозит уничтожением всех наших данных, ведь мы не стираем пул с данными, которые были записаны последними. Номер пула, куда будет происходить очередная запись, равен младшему биту номера эпохи. На старте системы мы сравниваем номера эпох (на числовой окружности), чтобы определить пул, записанный последним. Проблема стабильности незавершенных записей решается тоже довольно просто. Если на старте мы обнаруживаем запись, которую считаем неправильной, то мы можем ее просто ‘похоронить’, сделав новую запись с актуальными данными, если они есть, либо с ‘могильным камнем’, если таковых нет.
Тестовый проект
Лежит тут. Проект создан с помощью STM32CubeMX под компилятор IAR EWARM для платы STM32-H405. Использование STM32CubeMX для компиляции проекта оставило только положительные эмоции. Особенно радует дерево клоков — та часть, которая раньше была для меня областью магии, теперь упростилась до нескольких кликов мышью. Проект легко адаптировать под другие процессоры STM32 или компиляторы просто перегенерив его с помощью STM32CubeMX. Код хранилища данных легко адаптировать и под другие архитектуры, поскольку работа с флэшом вынесена в отдельный модуль с абстрактным интерфейсом. В составе пректа есть автоматический тест хранилища данных, который использует сторожевой таймер для сброса процессора в случайный момент времени. Кроме того, в проекте есть тестовая реализация USB CDC протокола, которая просто отсылает назад все принятые строки. Я добавил ее, поскольку меня интересовали 2 вопроса. Во-первых, что происходит с известными мне проблемами в реализации USB стэка. Оказалось, что ничего — старые проблемы не исправляются, новых не появляется. Видимо такова политика кампании — кто знает про ZLP — сделает сам, кто не знает — заплатит за поддержку. Во-вторых, было интересно, как стирание флэша влияет на работу USB, ведь при этом процессор может останавливать выборку команд из флэша. Оказалось, что не влияет.






