Адресация данных в микроконтроллерах

Адресация данных в микроконтроллерах

Микроконтроллеры и микропроцессоры как правило позволяют по-разному обращаться к данным. В этой заметке рассматривается адресация данных и ее типы в микроконтроллерах AVR с внутренним SRAM.

Это перевод немецкой статьи, оригинал которой можно найти вот здесь.

Непосредственные значения

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

    ldi     r16, 0xA0    ; Записываем значение 0xA0 в регистр r16

ldi значит «load immediate», т.е. «загрузить непосредственно». В микроконтроллерах AVR прямая загрузка значений возможна только для регистров с r16 по r31.

Прямая адресация

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

.dseg
variable:  .byte 1          ; Резервируем один байт в SRAM.
                            ; Метка "variable" говорит о том, что
                            ; каждое появление "variable" в коде будет заменено
                            ; адресом этой ячейки памяти
.cseg
    ldi     r16, 25         ; Записываем 25 непосредственно в регистр r16.
    sts     variable, r16   ; Значение регистра r16 (т.е. 25) записываем в ячейку
                            ; с адресом "variable". Как было сказано, ассемблер
                            ; заменяет все вхождения "variable" действительным
                            ; адресом ячейки с этой меткой.

Адрес ячейки памяти известен уже на этапе компиляции, а значит команда может обращаться только к ячейкам, чей адрес известен заранее. Т.к. variable в предыдущем примере представляет собой адрес, а значит обычное число, то это значение можно также складывать с константами:

.dseg
variable2: .byte 2            ; Резервируем два байта в памяти. "variable2"
                              ; указывает при этом на адрес первого из двух байтов.

.cseg
    ldi     r16, 17           ; Записываем значение 17 прямо в регистр r16.
    sts     variable2, r16    ; Значение регистра r16 (т.е. число 17) записываем
                              ; по адресу "variable2" (первый байт).
    inc     r16               ; Инкрементируем значение в регистре r16.
    sts     variable2+1, r16  ; Записываем значение регистра r16 во второй байт.

В результате исполнения этого кода в первый байт ячейки variable2 будет записано число 17, а во второй число 18. При этом нужно заметить, что суммирование в команде sts происходит на этапе ассемблирования, а не исполнения кода. Это возможно потому, что адрес, на который указывает variable2, известен в момент компиляции, а значит известно и на единицу большее значение.

Косвенная адресация

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

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

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

В микроконтроллерах AVR для этих целей предусмотрены три 16-ти битных регистра, каждый из которых состоит из двух восьмибитных регистров. Это обусловлено тем, что одного восьмибитного регистра хватает для адресации лишь 256 ячеек, что, конечно, маловато для микроконтроллеров с большим объемом памяти. Регистры (r26, r27), (r28, r29) и (r30, r31) образуют упомянутые 16-ти битные регистры, используемые для косвенной адресации. Т.к. эти регистры указывают на данные, их называют указывающими регистрами или указателями (англ. pointer). Называются эти регистры X, Y и Z, а каждый из восьмибитных регистров помимо своего rxx-названия доступен и по адресу XL, XH, YL, YH, ZL и ZH. L (low) и H (high) указывают на «верхний» и «нижний» (либо первый и второй) байты 16-ти битного регистра.

Указывающие регистры AVR
Регистр Альтернативное имя 16-битный регистр
r26 XL X
r27 XH
r28 YL Y
r29 YH
r30 ZL Z
r31 ZH

Для примера мы будем использовать регистр Z. Для этого нам нужно загрузить в него адрес первого складываемого числа. Т.к. регистр Z является 16-битным, мы должны использовать ZH и ZL в двух командах ldi. Ассемблер AVR предоставляет две удобные функции: с помощью LOW(…) и HIGH(…) можно получить соответсвенно младший и старший байты адреса. Это как раз то, что нам нужно, ведь мы хотим записать в ZL/ZH младший и старший байты адреса первого числа.

После этого с помощью команды ld мы можем прочитать значение ячейки, на которую указывает регистр Z. Значение будем записывать в регистр r17. Для суммирования используем регистр r18, значение в котором в самом начале алгоритма обнуляем с помощью clr.

.dseg
zahlen_start: .byte 20              ; Резервируем 20 байт, это максимальное
                                    ; количество складываемых чисел.

.cseg
; Предположим, что где-то ранее описаны сами числа. На данном этапе этот 
; шаг нас не интересует. Мы исходим из того, что начиная с адреса 
; zahlen_start в памяти располагается столько чисел, сколько указано в
; регистре r16.

    ldi     ZL, LOW(zahlen_start)   ; Загружаем в ZL младший байт адреса.
    ldi     ZH, HIGH(zahlen_start)  ; Загружаем в ZH старший байт адреса.
    clr     r18                     ; Обнуляем значение в r18.
schleife:
    ld      r17, Z                  ; Содержимое ячейки, на которую указывает 
                                    ; регистр Z, записываем в регистр r17.
    adiw    ZH:ZL, 1                ; Инкрементируем Z, т.к. нам надо прочитать
                                    ; следующее число. adiw подходит для 
                                    ; 16-битных операндов.
    add     r18, r17                ; Сложение.
    dec     r16                     ; Уменьшаем значение r16 на 1
    brne    schleife                ; Пока r16 не равен 0 "прыгаем" на метку
                                    ; "schleife".

; Здесь цикл завершается. Результат сложения лежит в r18.

Наша программа справляется с задачей, но есть один не самый удобный момент: после каждой итерации нужно вручную инкрементировать регистр Z, чтобы перейти к следующему числу. Т.к. последовательное чтение (или последовательная запись) данных из памяти встречается довольно часто, то в ассемблер AVR были введены вспомогательные инструменты.

Постинкремент

Эти две строки:

    ld      r17, Z        ; Содержимое ячейки, на которую указывает регистр Z,
                          ; записываем в регистр r17.
    adiw    ZH:ZL, 1      ; Инкрементируем Z.

можно заменить одной строкой вида:

    ld      r17, Z+       ; Прочитать содержимое и сразу инкрементировать Z.

Такая конструкция снижает время исполнения программы и делает код короче. Обратите внимание, что инкрементирование происходит после выполнения команды.

Преддекремент

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

    ld      r17, -Z       ; Декрементировать Z и ПОСЛЕ ЭТОГО значение ячейки, на
                          ; которую указывает регистр Z, записать в регистр r17.