Алтайский государственный университет
Опубликован: 12.07.2010 | Доступ: свободный | Студентов: 1465 / 390 | Оценка: 4.02 / 3.93 | Длительность: 16:32:00
ISBN: 978-5-9963-0349-6
Специальности: Разработчик аппаратуры
Лекция 4:

Многоядерные процессоры с низким энергопотреблением

Первые шаги программирования мультикомпьютеров SEAFORTH40

Введение

Подход к программированию мультикомпьютеров SEAforth во многом существенно отличается от программирования многоядерных процессоров общего назначения. Это выражается в специфике среды разработки, структуре процессора (массовый параллелизм, распределенная память), сокращенный набор команд, небольшой объем памяти для оперативного хранения программ и данных.

Простая арифметика

В данном разделе рассмотрим простые примеры, иллюстрирующие работу с константами, переменными и математическими операциями:

  • сложение/вычитание переменной с константой;
  • различные варианты умножения – сокращенный, подпрограмма, вызов слов ПЗУ.

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

Для начала, каждое из ядер должно настроить один из индексных регистров на порт соседнего ядра, положить на стек два числа, произвести назначенную операцию, записать результат в коммуникационный порт и получить данные от соседа (рисунки 4.14).

Схема передачи данных между ядрами при обмене результатами операций

Рис. 4.14. Схема передачи данных между ядрами при обмене результатами операций

Возможный источник взаимной блокировки ядер – момент передачи данных. Для исключения блокировки, одно из ядер должно начать процедуру обмена с чтения порта, другое с записи. Графически процесс взаимодействия ядер достаточно удобно изображать в виде диаграммы последовательностей действий [17] (рисунки 4.15).

Диаграмма взаимодействия двух ядер при обмене результатами

Рис. 4.15. Диаграмма взаимодействия двух ядер при обмене результатами

Т.к. в системе команд процессора отсутствует команда вычитания, ее придется реализовать, как сложение с числом в дополнительном коде.

\ макросы, определяющие операции сложения и вычитания чисел на стеке
macro: add
  . +	\ сложение
macro;

macro: sub	\ вычитание
  not 1 # . +	\ перевод числа на стеке в доп. код
  add
macro;

\ macro;
\ зададим пару констант
10 constant x
5 constant y
\ пусть ядро 10 выполняет сложение, ядро 20 - вычитание
\ т.к. сложение завершится быстрее, 10е ядро первым выполнит чтение с порта
\ дополнительно это позволит на некоторое время снизить энергопотребление процессора :

10 {node  0 org  here =p  '---u # a!  \
  x # y # add \ x+y 
  @a 		\ x+y x-y
  over !a  	\ x+y x-y
.
node}

20 {node  0 org  here =p  '---u # a! \
  x # y # sub \ x-y    
  !a @a 	 \ x+y
.
node}

При необходимости (например, многократное использование в коде программы), вычитание можно оформить в виде слова.

: - 
  not 1 # . + . +
;

Следующий интересный момент связан с выполнением операции умножения на данном процессоре. Аппаратного умножителя в процессорных ядрах нет, и умножение реализуется программно на базе команды +*.

+ * используется в качестве строительных блоков для перемножения двух чисел, находящихся в регистрах S и A. Работает по принципу вычисления частичных произведений (сложение-сдвиг) [18]. Результат размещается в регистрах Т и А, которые работают при этом как 36-битный сдвиговый регистр. В Т (вершина стека) – старшая часть, в А – младшая.

Если бит 0 (LSB) в А равен 0, то 37-бит (бит расширения знака) регистра Т.А просто сдвигается вправо на один бит. Если он равен 1, содержимое S прибавляется к Т, перед тем, как 37-битный регистр Т.А будет сдвинут. Младший бит Т сдвигается в старший бит регистра А.

Код для ядра 30 иллюстрирует беззнаковое перемножение 18-битных чисел. В А помещается один множитель, вершина стека обнуляется, второй элемент стека содержит второй множитель.

30 {node  0 org  here =p  \ "ручная" реализация беззнакового умножения
  '--l- # b!
  \ t=0 s=x-любого знака a=y-положительный
  155 # a! -2200 # \ t=4 a=5
  dup dup       \ -- 4 s=4 t=4 a=5
  xor   \ -- s=4 t=0 a=5
  17 # for . +* unext \ -- s=x t=x*y_h a=x*y_l 
  @b
  node}

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

Пример ниже демонстрирует умножение 25 на 3.

00 {node  0 org  here =p  \ умножение малоразрядных чисел
  '--l- # b!
   25 # 2* 2* 3 # dup a! . +* . +*
  @b
  node}

