У меня возникли проблемы с созданием и адресацией массива, созданного исключительно на сборке, с использованием набора инструкций для Atmel ATMega8535.

На данный момент я понимаю следующее:

  • Массив содержит непрерывные данные одинаковой длины.
  • Создание массива включает определение начального и конечного местоположений массива (так же, как и стек).
  • Вы можете адресовать индекс в массиве, добавляя смещение базового адреса массива.

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

Я попытался сделать это с небольшим успехом, используя следующие книги:

  • Требуется некоторая сборка: программирование на языке ассемблера с микроконтроллером AVR Тимоти С. Маргуш
  • Начните с ... микроконтроллеров AVR Питера Шарпа

Мы будем благодарны за любую помощь, совет или дополнительные ресурсы.

2
Daniel Dunn 8 Мар 2015 в 14:09

2 ответа

Лучший ответ

Если ваш массив доступен только для чтения, вам не нужно копировать его в ОЗУ. Вы можете сохранить его во Flash и при необходимости читать оттуда. Это сэкономит драгоценную оперативную память за счет более медленного доступа (чтение из оперативной памяти - 2 цикла, чтение из флэш-памяти - 3 цикла).

Вы можете объявить свой массив следующим образом:

.global my_array
.type   my_array, @object
my_array:
    .byte 12, 34, 56, 78

Затем, чтобы прочитать член массива, вы должны вычислить:

adress of member = array base address + member index

Если бы ваши члены были более одного байта, вам также пришлось бы умножить индекс по размеру, но здесь это не так. Затем вы помещаете адрес необходимого члена в регистре Z и выдать lpm инструкция. Вот функция, реализующая эту логику:

.global read_data
; input:    r24 = array index, r1 = 0
; output:   r24 = array value
; clobbers: r30, r31
read_data:
    ldi r30, lo8(my_array)  ; load Z = address of my_array
    ldi r31, hi8(my_array)  ; ...high byte also
    add r30, r24            ; add the array index
    adc r31, r1             ; ...and add 0 to propagate the carry
    lpm r24, Z
    ret

@scottt посоветовал вам сначала написать на C, а затем посмотреть сгенерированную сборку. Считаю это очень хорошим советом, давайте ему следовать:

#include <stdint.h>

__flash const uint8_t my_array[] = {12, 34, 56, 78};

uint8_t read_data(uint8_t index)
{
    return my_array[index];
}

Ключевое слово __flash, определяющее «именованное адресное пространство», является встроенным Расширение C поддерживается gcc. В сгенерированная сборка немного отличается от предыдущей: вместо этого вычисления base_address + index, gcc выполняет index − (−base_address):

read_data:
    mov r30, r24                ; load Z = array index
    ldi r31, 0                  ; ...high byte of index is 0
    subi r30, lo8(-(my_array))  ; subtract -(address of my array)
    sbci r31, hi8(-(my_array))  ; ...high byte also
    lpm r24, Z
    ret

Это так же эффективно, как и предыдущая ручная сборка, за исключением того, что не требуется, чтобы регистр r1 был инициализирован нулем. Но сохранение r1 равным нулю в любом случае является частью ABI gcc, поэтому это не должно иметь никакого значения.

Роль линкера

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

Сборка (с avr-gcc -c) и разборка (с avr-objdump -d) первый фрагмент кода дает следующее:

my_array.o, section .text:
00000000 <my_array>:
   0:   0c 22 38 4e        ."8N

Если бы мы компилировали из C, gcc поместил бы массив в раздел .progmem.data вместо .text, но это не имеет большого значения. Числа «0c 22 38 4e» - это содержимое массива в шестнадцатеричном формате. Символы справа являются эквивалентами ASCII, «.» - это символы-заполнители для непечатаемых символов.

Объектный файл также содержит эту таблицу символов, показанную avr-nm:

my_array.o:
00000000 T my_array

Означает, что символ «my_array» был определен как относящийся к смещению 0 в секции .text (подразумеваемой «T») этого объекта.

Сборка и разборка второго фрагмента кода дает следующее:

read_data.o, section .text:
00000000 <read_data>:
   0:   e0 e0        ldi r30, 0x00
   2:   f0 e0        ldi r31, 0x00
   4:   e8 0f        add r30, r24
   6:   f1 1d        adc r31, r1
   8:   84 91        lpm r24, Z
   a:   08 95        ret

