Некотрое время назад захотелось мне освоить ассемблер и после прочтения соответствующей литературы пришло время практики. Собственно о ней и пойдет дальше речь. Первое время я практиковался на Arduino Uno (Atmega328p), теперь решил двигаться дальше и взялся за STM32. В руки ко мне попала STM32F103C8 собственно на ней и будут проходить дальнейшие эксперименты.

Инструменты


Я использовал следующие инструменты:

  • Notepad++ — для написания кода
  • GNU Assembler — компилятор
  • STM32 ST-LINK Utility + ST-LINK V2 — для прошивки кода на микроконтроллер и отладки

Начало


Основная цель программирования на ассемблере для меня — это обучение. Так как никогда не знаешь где наткнешься на очередную интересную проблему, то было решено писать все с нуля. Первостепенной задачей было понять как работает вектор прерываний. В отличие от Atmega в STM32 вектор прерываний не содержит инструкций перехода:

jmp main

В нем прописываются конкретные адреса и во время прерывания процессор сам подставляет прописанный в векторе адрес в PC регистр. Вот пример моего вектора прерываний:

.org 0x00000000					
SP: .word STACKINIT				
RESET: .word main
NMI_HANDLER: .word nmi_fault
HARD_FAULT: .word hard_fault
MEMORY_FAULT: .word memory_fault
BUS_FAULT: .word bus_fault
USAGE_FAULT: .word usage_fault
.org 0x000000B0
TIMER2_INTERRUPT: .word timer2_interupt + 1

Хочу обратить внимание читателя, что первой строкой идет не reset вектор, а значения которым будет инициализироваться стэк. Сразу следом за ним идет reset вектор после которого следуют 5 обязательных векторов прерываний (NMI_HANDLER – USAGE_FAULT).

Разработка


Первое на чем я застрял был синтаксис ARM ассемблера. Еще во время изучения вектора прерываний я встретил упоминания того, что у ARM существует 2 вида инструкций Thumb и не Thumb. И что Cortex-M3 (STM32F103C8 именно Cortex-M3) поддерживает только набор Thumb инструкций. Я писал инструкции строго по документации, но ассемблер на них почемуто ругался.
unshifted register required
Выяснилось, что в начале программы надо поставить
.syntax unified
это говорит ассемблеру что можно использовать Thumb и не Thumb инструкции одновременно.

Следующее с чем я столкнулся были отключенные по умолчанию GPOI порты. Чтобы они заработали, кроме всего прочего надо выставить соответствующие значения в RCC (reset and clock control) регистрах. Я использовал PORT C, его можно включить установив бит 4 (нумерация битов идет с нуля) в RCC_APB2ENR (peripherial clock enable register 2).

Дальше мигание светодиодом. Прежде всего, как и в Arduino надо установить пин за запись. Это делается через GPIOx_CRL (control register low) или GPIOx_CRH (control register high). Тут надо отменить что за каждый пин отвечают 4 бита в одном из этих регистров (регистры 32 битные). 2 бита (MODEy) определяют максимальную скорость передачи данных и 2 бита (CNF) конфигурацию пина. Я использовал PORT C пин 14, для этого выставил в GPIOx_CRH регистре биты [25:24] = 10 и биты [27:26] = 00.

Чтобы диод горел надо в GPIOx_ODR (output data register) выставить соответствующий бит. В моем случае бит 14. На этом можно было бы закончить этот простой пример, сделав функцию задержки и поставив это все в цикл, но я этого сделать не смог. Я решил настроить прерывания по таймеру… Как выяснилось это было зря, прежде всего потому, что таймеры слишком быстрые для такого рода задач.

Не стану подробно описывать настройку таймера, кому интересно код есть на Github. Задумка была проста, в цикле отправлять процессор в Idle, по таймеру выходить из Idle зажигать/гасить светодиод и опять в Idle. Но таймер срабатывал гораздо быстрее чем я успевал сделать все вышеуказанное из-за чего пришлось ввести дополнительный счетчик.

Счетчик — 32 битная переменная, которая должна была находиться в SRAM. И тут меня ждали очередные грабли. Когда я программировал на Atmega чтобы поместить переменную в SRAM я через .org задавал адрес начала памяти, куда собственно помещался блок с данными. Сейчас, немного почитав об инициализации памяти, я не уверен, что это было правильно, но это работало. И я решил провернуть тоже самое с STM32. Адрес начала памяти в STM32F103C8 – 0x20000000. И когда я сделал .org по этому адресу, то получил бинарник на 512мб. Это отправило меня на пару вечеров «курить мануалы». Я все еще не на 100% понимаю как это работает, но на сколько я понял .data секция помещает значения, которыми надо инициализировать переменные в исполняемый файл, но во время выполнения программист должен сам инициализировать значения переменных в памяти. Поправьте меня пожалуйста если я не прав. В итоге я создал переменную так:

