Добро пожаловать на очередной шабаш любителей испортить себе жизнь странным хобби! Репортаж с предыдущей вечеринки вы можете найти по ссылке. На ней мы практически «с нуля» создали модель начального уровня встраиваемого контроллера на базе RISC-процессора. Сегодня мы с вами будем добавлять ром ROM и попробуем обзавестись сравнительно несложной защитой памяти. И правда, что за встраиваемая система без ПЗУ? Прежде чем мы это сделаем, неплохо бы набросать некоторые детали конечной архитектуры нашей системы. Почему сейчас? Потому что ROM нужно будет разместить по каким-то адресам, как-то нужно будет управлять логикой защиты памяти, оставить что-нибудь «на вырост» и при этом где-то должно быть ОЗУ. Впрочем, описание будет небольшим ;-)
![](https://habrastorage.org/getpro/habr/upload_files/9fc/1c8/292/9fc1c829222805e76bf7ef475bec77be.png)
Сначала был план, так повелел Джордан
Роберт Хайнлайн «Пасынки Вселенной»
План
Набросаем, хотя бы минимально, архитектуру нашей системы. В частности, карту памяти. Чтобы не усложнять, воспользуемся стандартным MMIO (Memory Mapped Input Output) подходом, при котором регистры и другие ресурсы устройств (например FIFO, SRAM) доступны по адресам обычной памяти. Скажем, за это будут отвечать первые 64кб. Начиная с 64кб разместим ПЗУ. Размер ПЗУ установим 256 килобайт. Далее разместим ОЗУ размером 4Мб – его постоянно не хватает, и так его будет проще наращивать. Итого:
![](https://habrastorage.org/getpro/habr/upload_files/4de/b6b/d52/4deb6bd52df4d00d88b13daeb8a9db70.png)
Добавляем RoM
Вернемся к модели. Извлечем файл компоненты myrisc_comp.py и посмотрим на реализацию подключения памяти (RAM):
...
mem = self.add_pre_obj('ram', 'ram')
mem_image = self.add_pre_obj('mem_image', 'image')
mem_image.size = self.mem_size.val
mem.image = mem_image
phys_mem.map = ...
Добавим ROM и оставим MMIO-окно:
DEF_RAM_SIZE = 0x400000
DEF_MMIO_SIZE = 0x10000
DEF_ROM_SIZE = 0x40000
...
rom = self.add_pre_obj('rom', 'rom')
rom_image = self.add_pre_obj('rom_image', 'image')
rom_image.size = DEF_ROM_SIZE
rom.image = rom_image
phys_mem.map = [[DEF_MMIO_SIZE, rom, 0, 0, DEF_ROM_SIZE],
[DEF_MMIO_SIZE+DEF_ROM_SIZE, mem, 0, 0, self.mem_size.val]
...
Обратите внимание: мы разместили ROM, начиная с адреса 0x10000, оставив, таким образом, диапазон адресов 0x0...0xFFFF для регистров ввода-вывода.
Скомпилируем и посмотрим что у нас получилось. Запустим Simics и загрузим модель используя подготовленный ранее скрипт:
simics> run-command-file "%simics%/targets/myrisc.simics"
Status of controller [class RISC_controller]
============================================
Setup:
Top component : controller
Instantiated : True
System Info :
Attributes:
freq_mhz : 40
mem_size : 4194304
Connections:
Попробуем записать и прочитать по адресам RAM и ROM:
simics> output-radix 16
simics> controller.phys_mem.write(0x10002, 0xFFFF)
simics> controller.phys_mem.read(0x10002)
0x0 (BE)
simics> controller.phys_mem.write(0x50002, 0xFFFF)
simics> controller.phys_mem.read(0x50002)
0xffff (BE)
ROM ведет себя ожидаемо - попытки записи просто игнорируются. Из этого следует, что по меньшей мере, функционально все запрограммировано правильно.
А как же узнать по каким адресам что расположено? Вот так — объект phys_mem представляет собой адресное пространство и у него есть свойство map, которое мы настраивали выше. Что и как «намаплено» можно посмотреть одноименной командой этого объекта:
simics> controller.phys_mem.map
┌───────┬──────────────┬──┬──────┬────────┬──────┬────┬─────┬────┐
│ Base│Object │Fn│Offset│ Length│Target│Prio│Align│Swap│
├───────┼──────────────┼──┼──────┼────────┼──────┼────┼─────┼────┤
│0x10000│controller.rom│ │ 0x0│ 0x40000│ │ 0│ │ │
│0x50000│controller.ram│ │ 0x0│0x400000│ │ 0│ │ │
└───────┴──────────────┴──┴──────┴────────┴──────┴────┴─────┴────┘
Но отладка встраиваемой системы предполагает регулярную перезапись firmware, как часть процесса разработки. А как нам записать в наш ROM что-нибудь полезное? В Simics есть замечательная возможность - без затей, прямо «подгрузить» двоичный файл по заданному адресу памяти. Я приведу изменения в файле myrisc_comp.py, а полностью файлы проекта можно посмотреть на Github-е:
DEF_RAM_SIZE = 0x400000
DEF_MMIO_SIZE = 0x10000
DEF_ROM_SIZE = 0x40000
class RISC_controller(StandardConnectorComponent):
"""Base class for RISC controller."""
...
class firmware(SimpleConfigAttribute('', 's')):
"""Controller's firmware file to use."""
def lookup(self):
if self.val:
lookup = simics.SIM_lookup_file(self.val)
if not lookup:
print('firmware file %s is not found' % self.val)
return ''
return lookup
return self.val
...
class component(StandardComponent.component):
def pre_instantiate(self):
return self._up.pre_instantiate_controller()
def post_instantiate(self):
self._up.post_instantiate_controller()
def pre_instantiate_controller(self):
return True
def post_instantiate_controller(self):
self.load_firmware();
...
def add_objects(self):
cpu_core = self.add_pre_obj('cpu_core', 'sample-risc-core')
cpu = self.add_pre_obj('cpu', 'sample-risc')
cpu.freq_mhz = self.freq_mhz.val
cpu.current_risc_core = cpu_core
cpu_core.sample_risc = cpu
phys_mem = self.add_pre_obj('phys_mem', 'memory-space')
phys_mem.map = []
cpu_core.physical_memory_space = phys_mem
mem = self.add_pre_obj('ram', 'ram')
mem_image = self.add_pre_obj('mem_image', 'image')
mem_image.size = self.mem_size.val
mem.image = mem_image
phys_mem.map += [[DEF_MMIO_SIZE+DEF_ROM_SIZE, mem, 0, 0, self.mem_size.val]]
# Firmware
rom = self.add_pre_obj('rom', 'rom')
rom_image = self.add_pre_obj('rom_image', 'image')
rom_image.size = DEF_ROM_SIZE
rom.image = rom_image
phys_mem.map += [[DEF_MMIO_SIZE, rom, 0, 0, DEF_ROM_SIZE]]
def load_firmware(self):
if self.firmware.lookup():
# Load the firmware into the ROM area
simics.SIM_load_file(self.get_slot('phys_mem'), self.firmware.val,
DEF_MMIO_SIZE, DEF_ROM_SIZE, True)
...
Что мы видим? При инстанциации объекта контроллера в память по адресу DEF_MMIO_SIZE (код метода load_firmware) записывается содержимое файла, передаваемое в модель через атрибут firmware. Чтобы не перегружать код примера, я оставил «за бортом» проверку размера файла (если он будет больше, то «залезет» в адреса ОЗУ). Кроме того, хотя мы в настоящем примере загружаем двоичный файл, никто не мешает нам читать текстовый шестнадцатеричный-дамп, srec-файл и т. д., да хоть настоящий ROM можно считывать — Python-с, господа.
Операция по вживлению ROM не будет выглядеть завершенной без некоторых изменений в нашем стартовом скрипте, файле myrisc.simics:
load-module myrisc-comp
$controller=(create-RISC-controller name="controller" firmware="%simics%/targets/risc.bin")
instantiate-components
output-radix 16
controller.status
Первое изменение касается собственно ROM, второе добавлено для удобства. Не знаю, кому как, но мне надоело вводить «output-radix 16» руками ;-)
Время проверить, как это все взлетает… Секунду, у нас нет пока файл firmware! Не беда: пока мы не запускаем реальные программы для наших целей вполне хватит файла, заполненного произвольными данными. Создадим файл risc.bin в каталоге targets нашего дерева проекта:
$> head -c 256k </dev/urandom >risc.bin
Запустим симуляцию и прочитаем содержимое ПЗУ по его начальному адресу:
simics> run-command-file "%simics%/targets/myrisc.simics"
…
simics> controller.phys_mem.examine-memory 0x10000 size=32
p:0x00010000 b225 241c a820 f6dd 55e2 977c bc24 1979 .%$.. ..U..|.$.y
p:0x00010010 4f01 1bb8 30ac b077 00b2 55be 9b28 b3a1 O...0..w..U..(..
Уже не 0, это радует. Если же заглянуть в сгенерированный файл, то это как раз те данные, которые там лежат, только порядок байт различается. Оно и правильно, хост система на процессоре Intel, а симулируем мы RISC big-endian:
$> hexdump risc.bin -n 32
0000000 25b2 1c24 20a8 ddf6 e255 7c97 24bc 7919
0000010 014f b81b ac30 77b0 b200 be55 289b a1b3
Вот так, достаточно рутинно, можно заставить наш встраиваемый контроллер обзавестись прошивкой.
Даже самому смелому ОЗУ нужна защита
Даже в небольшой системе у нас может присутствовать какой-либо монитор, гипервизор или же другое ядро, которое управляет загрузкой, диспетчеризацией, предоставляет API, в общем, играет роль операционной системы. И для надежной работы этого кода, неплохо было бы как-то защитить его структуры данных в ОЗУ. Добавим примитивную защиту памяти. Защищать будем два региона например, часть MMIO-области и область со структурами данных монитора. Для этого нам понадобится по паре регистров для определения каждого защищаемого региона и по регистру для управления.
В каталоге modules дерева Simics создадим папку mem-mng и в ней файлы — Makefile, module_load.my и mem-mng.dml.
Makefile:
# -*- Makefile -*-
# Simics module makefile
#
MODULE_CLASSES = mem_mng
SRC_FILES = mem-mng.dml
PYTHON_FILES = module_load.py
SIMICS_API := 6
THREAD_SAFE:= yes
include $(MODULE_MAKEFILE)
Файл module_load.my практически пустой. В дальнейшем, в нем можно будет разместить различные дополнительные команды, связанные с управлением памятью:
#
device_name = "mem_mng"
Ну и самый интересный файл mem-mng.dml — собственно модель устройства управления защитой памяти:
dml 1.4;
device mem_mng;
import "simics/devs/translator.dml";
import "simics/devs/memory-space.dml";
param TOTAL_PROTECTED_AREAS = 2;
template simple_map_target {
is connect;
session const map_target_t *map_target;
method set(conf_object_t *obj) {
default(obj);
map_target = SIM_new_map_target(this.obj, NULL, NULL);
}
}
connect mem_tgt {
param documentation = "Memory for default access";
param type = "o";
param required = true;
interface memory_space;
is simple_map_target;
}
connect unmapped_ff {
param documentation = "Dummy memory";
param type = "o";
param required = true;
is simple_map_target;
}
bank mmng {
param register_size = 4;
group range[i < TOTAL_PROTECTED_AREAS] {
register ctrl @ 0x0 + 3 * 4 * i {
field En @ [0:0];
field Rsvd @ [31:1];
}
register ladr @ 0x4 + 3 * 4 * i;
register hadr @ 0x8 + 3 * 4 * i;
}
}
port mem_decoder {
implement translator {
method translate(physical_address_t SA, access_t access,
const map_target_t *default_target) -> (translation_t) {
local translation_t trans;
local bool access_denied = false;
local int i;
if ((access & Sim_Access_Write) != 0) {
for (i = 0; i < TOTAL_PROTECTED_AREAS; i++) {
if (mmng.range[i].ctrl.En.val == 1
&& SA >= mmng.range[i].ladr.val
&& SA <= mmng.range[i].hadr.val) {
access_denied = true;
log info, 4:
"Address 0x%lx hits range %d - 0x%lx:0x%lx",
SA, i, mmng.range[i].ladr.val,
mmng.range[i].hadr.val;
break;
}
}
}
if (access_denied) {
log info, 4: "Address 0x%lx access denied", SA;
trans.base = SA;
trans.start = 0x0;
trans.size = 1;
trans.target = unmapped_ff.map_target;
} else {
trans.base = 0x0;
trans.start = 0x0;
trans.target = mem_tgt.map_target;
}
return trans;
}
}
}
Устройство представляет собой транслятор адресов, который мы подключим одним «концом» к собственному адресному пространству транслятора, другим к адресному пространству ОЗУ. Подключение будем производить в том же модуле myrisc_comp.py. Я приведу только важный участок кода:
def add_objects(self):
...
unmapped_ff = self.add_pre_obj('unmapped_ff', 'set-memory')
mem_mng = self.add_pre_obj('mem_mng', 'mem_mng') # "memory controller"
ram_space = self.add_pre_obj('ram_space', 'memory-space')
mem_image = self.add_pre_obj('mem_image', 'image')
mem_image.size = self.mem_size.val
mem = self.add_pre_obj('ram', 'ram')
mem.image = mem_image
mem_space = self.add_pre_obj('mem_space', 'memory-space')
mem_space.map = [[0, mem, 0, 0, self.mem_size.val]]
mem_mng.mem_tgt = mem_space
mem_mng.unmapped_ff = unmapped_ff
ram_space.default_target = [[mem_mng, 'mem_decoder'], 0, 0, mem_space]
phys_mem.map += [[MPROT_PORTS_ADDR, [mem_mng, "mmng"], 0, 0, MPROT_PORTS_SIZE, None, 0, 4],
[DEF_MMIO_SIZE+DEF_ROM_SIZE, ram_space, 0, 0, self.mem_size.val]
Что я сделал? Я создал адресное пространство mem_space, к которому подключил объект, моделирующий ОЗУ (строка mem_space.map=...), адресное пространство ram_space, с подключенным транслятором адресов mem_mng, и адресное пространство ОЗУ (объект mem_space) в качестве конечного получателя адреса. После чего заменил подключение ОЗУ (объект mem) к физическому адресному пространству на адресное пространство транслятора адресов (объект ram_space). Получился эдакий «паровозик»: phys_mem→ram_space→mem_mng→mem_space→mem. Благодаря этому мы встроились в цепочку обращений по адресной шине между процессором и ОЗУ и можем контролировать к каким адресам можно обращаться, а к каким — нет.
Отдельное внимание следует обратить на строку содержащую код: [MPROT_PORTS_ADDR, [mem_mng, "mmng"], 0, 0, MPROT_PORTS_SIZE, None, 0, 4]. Это одно из ключевых изменений. Данный код отвечает за подключение в адресное пространство процессора регистров управления защитой как MMIO.
Соберем:
$> make mem-mng
$> make myrisc_comp
Запустим. Посмотрим, как выглядит распределение памяти:
simics> controller.phys_mem.map
┌───────┬───────────────────────┬──┬──────┬────────┬──────┬────┬─────┬────┐
│ Base│Object │Fn│Offset│ Length│Target│Prio│Align│Swap│
├───────┼───────────────────────┼──┼──────┼────────┼──────┼────┼─────┼────┤
│ 0x0│controller.mem_mng:mmng│ │ 0x0│ 0xc│ │ 0│ 4│ │
│0x10000│controller.rom │ │ 0x0│ 0x40000│ │ 0│ │ │
│0x50000│controller.ram_space │ │ 0x0│0x400000│ │ 0│ │ │
└───────┴───────────────────────┴──┴──────┴────────┴──────┴────┴─────┴────┘
Запрограммируем защиту — защитим область ОЗУ между адресами 0x200...0x400 (абсолютные 0x50200...0x50400):
simics> controller.phys_mem.write(0, 0x1000000)
simics> controller.phys_mem.write(4, 0x0020000)
simics> controller.phys_mem.write(8, 0x0040000)
Надеюсь, что «программирование» защиты достаточно прозрачно. Попробуем «пробить»:
simics> log-level 4
New global log level: 4
simics> controller.phys_mem.write(0x50200, 0xFFFFF)
[controller.mem_mng.port.mem_decoder info] Address 0x200 hits range 0 - 0x200:0x400
[controller.mem_mng.port.mem_decoder info] Address 0x200 access denied
[controller.mem_mng.port.mem_decoder info] Address 0x201 hits range 0 - 0x200:0x400
[controller.mem_mng.port.mem_decoder info] Address 0x201 access denied
[controller.mem_mng.port.mem_decoder info] Address 0x202 hits range 0 - 0x200:0x400
[controller.mem_mng.port.mem_decoder info] Address 0x202 access denied
[controller.mem_mng.port.mem_decoder info] Address 0x203 hits range 0 - 0x200:0x400
[controller.mem_mng.port.mem_decoder info] Address 0x203 access denied
simics> controller.phys_mem.read(0x50200)
0x0 (BE)
И за пределами диапазона 0x50200...0x50400:
simics> controller.phys_mem.write(0x50404, 0xFFFFF)
simics> controller.phys_mem.read(0x50404)
0xfffff (BE)
Смотри-ка, работает! Точно также действия по программированию защиты должен будет выполнить код прошивки или самого монитора.
Получился осьминожек
Ну вот, с этим уже можно поиграть. Любопытные могут заглянуть в код simple-risc процессора, чтобы выяснить, какие команды он поддерживает. Написать на этом ассемблере пару команд. Вручную транслировать их в двоичный вид, записать в наше ПЗУ и даже заставить нашего подопечного выполнить этот код при старте — чем это вам не модель встраиваемой системы? Двадцать лет назад именно так и поступали, только с применением паяльника. Въедливый читатель укажет мне, что мы получили «вещь в себе» и назначение встраиваемой системы быть «встроенной» куда-нибудь и как-то взаимодействовать с этим чем-нибудь. Ваша правда, нам явно не хватаем внешних интерфейсов. Но об этом в следующей статье.
P.S. Я рад, что кто-то не только читает, но и пытается пойти дальше и сделать из этого нечто полезное для себя. Так, один из читателей обратил внимание на ошибку, сделанную мной в коде еще для первой статьи. Не то, чтобы она как-то мешала восприятию, однако запустить симуляцию командной Simics run не получиться :-( Ошибка состоит в том, что я предположил (и не проверил), что модель процессора sample-risc содержит собственные часы (тактовый генератор), а это не так. Однако Dmitry L. предложил простой патч (который уже внесен в код на Github-е) в файл myrisc_comp.py:
...
def add_objects(self):
clk = self.add_pre_obj ('clk' , 'clock')
clk.freq_mhz = self.freq_mhz.val
cpu_core = self.add_pre_obj('cpu_core', 'sample-risc-core')
cpu_core.queue = clk
...
Он добавляет в модель контроллера "тактовый генератор" и разрешает исполнять реальный код. Например, как это сделал Dmitry L.:
simics> controller.phys_mem.write(0x0, 0x60000000)
simics> controller.cpu_core.disassemble address = 0
v:0x0000000000000000 p:0x0000000000000000 0x60000000 add r0 + r0 + 0x0 -> r0
simics> controller.cpu_core->pc = 0
simics> controller.cpu_core->r0 = 2
simics> controller.cpu_core->r0
0x2
simics> run 700
simics> controller.cpu_core->r0
0x4
Dmitry, Спасибо!
![](https://habrastorage.org/getpro/habr/upload_files/7ed/f08/205/7edf08205d71b2c780b1a91662468641.png)
Sdima1357
Из двух статей не очень понятно, какой именно risc поддерживает этот эмулятор, причем тут xtensa арм и fpga и зачем это вообще нужно. Кроме того в младших адресах обычно кладут вектор прерываний, а не IO. Так что ничего не понял.
andy_pop Автор
По-порядку:
Simics это платформа симуляции.
Поддерживает любой процессор для которого создана соответствующая модель.
Xtensa имеет свой набор симулируемых процессоров - их можно подключить к Simics.
FPGA - Simics может использоваться в смешанных аппаратно-программных симуляциях. В частности совместно с FPGA
Куда и что ложить, дело хозяйское. Собственно статьи как раз о том, что смоделировать систему достаточно просто и без паяльника ;-)
Sdima1357
Тема simics( а также risc, xtensa, fpga ) не раскрыта...
Что за risc ? Risc -v или другой? Xtensa - tensilica ? Или другая? Как склеить с fpga? Зачем это все? Отсутствует понятная цель у статьи. Почему именно simics? Если модель процессора внешняя, то все можно и на С написать.
adanilov
Зачем писать стандартные вещи с нуля, проще взять готовую модель процессора и часть платформы, а кастомные части дописать самому или подцепить например через транзактор. Симикс хорош при проектировании и отладки софта когда железо ещё не готово или для обучающих целей, есть возможность детально посмотреть как железо исполняет код.