STM8 + STVD + ASSEMBLER: Быстрый старт

разделы: STM8 , АССЕМБЛЕР , дата: 22 августа 2018г.

Вновь возвращаюсь к фирменной среде разработки - 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

  1. Открытие шаблонного проекта на ассемблере
  2. Добавление в проект файла с таблицей векторов и обработчиками прерываний
  3. Добавление в проект констант с адресами периферии
  4. Добавление в проект файла с подпрограммой

II. Язык ассемблера STVD

  1. Основные сведения об ассемблере STVD
  2. Формат числовых констант
  3. Формат метки
  4. Сегментация
  5. Основные директивы ассемблера

III. Процесс отладки

  1. Копирование кода в ОЗУ и выполнение его оттуда
  2. Интерфейс отладки STVD
  3. Процесс отладки в STVD

IV. Макроассемблер

  1. Введение в макроассемблер STVD
  2. Макрос задержки delay_ms
  3. Макрос условного оператора сравнения регистра с числом
  4. Задержка длительностью 1 мкс на инструкции условного перехода

1. Открытие шаблонного проекта на ассемблере

Если сам ассемблер 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 нет.

2. Добавление в проект файла с таблицей векторов и обработчиками прерываний

Теперь попробуем перекинуть таблицу векторов с обработчиками прерываний в отдельный файл irq.asm, оставив в main.asm только главный цикл.

1) Для этого сначала нужно создать новый файл:

2) После создания файл нужно будет сохранить к директории текущего проекта:

3) В диалоговом окне выбираем директорию, имя файла, и сохраняем его:

4) После чего нужно будет добавить файл в проект. Для этого нужно правой кнопкой мыши щёлкнуть по Source Files и выбрать добавление файла, после чего новый файл отобразится в файловой структуре проекта:

В файл 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.

3. Добавление в проект констант с адресами периферии

Теперь нам нужно добавить адреса периферийных регистров ввода/вывода. Для этого нужно найти файлы 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

После компиляции такую программу можно уже загрузить в микроконтроллер, и прогнать в отладчике:

4. Добавление в проект файла с подпрограммой

Осталось добавить задержку для главного цикла, что бы программа корректно работала. Для этого, аналогично тому как добавляли в проект файл 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 готова. Теперь будем разбираться с тем, что мы напрограммировали.

5. Основные сведения об ассемблере STVD

Язык ассемблера состоит из ассемблерных инструкций, ассемблерных директив, директив препроцессора и языка макроопределений. Раньше все это вместе называлось макроассемблером, и технически, макроассемблер по возможностям вплотную приближается к ЯВУ. По крайней мере так считалось раньше, когда эти ЯВУ были в разы проще, навроде Бейсика.

Строка ассемблера STVD имеет следующий формат:

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

Заметьте, что даже при опущенной метке, ассемблерной инструкции или директиве, должен ОБЯЗАТЕЛЬНО предшествовать ПРОБЕЛ или ТАБУЛЯЦИЯ. ВНАЧАЛЕ СТРОКИ ИДЕТ ТОЛЬКО МЕТКА! Исключением из этого правила можно считать директиву "stm8/" вначале исходного кода.

Соответственно, если написать "segment 'rom'" или "#include "STM8S103F.inc"" сначала строки то, при компиляции выдаст ошибку. Видимо разработчики STVD были неравнодушны к питону ;)

6. Формат числовых констант

По умолчанию, в STVD установлен мотороловский формат числовых констант, когда десятичное число пишется как есть, перед шестнадцатеричным числом ставится значок доллара, перед двоичным - знак процента и перед восьмеричным - тильда.

С помощью директив: MOTOROLA, INTEL, TEXAS, ZILOG можно менять формат числовой константы:

Новый формат будет применим к тексту программы после директивы.

7. Формат метки

Метка обязательно должна начинаться с начала строки, и может содержать в себе: заглавные и строчные буквы английского алфавита, цифры и символ подчёркивания. Начинаться метка должна с буквы или символа подчёркивания, т.е. никак не с цифры. Метка может содержать до 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, может выступать точка в начале метки.

8. Сегментация

Сегментация позволяет управлять из проекта тем, что в 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 для разных устройств. Например:

9. Основные директивы ассемблера

Полный перечень директив с подробным описанием изложен в Руководство пользователя по ассемблеру и компоновщику 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

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

10. Копирование кода в ОЗУ и выполнение его оттуда

Выполнение кода из ОЗУ имеет смысл в 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.

11. Интерфейс отладки 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", но данное оконо тоже может быть полезно. Хотя бы тем, что там в интерактивном режиме можно менять содержимое ячеек памяти.

12. Процесс отладки в STVD

Для отладки алгоритма, поставим точку остановки на инструкции jp $100, т.е. после завершения операции копирования из eeprom в ram, и перед передачей управлению скопированному коду. Жмём иконку Run (выглядит как восклицательный знак) и после остановки сверяем оба сегмента: ram1 и eeprom:

Если код скопировался успешно, делаем прыжок на адрес 0x100, после чего в окне дизассемблера проверяем скопированный код. Особое внимание нужно обратить на адреса переходов.

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

По-моему, ничего сложного.

13. Введение в макроассемблер STVD

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

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

Главной проблемой макросов считается трудоёмкость их отладки на этапе работы препроцессора. Когда вам выдаст ошибку на макрос, вам выдаст ошибку на ту строку, где он вызывается, а не на проблемную конструкцию в теле макроса. Справедливости ради должен сказать, что в STVD имеется директива %OUT которая выводит какую либо строку в лог компиляции, но вот вывести число через нее не получится.

Директивы макроассемблера и формат задания макроса описан в руководстве: Руководство пользователя по ассемблеру и компоновщику STM8: ST Assembler-Linker UM0144. Я не буду повторять написанное там, вместо этого я приведу несколько примеров использования макросов.

Для начала совсем простой случай, попробуем заменить на макрос строку перехода по абсолютному адресу:

    jp {mloop-offset+$100}

Здесь необходимо вычисление адреса запихнуть в макрос, чтобы не писать это каждый раз. Макрос будет таким:

jp_ram MACRO adr
    jp {adr-offset+$100}
    MEND

Тогда использование макроса сведётся к строке:

    jp_ram mloop

14. Макрос задержки delay_ms

Теперь пример посложнее, постараемся повторить широко известный макрос _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

15. Макрос условного оператора сравнения регистра с числом

Как я уже говорил, на макросах можно сделать подобие операторов в ЯВУ. Например, в копировщике кода из 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

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

16. Задержка длительностью 1 мкс на инструкции условного перехода

Если организовать задержку по таймеру на 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, а для меньших задержек думаю сгодится такой способ.