.section .bss 
.offset 0x20000000
flash_counter: .word

Инициализировал ее в начале main функции и LED замигал. Надеюсь эта статья комуто поможет. Если есть вопросы буду рад на них ответить.

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


  1. golf2109
    14.08.2017 12:22
    +6

    А до написания программ, автор не пробовал посмотреть например на файл startup_stm32f103xb.s,
    поставляемый произодителем (ST Electronics) и являющийся как раз файлом правильной
    инициализациеи контроллера на ассемблере?


    1. mksma Автор
      14.08.2017 12:43

      Не пробовал. Обязательно его посмотрю, спасибо.


  1. byte46
    14.08.2017 12:27
    +4

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

    Не стоит вводить людей в заблуждение, с таймерами у STM всё отлично, надо внимательно читать даташит. Есть счетчик таймера, есть предделитель. Достичь секунд задержки — запросто.


    1. mksma Автор
      14.08.2017 12:49
      -2

      Возможно я что-то настроил не правильно. Как будет время посмотрю еще раз.


  1. golf2109
    14.08.2017 13:13

    ещё там есть RTC, то есть таймер часов реального времени


    1. mksma Автор
      14.08.2017 13:20

      Как раз про них я и подумал, что следовало бы настроить прерывания по RTC. Я настроил по TIM2. Возможно что-то настроил не правильно, но добиться задержки в секунду по нему у меня не получилось.


      1. golf2109
        14.08.2017 13:42

        RTC тактируется от отдельного тактового источника — внутреннего или внешнего по (умолчанию от внутреннего), этот источник тактирования включается независимо от основного.


    1. ploop
      14.08.2017 13:51

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


  1. Utyff
    14.08.2017 15:25

    На эту тему есть хорошие статьи тут


  1. Mirn
    14.08.2017 21:06
    -1

    спасибо большое за очень хорошую статью!
    вот бы мне когда нибудь так кодить на асме!

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

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

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

    и ещё в нём много минусов которые прямо рождены чтоб показать всю мощь асма:
    — он написан на C99 и далеко не самым крутым кодером, да что уж говорить, он там даже switch-case не использовал и не изпользует вызов фукций по номеру из таблицы!!! хотя есть немало команд внутри, и их обработку так и проситься свести если не в таблицу, то в switch-case, одни сплошные ифы, брррр!
    — он использует SPL — либу для работы с переферей от производителя, она монстроидальная и слишком абсрагирована
    — так же он использует вызовы библиотек языка, тоже ещё тот монстр

    тем не менее есть и плюсы:
    — этот загрузчик работает в 10 раз быстрее чем штатный для стм32Ф4 а он прошивается реально очень долго.
    — сделана неплохая программа для ПК с детализацией всех логов и ответную часть для пк писать не надо
    — загрузчик использует механизм гарантированной доставки взятый из TCP с «окнами» инфой о позиции, ретрансмисиями в случае сбоев и способен работать даже если уарт изредка глючит — всегда доведёт дело до конца.
    — есть немало людей которые его используют и он очень востребован
    — автор с радостью отвечает на вопросы и сделал полный гайд на хабре, включая детальное описание протокола на русском.
    — есть исходный опорный алгоритм на я бы сказал очень примитивном си

    Я уверен такой мастер как Вы запросто уменьшит размер этого загрузчика в десятки раз! — Что очень актуально особенно например для младшей серии STM32F100 например. АСМ сила!


    1. mksma Автор
      14.08.2017 21:44
      -1

      Спасибо за лестный отзыв :) Но я боюсь Вы переоценивайте мои способности. Я с ассемблером познакомился только пол года назад и сейчас крайне сомневаюсь что моих знаний хватит чтобы потянуть чего-то серьезное. Пока хочу сосредоточится на обучении. Когда-нибудь я планирую разобраться с загрузчиками, но пока я к этому не готов.


    1. saboteur_kiev
      14.08.2017 22:06
      +1

      То, что работает в 10 раз быстрее, обычно умеет в 10 раз меньше.
      Если есть люди, которым он очень востребован — в чем дело найти спеца на фрилансе, заплатить за пару вечеров и получить изделие?


    1. ITMatika
      15.08.2017 07:00

      Тонко )


  1. leocat33
    15.08.2017 04:07
    -1

    Процесс обучения пойдет быстрее, если в том же KEIL, в режиме отладки, смотреть, что же он там генерит на макро.


  1. besitzeruf
    15.08.2017 07:20
    +2

    По мне, так это хороший пример того, как НЕ надо писать на ASM для STM32 и прочих микроконтреллеров. Вопрос к автору, что будет, с мейном, если я дефайны не установлю для включения отдельных «модулей»?


    1. mksma Автор
      15.08.2017 07:44
      -1

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