Вновь возвращаюсь к фирменной среде разработки - ST Visual Develop, для чего есть две причины. Во-первых, оказалось, что писать на ассемблере сколь-либо сложные прошивки без отладчика невозможно, у меня по крайней мере не получилось, т.к. программа все-равно так или иначе отлаживается с помощью светодиода или по UART, через отладочный интерфейс это просто делается быстрее. Во-вторых, мне показалось, что изучать архитектуру только лишь руководствуясь datasheet'ом не совсем правильно. Что-то может быть неправильно понято, что-то может быть упущено. С такими штуками как DMA, встроенный RTC или выполнение кода из ОЗУ, будет проще разобраться с помощью отладчика, не забывая при этом посматривать в datasheet.
STVD - довольно простая среда разработки, я ее освоил за вечер. В этой статье я хочу рассказать, как "с нуля" начать писать и отлаживать прошивки на ассемблере STM8, используя ST Visual Develop.
STVD - работает в ОС семейства Windows, начиная с XP и выше. При этом она прекрасно работает из-под виртуальной машины в Linux. В этой статье я использую STVD 4.3.12, последнюю доступную версию на этот момент, и Windows XP SP3 в качестве гостевой ОС. В качестве микроконтроллера я буду использовать 20-пиновый STM8S103F3P6.
В качестве дизассемблера я буду использовать комплект утилит stm8-binutils. Бинарные файлы этого комплекта для Windows скомпилированы для работы в CYGWIN, т.е. они понимают unix'овский формат пути файла с прямым слешем в качестве разделителя. CYGWIN для Windows 7 и выше ставится без проблем следуя инструкциям на сайте https://cygwin.com/install.html, для Windows XP нужно следовать инструкциям в этом HowTo: windows xp - cygwin 2.5.2 mirror -- getting the last XP release - Stack Overflow.
В качестве альтернативы связке binutils+cygwin, можно использовать naken_util из комплекта naken_asm.
I. Создание минимального проекта Blink
II. Язык ассемблера STVD
III. Процесс отладки
IV. Макроассемблер
Если сам ассемблер STVD заслуживает всяческих похвал, то система управления проектами сделана довольно бестолково. В STVD существует понятие Workspace - хранящее настройки самой среды. Workspace может содержать в себе несколько проектов.
Первым делом, нужно будет создать пустую папку для workspace и вложенную в нее папку будущего проекта. Штатными средствами STVD это будет сделать затруднительно, т.к. там нет диалоговых форм выбора каталога:
Теперь запускаем STVD и через меню выбираем New Workspace:
В диалоговом окне задаём имя workspace и указываем ранее созданную папку для сохранения:
Аналогично для проекта выбираем название, указываем ранее созданную папку проекта, и в выпадающем списке в качестве Toolchain указываем ST Assembler Linker:
Осталось указать целевой микроконтроллер:
Перед нами открывается шаблон проекта, можно сразу его скомпилировать:
Осталось указать формат прошивки в настройках проекта:
В качестве отладчика можно выбрать Simulator или аппаратный отладчик на SWIM интерфейсе. Алгоритмы вполне можно отлаживать на симуляторе, при работе с внешними интерфейсами уже будет не обойтись без железки:
В опциях STVD можно сконфигурировать среду под себя, например можно задать длину строки, после которой строка будет выделяться красным фоном:
Так же обращу внимание, что там можно задать ширину отступа - Tab size. По умолчанию используется два пробела, мне же более привычно использовать четыре пробела.
После повторной компиляции и сохранения workspace, структура файлов проекта будет выглядеть так:
$ tree ~/docs/stm8_workspace /home/flanker/docs/stm8_workspace ├── 01_blink │ ├── 01_blink.dep │ ├── 01_blink.stp │ ├── cbe.err │ ├── Debug │ │ ├── 01_blink.cod │ │ ├── 01_blink.grp │ │ ├── 01_blink.hex │ │ ├── 01_blink.map │ │ ├── 01_blink.s19 │ │ ├── 01_blink.sym │ │ ├── main.lsr │ │ ├── main.lst │ │ ├── main.obj │ │ ├── mapping.lsr │ │ ├── mapping.lst │ │ └── mapping.obj │ ├── main.asm │ ├── mapping.asm │ └── mapping.inc ├── my_workspace.stw └── my_workspace.wed 2 directories, 20 files
Файл прошивки можно дизассемблировать из CYGWIN:
$ stm8-objdump.exe -m stm8 -D ~/docs/stm8_workspace/01_blink/Debug/01_blink.hex /home/flanker/docs/stm8_workspace/01_blink/Debug/01_blink.hex: формат файла ihex Дизассемблирование раздела .sec1: 00008080 <.sec1>: 8080: ae 03 ff ldw X,#0x03ff ;0x3ff 8083: 94 ldw SP,X 8084: ae 00 00 ldw X,#0x0000 8087: 7f clr (X) 8088: 5c incw X 8089: a3 00 ff cpw X,#0x00ff ;0xff 808c: 23 f9 jrule 0x8087 ;0x8087 808e: ae 01 00 ldw X,#0x0100 ;0x100 8091: 7f clr (X) 8092: 5c incw X 8093: a3 01 ff cpw X,#0x01ff ;0x1ff 8096: 23 f9 jrule 0x8091 ;0x8091 8098: ae 02 00 ldw X,#0x0200 ;0x200 809b: 7f clr (X) 809c: 5c incw X 809d: a3 03 ff cpw X,#0x03ff ;0x3ff 80a0: 23 f9 jrule 0x809b ;0x809b 80a2: 20 fe jra 0x80a2 ;0x80a2 80a4: 80 iret Дизассемблирование раздела .sec2: 00008000 <.sec2>: 8000: 82 00 80 80 int 0x008080 ;0x8080 8004: 82 00 80 a4 int 0x0080a4 ;0x80a4 8008: 82 00 80 a4 int 0x0080a4 ;0x80a4 800c: 82 00 80 a4 int 0x0080a4 ;0x80a4 8010: 82 00 80 a4 int 0x0080a4 ;0x80a4 8014: 82 00 80 a4 int 0x0080a4 ;0x80a4 8018: 82 00 80 a4 int 0x0080a4 ;0x80a4 801c: 82 00 80 a4 int 0x0080a4 ;0x80a4 8020: 82 00 80 a4 int 0x0080a4 ;0x80a4 8024: 82 00 80 a4 int 0x0080a4 ;0x80a4 8028: 82 00 80 a4 int 0x0080a4 ;0x80a4 802c: 82 00 80 a4 int 0x0080a4 ;0x80a4 8030: 82 00 80 a4 int 0x0080a4 ;0x80a4 8034: 82 00 80 a4 int 0x0080a4 ;0x80a4 8038: 82 00 80 a4 int 0x0080a4 ;0x80a4 803c: 82 00 80 a4 int 0x0080a4 ;0x80a4 8040: 82 00 80 a4 int 0x0080a4 ;0x80a4 8044: 82 00 80 a4 int 0x0080a4 ;0x80a4 8048: 82 00 80 a4 int 0x0080a4 ;0x80a4 804c: 82 00 80 a4 int 0x0080a4 ;0x80a4 8050: 82 00 80 a4 int 0x0080a4 ;0x80a4 8054: 82 00 80 a4 int 0x0080a4 ;0x80a4 8058: 82 00 80 a4 int 0x0080a4 ;0x80a4 805c: 82 00 80 a4 int 0x0080a4 ;0x80a4 8060: 82 00 80 a4 int 0x0080a4 ;0x80a4 8064: 82 00 80 a4 int 0x0080a4 ;0x80a4 8068: 82 00 80 a4 int 0x0080a4 ;0x80a4 806c: 82 00 80 a4 int 0x0080a4 ;0x80a4 8070: 82 00 80 a4 int 0x0080a4 ;0x80a4 8074: 82 00 80 a4 int 0x0080a4 ;0x80a4 8078: 82 00 80 a4 int 0x0080a4 ;0x80a4 807c: 82 00 80 a4 int 0x0080a4 ;0x80a4
Или прошивку можно дизассемблировать из командной строки Windows с помощью дизассемблера naken_utils:
Шаблон проекта состоит из файлов: main.asm, mapping.inc и mapping.asm. Файл mapping.inc содержит константы деления ОЗУ на сегменты:
;------------------------------------------------------ ; SEGMENT MAPPING FILE AUTOMATICALLY GENERATED BY STVD ; SHOULD NOT BE MANUALLY MODIFIED. ; CHANGES WILL BE LOST WHEN FILE IS REGENERATED. ;------------------------------------------------------ #define RAM0 1 #define ram0_segment_start 0 #define ram0_segment_end FF #define RAM1 1 #define ram1_segment_start 100 #define ram1_segment_end 1FF #define stack_segment_start 200 #define stack_segment_end 3FF
Здесь первые две страницы ОЗУ выделены в сегменты RAM0 и RAM1, остальные 513 байт отданы под сегмент стека. Т.е. всё согласно datasheet:
В файле mapping.asm определяются сегменты:
Хочу обратить внимание на то, что ассемблерный файл в STVD начинается строкой 'stm8/' и заканчивается строкой 'end'. Вместо 'stm8' можно указать 'st7', но это для тех кто пишет для архитектуры stm7.
Теперь посмотрим на файл main.asm
stm8/ #include "mapping.inc" segment 'rom' main.l ; initialize SP ldw X,#stack_end ldw SP,X #ifdef RAM0 ; clear RAM0 ram0_start.b EQU $ram0_segment_start ram0_end.b EQU $ram0_segment_end ldw X,#ram0_start clear_ram0.l clr (X) incw X cpw X,#ram0_end jrule clear_ram0 #endif #ifdef RAM1 ; clear RAM1 ram1_start.w EQU $ram1_segment_start ram1_end.w EQU $ram1_segment_end ldw X,#ram1_start clear_ram1.l clr (X) incw X cpw X,#ram1_end jrule clear_ram1 #endif ; clear stack stack_start.w EQU $stack_segment_start stack_end.w EQU $stack_segment_end ldw X,#stack_start clear_stack.l clr (X) incw X cpw X,#stack_end jrule clear_stack infinite_loop.l jra infinite_loop interrupt NonHandledInterrupt NonHandledInterrupt.l iret segment 'vectit' dc.l {$82000000+main} ; reset dc.l {$82000000+NonHandledInterrupt} ; trap dc.l {$82000000+NonHandledInterrupt} ; irq0 dc.l {$82000000+NonHandledInterrupt} ; irq1 dc.l {$82000000+NonHandledInterrupt} ; irq2 dc.l {$82000000+NonHandledInterrupt} ; irq3 dc.l {$82000000+NonHandledInterrupt} ; irq4 dc.l {$82000000+NonHandledInterrupt} ; irq5 dc.l {$82000000+NonHandledInterrupt} ; irq6 dc.l {$82000000+NonHandledInterrupt} ; irq7 dc.l {$82000000+NonHandledInterrupt} ; irq8 dc.l {$82000000+NonHandledInterrupt} ; irq9 dc.l {$82000000+NonHandledInterrupt} ; irq10 dc.l {$82000000+NonHandledInterrupt} ; irq11 dc.l {$82000000+NonHandledInterrupt} ; irq12 dc.l {$82000000+NonHandledInterrupt} ; irq13 dc.l {$82000000+NonHandledInterrupt} ; irq14 dc.l {$82000000+NonHandledInterrupt} ; irq15 dc.l {$82000000+NonHandledInterrupt} ; irq16 dc.l {$82000000+NonHandledInterrupt} ; irq17 dc.l {$82000000+NonHandledInterrupt} ; irq18 dc.l {$82000000+NonHandledInterrupt} ; irq19 dc.l {$82000000+NonHandledInterrupt} ; irq20 dc.l {$82000000+NonHandledInterrupt} ; irq21 dc.l {$82000000+NonHandledInterrupt} ; irq22 dc.l {$82000000+NonHandledInterrupt} ; irq23 dc.l {$82000000+NonHandledInterrupt} ; irq24 dc.l {$82000000+NonHandledInterrupt} ; irq25 dc.l {$82000000+NonHandledInterrupt} ; irq26 dc.l {$82000000+NonHandledInterrupt} ; irq27 dc.l {$82000000+NonHandledInterrupt} ; irq28 dc.l {$82000000+NonHandledInterrupt} ; irq29 end
Здесь имеется таблица прерываний и обработчик прерывания Reset, который заканчивается главным циклом. В обработчике Reset устанавливается значение указателя стека и очищается оперативная память.
Первое на что бросается взгляд в выше-приведенном листинге, это оригинальное задание таблицы векторов в виде опкода инструкции INT и адреса метки. Если попробовать заменить эту конструкцию например на такую строку: "INT main", то выдаст ошибку: "Неизвестный опкод". Сложно сказать почему так сделано, но факт есть факт. Инструкции INT в ассемблере STVD нет.
Теперь попробуем перекинуть таблицу векторов с обработчиками прерываний в отдельный файл irq.asm, оставив в main.asm только главный цикл.
В файл irq.asm скинем таблицу прерываний и все их обработчики:
stm8/ extern main #include "mapping.inc" segment 'rom' reset.l ; initialize SP ldw X,#$03ff ldw SP,X jp main jra reset interrupt NonHandledInterrupt NonHandledInterrupt.l iret segment 'vectit' dc.l {$82000000+reset} ; reset dc.l {$82000000+NonHandledInterrupt} ; trap dc.l {$82000000+NonHandledInterrupt} ; irq0 dc.l {$82000000+NonHandledInterrupt} ; irq1 dc.l {$82000000+NonHandledInterrupt} ; irq2 dc.l {$82000000+NonHandledInterrupt} ; irq3 dc.l {$82000000+NonHandledInterrupt} ; irq4 dc.l {$82000000+NonHandledInterrupt} ; irq5 dc.l {$82000000+NonHandledInterrupt} ; irq6 dc.l {$82000000+NonHandledInterrupt} ; irq7 dc.l {$82000000+NonHandledInterrupt} ; irq8 dc.l {$82000000+NonHandledInterrupt} ; irq9 dc.l {$82000000+NonHandledInterrupt} ; irq10 dc.l {$82000000+NonHandledInterrupt} ; irq11 dc.l {$82000000+NonHandledInterrupt} ; irq12 dc.l {$82000000+NonHandledInterrupt} ; irq13 dc.l {$82000000+NonHandledInterrupt} ; irq14 dc.l {$82000000+NonHandledInterrupt} ; irq15 dc.l {$82000000+NonHandledInterrupt} ; irq16 dc.l {$82000000+NonHandledInterrupt} ; irq17 dc.l {$82000000+NonHandledInterrupt} ; irq18 dc.l {$82000000+NonHandledInterrupt} ; irq19 dc.l {$82000000+NonHandledInterrupt} ; irq20 dc.l {$82000000+NonHandledInterrupt} ; irq21 dc.l {$82000000+NonHandledInterrupt} ; irq22 dc.l {$82000000+NonHandledInterrupt} ; irq23 dc.l {$82000000+NonHandledInterrupt} ; irq24 dc.l {$82000000+NonHandledInterrupt} ; irq25 dc.l {$82000000+NonHandledInterrupt} ; irq26 dc.l {$82000000+NonHandledInterrupt} ; irq27 dc.l {$82000000+NonHandledInterrupt} ; irq28 dc.l {$82000000+NonHandledInterrupt} ; irq29 END
В итоге, в main.asm останется лишь главный цикл:
stm8/ segment 'rom' .main jp main end
При компиляции может выдать ошибку на некорректный EOF, то исправляется в самом редакторе, для этого нужно нажать enter после end.
Теперь нам нужно добавить адреса периферийных регистров ввода/вывода. Для этого нужно найти файлы STM8S103F.asm и STM8S103F.inc в каталоге с установки STVD и скопировать их в папку проекта:
После чего нужно добавить файл STM8S103F.asm в проект, а содержимое main.asm привести к виду:
stm8/ #include "STM8S103F.inc" LED equ 5 segment 'rom' .main bset PB_DDR, #LED ; PB_DDR|=(1<<LED) bset PB_CR1, #LED ; PB_CR1|=(1<<LED) mloop: bcpl PB_ODR, #LED ; PB_ODR^=(1<<LED) jp mloop end
После компиляции такую программу можно уже загрузить в микроконтроллер, и прогнать в отладчике:
Осталось добавить задержку для главного цикла, что бы программа корректно работала. Для этого, аналогично тому как добавляли в проект файл irq.asm, добавим в него ещё файл utils.asm следующего содержания:
stm8/ segment 'rom' .delay: ld a, #$06 ldw y, #$1a80 ; 0x61a80 = 400000 i.e. (2*10^6 MHz)/5cycles loop: subw y, #$01 ; decrement with set carry sbc a,#0 ; decrement carry flag i.e. a = a - carry_flag jrne loop ret end
Теперь в файле main.asm после #include "STM8S103F.inc" нужно добавить: "extern delay", а в главный цикл можно вставить вызов подпрограммы: "call delay":
Минимальная программа на ассемблере STVD готова. Теперь будем разбираться с тем, что мы напрограммировали.
Язык ассемблера состоит из ассемблерных инструкций, ассемблерных директив, директив препроцессора и языка макроопределений. Раньше все это вместе называлось макроассемблером, и технически, макроассемблер по возможностям вплотную приближается к ЯВУ. По крайней мере так считалось раньше, когда эти ЯВУ были в разы проще, навроде Бейсика.
Строка ассемблера STVD имеет следующий формат:
Вначале идёт необязательная метка, затем идёт необязательный опкод, затем идут операнды (если они есть), и завершает строку необязательный комментарий.
Заметьте, что даже при опущенной метке, ассемблерной инструкции или директиве, должен ОБЯЗАТЕЛЬНО предшествовать ПРОБЕЛ или ТАБУЛЯЦИЯ. ВНАЧАЛЕ СТРОКИ ИДЕТ ТОЛЬКО МЕТКА! Исключением из этого правила можно считать директиву "stm8/" вначале исходного кода.
Соответственно, если написать "segment 'rom'" или "#include "STM8S103F.inc"" сначала строки то, при компиляции выдаст ошибку. Видимо разработчики STVD были неравнодушны к питону ;)
По умолчанию, в STVD установлен мотороловский формат числовых констант, когда десятичное число пишется как есть, перед шестнадцатеричным числом ставится значок доллара, перед двоичным - знак процента и перед восьмеричным - тильда.
С помощью директив: MOTOROLA, INTEL, TEXAS, ZILOG можно менять формат числовой константы:
Новый формат будет применим к тексту программы после директивы.
Метка обязательно должна начинаться с начала строки, и может содержать в себе: заглавные и строчные буквы английского алфавита, цифры и символ подчёркивания. Начинаться метка должна с буквы или символа подчёркивания, т.е. никак не с цифры. Метка может содержать до 30 символов.
Метка может заканчиваться символом двоеточия или не содержать его вообще. Символ двоеточия игнорируется.
Метка может содержать суффикс вида: label[.b|.w|.l], который состоит из точки и добавочных букв: b, w, l.
С помощью суффиксов: b, w, l, метка может быть одно-, двух-, или четырёхбайтной. Т.о. метка может быть использована для одно-,двух или трехбайтной адресации.
В качестве примера можно привести такой исходник:
stm8/ segment byte at 10 'ram0' var1.b ds.b segment byte at 100 'ram1' var2.w ds.b segment 'rom' .main ld a,var1 ld a,var2 ld a, const mloop.l jpf mloop const: dc.b $aa end
Который будет скомпилирован в следующий машкод:
808a: b6 10 ld A,0x10 ;0x10 808c: c6 01 00 ld A,0x0100 ;0x100 808f: c6 80 96 ld A,0x8096 ;0x8096 8092: ac 00 80 92 jpf 0x008092 ;0x8092 8096: aa 00 or
Здесь задаётся две переменные: var1 в нулевой странице ОЗУ, var2 в первой странице OЗУ, и константа const в области флеш-памяти. При обращении к первой переменной используется короткая адресация (shortmem), при обращении к второй переменной и к константе используется длинная адресация (longmem), и инструкция jpf осуществляет переход метке с расширенной адресацией (extmem).
По умолчанию размер метки равен двум байтам (одному слову), но он может быть изменён директивами: BYTES, WORDS, LONGS.
Метка может быть абсолютной и относительной. Относительную метку вычисляет компоновщик на этапе компиляции прошивки.
По умолчанию, все метки локальные. C помощью директив PUBLIC и EXTERN можно обмениваться метками между отдельными модулями программы. Директива PUBLIC делает метку видимой компоновщику. Директива EXTERN делает внешнюю метку видимой данному модулю. В качестве альтернативы директиве PUBLIC, может выступать точка в начале метки.
Сегментация позволяет управлять из проекта тем, что в GCC делает скрипт компоновщика, т.е. с помощью директивы сигментации можно указывать компоновщику, где должны располагаться тот или иной код или данные. Один программный модуль (файл) может содержать до 128 сегментов.
Сегмент устанавливается с помощью директивы сегментации. Ее формат представлен ниже:
[<name>] SEGMENT [<align>] [<combine>] '<class>' [cod]
Вначале идет необязательный параметр - имя (name). Он может иметь длину до 12 символов. Сегменты одного класса, но с разными именами группируются компоновщиком друг за другом в порядке определения.
Параметр выравнивания - "align", может принимать следующие значения:
Выше я приводил пример, как с помощью сегментов задавать переменные в области оперативной памяти. Ниже приведены еще примеры из фирменного руководства:
Здесь метке counter соответствует адрес 0x80, метке address - 0x81, stack - 0x92. Во втором файле, метка serialtemp имеет адрес 0x93, а serialcou - 0x94. Т.к. оба файла пишут в сегмент "eprom", то второй файл пишет следом за первым.
Следующий параметр директивы segment - <combine> указывает компоновщику где следует располагать сегмент. Имеется три варианта для этого параметра:
Вариант at- ДОЛЖЕН использоваться при объявлении класса, и использоваться ТОЛЬКО ОДИН РАЗ.
Также в случае at должен обязательно задан начальный адрес сегмента, и опционально последний адрес сегмента или его размер. Все адреса пишутся в ШЕСТНАДЦАТЕРИЧНОМ виде КАК ЕСТЬ без префиксов.
Вариант common позволяет определить общие области данных. Области этого типа с одинаковым именем класса, будут иметь одинаковый стартовый адрес. Это позволяет использовать такие сегменты для обмена данными.
Рассмотрим такой пример:
Здесь метки lab1 и lab2 будут иметь адрес 0x12, lab3 и lab4 будут иметь адрес 0x1a, а lab5 - 0x1e.
Нельзя комбинировать вместе common и at- сегменты под одинаковыми именами. Следующий пример демонстрирует ошибку:
Последний параметр директивы segment - cod, позволяет управлять получаемым при линковке выходным файлом, в котором будут размещены данные сегмента. Если параметр cod опущен, то все скомпануется в единый default.cod. Если же в качестве параметра cod задать число от нуля до девяти, то компоновщик все сегменты данного класса разместит в файле prog_x.cod, где x - номер кода. Это может быть полезно, например при формировании разных eeprom для разных устройств. Например:
Полный перечень директив с подробным описанием изложен в Руководство пользователя по ассемблеру и компоновщику STM8: ST Assembler-Linker UM0144, приложение A. Я бы хотел упомянуть наиболее востребованные директивы.
EQU - директива подмены или соответствия. Имеется наверно во всех ассемблерных языках. Формат:
метка EQU выражение
При компиляции программы, метка заменяется своим выражением. Пример:
var1 equ 5
#define - директива похожая на EQU, но имеющая существенное отличие. В EQU метке присваивается число, его размерность: байт, слово, двойное слово - определяется в момент присваивания. В случае использования #define, метке присваивается строка которая уже при компиляции преобразуется в число, и его размерность определяется в самой программе. Формат:
#define <псевдоним> <строка>
Пример использования:
#define value 5 ld a,#value ; ld a,#5
CEQU - директива похожая не EQU но позволяет менять свое содержимое:
lab1 CEQU {lab1+1} ; inc lab1
Используется в сочетании с директивами REPEAT и UNTIL.
Директивы: DC.B, DC.W, DS.L - позволяют записать константу, или массив. Также возможна запись строки ASCII.
Директивы: DS.B, DS.W, DS.L - позволяют зарезервировать место. Через запятую указывается количество резервируемых ячеек.
Директива STRING позволяет зарезервировать ASCII строку. Является синонимом директивы DC.B. Примеры использования:
STRING 1,2,3 ; generates 01,02,03 STRING “HELLO” ; generates 48,45,4C,4C,4F STRING “HELLO”,0 ; generates 48,45,4C,4C,4F,00
Директивы предназначенные для написания макросов хотелось бы рассмотреть в соответствующей главе.
Выполнение кода из ОЗУ имеет смысл в STM8L - серии, там есть режим энергосбережения WFE который позволяет работать с периферией без прерываний, что позволяет исключить использование флеш-памяти при работе главного цикла. Отказ от использования флеш-памяти позволяет снизить энергопотребление, но при этом не следует забывать, что программа из оперативной памяти выполняется дольше нежели с флеша. Сейчас мы в этом убедимся, но перед тем как запустить программу из ОЗУ, ее нужно туда скопировать. При этом все абсолютные адреса надо будет как-то проиндексировать.
В Руководство пользователя по ассемблеру и компоновщику STM8: ST Assembler-Linker UM0144 описывается механизм написания кода специально предназначенный для копирования в ОЗУ. Для этого декларируется два сегмента, где один будет использоваться для выполнения, а другой для хранения кода. Например:
здесь код начинающийся с метки label1 будет сохранен в 'code' сегменте, но все метки будут пересчитаны относительно 'ram' сегмента. Кроме того, адресное пространство 'ram' сегмента будет зарезервировано под этот код.
Все это хорошо, но вот скомпилировать у меня этот пример никак не получалось. Впрочем, создать перемещаемый код вручную не так сложно. Для эксперимента возмем пример базового проекта из первой части. Из него потребуется удалить файл utils.asm, и тогда main.asm будет выглядеть так:
stm8/ #include "STM8S103F.inc" LED equ 5 offset equ $4000 segment 'rom' .main: clrw x lp ld a,xl ; 18.11.2022 исправил ошибку, ниже был пропущен знак решетки cp a, #$20 ; 32 bytes length of ram_loader jreq jump ld a, (code,x) ld ($100,x),a incw x jp lp jump: jp $100 segment 'eeprom' code bset PB_DDR, #LED ; PB_DDR|=(1<<LED) bset PB_CR1, #LED ; PB_CR1|=(1<<LED) mloop: bcpl PB_ODR, #LED ; PB_ODR^=(1<<LED) ; --- delay 1 sec ---- ld a, #$06 ldw y, #$1a80 ; 0x61a80 = 400000 i.e. (2*10^6 MHz)/5cycles loop: subw y, #$01 ; decrement with set carry sbc a,#0 ; decrement carry flag i.e. a = a - carry_flag jrne loop ;---------------------- ;jra mloop ; relative label jp {mloop-offset+$100} ; absolute label end
Здесь сегмент 'eeprom' содержит программу размещаемую в оперативной памяти, а сегмент 'rom' содержит загрузчик. Программа размещается в первой странице т.е. с адреса 0х100, тогда как нулевая страница отведена на переменные. В программе все относительные метки оставлены как есть, в то время как абсолютные записываются со смещением: адрес-(начальный адрес eeprom)+(адрес начала первой страницы ОЗУ).
После прошивки микроконтроллера будет видно, что светодиод имеет полупериод мигания гораздо больше чем одна секунда. Определяя "на глаз", у меня получилось где-то трёхкратное снижение быстродействия, о чем собственно и говорилось в документации: 3-х уровневый конвейер STM8: перевод глав 3, 4, 5 руководства по программированию микроконтроллеров STM8 (PM0044)
Чтение инструкции из ОЗУ походит на чтение из ПЗУ. Однако, вследствие того, что шина ОЗУ является лишь 8-битной, потребуется 4 последовательных операции чтения, чтобы заполнить одно Fx слово. Вследствие этого код из ОЗУ выполняется медленнее, нежели с флеш-памяти.
В дальнейшем, эту программу я буду использовать для демонстрации процесса отладки в STVD.
В качестве отладчика в STVD используется мощнейший GDB, полные его возможности доступны через вкладку "Консоль" в окне "Output Window". STVD выступает в качестве фронтенда к GDB:
У меня пока не очень большой опыт использования STVD, но из всех возможностей GDB, через консоль я печатал только дампы памяти. Все остальное я делал через графический интерфейс, Работа с ним экономит много времени.
Итак, после после запуска отладки, становится доступна панель управления отладкой:
Она реализует режим трассировки и содержит операции: начать отладку, завершить отладку, команда GDB - Run, команда GDB - Continue, команда GDB - next, команда GDB - nexti, команда GDB - step, команда GDB - stepi и пр. У всех иконок есть всплывающие подсказки, раскрывающие их функции.
Открыть то или иное окно можно через меню->View:
Все окна стекабельны, закрываются крестиком. Настройки окон сохранятся в workspace. Самые полезные на мой взгляд следующие окна:
1. Окно дизассемблера. Наиболее полезно оно наверное при отладке Си программы, позволяет увидеть, в какой код преобразовалась та или иная программная конструкция. Для ассемблера это окно полезно тем, что позволяет увидеть адреса и размерность меток, а также числовой адрес порта ввода-вывода. Через "Display Options" можно настроить отображаемые столбцы. Опция Refresh нужна когда вводишь отладочные команды через консоль gdb. Потом чтобы увидеть изменения приходиться обновлять.
2. Следующее окно "Core Rigisters", показывает содержимое регистров микроконтроллера и флагов состояния. Прямо в окне можно менять значение регистров, снимать и устанавливать нужные флаги.
3. Окно периферийных регистров - это древовидный список сгруппированный по аппаратным модулям. Здесь также можно менять содержимое РВВ:
4. Следующее полезное окошко - окно переменных. Переменную можно ввести по имени или по адресу. Можно менять значение переменной.
5. И последнее оконо - окно с дампом памяти. Лично мне как-то проще вводить в консоли gdb команду: "x/10xb", но данное оконо тоже может быть полезно. Хотя бы тем, что там в интерактивном режиме можно менять содержимое ячеек памяти.
Для отладки алгоритма, поставим точку остановки на инструкции jp $100, т.е. после завершения операции копирования из eeprom в ram, и перед передачей управлению скопированному коду. Жмём иконку Run (выглядит как восклицательный знак) и после остановки сверяем оба сегмента: ram1 и eeprom:
Если код скопировался успешно, делаем прыжок на адрес 0x100, после чего в окне дизассемблера проверяем скопированный код. Особое внимание нужно обратить на адреса переходов.
Если алгоритм скопирован успешно, можно приступать о обычной отладке алгоритма. На инструкциях условных переходов бывает удобно выставлять нужные флаги, чтобы без проволочек проходить по тем или иным веткам алгоритма.
По-моему, ничего сложного.
Я не спец по макроассемблеру, прямо говоря, я им никогда не пользовался и поэтому имею о нем только общее представление. Мне показалось, что это хороший повод заполнить досадный пробел в образовании, тем не менее, к нижеизложенному тексту не следует относиться как какому-то руководству.
Макроассемблер позволяет сократить рутину при написании ассемблерных программ, а также уменьшить ваше время на отладку оных. Принцип заключается в объединении группы ассемблерных инструкций в единый макрос которому, навроде функции или подпрограмме, можно задавать параметры, и управлять с помощью препроцессора. Однажды написав и отладив такой макрос вам не придётся делать это снова. Использование макросов может приблизить читаемость программы к языкам высокого уровня навроде бейсика. При этом у вас по прежнему будет вся мощь ассемблера, и вы не будете ограничены рамками какого-либо языка программирования.
Главной проблемой макросов считается трудоёмкость их отладки на этапе работы препроцессора. Когда вам выдаст ошибку на макрос, вам выдаст ошибку на ту строку, где он вызывается, а не на проблемную конструкцию в теле макроса. Справедливости ради должен сказать, что в STVD имеется директива %OUT которая выводит какую либо строку в лог компиляции, но вот вывести число через нее не получится.
Директивы макроассемблера и формат задания макроса описан в руководстве: Руководство пользователя по ассемблеру и компоновщику STM8: ST Assembler-Linker UM0144. Я не буду повторять написанное там, вместо этого я приведу несколько примеров использования макросов.
Для начала совсем простой случай, попробуем заменить на макрос строку перехода по абсолютному адресу:
jp {mloop-offset+$100}
Здесь необходимо вычисление адреса запихнуть в макрос, чтобы не писать это каждый раз. Макрос будет таким:
jp_ram MACRO adr jp {adr-offset+$100} MEND
Тогда использование макроса сведётся к строке:
jp_ram mloop
Теперь пример посложнее, постараемся повторить широко известный макрос _delay_ms который имеется в gcc-avr и используется довольно часто. Для этого, сначала в настройках проекта нужно будет с помощью именованной константы указать частоту F_CPU:
Кроме этого, нам понадобится отдельный файл macros.inc, в который мы будем скидывать макросы. Этот файл потом нужно будет включить через директиву #include:
Макрос у меня получился таким:
delay_ms MACRO ms #IFDEF F_CPU #ELSE #define F_CPU 2000000 #ENDIF ld a, #{{SEG {ms mult {F_CPU div 1000}}} div 5} ldw y, #{{OFFSET {ms mult {F_CPU div 1000}}} div 5} LOCAL loop loop: subw y, #$01 ; decrement with set carry sbc a,#0 ; decrement carry flag i.e. a = a - carry_flag jrne loop MEND
Тогда код в модуле main.asm примет следующий вид:
stm8/ #include "STM8S103F.inc" #include "macros.inc" LED equ 5 offset equ $4000 jp_ram MACRO adr jp {adr-offset+$100} MEND segment 'rom' .main: ; --- GPIO Setup --------- bset PB_DDR, #LED ; PB_DDR|=(1<<LED) bset PB_CR1, #LED ; PB_CR1|=(1<<LED) ; --- Copy to RAM --------- clrw x lp ld a,xl cp a,$20 ; 32 bytes length of ram_loader jreq jump ld a, (code,x) ld ($100,x),a incw x jra lp jump: jp $100 segment 'eeprom' code mloop: bcpl PB_ODR, #LED ; PB_ODR^=(1<<LED) delay_ms 1000 jp_ram mloop end
Как я уже говорил, на макросах можно сделать подобие операторов в ЯВУ. Например, в копировщике кода из eeprom в ram имеется сравнение числа с регистром. Можно написать макрос который сравнивает регистр с числом и осуществляет переход по метке в случае совпадения.
У меня получилось два макроса. Один для 16-битных регистров, другой для 8-битных:
if_reg_eq8 MACRO reg value label #IFIDN reg yh ld a, y #ENDIF #IFIDN reg yl ld a,yl #ENDIF #IFIDN reg xh ld a, xh #ENDIF #IFIDN reg xl ld a,xl #ENDIF cp a,value jreq label MEND ;-------------------------- if_reg_eq16 MACRO reg value label #IFIDN reg x pushw x ldw x,#{value} cpw x,($01,sp) popw x jreq label #ELSE pushw x pushw y ldw x,#{value} cpw x,($01,sp) popw y popw x jreq label #ENDIF MEND
main.c в таком случае принимает вид:
stm8/ #include "STM8S103F.inc" #include "macros.inc" ;----------------------- LED equ 5 offset equ $4000 ;----------------------- jp_ram MACRO adr jp {adr-offset+$100} MEND ;------------------------ segment 'rom' .main: ; GPIO SETUP bset PB_DDR, #LED ; PB_DDR|=(1<<LED) bset PB_CR1, #LED ; PB_CR1|=(1<<LED) ; Copy EEPROM to RAM1 clrw x lp if_reg_eq8 xl $20 jump ld a, (code,x) ld ($100,x),a incw x jra lp jump: jp $100 ; go to RAM ;----------------------- segment 'eeprom' code: mloop: bcpl PB_ODR, #LED ; PB_ODR^=(1<<LED) delay_ms 1000 jp_ram mloop end
Аналогично можно написать макросы для сравнения числа с ячейкой памяти, или сравнения по признаку больше, меньше, меньше или равно и т.д.
Если организовать задержку по таймеру на 1 мс и более не составляет проблем, то с задержкой на 1мкс не все так просто. Обработчики прерываний таймера будут не успевать обрабатывать поступающие прерывания. Единственным выходом будет сделать задержку на инструкциях. Но считать циклы при работающем конвейере, дело неблагодарное. Количество циклов будет зависеть от предыдущих инструкций и от способа адресации. Кроме того, я натыкался на информацию, что на разных тактовых частотах одни и те же инструкции будут выполняться за разное количество циклов. И вроде бы я сталкивался с этим на практике. В качестве способа решения этой проблемы рекомендуют заблокировать работу конвейера. Делается это с помощью инструкций выполняющих сброс очереди конвейера - FLUSH. Такими инструкциями являются инструкции условного перехода. Т.о. задержку можно организовать последовательном выполнении операций условного перехода с нулевым смещением. Одна такая инструкция должна выполняться за 2 цикла ВСЕГДА.
Записать последовательность инструкций помогут директивы REPEAT и UNTIL. К сожалению их нельзя использовать в макросах поэтому их использование выглядит так:
scf ; Set Carry Flag us CEQU 5 REPEAT dc.b $25, $0 ; JRC label us CEQU {us-1} UNTIL {us eq 0}
Здесь предполагается, что fCPU = 2MHz и т.к. инструкция условного перехода с случае перехода выполняется за два такта, то на выполнении каждой инструкции JRC уходит 1 мкс. Директивы REPEAT/UNTIL вставляют в текст программы пять таких инструкций, следовательно формируется задержка длительностью 5мкс.
Для задержек 10мкс и больше можно использовать таймеры TIM4/TIM6, а для меньших задержек думаю сгодится такой способ.