Изучение ассемблера на PIC32: последовательный порт (COM)

PIC
Передаем один байт с платы Chipkit Uno32 на компьютер через UART. Углубляемся в архитектуру процессоров MIPS32.

Первую часть раcсказа можно найти здесь.

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

Немного об архитектуре процессора PIC32
PIC32 реализует MIPS32 архитектуру. Число 32 в здесь магическое:
  • 32 бита на команду
  • 32 регистра


Все регистры кроме нулевого устроены одинаково в железе, нумерация идет от нуля до 31.
Несколько регистров выполняют специальные функции, либо используются специальные соглашения по их использованию. Компиляторы/декомпиляторы из набора gcc используют сквозную нумерацию регистров, но для ручного написания кода гораздо удобнее использовать мнемонические названия регистров с указанием функции:

  • $at, $k0, $k1: зарезервированы для операционной системы и ассемблера
  • $a0-$a3: служат для передачи первых четырёх аргументов функции (остальные передаются через стек)
  • $v0, $v1: используются для возврата результата работы функции
  • $t0-$t9: используются для временных переменных, при вызове функции, функция может затереть значение
  • $s0-$s7: используется для переменных с длительным временем жизни, вызываемая функция должна сохранить значения перед использованием.
  • $gp: указатель на сегмент статических данных (.data)
  • $sp: указатель стека
  • $fp: указатель текущего кадра (нужен для отладки, чтобы получать стек вызова функций
  • $ra: адрес возврата текущей функции


Зачем нужны такие сложности? Почему нельзя сделать просто 31 одинаковый регистр?
В системе команд MIPS32 нет специализированных команд для работы со специальными регистрами. мы можем использовать все 32 регистра за вычетом нулевого, как регистры общего назначения в своём коде. Для того чтобы наш код правильно взаимодействовал с чужими модулями надо придерживаться соглашений.

Зачем нужно делить делить регистры на временные и долго хранящие значения? Вызываемая функция (которую вызвали) не должна повреждать регистры вызывающей функции (которая вызывает). Сохранять 31 регистр, довольно длительная процедура, вносимая задержка приведет к снижению эффективности использования процедур и как следствие код, которому нужна высокая скорость будет большего размера. С другой стороны, тяжелая вычислительная задача может использовать все регистры кроме нулевого и регистра стека.

Каким же образом сделать вызов функции?
  1. Сохранить адрес возврата функции $ra($31) на стеке
  2. Сохранить $t0-$t9 на стеке, если там содержится что-то полезное
  3. Поместить аргументы в $a0-$a3, если регистров не хватило остатки положить в стек
  4. Выполнить команду jal метка_функции


Что необходимо делать внутри вызываемой функции?
  1. Сохранить адрес возврата, если будет ещё один уровень вложенности функций
  2. Сохранить $s0-$s7, если собираемся их перезаписывать
  3. В самом конце вызова функции надо достать адрес возврата из стека, если он менялся
  4. Прыгаем по адресу возврата j $ra


Приступаем к реализации
В первой части статьи мы использовали две задержки по одной секунде. Реализовано это было в виде двух циклов. Поменяем реализацию чтобы использовать функцию и два её вызова.

delay_1s:
        # 14M ~ 1s  0x0E20E5E = 0x0E20000 + 0x0E5E
        li      $t2,0x0E20000
        addiu   $t2,$t2,0x0E5E
.delay:
        addiu   $t2,$t2,-1
        bne     $t2,$0,.delay
        j       $ra
.size   delay_1s, .-delay_1s


Как видно из кода, мы ничего не сохраняем на стеке, потому что используются только «временные» регистры и нет вызова других функций. В конце функции мы рассчитываем её размер в байтах, что может помочь линковщику.

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

Функция настройки UART
Реализуем инициализацию оборудования в виде отдельной процедуры:
init_serial:
        # U1BRG = CPU_CLOCK / 16 / BAUD_RATE -1;
        # U1BRG = 80000000L / 16 / 9600 -1;
        li      $t2,0xBF800000
        li      $t3,519
        sw      $t3,0x6040($t2)

        sw      $0,0x6010($t2)  # U1STA

        li      $t3,0x8000
        sw      $t3,0x6000($t2) # U1MODE

        li      $t3,0x1400      # U1STASET bit 10 & bit 12
        sw      $t3,0x6018($t2)

        j       $ra
.size   init_serial, .-init_serial


Что должна делать функция настройки UART?
  • Настроить ножки контроллера
  • Настроить скорость работы интерфейса
  • Сбросить все флаги модуля UART
  • Включить модуль UART
  • Включить прием и передачу

Здесь мы можем немного упростить задачу: UART1 уже используется загрузчиком и ножки уже настроены.

Подробную информацию а регистрах блока UART, англ. Universal Asynchronous Receiver-Transmitter (UART), можно найти по ссылке:
http://ww1.microchip.com/downloads/en/DeviceDoc/61107F.pdf

Стоит отметить, что в данном примере базовый адрес блока UART (0xBF800000) был загружен в регистр $t2 только один раз, а потом был повторно использован для относительной адресации. Загрузка нулевого значения так же не нужна: нулевой регистр в MIPS32 всегда содержит значение 0.

Мы произвели настройку порта UART1. Данный порт подключен к преобразователю UART<=>USB и уже использовался нами для загрузки прошивки в контроллер.

Отправляем один байт на компьютер:

        li      $t2,0xBF800000
        li      $t3, 0x78 # 'x'
        sw      $t3,0x6020($t2)


Всё готово для полной реализации задуманного проекта:
.text

delay_1s:
	# 14M ~ 1s  0x0E20E5E = 0x0E20000 + 0x0E5E
        li      $t2,0x0E20000
        addiu   $t2,$t2,0x0E5E
.delay:
        addiu   $t2,$t2,-1
        bne     $t2,$0,.delay
	j	$ra
.size	delay_1s, .-delay_1s

init_serial:
 	# U1BRG = CPU_CLOCK / 16 / BAUD_RATE -1;
	# U1BRG = 80000000L / 16 / 9600 -1;
	li	$t2,0xBF800000
	li	$t3,519
	sw	$t3,0x6040($t2)

	sw	$0,0x6010($t2) 	# U1STA

	li	$t3,0x8000
	sw	$t3,0x6000($t2) # U1MODE

	li	$t3,0x1400	# U1STASET bit 10 & bit 12
	sw	$t3,0x6018($t2)

	j	$ra
.size	init_serial, .-init_serial

####################################################################
# Зажечь лампочку на ножке RF0 ассемблером
main:   .global main                # Помечаем метку main как глобальную
	jal	delay_1s	# ждём одну секунду
	jal	delay_1s	# ждём одну секунду
	jal	delay_1s	# ждём одну секунду

	jal	init_serial	# Включаем COM порт

# Включаем ножку как ножку выхода
	li	$s0,0xbf880000
	li	$s1,1
	sw	$s1,0x6144($s0)

.loop:
# зажигаем ножку
        sw      $s1,0x6168($s0)

	jal	delay_1s	# ждём одну секунду

# гасим ножку
        sw      $s1,0x6164($s0)

	jal	delay_1s	# ждём одну секунду

# посылаем байт
	li	$t2,0xBF800000
	li	$t3, 0x78 # 'x'
	sw	$t3,0x6020($t2)

	j    	.loop


После прошивки avrdude производит сверку контрольных сумм. В это время наша прошивка уже может исполняться и отсутствие задержки в начале программы приведет к ошибкам контрольной суммы. Я добавил 3 секунды задержки.

По умолчанию, плата ChipKit Uno32 подает reset при каждом подключении по COM порту.
Я использовал терминал CoolTerm под os x, под linux — GTKTerm и RealTerm под Windows.

После подключения терминала и 6-8 секунд наша плата начинает мигать пятым светодиодом и каждые две секунды посылать английскую букву 'x' на компьютер.
  • 0
  • 12 марта 2014, 07:43
  • ihanick
  • 1
Файлы в топике: uno32-asm-serial.zip

Комментарии (4)

RSS свернуть / развернуть
А какова функция у нулевого регистра? И где расположен регистр PC?
Почему в коде везде магические числа? Неужели для ассемблера нет файлов описания камней?
После прошивки avrdude производит сверку контрольных сумм. В это время наша прошивка уже может исполняться и отсутствие задержки в начале программы приведет к ошибкам контрольной суммы.
Это как? По идее, если оно сверяет КС — значит еще работает загрузчик, вычитывая и передавая прошивку, а значит сама прошивка работать еще не может.
0
  • avatar
  • Vga
  • 13 марта 2014, 03:56
А какова функция у нулевого регистра?
Во многих опкодах есть возможность работать только с регистрами. Чтобы например, обнулять регистры за одну команду move $dst, $src надо или в $src подгружать ноль или уже иметь загруженный ноль в специальный регистр, регистр этот доступен только для чтения и находится в тот же месте что и остальные 31 регистр. Если энтузиазм не угаснет, то я сделаю цикл статей по реализации MIPS32 на ПЛИС Spartan 3E и станет понятно на 100% откуда этот нулевой регистр берется

И где расположен регистр PC?
PC доступен только через команды j* и b*

Почему в коде везде магические числа? Неужели для ассемблера нет файлов описания камней?
Я стараюсь подавать информацию от простого к сложному. Если «контрольный» читатель, без навыков программирования задаёт слишком много вопросов, материал безжалостно переносится в следующую статью.

Это как? По идее, если оно сверяет КС — значит еще работает загрузчик, вычитывая и передавая прошивку, а значит сама прошивка работать еще не может.
Проверка контрольной суммы происходит уже после начала выполнения функции main.
Загрузчик работает на прерываниях, я прерывания не переопределяю и поэтому не мешаю проверке КС.
Если я перенастраиваю COM port сразу после начала выполнения функции main, то проверка КС — дохнет.
0
Другими словами, $0 — CG.
PC доступен только через команды j* и b*
Что это за команды? j* — по видимому прыжки. b* — в других архитектурах те же прыжки, только по другому названные.
А считать PC можно?
Я стараюсь подавать информацию от простого к сложному.
В таком случае, ИМХО, следовало бы начать с констант.
Загрузчик работает на прерываниях, я прерывания не переопределяю и поэтому не мешаю проверке КС.
А, забавно.
0
Что это за команды? j* — по видимому прыжки. b* — в других архитектурах те же прыжки, только по другому названные.
j это всевозможные jump (безусловные прыжки, например jal сохраняет PC в $ra(или по-другому $31), jr прыгает по адресу из регистра)

b это условные прыжки (branch), (например bne или BGEZAL (вызываем функцию если больше или равно) )

А считать PC можно?
С помощью дебагера или сильной магии.
la $ra, current
addiu $ra, $ra, 8
current:
j example
nop
return:
j return
nop

example:
jr $ra
nop
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.