Сравнивая разборку с реальным исходным кодом, можно увидеть что ассемблер заменил адрес my_array на 0x00, что почти гарантированно ошибаюсь. Но он также оставил записку компоновщику в форма «записей о перемещении», показанная avr-objdump -r:

read_data.o, RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE 
00000000 R_AVR_LO8_LDI     my_array
00000002 R_AVR_HI8_LDI     my_array

Это сообщает компоновщику, что инструкции ldi со смещениями 0x00 и 0x02 предназначены для загрузки младшего и старшего байтов (соответственно) окончательного адреса my_array. Объектный файл также содержит это таблица символов:

read_data.o:
         U my_array
00000000 T read_data

Где строка «U» означает, что в файле используется неопределенный символ с именем «my_array».

Связывание этих частей вместе с подходящей функцией main () дает двоичный файл, содержащий среду выполнения C из avr-lbc вместе с нашим кодом:

0000003c <my_array>:
  3c:   0c 22 38 4e        ."8N

00000040 <read_data>:
  40:   ec e3        ldi r30, 0x3C
  42:   f0 e0        ldi r31, 0x00
  44:   e8 0f        add r30, r24
  46:   f1 1d        adc r31, r1
  48:   84 91        lpm r24, Z
  4a:   08 95        ret

Следует отметить, что не только компоновщик перемещал части к их конечным адресам, он также исправил аргументы ldi инструкции, чтобы теперь они указывали на правильный адрес my_array.

3
Edgar Bonet 11 Мар 2015 в 13:13

Код должен выглядеть примерно так:

    .section    .text
    .global main
main:
    ldi r30,lo8(data)
    ldi r31,hi8(data)
    ldd r24,Z+3
    sts output,r24
    ld r24,Z
    sts output,r24
    ldi r24,0
    ldi r25,0
    ret
    .global data
    .data
data:
    .byte   1, 2, 3, 4
    .comm   output,1,1

Объяснение

Для людей, которые раньше программировали на ассемблере с использованием инструментария GNU, есть уроки, которые можно перенести даже на незнакомые наборы инструкций:

  1. Вы резервируете место для массива с помощью директив ассемблера .byte 1, 2, 3, 4, .word 1, 2 (.word - это 16 бит для AVR) или .space 100.
  2. При изучении нового набора команд напишите программы на C и попросите компилятор C сгенерировать вывод ассемблера. По мере чтения кода ассемблера найдите хороший справочник по программированию на ассемблере для набора команд.

Применяем этот трюк ниже.

Байт-массив.c

/* volatile our code doesn't get optimized out even when compiler optimization is on */
volatile char output;

char data[] = { 1, 2, 3, 4 };

int main(void)
{
    output = data[3];
    output = data[0];
    return 0;
}

Создать ассемблер из C

avr-gcc -mmcu=atmega8 -Wall -Os -S byte-array.c

Это сгенерирует файл ассемблера byte-array.s.

Byte-array.s

    .file   "byte-array.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
    .section    .text.startup,"ax",@progbits
.global main
    .type   main, @function
main:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
    ldi r30,lo8(data)
    ldi r31,hi8(data)
    ldd r24,Z+3
    sts output,r24
    ld r24,Z
    sts output,r24
    ldi r24,0
    ldi r25,0
    ret
    .size   main, .-main
.global data
    .data
    .type   data, @object
    .size   data, 4
data:
    .byte   1
    .byte   2
    .byte   3
    .byte   4
    .comm   output,1,1
    .ident  "GCC: (Fedora 4.9.2-1.fc21) 4.9.2"
.global __do_copy_data
.global __do_clear_bss

Прочтите это объяснение регистров указателя, чтобы узнать, как набор инструкций AVR использует регистровую пару r30, r31 как регистр указателя Z. Прочтите инструкции ld, st, ldi, ldd, sts и std.

Замечания по реализации

Если связать программу то разобрать ее:

avr-gcc -mmcu=atmega8 -Os byte-array.c -o byte-array.elf
avr-objdump -d byte-array.elf

