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

Первую часть ра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 регистр, довольно длительная процедура, вносимая задержка приведет к снижению эффективности использования процедур и как следствие код, которому нужна высокая скорость будет большего размера. С другой стороны, тяжелая вычислительная задача может использовать все регистры кроме нулевого и регистра стека.
Каким же образом сделать вызов функции?
- Сохранить адрес возврата функции $ra($31) на стеке
- Сохранить $t0-$t9 на стеке, если там содержится что-то полезное
- Поместить аргументы в $a0-$a3, если регистров не хватило остатки положить в стек
- Выполнить команду jal метка_функции
Что необходимо делать внутри вызываемой функции?
- Сохранить адрес возврата, если будет ещё один уровень вложенности функций
- Сохранить $s0-$s7, если собираемся их перезаписывать
- В самом конце вызова функции надо достать адрес возврата из стека, если он менялся
- Прыгаем по адресу возврата 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
А какова функция у нулевого регистра? И где расположен регистр PC?
Почему в коде везде магические числа? Неужели для ассемблера нет файлов описания камней?
Почему в коде везде магические числа? Неужели для ассемблера нет файлов описания камней?
После прошивки avrdude производит сверку контрольных сумм. В это время наша прошивка уже может исполняться и отсутствие задержки в начале программы приведет к ошибкам контрольной суммы.Это как? По идее, если оно сверяет КС — значит еще работает загрузчик, вычитывая и передавая прошивку, а значит сама прошивка работать еще не может.
А какова функция у нулевого регистра?Во многих опкодах есть возможность работать только с регистрами. Чтобы например, обнулять регистры за одну команду move $dst, $src надо или в $src подгружать ноль или уже иметь загруженный ноль в специальный регистр, регистр этот доступен только для чтения и находится в тот же месте что и остальные 31 регистр. Если энтузиазм не угаснет, то я сделаю цикл статей по реализации MIPS32 на ПЛИС Spartan 3E и станет понятно на 100% откуда этот нулевой регистр берется
И где расположен регистр PC?PC доступен только через команды j* и b*
Почему в коде везде магические числа? Неужели для ассемблера нет файлов описания камней?Я стараюсь подавать информацию от простого к сложному. Если «контрольный» читатель, без навыков программирования задаёт слишком много вопросов, материал безжалостно переносится в следующую статью.
Это как? По идее, если оно сверяет КС — значит еще работает загрузчик, вычитывая и передавая прошивку, а значит сама прошивка работать еще не может.Проверка контрольной суммы происходит уже после начала выполнения функции main.
Загрузчик работает на прерываниях, я прерывания не переопределяю и поэтому не мешаю проверке КС.
Если я перенастраиваю COM port сразу после начала выполнения функции main, то проверка КС — дохнет.
Другими словами, $0 — CG.
А считать PC можно?
PC доступен только через команды j* и b*Что это за команды? j* — по видимому прыжки. b* — в других архитектурах те же прыжки, только по другому названные.
А считать PC можно?
Я стараюсь подавать информацию от простого к сложному.В таком случае, ИМХО, следовало бы начать с констант.
Загрузчик работает на прерываниях, я прерывания не переопределяю и поэтому не мешаю проверке КС.А, забавно.
Что это за команды? 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
Комментарии (4)
RSS свернуть / развернуть