Для экономии пространства ОЗУ можно использовать вызов слова * из ПЗУ ядер (есть практически во всех ядрах). Кроме всего прочего, это слово поддерживает знаковое умножение.

01 {node  0 org  here =p  \ работа подпрограммы умножения
  '---u # b!
  155 # -2200 # * 2/
  @b
  node}                                                       

02 {node  0 org  here =p  \ работа подпрограммы умножения
  '---u # b!
  155 # 2200 # * 2/
  @b
  node}

Обмен данных между ядрами

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

Для начала зададим путь обхода процессора – рисунок 4.16.

Путь передачи слова по ядрам процессора

увеличить изображение
Рис. 4.16. Путь передачи слова по ядрам процессора

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

Программный код ядер будет одинаковым - различаются только адреса портов. Снизить объем работы по написанию исходного текста можно, используя макроопределения (они же макросы).

Одно из возможных решений приводится ниже.

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

macro: pipel, ( $in $out -- )
: start
 here =p
   # b! # a!
   @a dup !b
macro;

Он задает точку старта процессорного ядра, берет со стека компиляции пару чисел – адресов портов источника и приемника слова и компилирует код, отвечающий за прием-передачу слова.

Согласно структурной схеме процессора выделим те пары портов, между которыми происходят передачи – источник-приемник. Для каждой из пар определим макрос с нужными для данной пары адресами портов:

macro: lr, ( $in $out )
 '--l- 'r--- pipel,
macro;

macro: rd, ( $in $out )
 'r--- '-d-- pipel,
macro;

macro: du, ( $in $out )
 '-d-- '---u pipel,
macro;

macro:	 ud, ( $in $out )
 '---u '-d-- pipel,
macro;

macro: ul, ( $in $out )
 '---u '--l- pipel,
macro;

macro: dr, ( $in $out )
 '-d-- 'r--- pipel,
macro;

macro: rl, ( $in $out )
 'r--- '--l- pipel,
macro;

macro: lu, ( $in $out )
 '--l- '---u pipel,
macro;

Как видно из исходного кода, компилятором допускается использование вложенных макросов. Последним шагом зададим код для каждого из ядер.

\ начальное ядро
32 {node  0 org  here =p '--l- # b! 0 # !b 'r--- # a! @a node}

\ ядра с направлением передачи от левого порта правому
31 {node  lr, node} 35 {node  lr, node} 37 {node  lr, node} 33 {node  lr, node}

\ от правого - нижнему
30 {node  rd, node} 1  {node  rd, node} 3  {node  rd, node} 5  {node  rd, node}
7  {node  rd, node} 9  {node  rd, node}

\ от нижнего - верхнему
20 {node  du, node} 11 {node  du, node} 13 {node  du, node} 15 {node  du, node}
17 {node  du, node} 19 {node  du, node}

 	\ от верхнего - нижнему
10 {node  ud, node} 12 {node  ud, node} 14 {node  ud, node} 16 {node  ud, node}
18 {node  ud, node} 29 {node  ud, node}

\ от  верхнего - левому
21 {node  ul, node} 23 {node  ul, node} 25 {node  ul, node} 27 {node  ul, node}

\ от нижнего - правому
0 {node  dr, node} 2 {node  dr, node} 4 {node  dr, node} 6 {node  dr, node}
8 {node  dr, node} 39 {node  dr, node}

\ от правого - левому
34 {node  rl, node} 36 {node  rl, node} 38 {node  rl, node}

\ от левого - верхнему
22 {node  lu, node} 24 {node  lu, node} 26 {node  lu, node} 28 {node  lu, node}

Запуская на симуляторе, можно видеть примерно следующее – область с активными ядрами перемещается согласно заданному нами пути (рисунок 4.17).

Перемещение слова по ядрам процессора

Рис. 4.17. Перемещение слова по ядрам процессора

Работа с периферийными устройствами

АЦП

Запустим АЦП на ядре 36 и данные, генерируемые на нем будем выдавать в параллельном виде на линиях ядра №5. В этом случае в работе приложения будут задействованы несколько ядер (а именно – 36, 26, 16, 15 и 5-е), выполняющих каждое свою задачу. Ядро 36 считывает данные с АЦП, ядра 26, 16, 15 служат для передачи значений АЦП 5-му ядру, и ядро 5 по мере поступления данных выставляет их в параллельный порт. Путь данных показан на рисунке 4.18.

Вывод данных с АЦП на параллельный порт

Рис. 4.18. Вывод данных с АЦП на параллельный порт

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

Диаграмма последовательностей работы ядер

Рис. 4.19. Диаграмма последовательностей работы ядер
Сергей Горбунов
Сергей Горбунов

 

прошел курс и сдал экзамен   Многоядерные процессоры   

система сертификат не выдала. почему?