mirror of
https://github.com/MPSU/APS.git
synced 2025-09-15 09:10:10 +00:00
ЛР14. Рефактор методички
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
|
||||
> — Но разве в процессе компиляции исходного кода на языке Си мы не получаем программу, написанную на языке ассемблера? Получится ведь тот же код, что мы могли написать и сами.
|
||||
|
||||
Штука в том, что ассемблерный код, который писали ранее вы отличается от ассемблерного кода, генерируемого компилятором. Код, написанный вами, обладал, скажем так... более тонким микро-контролем хода программы. Когда вы писали программу, вы знали какой у вас размер памяти, где в памяти расположены инструкции, а где данные (ну, при написании программ вы почти не пользовались памятью данных, а когда пользовались — просто лупили по случайным адресам и все получалось). Вы пользовались всеми регистрами регистрового файла по своему усмотрению, без ограничений. Однако, представьте на секунду, что вы пишете проект на ассемблере вместе с коллегой: вы пишите одни функции, а он другие. Как в таком случае вы будете пользоваться регистрами регистрового файла? Поделите его напополам и будете пользоваться каждый своей половиной? Но что будет, если к проекту присоединится еще один коллега — придется делить регистровый файл уже на три части? Так от него уже ничего не останется. Для разрешения таких ситуаций было разработано [соглашение о вызовах](#соглашение-о-вызовах) (calling convention).
|
||||
Штука в том, что ассемблерный код, который писали ранее вы отличается от ассемблерного кода, генерируемого компилятором. Код, написанный вами, обладал, скажем так... более тонким микро-контролем хода программы. Когда вы писали программу, вы знали какой у вас размер памяти, где в памяти расположены инструкции, а где данные (ну, при написании программ вы почти не пользовались памятью данных, а когда пользовались — просто лупили по случайным адресам и все получалось). Вы пользовались всеми регистрами регистрового файла по своему усмотрению, без ограничений. Однако, представьте на секунду, что вы пишете проект на ассемблере вместе с коллегой: вы пишите одни функции, а он другие. Как в таком случае вы будете пользоваться регистрами регистрового файла? Ведь если вы будете пользоваться одними и теми же регистрами, вызов одной функции может испортить данные в другой. Поделите его напополам и будете пользоваться каждый своей половиной? Но что будет, если к проекту присоединится еще один коллега — придется делить регистровый файл уже на три части? Так от него уже ничего не останется. Для разрешения таких ситуаций было разработано [соглашение о вызовах](#соглашение-о-вызовах) (calling convention).
|
||||
|
||||
Таким образом, генерируя ассемблерный код, компилятор не может так же, как это делали вы, использовать все ресурсы без каких-либо ограничений — он должен следовать ограничениям, накладываемым на него соглашением о вызовах, а также ограничениям, связанным с тем, что он ничего не знает о памяти устройства, в котором будет исполняться программа — а потому он не может работать с памятью абы как. Работая с памятью, компилятор следует некоторым правилам, благодаря которым после компиляции компоновщик сможет собрать программу под ваше устройство с помощью специального скрипта.
|
||||
|
||||
@@ -67,11 +67,11 @@
|
||||
| x18–27 | s2–11 |Saved registers |Callee |
|
||||
| x28–31 | t3–6 |Temporaries |Caller |
|
||||
|
||||
_Таблица 1. Ассемблерные мнемоники для целочисленных регистров RISC-V и их назначение в соглашении о вызовах._
|
||||
_Таблица 1. Ассемблерные мнемоники для целочисленных регистров RISC-V и их назначение в соглашении о вызовах[1, стр. 6]._
|
||||
|
||||
Несмотря на то, что указатель на стек помечен как Callee-saved регистр, это не означает, вызываемая функция может записать в него что заблагорассудится, предварительно сохранив его значение на стек. Ведь как вы вернете значение указателя на стек со стека, если в регистре указателя на стек лежит что-то не то?
|
||||
|
||||
Запись `Callee` означает, что к моменту возврата из вызываемой функции, значение Callee-saved регистров должно быть ровно таким же, каким было в момент вызова функций. Для s0-s11 регистров это осуществляется путем сохранения их значений на стек. При этом, перед каждым сохранением на стек, изменяется значение указателя на стек таким образом, чтобы он указывал на сохраняемое значение (обычно он декрементируется). Затем, перед возвратом из функций все сохраненные на стек значения восстанавливаются, попутно изменяя значение указателя на стек противоположным образом (инкрементируют его). Таким образом, несмотря на то, что значение указателя на стек менялось в процессе работы вызываемой функции, к моменту выхода из нее, его значение в итоге останется тем же.
|
||||
Запись `Callee` означает, что к моменту возврата из вызываемой функции, значение Callee-saved регистров должно быть ровно таким же, каким было в момент вызова функций. Для s0-s11 регистров это осуществляется путем сохранения их значений на стек. При этом, перед каждым сохранением на стек, изменяется значение указателя на стек таким образом, чтобы он указывал на сохраняемое значение (обычно он декрементируется). Затем, перед возвратом из функции все сохраненные на стек значения восстанавливаются, попутно изменяя значение указателя на стек противоположным образом (инкрементируют его). Таким образом, несмотря на то, что значение указателя на стек менялось в процессе работы вызываемой функции, к моменту выхода из нее, его значение в итоге останется тем же.
|
||||
|
||||
### Скрипт для компоновки (linker_script.ld)
|
||||
|
||||
@@ -79,56 +79,73 @@ _Таблица 1. Ассемблерные мнемоники для целоч
|
||||
|
||||
В самом простом виде скрипт компоновки состоит из одного раздела: раздела секций, в котором вы и описываете какие части программы куда и в каком порядке необходимо разместить.
|
||||
|
||||
Для удобства этого описания существует вспомогательная переменная: счетчик адресов. В начале скрипта этот счетчик равен нулю. Размещая очередную секцию, этот счетчик увеличивается на размер этой секции. Допустим, у нас есть два файла `fourier.o` и `main.o`, в каждом из которых есть секции `.text` и `.data`. Мы хотим разместить их в памяти следующим образом: сперва разместить секции `.text` обоих файлов, а затем секции `.data`.
|
||||
Для удобства этого описания существует вспомогательная переменная: счетчик адресов. Этот счетчик показывает в какое место в памяти будет размещена очередная секция (если при размещении секции в явном виде не будет указано иного). На момент начала исполнения скрипта этот счетчик равен нулю. Размещая очередную секцию, счетчик увеличивается на размер размещаемой секции. Допустим, у нас есть два файла `startup.o` и `main.o`, в каждом из которых есть секции `.text` и `.data`. Мы хотим разместить их в памяти следующим образом: сперва разместить секции `.text` обоих файлов, а затем секции `.data`.
|
||||
|
||||
В итоге начиная с нулевого адреса будет размещена секция `.text` файла `fourier.o`. Она будет размещена именно там, поскольку счетчик адресов в начале скрипта равен нулю, а очередная секция размещается по адресу, куда указывает счетчик адресов. После этого, счетчик адресов будет увеличен на размер этой секции и секция `.text` файла `main.o` будет размещена сразу же за секцией `.text` файла `fourier.o`. После этого счетчик адресов будет увеличен на размер этой секции. То же самое произойдет и при размещении оставшихся секций.
|
||||
В итоге начиная с нулевого адреса будет размещена секция `.text` файла `startup.o`. Она будет размещена именно там, поскольку счетчик адресов в начале скрипта равен нулю, а очередная секция размещается по адресу, куда указывает счетчик адресов. После этого, счетчик будет увеличен на размер этой секции и секция `.text` файла `main.o` будет размещена сразу же за секцией `.text` файла `startup.o`. После этого счетчик адресов будет увеличен на размер этой секции. То же самое произойдет и при размещении оставшихся секций.
|
||||
|
||||
Кроме того, вы в любой момент можете изменить значение счетчика адресов. Например, у вас две раздельные памяти: память инструкций объемом 512 байт и память данных объемом 1024 байта. Эти памяти находятся в одном адресном пространстве. Диапазон адресов памяти инструкций: `[0:511]`, диапазон памяти данных: `[512:1535]`. При этом общий объем секций `.text` составляет 416 байт. В этом случае, вы можете сперва разместить секции `.text` так же, как было описано в предыдущем примере, а затем, выставив значение на счетчике адресов равное `512`, описываете размещение секций данных. Тогда, между секциями появится разрыв в 96 байт, а данные окажутся в диапазоне адресов, выделенном для памяти данных.
|
||||
Кроме того, вы в любой момент можете изменить значение счетчика адресов. Например, если адресное пространство памяти поделено на две части: под инструкции отводится 512 байт, а под данные 1024 байта. Таким образом, выделенный диапазон адресов для инструкций: `[0:511]`, а для данных: `[512:1535]`. Предположим при этом, что общий объем секций `.text` составляет 416 байт. В этом случае, вы можете сперва разместить секции `.text` так же, как было описано в предыдущем примере, а затем, выставив значение на счетчике адресов равное `512`, описать размещение секций данных. Тогда, между секциями появится разрыв в 96 байт, а данные окажутся в выделенном для них диапазоне адресов.
|
||||
|
||||
Помимо прочего, в скрипте компоновщика необходимо прописать, где будет находиться стек, и какое будет значение у указателя на глобальную область памяти.
|
||||
В нашей процессорной системе гарвардская архитектура. Это значит, что память инструкций и данных у нас независимы друг от друга. Это физически разные устройства, с разными шинами и разным адресным пространством. Однако обе эти памяти имеют общие значения младших адресов: самый младший имеет адрес ноль, следующий адрес 1 и т.д. Таким образом, происходит наложение адресных пространств памяти инструкций и памяти данных. Компоновщику трудно работать в таких условиях: "как я записать что по этому адресу будет размещаться секция данных, когда здесь уже размещена секция инструкций".
|
||||
|
||||
Есть два механизма для решения этого вопроса. Первый: компоновать секции инструкций и данных по отдельности. В этом случае будет два отдельных скрипта компоновщика. Однако, компоновка секций инструкций зависит от компоновки секций данных (в частности, от того по каким адресам будут размещены стек и .bss-секция, а также указатель на глобальную область данных), поскольку в часть инструкций необходимо прописать конкретные адреса. В этом случае, придется делать промежуточные операции в виде экспорта глобальных символов в отдельный объектный файл, который будет использован при компоновке секции инструкций, что кажется некоторым переусложнением.
|
||||
|
||||
Вместо этого, будет использован другой подход, механизм виртуальных адресов (**Virtual Memory Address**, **VMA**) и адресов загрузки (**Load Memory Address**, **LMA**).
|
||||
|
||||
- VMA — это адрес, по которому секция будет доступна во время выполнения программы. По этому адресу процессор будет обращаться к секции.
|
||||
- LMA — это адрес, по которому секция будет загружена в память при старте программы.
|
||||
|
||||
Обычно LMA совпадает с VMA. Однако в некоторых случаях они могут быть и различны (например, изначально секция данных записывается в ROM, а перед выполнением программы, копируется из ROM в RAM). В этом случае, LMA — это адрес секции в ROM, а VMA — адрес секции в RAM.
|
||||
|
||||
Таким образом, мы можем сделать общие VMA (процессор, обращаясь к секциям инструкций и данных будет использовать пересекающееся адресное пространство), а конфликт размещения секций компоновщиком разрешить задав какой-нибудь заведомо большой VMA для секции данных. В последствии, мы просто проигнорируем этот адрес, проинициализировав память данных начиная с нуля.
|
||||
|
||||
Помимо прочего, в скрипте компоновщика необходимо прописать, каков [порядок следования байт](https://en.wikipedia.org/wiki/Endianness), где будет находиться стек, и какое будет значение у указателя на глобальную область памяти.
|
||||
|
||||
Все это с подробными комментариями описано в файле `linker_script.ld`.
|
||||
|
||||
```ld
|
||||
/* -----------------------------------------------------------------------------
|
||||
* Project Name : Architectures of Processor Systems (APS) lab work
|
||||
* Organization : National Research University of Electronic Technology (MIET)
|
||||
* Department : Institute of Microdevices and Control Systems
|
||||
* Author(s) : Andrei Solodovnikov
|
||||
* Email(s) : hepoh@org.miet.ru
|
||||
|
||||
See https://github.com/MPSU/APS/blob/master/LICENSE file for licensing details.
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
OUTPUT_FORMAT("elf32-littleriscv") /* Указываем порядок следования байт */
|
||||
|
||||
ENTRY(_start) /* мы сообщаем компоновщику, что первая
|
||||
исполняемая процессором инструкция
|
||||
находится у метки "start"
|
||||
*/
|
||||
|
||||
_text_size = 0x4000; /* Размер памяти инстр.: 16KiB */
|
||||
_data_base_addr = _text_size; /* Стартовый адрес секции данных */
|
||||
_data_size = 0x4000; /* Размер памяти данных: 16KiB */
|
||||
|
||||
_data_end = _data_base_addr + _data_size;
|
||||
|
||||
_trap_stack_size = 2560; /* Размер стека обработчика перехватов.
|
||||
Данный размер позволяет выполнить
|
||||
до 32 вложенных вызовов при обработке
|
||||
перехватов.
|
||||
*/
|
||||
|
||||
_stack_size = 1280; /* Размер программного стека.
|
||||
Данный размер позволяет выполнить
|
||||
до 16 вложенных вызовов.
|
||||
находится у метки "_start"
|
||||
*/
|
||||
|
||||
/*
|
||||
В данном разделе указывается структура памяти:
|
||||
Сперва идет регион "rom", являющийся памятью с исполняемым кодом
|
||||
(об этом говорит аттрибут 'x'). Этот регион начинается
|
||||
с адреса 0x00000000 и занимает _text_size байт.
|
||||
Далее идет регион "ram", начинающийся с адреса _data_base_addr и занимающий
|
||||
_data_size байт. Этот регион является памятью, противоположной региону "rom"
|
||||
(в том смысле, что это не память с исполняемым кодом).
|
||||
Сперва идет регион "instr_mem", являющийся памятью с исполняемым кодом
|
||||
(об этом говорит аттрибут 'x'). Этот регион начинается
|
||||
с адреса 0x00000000 и занимает 1024 байта.
|
||||
Далее идет регион "data_mem", начинающийся с адреса 0x00000000 и занимающий
|
||||
2048 байт. Этот регион является памятью, противоположной региону "instr_mem"
|
||||
(в том смысле, что это не память с исполняемым кодом).
|
||||
*/
|
||||
MEMORY
|
||||
{
|
||||
rom (x) : ORIGIN = 0x00000000, LENGTH = _text_size
|
||||
ram (!x) : ORIGIN = _data_base_addr, LENGTH = _data_size
|
||||
instr_mem (x) : ORIGIN = 0x00000000, LENGTH = 1K
|
||||
data_mem (!x) : ORIGIN = 0x00000000, LENGTH = 2K
|
||||
}
|
||||
|
||||
_trap_stack_size = 640; /* Размер стека обработчика перехватов.
|
||||
Данный размер позволяет выполнить
|
||||
до 8 вложенных вызовов при обработке
|
||||
перехватов.
|
||||
*/
|
||||
|
||||
_stack_size = 640; /* Размер программного стека.
|
||||
Данный размер позволяет выполнить
|
||||
до 8 вложенных вызовов.
|
||||
*/
|
||||
|
||||
/*
|
||||
В данном разделе описывается размещение программы в памяти.
|
||||
@@ -141,7 +158,7 @@ MEMORY
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
PROVIDE( _start = 0x00000000 ); /* Позиция start в памяти*/
|
||||
|
||||
/*
|
||||
В скриптах компоновщика есть внутренняя переменная, записываемая как '.'
|
||||
Эта переменная называется "счетчиком адресов". Она хранит текущий адрес в
|
||||
@@ -163,32 +180,20 @@ SECTIONS
|
||||
секций, начинающихся на .text во всех переданных компоновщику двоичных
|
||||
файлах.
|
||||
Дополнительно мы указываем, что данная секция должна быть размещена в
|
||||
регионе "rom".
|
||||
регионе "instr_mem".
|
||||
*/
|
||||
.text : {*(.boot) *(.text*)} >rom
|
||||
.text : {
|
||||
PROVIDE(_start = .);
|
||||
*(.boot)
|
||||
*(.text*)
|
||||
} > instr_mem
|
||||
|
||||
|
||||
/*
|
||||
Поскольку мы не знаем суммарного размера получившейся секции, мы проверяем
|
||||
что не вышли за границы памяти инструкций и переносим счетчик адресов за
|
||||
пределы памяти инструкций в область памяти данных.
|
||||
Дополнительно мы указываем, что данная секция должна быть размещена в
|
||||
регионе "ram".
|
||||
*/
|
||||
ASSERT(. < _text_size, ".text section exceeds instruction memory size")
|
||||
. = _data_base_addr;
|
||||
|
||||
/*
|
||||
Следующая команда сообщает, что начиная с адреса, которому в данных момент
|
||||
равен счетчик адресов (_data_base_addr) будет находиться секция .data
|
||||
итогового файла, которая состоит из секций всех секций, начинающихся
|
||||
на .data во всех переданных компоновщику двоичных файлах.
|
||||
Дополнительно мы указываем, что данная секция должна быть размещена в
|
||||
регионе "ram".
|
||||
*/
|
||||
.data : {*(.*data*)} >ram
|
||||
|
||||
/*
|
||||
В скобках после оператора AT указывается Load Memory Address (LMA). Чтобы
|
||||
адреса памяти инструкций и памяти данных при компоновке не пересекались, мы
|
||||
будем использовать заведомо несуществующий LMA, который в последствии будем
|
||||
игнорировать.
|
||||
.data : AT (0x80000000) {
|
||||
/*
|
||||
Общепринято присваивать GP значение равное началу секции данных, смещенное
|
||||
на 2048 байт вперед.
|
||||
Благодаря относительной адресации со смещением в 12 бит, можно адресоваться
|
||||
@@ -198,8 +203,10 @@ SECTIONS
|
||||
уже хранит базовый адрес и нужно только смещение).
|
||||
Подробнее:
|
||||
https://groups.google.com/a/groups.riscv.org/g/sw-dev/c/60IdaZj27dY/m/s1eJMlrUAQAJ
|
||||
*/
|
||||
_gbl_ptr = _data_base_addr + 0x800;
|
||||
*/
|
||||
_gbl_ptr = . + 2048;
|
||||
*(.*data*)
|
||||
} > data_mem
|
||||
|
||||
|
||||
/*
|
||||
@@ -233,11 +240,14 @@ SECTIONS
|
||||
https://en.wikipedia.org/wiki/.bss
|
||||
|
||||
Дополнительно мы указываем, что данная секция должна быть размещена в
|
||||
регионе "ram".
|
||||
регионе "data_mem".
|
||||
*/
|
||||
_bss_start = .;
|
||||
.bss : {*(.bss*)} >ram
|
||||
_bss_end = .;
|
||||
.bss : {
|
||||
_bss_start = .;
|
||||
*bss*
|
||||
*(COMMON)
|
||||
_bss_end = .;
|
||||
} > data_mem
|
||||
|
||||
|
||||
/*=================================
|
||||
@@ -248,20 +258,17 @@ SECTIONS
|
||||
Поскольку стеков у нас два, в самом низу мы разместим стек прерываний, а
|
||||
над ним программный стек. При этом надо обеспечить защиту программного
|
||||
стека от наложения на него стека прерываний.
|
||||
Однако перед этим, мы должны убедиться, что под программный стек останется
|
||||
хотя бы 1280 байт (ничем не обоснованное число, взятое с потолка).
|
||||
Такое значение обеспечивает до 16 вложенных вызовов (если сохранять только
|
||||
необерегаемые регистры).
|
||||
Однако перед этим, мы должны убедиться, что под оба стека хватит места.
|
||||
=================================
|
||||
*/
|
||||
|
||||
/* Мы хотим гарантировать, что под стек останется как минимум 1280 байт */
|
||||
ASSERT(. < (_data_end - _trap_stack_size - _stack_size),
|
||||
/* Мы хотим гарантировать, что под стек останется место */
|
||||
ASSERT(. < (LENGTH(data_mem) - _trap_stack_size - _stack_size),
|
||||
"Program size is too big")
|
||||
|
||||
/* Перемещаем счетчик адресов над стеком прерываний (чтобы после мы могли
|
||||
использовать его в вызове ALIGN) */
|
||||
. = _data_end - _trap_stack_size;
|
||||
. = LENGTH(data_mem) - _trap_stack_size;
|
||||
|
||||
/*
|
||||
Размещаем указатель программного стека так близко к границе стека
|
||||
@@ -270,13 +277,14 @@ SECTIONS
|
||||
Подробнее:
|
||||
https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf
|
||||
*/
|
||||
_stack_ptr = ALIGN(16) <= _data_end - _trap_stack_size?
|
||||
_stack_ptr = ALIGN(16) <= LENGTH(data_mem) - _trap_stack_size?
|
||||
ALIGN(16) : ALIGN(16) - 16;
|
||||
ASSERT(_stack_ptr <= _data_end - _trap_stack_size, "SP exceed memory size")
|
||||
ASSERT(_stack_ptr <= LENGTH(data_mem) - _trap_stack_size,
|
||||
"SP exceed memory size")
|
||||
|
||||
/* Перемещаем счетчик адресов в конец памяти (чтобы после мы могли
|
||||
использовать его в вызове ALIGN) */
|
||||
. = _data_end;
|
||||
. = LENGTH(data_mem);
|
||||
|
||||
/*
|
||||
Обычно память имеет размер, кратный 16, но на случай, если это не так, мы
|
||||
@@ -284,8 +292,8 @@ SECTIONS
|
||||
конец кратен 16), либо поднимаемся на 16 байт вверх от края памяти,
|
||||
округленного до 16 в сторону большего значения
|
||||
*/
|
||||
_trap_stack_ptr = ALIGN(16) <= _data_end ? ALIGN(16) : ALIGN(16) - 16;
|
||||
ASSERT(_trap_stack_ptr <= _data_end, "ISP exceed memory size")
|
||||
_trap_stack_ptr = ALIGN(16) <= LENGTH(data_mem) ? ALIGN(16) : ALIGN(16) - 16;
|
||||
ASSERT(_trap_stack_ptr <= LENGTH(data_mem), "ISP exceed memory size")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -293,7 +301,7 @@ _Листинг 1. Пример скрипта компоновщика с ко
|
||||
|
||||
### Файл первичных команд при загрузке (startup.S)
|
||||
|
||||
В стартап-файле хранятся инструкции, которые обязательно необходимо выполнить перед началом исполнения любой программы. Это инициализация регистров указателей на стек и глобальную область данных, регистра, хранящего адрес вектора прерываний и т.п.
|
||||
В стартап-файле хранятся инструкции, которые обязательно необходимо выполнить перед началом исполнения любой программы. Это инициализация регистров указателей на стек и глобальную область данных, контрольных регистров системы прерывания и т.п.
|
||||
|
||||
По завершению инициализации, стартап-файл выполняет процедуру передачи управления точке входа в запускаемую программу.
|
||||
|
||||
@@ -450,7 +458,7 @@ _Листинг 2. Пример содержимого файла первичн
|
||||
|
||||
Для того, чтобы запустить моделирование исполнения программы на вашем процессоре, сперва эту программу необходимо скомпилировать и преобразовать в текстовый файл, которым САПР сможет проинициализировать память процессора. Для компиляции программы, вам потребуется особый компилятор, который называется "кросскомпилятор". Он позволяет компилировать исходный код под архитектуру компьютера, отличную от компьютера, на котором ведется компиляция. В нашем случае, вы будете собирать код под архитектуру `RISC-V` на компьютере с архитектурой `x86_64`.
|
||||
|
||||
Компилятор, который подойдет для данной задачи (для запуска в операционной системе Windows) уже установлен в аудиториях. Но если что, вы можете скачать его [отсюда](https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v13.2.0-1/xpack-riscv-none-elf-gcc-13.2.0-1-win32-x64.zip) (обратите внимание, что размер архива составляет ~550 МБ, попытка скачивания этого архива из учебной аудитории может потратить вашу месячную квоту интернет-трафика).
|
||||
Компилятор, который подойдет для данной задачи должен быть установлен в учебной аудитории. Но если что, вы можете скачать его [отсюда](https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v13.2.0-1/xpack-riscv-none-elf-gcc-13.2.0-1-win32-x64.zip) (обратите внимание, что размер архива составляет ~550 МБ, попытка скачивания этого архива из учебной аудитории может потратить вашу месячную квоту интернет-трафика).
|
||||
|
||||
### Компиляция объектных файлов
|
||||
|
||||
@@ -467,7 +475,7 @@ _Листинг 2. Пример содержимого файла первичн
|
||||
|
||||
Есть очень [хорошее видео](https://youtu.be/29iNHEhHmd0?t=141), описывающее состав тулчейнов, именование исполняемых файлов компиляторов, как формируются ключи архитектуры и двоичного интерфейса приложений.
|
||||
|
||||
С учетом названия исполняемого файла скачанного вами компилятора (при условии, что папку из архива вы переименовали в `riscv_cc` и скопировали в корень диска `C:`, а команду запускаете из терминала `git bash`), командой для компиляции файла [`startup.S`](startup.S) может быть:
|
||||
С учетом названия исполняемого файла скачанного вами компилятора (при условии, что папку из архива вы переименовали в `riscv_cc` и скопировали в корень диска `C:`, а команду запускаете из оболочки `git bash`), командой для компиляции файла [`startup.S`](startup.S) может быть:
|
||||
|
||||
```bash
|
||||
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 startup.S -o startup.o
|
||||
@@ -541,7 +549,7 @@ FF5FF06F 04400293 FFF00313 30529073
|
||||
|
||||
Обратите внимание что байты не просто склеились в четверки, изменился так же и порядок следования байт. Это важно, т.к. в память данные должны лечь именно в таком (обновленном) порядке байт (см. первую строчку скрипта компоновщика). Когда-то `objcopy` содержал [баг](https://sourceware.org/bugzilla/show_bug.cgi?id=25202), из-за которого порядок следования байт не менялся. В каких-то версиях тулчейна (отличных от представленного в данной лабораторной работе) вы все еще можете столкнуться с подобным поведением.
|
||||
|
||||
Вернемся к первой строке: `@00000000`. Как уже говорилось, число, начинающееся с символа `@` говорит САПР, что с этого момента инициализация идет начиная с ячейки памяти, номер которой совпадает с этим числом. Когда вы будете экспортировать секции данных, первой строкой будет: `@00001000`. Так произойдет, поскольку в скрипте компоновщика сказано, что секция данных идет с `0x00004000` адреса. Это было сделано, чтобы не произошло наложения адресов памяти инструкций и памяти данных. **Чтобы система работала корректно, эту строчку необходимо удалить.**
|
||||
Вернемся к первой строке: `@00000000`. Как уже говорилось, число, начинающееся с символа `@` говорит САПР, что с этого момента инициализация идет начиная с ячейки памяти, номер которой совпадает с этим числом. Когда вы будете экспортировать секции данных, первой строкой будет: `@20000000`. Так произойдет, поскольку в скрипте компоновщика сказано, указано инициализировать память данных с `0x80000000` адреса (значение которого было поделено на 4, чтобы получить номер 32-битной ячейки памяти). Это было сделано, чтобы не произошло наложения адресов памяти инструкций и памяти данных (см раздел [скрипт для компоновки](#скрипт-для-компоновки-linker_scriptld)). **Чтобы система работала корректно, эту строчку необходимо удалить.**
|
||||
|
||||
### Дизассемблирование
|
||||
|
||||
@@ -632,7 +640,7 @@ _Листинг 3. Пример дизасемблированного файл
|
||||
|
||||
Вам необходимо написать программу для вашего [индивидуального задания](../04.%20Primitive%20programmable%20device/Индивидуальное%20задание#индивидуальные-задания) к 4-ой лабораторной работе на языке C или C++ (в зависимости от выбранного языка необходимо использовать соответствующий компилятор: gcc для C, g++ для C++).
|
||||
|
||||
Для того чтобы ваша программа собралась, необходимо описать две функции: `main` и `int_handler`. Аргументы и возвращаемые значения могут быть любыми, но использоваться они не смогут. Функция `main` будет вызвана в начале работы программы (после исполнения подготовительной части startup-файла), функция `int_handler` будет вызываться автоматически каждый раз, когда ваш контроллер устройства ввода будет генерировать запрос прерывания (если процессор закончил обрабатывать предыдущий запрос).
|
||||
Для того чтобы ваша программа собралась, необходимо описать две функции: `main` и `int_handler`. Аргументы и возвращаемые значения могут быть любыми, но использоваться они не смогут. Функция `main` будет вызвана в начале работы программы (после исполнения .boot-секции startup-файла), функция `int_handler` будет вызываться автоматически каждый раз, когда ваш контроллер устройства ввода будет генерировать запрос прерывания (если процессор закончил обрабатывать предыдущий запрос).
|
||||
|
||||
Таким образом, минимальный алгоритм работы заключается в том, чтобы считать по прерыванию данные от устройства ввода (в индивидуальном задании обозначалось как sw_i), выполнить обработку из вашего варианта, и записать результат в устройство вывода. При этом необходимо помнить о следующем:
|
||||
|
||||
@@ -647,26 +655,32 @@ _Листинг 3. Пример дизасемблированного файл
|
||||
|
||||
При написании программы помните, что в C++ сильно ограничена арифметика указателей, поэтому при присваивании указателю целочисленного значения адреса, необходимо использовать оператор `reinterpret_cast`.
|
||||
|
||||
Для того, чтобы уменьшить ваше взаимодействие с черной магией указателей, вам представлен файл [platform.h](platform.h), в котором объявлены указатели структуры, отвечающие за отображение полей на физические адреса периферийных устройств. Вам нужно лишь воспользоваться указателем на ваше периферийное устройство.
|
||||
Для того, чтобы уменьшить ваше взаимодействие с черной магией указателей, вам представлен файл [platform.h](platform.h), в котором объявлены указатели на структуры, отвечающие за отображение полей на физические адреса периферийных устройств. Вам нужно лишь воспользоваться указателем на ваше периферийное устройство.
|
||||
|
||||
Если вашим устройством вывода является VGA-контроллер, то вам необходимо использовать экземпляр структуры, а не указатель на нее. Внутри данной структуры представлены указатели на байты: `char_map`, `color_map`, `tiff_map`. Как вы знаете, указатель может использоваться в качестве имени массива, а значит вы можете обращаться к нужному вам байту в соответствующей области памяти VGA-контроллера как к элементу массива. Например, для того, чтобы записать символ в шестое знакоместо второй строки, вам необходимо будет обратиться к `char_map[2*80+6]` (2*80 — индекс начала второй строки).
|
||||
|
||||
Пример взаимодействия с периферийным устройством через структуру **ВЫМЫШЛЕННОГО** периферийного устройства. Данная программа является лишь примером, иллюстрирующим взаимодействие с периферией через представленные указатели на структуры. Вам необходимо разобраться в том, как осуществляется работа с вымышленным устройством, а затем написать собственную программу, работающую по логике вашего индивидуального задания, которая взаимодействует с вашим реальным устройством.
|
||||
|
||||
```C++
|
||||
/*
|
||||
Не надо копировать и использовать в качестве основы вашей программы этот файл.
|
||||
Не надо копировать и использовать в качестве основы вашей программы этот код.
|
||||
Он для этого не подходит. В вашей процессорной системе нет никаких коллайдеров
|
||||
DEADLY_SERIOUS-событий и аварийных выключателей.
|
||||
Просто разберитесь в операторе `->` и использовании указателей в качестве имени
|
||||
массива и напишите собственную программу.
|
||||
Просто разберитесь в операторах `->`, ".", использовании указателей в качестве
|
||||
имени массива и напишите собственную программу.
|
||||
*/
|
||||
#include "platform.h"
|
||||
|
||||
/*
|
||||
Создаем заголовочном файле "platform.h" объявлен collider_ptr — указатель на структуру SUPER_COLLIDER_HANDLE.
|
||||
Доступ к полям этой структуры можно осуществлять через оператор "->".
|
||||
Также в этом файле объявлен указатель collider_mem, который указывает на
|
||||
некоторую память этого периферийного устройства. Данный указатель можно
|
||||
использовать в качестве имени массива.
|
||||
Создаем заголовочном файле "platform.h" объявлены collider_ptr — указатель на
|
||||
структуру SUPER_COLLIDER_HANDLE и collider_obj — экземпляр аналогичной
|
||||
структуры.
|
||||
Доступ к полям этой структуры через указатель можно осуществлять посредством
|
||||
оператора "->". Доступ к полям через экземпляр осуществляется с помощью
|
||||
оператора ".".
|
||||
Среди прочих полей, структура содержит указатель collider_mem, который
|
||||
указывает на некоторую память этого периферийного устройства. Данный указатель
|
||||
можно использовать в качестве имени массива.
|
||||
*/
|
||||
|
||||
int main(int argc, char** argv)
|
||||
@@ -677,8 +691,8 @@ int main(int argc, char** argv)
|
||||
|
||||
// После чего запускаем коллайдер,
|
||||
collider_ptr->start = 1; // записав 1 в контрольный регистр start
|
||||
collider_mem[0] += collider_mem[1]; // Пример взаимодействия с памятью,
|
||||
// Используя объявленный в platform.h
|
||||
collider_obj.mem[0] = 300; // Пример взаимодействия с памятью,
|
||||
// Используя объявленный в структуре
|
||||
// указатель в качестве имени массива.
|
||||
}
|
||||
}
|
||||
@@ -690,11 +704,12 @@ int main(int argc, char** argv)
|
||||
// при объединении кода с startup.S
|
||||
// Без extern "C", при компиляции C++ кода имя функции в объектном файле будет
|
||||
// немного другим (что-то типа _Z11int_handlerv), из-за чего возникнут проблемы
|
||||
// в компоновке.
|
||||
// в процессе компоновки.
|
||||
extern "C" void int_handler()
|
||||
{
|
||||
// Если от коллайдера приходит прерывание, сразу же проверяем регистр статуса
|
||||
// и если его код равен DEADLY_SERIOUS_EVENT, экстренно останавливаем коллайдер
|
||||
// и если его код равен DEADLY_SERIOUS_EVENT, экстренно останавливаем
|
||||
// коллайдер
|
||||
if(DEADLY_SERIOUS_EVENT == collider_ptr->status)
|
||||
{
|
||||
collider_ptr->emergency_switch = 1;
|
||||
@@ -704,8 +719,6 @@ extern "C" void int_handler()
|
||||
|
||||
_Листинг 4. Пример кода на C++, взаимодействующего с выдуманным периферийным устройством через указатели на структуру и массив, объявленные в platform.h._
|
||||
|
||||
Если одним из ваших периферийных устройств был VGA-контроллер, то вы можете использовать не указатель на структуру, а объявленные в том же файле указатели на байты: `char_map`, `color_map`, `tiff_map`. Как вы знаете, указатель может использоваться в качестве имени массива, а значит вы можете обращаться к нужному вам байту в соответствующей области памяти VGA-контроллера как к элементу массива. Например, для того, чтобы записать символ в шестое знакоместо второй строки, вам необходимо будет обратиться к `char_map[2*80+6]` (2*80 — индекс начала второй строки).
|
||||
|
||||
---
|
||||
|
||||
### Порядок выполнения задания
|
||||
@@ -713,11 +726,16 @@ _Листинг 4. Пример кода на C++, взаимодействую
|
||||
1. Внимательно изучить разделы теории и практики.
|
||||
2. Разобрать принцип взаимодействия с контрольными и статусными регистрами периферийного устройства на примере _Листинга 4_.
|
||||
3. Написать программу для своего индивидуального задания и набора периферийных устройств на языке C или C++. В случае написания кода на C++ помните о необходимости добавления `extern "C"` перед определением функции `int_handler`.
|
||||
1. В описываемой программе обязательно должны присутствовать функции `main` и `int_handler`, т.к. в стартап-файле описаны вызовы этих функций. При необходимости, вы можете описать необходимые вам вспомогательные функции — ограничений на то что должно быть ровне две этих функции нет.
|
||||
2. Функция `main` может быть пустой — по ее завершению в стартап-файле предусмотрен бесконечный цикл, из которого процессор сможет выходить только по прерыванию.
|
||||
3. В функции `int_handler` вам необходимо считать поступившие от устройства ввода входные данные.
|
||||
4. Вам необходимо самостоятельно решить, как вы хотите построить ход работы вашей программы: будет ли ваше индивидуальное задание вычисляться всего лишь 1 раз в функции `main`, данные в которую поступят от функции `int_handler` через глобальные переменные, или же оно будет постоянно пересчитываться при каждом вызове `int_handler`.
|
||||
5. Доступ к регистрам контроля и статуса необходимо осуществлять посредством указателей на структуры, объявленные в файле [platform.h](./platform.h). В случае VGA-контроллера, доступ к областям памяти осуществляется через экземпляр структуры (а не указатель на нее), содержащий имена массивов `char_map`, `color_map` и `tiff_map`.
|
||||
4. [Скомпилировать](#практика) программу и [стартап-файл](startup.S) в объектные файлы.
|
||||
5. Скомпоновать объектные файлы исполняемый файл, передав компоновщику соответствующий [скрипт](linker_script.ld).
|
||||
6. Экспортировать из объектного файла секции `.text` и `.data` в текстовые файлы `init_instr.mem`, `init_data.mem`. Если вы не создавали инициализированных статических массивов или глобальных переменных, то файл `init_data.mem` может быть оказаться пустым.
|
||||
1. Если файл `init_data.mem` не пустой, необходимо проинициализировать память в модуле `ext_mem` c помощью системной функции `$readmemh` как это было сделано для памяти инструкций.
|
||||
2. Перед этим из файла `init_data.mem` необходимо удалить первую строку (вида `@00001000`), указывающую начальный адрес инициализации.
|
||||
2. Перед этим из файла `init_data.mem` необходимо удалить первую строку (вида `@20000000`), указывающую начальный адрес инициализации.
|
||||
7. Добавить получившиеся текстовые файлы в проект Vivado.
|
||||
8. Запустить моделирование исполнения программы вашим процессором. Для отладки во время моделирования будет удобно использовать дизасемблерный файл, ориентируясь на сигналы адреса и данных шины инструкций.
|
||||
9. Проверить корректное исполнение программы процессором в ПЛИС.
|
||||
|
@@ -8,53 +8,39 @@
|
||||
See https://github.com/MPSU/APS/blob/master/LICENSE file for licensing details.
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
OUTPUT_FORMAT("elf32-littleriscv") /* Указываем порядок следования байт */
|
||||
|
||||
ENTRY(_start) /* мы сообщаем компоновщику, что первая
|
||||
исполняемая процессором инструкция
|
||||
находится у метки "start"
|
||||
*/
|
||||
|
||||
_text_size = 0x1000; /* Размер памяти инстр.: 4KiB */
|
||||
_data_size = 0x4000; /* Размер памяти данных: 16KiB */
|
||||
|
||||
ASSERT(!(_text_size & (_text_size-1)), /* Проверка что размеры памяти */
|
||||
"Instr mem size is not power of 2") /* являются степенью двойки */
|
||||
ASSERT(!(_data_size & (_data_size-1)),
|
||||
"Data mem size is not power of 2")
|
||||
|
||||
_data_base_addr = _text_size >= _data_size ? /* Стартовый адрес секции данных */
|
||||
_text_size : _data_size; /* указан как больший из размеров*/
|
||||
/* секции инструкций/данных */
|
||||
|
||||
_data_end = _data_base_addr + _data_size;
|
||||
|
||||
_trap_stack_size = 2560; /* Размер стека обработчика перехватов.
|
||||
Данный размер позволяет выполнить
|
||||
до 32 вложенных вызовов при обработке
|
||||
перехватов.
|
||||
*/
|
||||
|
||||
_stack_size = 1280; /* Размер программного стека.
|
||||
Данный размер позволяет выполнить
|
||||
до 16 вложенных вызовов.
|
||||
находится у метки "_start"
|
||||
*/
|
||||
|
||||
/*
|
||||
В данном разделе указывается структура памяти:
|
||||
Сперва идет регион "rom", являющийся памятью с исполняемым кодом
|
||||
(об этом говорит аттрибут 'x'). Этот регион начинается
|
||||
с адреса 0x00000000 и занимает _text_size байт.
|
||||
Далее идет регион "ram", начинающийся с адреса _data_base_addr и занимающий
|
||||
_data_size байт. Этот регион является памятью, противоположной региону "rom"
|
||||
(в том смысле, что это не память с исполняемым кодом).
|
||||
Сперва идет регион "instr_mem", являющийся памятью с исполняемым кодом
|
||||
(об этом говорит аттрибут 'x'). Этот регион начинается
|
||||
с адреса 0x00000000 и занимает 1024 байта.
|
||||
Далее идет регион "data_mem", начинающийся с адреса 0x00000000 и занимающий
|
||||
2048 байт. Этот регион является памятью, противоположной региону "instr_mem"
|
||||
(в том смысле, что это не память с исполняемым кодом).
|
||||
*/
|
||||
MEMORY
|
||||
{
|
||||
rom (x) : ORIGIN = 0x00000000, LENGTH = _text_size
|
||||
ram (!x) : ORIGIN = _data_base_addr, LENGTH = _data_size
|
||||
instr_mem (x) : ORIGIN = 0x00000000, LENGTH = 1K
|
||||
data_mem (!x) : ORIGIN = 0x00000000, LENGTH = 2K
|
||||
}
|
||||
|
||||
_trap_stack_size = 640; /* Размер стека обработчика перехватов.
|
||||
Данный размер позволяет выполнить
|
||||
до 8 вложенных вызовов при обработке
|
||||
перехватов.
|
||||
*/
|
||||
|
||||
_stack_size = 640; /* Размер программного стека.
|
||||
Данный размер позволяет выполнить
|
||||
до 8 вложенных вызовов.
|
||||
*/
|
||||
|
||||
/*
|
||||
В данном разделе описывается размещение программы в памяти.
|
||||
@@ -67,7 +53,7 @@ MEMORY
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
PROVIDE( _start = 0x00000000 ); /* Позиция start в памяти*/
|
||||
|
||||
/*
|
||||
В скриптах компоновщика есть внутренняя переменная, записываемая как '.'
|
||||
Эта переменная называется "счетчиком адресов". Она хранит текущий адрес в
|
||||
@@ -89,32 +75,20 @@ SECTIONS
|
||||
секций, начинающихся на .text во всех переданных компоновщику двоичных
|
||||
файлах.
|
||||
Дополнительно мы указываем, что данная секция должна быть размещена в
|
||||
регионе "rom".
|
||||
регионе "instr_mem".
|
||||
*/
|
||||
.text : {*(.boot) *(.text*)} >rom
|
||||
.text : {
|
||||
PROVIDE(_start = .);
|
||||
*(.boot)
|
||||
*(.text*)
|
||||
} > instr_mem
|
||||
|
||||
|
||||
/*
|
||||
Поскольку мы не знаем суммарного размера получившейся секции, мы проверяем
|
||||
что не вышли за границы памяти инструкций и переносим счетчик адресов за
|
||||
пределы памяти инструкций в область памяти данных.
|
||||
Дополнительно мы указываем, что данная секция должна быть размещена в
|
||||
регионе "ram".
|
||||
*/
|
||||
ASSERT(. < _text_size, ".text section exceeds instruction memory size")
|
||||
. = _data_base_addr;
|
||||
|
||||
/*
|
||||
Следующая команда сообщает, что начиная с адреса, которому в данных момент
|
||||
равен счетчик адресов (_data_base_addr) будет находиться секция .data
|
||||
итогового файла, которая состоит из секций всех секций, начинающихся
|
||||
на .data во всех переданных компоновщику двоичных файлах.
|
||||
Дополнительно мы указываем, что данная секция должна быть размещена в
|
||||
регионе "ram".
|
||||
*/
|
||||
.data : {*(.*data*)} >ram
|
||||
|
||||
/*
|
||||
В скобках после оператора AT указывается Load Memory Address (LMA). Чтобы
|
||||
адреса памяти инструкций и памяти данных при компоновке не пересекались, мы
|
||||
будем использовать заведомо несуществующий LMA, который в последствии будем
|
||||
игнорировать.
|
||||
.data : AT (0x80000000) {
|
||||
/*
|
||||
Общепринято присваивать GP значение равное началу секции данных, смещенное
|
||||
на 2048 байт вперед.
|
||||
Благодаря относительной адресации со смещением в 12 бит, можно адресоваться
|
||||
@@ -124,13 +98,15 @@ SECTIONS
|
||||
уже хранит базовый адрес и нужно только смещение).
|
||||
Подробнее:
|
||||
https://groups.google.com/a/groups.riscv.org/g/sw-dev/c/60IdaZj27dY/m/s1eJMlrUAQAJ
|
||||
*/
|
||||
_gbl_ptr = _data_base_addr + 0x800;
|
||||
*/
|
||||
_gbl_ptr = . + 2048;
|
||||
*(.*data*)
|
||||
} > data_mem
|
||||
|
||||
|
||||
/*
|
||||
Поскольку мы не знаем суммарный размер всех используемых секций данных,
|
||||
перед размещением других секций, необходимо выравнять счетчик адресов по
|
||||
перед размещением других секций, необходимо выровнять счетчик адресов по
|
||||
4х-байтной границе.
|
||||
*/
|
||||
. = ALIGN(4);
|
||||
@@ -159,11 +135,14 @@ SECTIONS
|
||||
https://en.wikipedia.org/wiki/.bss
|
||||
|
||||
Дополнительно мы указываем, что данная секция должна быть размещена в
|
||||
регионе "ram".
|
||||
регионе "data_mem".
|
||||
*/
|
||||
_bss_start = .;
|
||||
.bss : {*(.*bss*)} >ram
|
||||
_bss_end = .;
|
||||
.bss : {
|
||||
_bss_start = .;
|
||||
*bss*
|
||||
*(COMMON)
|
||||
_bss_end = .;
|
||||
} > data_mem
|
||||
|
||||
|
||||
/*=================================
|
||||
@@ -174,20 +153,17 @@ SECTIONS
|
||||
Поскольку стеков у нас два, в самом низу мы разместим стек прерываний, а
|
||||
над ним программный стек. При этом надо обеспечить защиту программного
|
||||
стека от наложения на него стека прерываний.
|
||||
Однако перед этим, мы должны убедиться, что под программный стек останется
|
||||
хотя бы 1280 байт (ничем не обоснованное число, взятое с потолка).
|
||||
Такое значение обеспечивает до 16 вложенных вызовов (если сохранять только
|
||||
необерегаемые регистры).
|
||||
Однако перед этим, мы должны убедиться, что под оба стека хватит места.
|
||||
=================================
|
||||
*/
|
||||
|
||||
/* Мы хотим гарантировать, что под стек останется как минимум 1280 байт */
|
||||
ASSERT(. < (_data_end - _trap_stack_size - _stack_size),
|
||||
/* Мы хотим гарантировать, что под стек останется место */
|
||||
ASSERT(. < (LENGTH(data_mem) - _trap_stack_size - _stack_size),
|
||||
"Program size is too big")
|
||||
|
||||
/* Перемещаем счетчик адресов над стеком прерываний (чтобы после мы могли
|
||||
использовать его в вызове ALIGN) */
|
||||
. = _data_end - _trap_stack_size;
|
||||
. = LENGTH(data_mem) - _trap_stack_size;
|
||||
|
||||
/*
|
||||
Размещаем указатель программного стека так близко к границе стека
|
||||
@@ -196,13 +172,14 @@ SECTIONS
|
||||
Подробнее:
|
||||
https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf
|
||||
*/
|
||||
_stack_ptr = ALIGN(16) <= _data_end - _trap_stack_size?
|
||||
_stack_ptr = ALIGN(16) <= LENGTH(data_mem) - _trap_stack_size?
|
||||
ALIGN(16) : ALIGN(16) - 16;
|
||||
ASSERT(_stack_ptr <= _data_end - _trap_stack_size, "SP exceed memory size")
|
||||
ASSERT(_stack_ptr <= LENGTH(data_mem) - _trap_stack_size,
|
||||
"SP exceed memory size")
|
||||
|
||||
/* Перемещаем счетчик адресов в конец памяти (чтобы после мы могли
|
||||
использовать его в вызове ALIGN) */
|
||||
. = _data_end;
|
||||
. = LENGTH(data_mem);
|
||||
|
||||
/*
|
||||
Обычно память имеет размер, кратный 16, но на случай, если это не так, мы
|
||||
@@ -210,6 +187,6 @@ SECTIONS
|
||||
конец кратен 16), либо поднимаемся на 16 байт вверх от края памяти,
|
||||
округленного до 16 в сторону большего значения
|
||||
*/
|
||||
_trap_stack_ptr = ALIGN(16) <= _data_end ? ALIGN(16) : ALIGN(16) - 16;
|
||||
ASSERT(_trap_stack_ptr <= _data_end, "ISP exceed memory size")
|
||||
_trap_stack_ptr = ALIGN(16) <= LENGTH(data_mem) ? ALIGN(16) : ALIGN(16) - 16;
|
||||
ASSERT(_trap_stack_ptr <= LENGTH(data_mem), "ISP exceed memory size")
|
||||
}
|
||||
|
@@ -114,10 +114,11 @@ struct TIMER_HANDLE *const timer_ptr = CAST(struct TIMER_HANDLE *const, 0x080000
|
||||
|
||||
struct SUPER_COLLIDER_HANDLE
|
||||
{
|
||||
volatile const uint32_t ready;
|
||||
volatile uint32_t start;
|
||||
volatile const uint32_t status;
|
||||
volatile uint32_t emergency_switch;
|
||||
volatile const uint32_t ready;
|
||||
volatile uint32_t start;
|
||||
volatile const uint32_t status;
|
||||
volatile uint32_t emergency_switch;
|
||||
volatile uint8_t *const mem;
|
||||
};
|
||||
struct SUPER_COLLIDER_HANDLE *const collider_ptr = CAST(struct SUPER_COLLIDER_HANDLE *const, 0xFF000000);
|
||||
volatile uint8_t *const collider_mem = CAST(uint8_t *const, 0xFF000100);
|
||||
struct SUPER_COLLIDER_HANDLE collider_obj = {0, 0, 0, 0, CAST(uint8_t *const, 0xFF000100)};
|
||||
|
Reference in New Issue
Block a user