С данного урока начнём знакомиться с таким интересным языком, как ассемблер.
Я всегда люблю говорить, что ассемблер — это очень лёгкий язык, но писать на нём очень трудно. И вскоре вы поймёте, почему.
Язык C, который мы использовали, является языком высокого уровня, так же как и C++. Инструкции данных языков представляют собой практически английские слова, обозначающие те или иные действия. Только данные действия производятся над абстрактными величинами — массивами, переменными и т.д. Процессор, для которого мы пишем код, таких величин не знает. Поэтому данные языки ещё называют человеко-понятными.
Assembler же является машино-понятным языком, так как оперирует он с помощью своих команд, представляющих собой мнемоники тех или иных действий, регистрами процессора, ячейками памяти, адреса которых также, как правило, находятся в регистрах процессора (регистрах общего назначения или РОН).
Хоть и тяжело писать на ассемблере, всё же я буду стараться построить данный курс так, чтобы большинству было легче понять, что и как творится в коде. Поэтому мы язык ассемблер будем проходить постепенно, по несколько команд за урок, а не будем их изучать сразу все, прежде чем писать код.
Писать мы будем в среде программирования Keil, хотя вполне могли бы писать и в Eclipse, просто там мы много бы сил отдали бы настройке проекта, также синтаксис AT&T, который используется по умолчанию в ассемблере GNU с его бесконечными знаками процентов и долларов, мне не очень нравится, хотя в Eclipse также можно настроить и синтаксис Intel. В Keil же по умолчанию используется Intel, а также присутствует ряд полезных макросов, а также настройка проекта там занимает минимальное время.
Я, конечно, не буду томить вас бесконечной теорией, мы будем к ней возвращаться постепенно, но несколько слов по теории сказать всё же придётся.
Писать код мы будем пока для контроллера STM32F103, установленном на недорогой отладочной плате. Пока будем использовать три мануала — STM32F10xxx-20xxx-21xxx-L1xxxx-Programming-Manual, Cortex M3 Technical Reference Manual и ARMv7-M Architecture Reference Manual, которые найти, я думаю, у вас не составит особого труда, ссылки я на них давать не буду по понятным причинам, дабы не нарушить чьих-то авторских прав. Также будут использоваться некоторые интернет ресурсы — понятно, что больше всего будет использоваться ресурс разработчика ядра — arm.com.
Вот как выглядит схематически процессор Cortex M3, который является сердцем контроллера
Мы видим, что в самом центре находится ядро, которое взаимодействует с остальными блоками, в частности в процессор входит сторожевой таймер (DWT), модуль защиты памяти (MPU), контроллер вложенных векторных прерываний NVIC (Nested Vectored Interrupt Controller), модуль точек останова FPB (Flash Patch and Breakpoint), шинная матрица (Bus Matrix), которая соединяет процессор и интерфейс отладки с внешними шинами, расширенный высокопроизводительный порт доступа к шине (AHB-AP).
Ядро, выполненное по архитектуре ARMv7-M, по сравнению с предыдущими архитектурами, претерпело ряд изменений, в частности, применение инструкций нового набора инструкций Thumb-2, позволило в некоторых случаях добиться производительности на 70% больше по сравнению с ядром ARM7TDMI-S, исполняющим инструкции Thumb, и на 35% по сравнению с тем же ядром, исполняющим инструкции ARM. Также применение 3-ступенчатого конвейера обработки инструкций (этап выборки, этап декодирования, этап исполнения) также приводит к ускорению выполнению программы. Также наличие раздельных шин для данных и команд позволяет одновременно считывать процессором и то и другое, что тоже сказывается положительно на производительности работы ядра.
Ядро включает следующий набор регистров
Объяснять, для чего служит тот или иной регистр, сейчас нет никакого смысла, так как все равно никто этого не запомнит, запоминается лучше тогда, когда происходит использование на практике.
Как выглядят ассемблерные инструкции, мы также будем разбираться в процессе написания кода.
В нашем коде мы сегодня добьёмся самой простой задачи. Мы попытаемся зажечь светодиод, установленный на плате и подключенный к ножке PC13.
Чтобы нам писать ассемблерный код, надо будет для начала создать пустой проект, поэтому откроем Keil и создадим пустой проект, выбрав следующий пункт меню
Зададим имя проекту и сохраним его в заранее созданном каталоге
Выберем контроллер
Здесь не выбираем вообще ничего
У нас появится вот такое дерево проекта
Переименуем группу, а то как-то некрасиво
Прежде чем наполнять наш проект файлами с исходным кодом, надо его настроить.
В настройках проекта сначала выставим правильную частоту тактирования кварцевого резонатора
Разрешим создание файла прошивки формата HEX
Также настроим создание файла прошивке в бинарном формате, введя вот такую строку:
fromelf --bin --output .\Objects\ASM_GPIO.bin .\Objects\ASM_GPIO.axf
в данное поле
Вышеуказанная строка может портиться благодаря функционалу WordPress, поэтому скопировать её лучше из моего проекта, который будет прикреплен в виде архива внизу страницы.
В каталоге с проектом создадим подкаталоги user и inc, подключив путь к последнему здесь
Отключим генерирование предупреждений с кодом 6314
Для тех, кто не знает, предупреждение с кодом 6314 — это сообщение о несоответствии секций определённым шаблонам. Для нас это не страшно, поэтому мы данное предупреждение и отключили, чтобы не маячило.
Выберем правильный отладчик
В свойствах отладчика включим данную настройку
Для того, чтобы были доступны настройки отладчика, плата должна быть подключена к компьютеру.
Включим автоперезагрузку после заливки ПО
Пока, в принципе, с настройками всё. Если, что, то потом донастроим.
В дереве с проектом создадим новый файл с расширением s, обычно такие файлы используются для ассемблерного кода, мы помним это по файлу startup. Назовём наш файл main.s, он будет главным, здесь будет точка входа в программу — реальная точка входа, а не какой-то там main()
Добавим в данный файл вот такие две строки
1 2 |
Stack_Size EQU 0x400 top_of_stack EQU 0x20000000 + Stack_Size |
Давайте разберёмся, что у нас в данных строках.
Слова, стоящие первыми в строках (Stack_Size и top_of_stack) — это метки.
Метка – это символьное имя, обозначающее ячейку памяти, которая содержит некоторую команду.
Метка в языке ассемблера может содержать следующие символы:
Буквы: от A до Z и от a до z
Цифры: от 0 до 9
Спецсимволы: знак вопроса (?), точка (.) (только первый символ), знак @, подчеркивание (_), доллар ($)
Правда, здесь у нас метка не обозначает команду.
Также здесь используется директива EQU, которая присваивает метке значение, которое определяется как результат целочисленного выражения в правой части. Результатом этого выражения может быть целое число, адрес или любая строка символов.
Идём дальше.
Напишем следующие две директивы
1 2 3 4 |
top_of_stack EQU 0x20000000 + Stack_Size PRESERVE8 THUMB |
Первая директива необходима для выравнивания, а вторая разрешает использования набора инструкций Thumb (в том числе thumb-2, который поддерживается нашим ядром).
Не забываем перед директивами и командами делать отступы, иначе будет ошибка, отступов нет перед метками.
Добавим следующую строку
1 2 3 |
THUMB AREA RESET, DATA, READONLY |
Код, который мы пишем, разбивается на секции или области, директива AREA как раз и добавляет область в секцию DATA, то есть после данной директивы пойдут данные. RESET — это имя области, может быть любым. Если оно встретится повторно при объявлении области, то код, находящийся в данной области, добавится в область с именем RESET. Ключевое слово READONLY означает, что данные, находящиеся в данной секции памяти, изменять нельзя, они будут только для чтения.
Область завершается либо когда будет объявлена новая область либо по окончанию файла.
Наполним нашу область данными
1 2 3 |
AREA RESET, DATA, READONLY DCD top_of_stack ;Top of Stack DCD Start |
Директива DCD означает то, что вслед за ней идут данные 4-байтного типа (двойные слова). Таким образом в первой строке с данными мы заполнили данное место области 4-байтным числом 0x20000400, что будет границей стека в памяти. следующая строка добавляет в текущее место данные также в виде 4-байтного типа, содержащие адрес метки Start, которая будет далее. Адрес метки Start и будет адресом точки входа в программу, оттуда программа и начнёт своё выполнение. Таким образом, в самом начале файла прошивки должен быть адрес вершины стека и адрес точки входа.
Данные бывают следующих типов
DCB — байты,
DCW — полуслова или 16-битные знаковые величины,
DCWU — полуслова или 16-битные беззнаковые величины,
DCD — слова или 32-битные знаковые величины,
DCDU — слова или 32-битные беззнаковые величины,
DCQ — двойные слова или 64-битные знаковые величины,
DCQU — двойные слова или 64-битные беззнаковые величины,
SPACE — резервирование области памяти в определённом количестве байтов
FILL — заполнение области памяти повторяющимся числом в определённом количестве.
Подробно мы пока разбирать разновидности данных не будем, они обязательно нам впоследствии встретятся все в коде.
Объявим следующую область
1 2 3 |
DCD Start AREA |.text|, CODE, READONLY |
Данная область попадёт в секцию .text, содержать она будет уже не данные, а машинный код и также доступна будет только для чтения.
Добавим следующие строки
1 2 3 4 5 |
AREA |.text|, CODE, READONLY ENTRY Start PROC |
В данных строках идёт объявление точки входа, а затем следует строка с меткой Start с объявлением процедуры.
Процедура (подпрограмма) — это основная функциональная единица декомпозиции (разделения на несколько частей) некоторой задачи. Процедура представляет собой группу команд для решения конкретной подзадачи и обладает средствами получения управления из точки вызова задачи более высокого приоритета и возврата управления в эту точку. В простейшем случае программа может состоять из одной процедуры. Процедуру можно определить и как правильным образом оформленную совокупность команд, которая, будучи однократно описана, при необходимости может быть вызвана в любом месте программы.
Окончание процедуры должно быть отмечено специальной директивой ENDP, поэтому добавим такую директиву в наш код
1 2 3 |
Start PROC ENDP |
Также файл с исходным кодом должен заканчиваться директивой END. Добавим и такую директиву, перед которой неплохо бы добавить ещё и директиву выравнивания кода
1 2 3 4 |
ENDP ALIGN END |
В следующей части нашего урока мы изучим несколько команд ассемблера, научимся подключать внешние файлы, а также напишем код, с помощью которого настроим ножку порта на выход и зажжем светодиод, подключенный к ней.
Предыдущий урок Программирование МК STM32 Следующая часть
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Наверно для Cortex m полностью писать программу (полноценную) на ассемблере особого смысла нет, уж слишком затратно по времени и отнимает много сил. Однако, как я понимаю для критических задач, требовательных к времени выполнения (в системах реального времени) используют именно ассемблерные вставки. Помимо этого знание ассемблера, как мне кажется может здорово помочь при отладке проекта, когда можно проверять что же там наделал компилятор. Так что тема безусловно очень интересная и нужная. Однако всё же жаль, что Вы решили обойти стороной gnu asm. Во первых как ни крути keil всё же платный а это само по себе сильно ограничивает его использование в боевых задачах. А во вторых, как мне кажется не менее важным является то, что keil не выходит под Linux, что опять таки сильно ограничивает круг его применения. Но всёравно, большое спасибо за урок и надеюсь, что Вы всё же попытаетесь перебороть чувство негатива испытываемого к gcc asm и выпустите хотя бы немного базовых уроков и для него.
Ура! Ура! Ура! Ассемблер! GodMode!!!
Где можно скачать презентацию по ассемблеру, которую вы открываете на 19й минуте ролика?
Здравствуйте,
Очень хорошо, когда есть подобные статьи.
Многие значительно преувеличивают плюсы Си, и негативно отзываются об Ассемблере. В принципе я с ними был бы согласен, если бы речь шла о десктопных приложениях на PC.
Но в области микроконтроллеров Си не дает больших преимуществ, даже с таким богатым функционалом как у STM32, а на маломощных устройствах даже проигрывает. Ведь по большей части код для микроконтроллера сводится к работе с его периферией, поэтому нет большой разницы на каком языке вы будете менять значения регистров. Однако, на Ассемблере взаимодействие с регистрами более прозрачно.
Опять же, Си выглядит лучше, если Вы пытаетесь реализовать проект, под который лучше всего подойдет микрокомпьютер типа Raspbery Pi. Но тогда стоит взять именно микрокомпьютер и писать на чем-то типа Java.
Также многие забывают, что преимущество Си, в основном, дают написанные для него библиотеки, а не сам язык. Если у Вас есть наработки библиотек на ассемблере, то все значительно упрощается.
Я предпочитаю писать прошивки для МК на ассемблере (хотя у меня есть большой профессиональный опыт работы на Си и высокоуровневых языках).
В основном я пишу для Atmel микроконтроллеров типа ATmega8. Но сейчас мне нужно реализовать решение для STM32H7. Я уже набил много шишек пытаясь реализовать задачу в STM32CubeIDE, на Си, но это просто ужас, IDE не удобна, медлительна и содержит недоработки. А стандартный HAL вообще стоит отдельной беседы. При этом, для моей работы на AVR достаточно текстового редактора Geany, ассемблера avra и прошивальщика avrdude(и да, работаю под GNU Linux).
Все это пишу в надежде, может быть Вы мне подскажете набор утилит для разработки под STM32 под GNU Linux, и крайне желательно Intel'овский ассемблер(GNU ассемблер просто ужасен).
В любом случае спасибо Вам, как минимум за эту статью.
откуда вот это всё взялось:
DCW – полуслова,
DCD – слова,
DCQ – двойные слова
???????????????
если дословно:
DCW – Define Code WORD (слово!),
DCD – Define Code DOUBLE WORD (двойное слово!),
DCQ – Define Code QUAD WORD (четверное слово!)
А это смотря для какой архитектуры.