00000000 <__vectors>:
   0:   12 c0           rjmp    .+36        ; 0x26 <__ctors_end>
   2:   2c c0           rjmp    .+88        ; 0x5c <__bad_interrupt>
   4:   2b c0           rjmp    .+86        ; 0x5c <__bad_interrupt>
   6:   2a c0           rjmp    .+84        ; 0x5c <__bad_interrupt>
   8:   29 c0           rjmp    .+82        ; 0x5c <__bad_interrupt>
   a:   28 c0           rjmp    .+80        ; 0x5c <__bad_interrupt>
   c:   27 c0           rjmp    .+78        ; 0x5c <__bad_interrupt>
   e:   26 c0           rjmp    .+76        ; 0x5c <__bad_interrupt>
  10:   25 c0           rjmp    .+74        ; 0x5c <__bad_interrupt>
  12:   24 c0           rjmp    .+72        ; 0x5c <__bad_interrupt>
  14:   23 c0           rjmp    .+70        ; 0x5c <__bad_interrupt>
  16:   22 c0           rjmp    .+68        ; 0x5c <__bad_interrupt>
  18:   21 c0           rjmp    .+66        ; 0x5c <__bad_interrupt>
  1a:   20 c0           rjmp    .+64        ; 0x5c <__bad_interrupt>
  1c:   1f c0           rjmp    .+62        ; 0x5c <__bad_interrupt>
  1e:   1e c0           rjmp    .+60        ; 0x5c <__bad_interrupt>
  20:   1d c0           rjmp    .+58        ; 0x5c <__bad_interrupt>
  22:   1c c0           rjmp    .+56        ; 0x5c <__bad_interrupt>
  24:   1b c0           rjmp    .+54        ; 0x5c <__bad_interrupt>

00000026 <__ctors_end>:
  26:   11 24           eor r1, r1
  28:   1f be           out 0x3f, r1    ; 63
  2a:   cf e5           ldi r28, 0x5F   ; 95
  2c:   d4 e0           ldi r29, 0x04   ; 4
  2e:   de bf           out 0x3e, r29   ; 62
  30:   cd bf           out 0x3d, r28   ; 61

00000032 <__do_copy_data>:
  32:   10 e0           ldi r17, 0x00   ; 0
  34:   a0 e6           ldi r26, 0x60   ; 96
  36:   b0 e0           ldi r27, 0x00   ; 0
  38:   e4 e8           ldi r30, 0x84   ; 132
  3a:   f0 e0           ldi r31, 0x00   ; 0
  3c:   02 c0           rjmp    .+4         ; 0x42 <__SREG__+0x3>
  3e:   05 90           lpm r0, Z+
  40:   0d 92           st  X+, r0
  42:   ac 36           cpi r26, 0x6C   ; 108
  44:   b1 07           cpc r27, r17
  46:   d9 f7           brne    .-10        ; 0x3e <__SP_H__>

00000048 <__do_clear_bss>:
  48:   10 e0           ldi r17, 0x00   ; 0
  4a:   ac e6           ldi r26, 0x6C   ; 108
  4c:   b0 e0           ldi r27, 0x00   ; 0
  4e:   01 c0           rjmp    .+2         ; 0x52 <.do_clear_bss_start>

00000050 <.do_clear_bss_loop>:
  50:   1d 92           st  X+, r1

00000052 <.do_clear_bss_start>:
  52:   ad 36           cpi r26, 0x6D   ; 109
  54:   b1 07           cpc r27, r17
  56:   e1 f7           brne    .-8         ; 0x50 <.do_clear_bss_loop>
  58:   02 d0           rcall   .+4         ; 0x5e <main>
  5a:   12 c0           rjmp    .+36        ; 0x80 <_exit>

0000005c <__bad_interrupt>:
  5c:   d1 cf           rjmp    .-94        ; 0x0 <__vectors>

0000005e <main>: ...

00000080 <_exit>:
  80:   f8 94           cli

00000082 <__stop_program>:
  82:   ff cf           rjmp    .-2         ; 0x82 <__stop_program>

Вы можете видеть, что avr-gcc автоматически генерирует код запуска , включая:

  • вектор прерывания (__vectors), который использует rjmp для перехода к подпрограммам обслуживания прерываний.
  • инициализировать регистр состояния SREG и указатель стека SPL/SPH (__ctors_end)
  • копирует содержимое сегмента данных из FLASH в RAM для инициализированных глобальных переменных с возможностью записи (__do_copy_data)
  • очищает сегмент BSS для неинициализированных доступных для записи глобальных переменных (__do_clear_bss и т. д.)
  • вызывает нашу функцию main()
  • вызывает _exit(), если main() когда-либо возвращается
  • _exit() - это просто cli для отключения прерываний
  • и бесконечный цикл (__stop_program)
1
scottt 8 Мар 2015 в 13:20