Доброго времени суток, сегодня я хотел бы поделиться своим опытом создания шаблона проекта в CubeIDE для программирование на Ассемблере.

Так как CubeIDE использует средства GNU то и синтаксис ассемблера у нас будет советующий. Для начала откроем CubeIDE и создадим новый проект. В качестве испытуемого микроконтроллера возьму STM32G030F6P6 уж очень мне они нравятся. А так данный способ работает и с другими сериями микроконтроллера STM32.

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

Теперь необходимо удалить лишние файлы и переименовать main.c в main.s.

Очищаем main.s и удаляем syscall.c и sysmem.c. , но startup файл мы оставим обязательно, так как там прописаны первоначальная настройка стека и таблицы векторов после чего вызывается main, в рамках данной статьи автор рассчитывает на новичков которые только познают азы ассемблера. Должно получиться примерно так.

Теперь нам необходимо открыть файл startup_stm32....s и скопировать директивы среды. Находим данные строки ниже и копируем в наш main.s

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

Теперь давайте напишем небольшой код, в самых лучших традициях помигаем светодиодом) , для этого нам понадобиться два адреса RCC и GPIO. Можно взять из datasheet их, но мне лень поэтому немного схитрим. Необходимо войти в режим отладки.

Тут просто жмем Ок.

после чего начнет загружаться прошивка и нам предложат подключиться на что мы соглашаемся и жмем Switch.

В правом окне нам необходимо найти раздел SFRs и выбрать нам интересную периферию. Тут мы находим наш начальный адрес, для RCC он находиться 0x40021000 а для GPIOB 0x50000400. Желтым подсвечивается изменение содержимого регистра. Так как мы только что открыли их, для нас все эти значения новые.

В этом разделе можно посмотреть текущее состояние битов периферии. Так же нам понадобиться раздел Registers в котором находиться регистры общего назначения.

После чего напишем простой скетч.

.syntax unified
.cpu cortex-m0plus
.fpu softvfp
.thumb

.equ RCC_BASE, 0x40021000
.equ GPIOB_BASE, 0x50000400

.global delay
.global main

main:

//Включения тактирования GPIOB
	ldr r0, =RCC_BASE
	ldr r1, [r0, #0x34]
	movs r2, #0b10
	adds r1, r2
	str r1, [R0, #0x34]

//Настройка GPIOB_PIN_0 как выход
	ldr r0, =GPIOB_BASE
	ldr r1, [r0, #0x0]
	movs r2, #0b11
	bics r1, r2
	adds r1, #1
	str r1, [r0, #0x0]


Loop:

//Установка PIN_0 в лог. ед.
	ldr r1, [r0, #0x18]
	movs r2, #1
	orrs r1, r2
	str r1, [r0, #0x18]
	bl delay

//Установка PIN_0 в лог. ноль
	ldr r1, [r0, #0x18]
	movs r2, #1
	lsls r2, #16
	orrs r1, r2
	str r1, [r0, #0x18]
	bl delay

	b Loop

//задержка
delay:
	push {r3}
	ldr r3, =#0x00100000;
delay_loop:
	subs r3, #1
	bne delay_loop
	pop {r3}
	bx lr

Скомпилируем наш код и зашьем его. В качестве примера использую китайскую плату. У данной платы к ножке 0 порта В подлечен светодиод катодом т.е. когда мы подаем лог. ед. на вывод светодиод не горит, когда подаем лог. ноль то светодиод загорается.

Подключаемся в режиме отладки и начинаем идти по коду. Сначала отработает код startup файла. В нем процессор сначала вызывает обработчик сброса в котором формирует стек и векторы прерываний а после вызывает наш main. В первом блоке кода происходит включение тактирования GPIOB.

Команда LDR загружает в R0 базовый адрес RCC который объявлен у нас константой. как можно видеть после выполнения команды в регистр был положен адрес. Так же можно будет заметить что счетчик команд постоянно смещается. Если перейти в окно disassembly можно сопоставить данные адреса.

после чего следующей командой LDR мы загружаем значение в регистр R1 находящееся по адресу R0 смешенное на 0x34. Для проверки сравним считанное значение и посмотренное через средства отладки. Для этого откроем окно SFRs и посмотрим значения регистра RCC_IOPENB.

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

Далее нам надо произвести манипуляции над значениями, мы загружаем чисто 2 в регистр r2 или же во второй бит записываем единицу, и потом просто складываем значения регистра r1 и r2, значения запишется в регистр r1.

Команда STR обратная LDR, т.е. она выгружает значения из регистра общего назначения в регистр памяти по адресу R0 + смещение. Значит наше число которое должно будет оказаться в регистре RCC_IOPENB, жмем далее и убеждаемся в этом.

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

Теперь шагами проходим и устанавливаем вывод в лог. ед. и можем убедиться наглядно.

Теперь у нас вызывается подпрограмма задержки, нажимаем на следящий шаг и переходим в подпрограмму, как можно заметить у нас изменился регистр LR, он сохранил адрес возврата из подпрограммы, т.е. адрес следующей команды после bl delay. Тут мы впервые используем команды для работы с стеком, условно PUSH загружает значения регистров в стек а POP выгружает, но нам это в данной программе не особо и нужно, но в качестве примера.

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

Для этого нам надо тыкнуть на значения регистра исправить его и нажать enter.

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

Надеюсь данная статья кому ни будь окажется полезной. Это моя первая статья так что не судите строго. В качестве литературы могу порекомендовать:

  1. Джозеф Ю: Ядро Cortex-M3 компании ARM. Полное руководство

  2. Харрис, Харрис: Цифровая схемотехника и архитектура компьютера

  3. Харрис, Харрис: Цифровая схемотехника и архитектура компьютера. Дополнение по архитектуре ARM

Комментарии (6)


  1. VelocidadAbsurda
    16.07.2022 18:09
    +3

    В чём цель? (не в смысле «бросьте это», чего хотим добиться?). Если цель - максимально компактный проект, рубите на корню и startup: чтобы нормально стартануть прямо в свой минимальный asm, достаточно в самом начале объявить таблицу векторов из двух элементов - начального значения SP (подставьте максимальный адрес RAM) и собственно точки входа. Родной же startup, скорее всего, до вашего кода вообще «тайно» вызывает довольно увесистый Сишный SystemInit() из stm’овских библиотек.


    1. iShrimp
      16.07.2022 18:52
      +3

      Кстати, вот здесь человек очень детально разобрал процедуру инициализации STM-овской периферии на ассемблере и написал весь код практически с нуля (мануал от 2014 года).


    1. Polzuchy_haos Автор
      16.07.2022 19:36
      +2

      Да согласен, можно просто сделать пустой файл и все самому прописать. Целью же является простой вход для новичков которые и так уже используют CubeIDE и хотят ручками прощупать команды ассемблера, а уже остальное придёт со временем. Как писать на ассемблере в IAR или Keil и так полно в интернете.


  1. Butaforsky
    17.07.2022 11:31
    +1

    Добрый день! Вопрос к вам в частности и к сообществу инженеров встраиваемых систем в целом: Как вы думаете, почему, например, Мозила переписывает код своего браузера на Rust, убирая элементы C, https://habr.com/ru/post/445670/ новые патчи Linux, https://securitylab-ru.turbopages.org/securitylab.ru/s/news/532529.php получают поддержку Rust, но что касается встраиваемых систем, по статьям на Хабр складывается впечатлени чуть ли не религиозного стремления писать руками все с нуля, используя инструменты прошлого века?


    1. Polzuchy_haos Автор
      17.07.2022 12:06
      +1

      Добрый день. Вопрос довольно интересный, я считаю есть несколько факторов:

      1) Сложность проекта. Так как большенство проектов основанных на микроконтроллерах имеют одно ядро и немного памяти. Задачи которые стоят перед ним не имеют такую сложность где необходимо уходить в параллелизм и делать сложные комплексы безопасности. Вот для работы частей обработки данных на сервере и вправду лучше сделать на Rust или на Go.

      2) Rust пока молодой язык и у него ещё не сильно развито комьюнити как у C\C++, поэтому даже гуглиться какие то вопросы на С\С++ проще нежели на Rust.

      3) Уже готовые ряд проектов, библиотек и фреймворков, которые писались годами, зачем менять то что и так хорошо работает. И из-за этого зачем людям изучать что то новое.

      Думаю есть ещё ряд факторов которых я не задел, но эти одни из основных. Лично я Rust ещё не пробовал, читал пару статей и смотрел пару видео, на этом пока все мои знания о нем.


    1. grmile
      17.07.2022 12:07

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