Изучение ассемблера на PIC32: COM, буферизованная запись

PIC
Учимся работать со структурами данных, вложенными функциями. Использование препроцессора языка C совместно с ассемблером. На этот раз микроконтроллер PIC будет выводить «Hello World» через COM порт.

Писать код на чистом ассемблере довольно утомительное занятие, поэтому скрасим наш быт «синтаксическим сахаром» от gcc.

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

Заголовочные файлы C можно использовать в ассемблере с помощью препроцессора C.
Добавим в начало файла serial.S директиву включения библиотечного файла:
#include <p32xxxx.h>

После этого можно смело использовать U1STA вместо 0xBF800000 + 0x6010.
Да, мы упускаем из вида информацию, что U1STA и U1MODE имеют одинаковые базовые смещения, но сам код становится понятней.

Кроме новых констант заголовочный файл p32xxxx.h вносит дополнительные проблемы:
send_buffer:
	# a0 = buffer, a1 = cnt
	move	t2,zero		# Устанавливаем текущий символ на начало буфера (t2) = 0
	lui	t0,%hi(U1STA)	# Базовый адрес блока UART теперь в t0
.retry:
	lw	t1,%lo(U1STA)(t0) # U1STA проверяем Busy Flag (BF)
	andi	t1,t1,0x200
	bne	t1,zero,.retry	# если предыдущий символ не отправлен, ждём

	addu	t3,a0,t2	# Посчитать какой адрес у текущего символа (начало буфера(a0)+t2)
	lbu	t4,0(t3)	# Считать текущий символ из памяти

	sw	t4,%lo(U1TXREG)(t0) # Записать текущий символ в COM порт (U1TXREG)

	addiu	t2,t2,1	# Увеличиваем индекс символа (t2), чтобы получить следующий
	slt	t4,t2,a1	# Проверяем t2 == a1, результат пишем в t4

	bne	t4,zero,.retry	# Начало обработки следующего символа совпадает с повтором проверки BF

	j	ra
.size	send_buffer, .-send_buffer


Как видно из кода мы больше не можем использовать $ra в качестве названия регистра возврата, $0 превратился в zero, а $t* в t*.

Новая функция send_buffer принимает два аргумента. Так как аргументов меньше четырёх все они передаются через регистры, начиная с a0.

Первым аргументом функции (регистр a0) передается адрес непрерывной области памяти, по которому находится массив байт (или строка из символов) который требуется передать через COM порт.

Второй аргумент (регистр a1) определяет сколько байт мы должны передать.

Обработка организована циклом со счетчиком (аналог из мира C: оператор for). Я выбрал регистр t2 в качестве места хранения счетчика. Перед началом выполнения мы помещаем число 0 в t2, с каждым новым проходом (итерацией) цикла увеличиваем t2 на единицу до тех пор пока не достигнем значения a1. Таким образом счетчик меняет своё значение от нуля до (a1-1) включая оба конца.

По адресу a0+0 находится первый символ, значит последний будет находится по адресу a0+a1-1.

Внутри цикла происходит две операции:
  • Проверка готовности к отправке байта (Busy Flag) == 0
  • Отправка одного байта через com порт


Хранение данных в ассемблере
Хранение невозможно без хорошего хламохранилища. Данные можно хранить в долговременной или оперативной памяти. В микроконтроллерах в качестве долговременной памяти используется flash память встроенная в кристалл самого МК, а в качестве оперативной памяти SRAM, так же находящаяся на кристалле.
Flash память довольно сложно перезаписывать, поэтому мы будем рассматривать эту область памяти как только для чтения. Нет ничего плохого что у нас будет память которую мы можем только прочитать, запись такой памяти происходит при прошивки контроллера с помощью avrdude.

Что можно хранить во флеше?
  • Константы, например число pi или скорость света в вакууме.
  • Код программы в виде машинных кодов

Для чего нужна оперативная память?
  1. Быстро изменяющиеся данные
  2. Сохранения регистров
  3. Временных переменных, если регистры закончились
  4. Временных массивов для хранения больших данных, например наша программа может сначала подготовить данные для компьютера, а потом уже переслать всё за один присест через serial порт.

Имея разные способы хранения данных в памяти микроконтроллера, программист хочет распределять свои данные по разным местам. Распределение производится с помощью спецальной программы — линковщика (linker).
Линковщик не работает напрямую с пользовательскими данными. Эти данные надо предварительно поместить в «секции», а расположение секций и их размер берётся из скриптов линковщика (наш случай это chipKIT-UNO32-application-32MX320F128L.ld)

Наши пользовательские данные будут находится в области глобальных переменных: секции ".bss"
Код мы уже помещали в секцию ".text".

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

Наша структура на языке си описывается как:
typedef struct {
        uint32_t sz;
        uint8_t  buf[1024];
} uart_buffer_t;


Первым элементом идёт 32-битное число, которое содержит количество занятых элементов в буфере.
Второй элемент структуры это массив из однобайтовых элементов размером в один килобайт.

.section	.bss
	.size	uart_buf, 1028
uart_buf:
	.space	1028


Помимо хранения нам надо определить местонахождения внутренних элементов:
.globl uart_buf
.equ uart_buf.sz,	uart_buf+0x00
.equ uart_buf.buf,	uart_buf+0x04


Данный код определяет три константы:
  • адрес нашей структуры в памяти uart_buf
  • адрес количества элементов uart_buf.sz
  • адрес первого элемента массива данных буфера uart_buf.buf

Помещаем один байт в буфер:
outch:
	# помещаем 
	lui	t0,%hi(uart_buf.buf)
	addiu	t0,t0,%lo(uart_buf.buf)	# t0 = адрес первого символа буфера

	lui	t1,%hi(uart_buf.sz)		# старший разряд адреса
	lw	t2,%lo(uart_buf.sz)(t1)	# t2 = позиция после последнего символа в буфере
	addiu	t1,t1,%lo(uart_buf.sz)	# t1 = адрес количества символов в буфере

	addu	t0,t0,t2			# t0 = адрес нового символа в буфере

	sb	a0,0(t0)			# Пишем один байт в буфер
	addiu	t2,t2,1			# Увеличиваем размер буфера на 1
	sw	t2,0(t1)			# сохраняем новое значение в памяти

	j	ra
.size outch, .-outch


  1. Функция outch принимает один аргумент a0.
  2. Рассчитываем адрес символа находящегося за последним
  3. Помещаем младщий байт a0 по найденному адресу
  4. Увеличиваем размер буфера на единицу

Выводим «Hello World!»
print_hello:
	addiu	sp,sp,-4
	sw	ra, 0(sp) 	# Мы будем вызывать другие функции, надо сохранить адрес возврата на стеке

	lui	t0,%hi(uart_buf.sz)
	addiu	t0,t0,%lo(uart_buf.sz)
	sw	zero,0(t0)	# очистить буфер

	li	a0, 'H'
	jal	outch
	li	a0, 'e'
	jal	outch
....
	li	a0, '\r'
	jal	outch
	li	a0, '\n'
	jal	outch

	lui	t0,%hi(uart_buf.buf)
	addiu	a0,t0,%lo(uart_buf.buf)
	lui	t0,%hi(uart_buf.sz)
	lw	a1,%lo(uart_buf.sz)(t0)
	jal	send_buffer

	lui	t0,%hi(uart_buf.sz)
	addiu	t0,t0,%lo(uart_buf.sz)
	sw	zero,0(t0)	# очистить буфер

	lw	ra, 0(sp) 	# Востаналивааем адрес возврата
	addiu	sp,sp,4
	j	ra
.size	print_hello, .-print_hello


Функция print_hello будет вызывать другие функции. Чтобы покинуть print_hello надо знать адрес, куда вернуться. print_hello вызывает функции outch и send_buffer, при каждом вызове адрес возрата из print_hello будет затёрт.

Как же сохранить ra?
Спецальная область оперативной памяти для небольших временных переменных называется стек. Последний занятый элемент стека находится по адресу в регистре sp (stack pointer).
Выделение памяти происходит путём уменьшения регистра стека (стек растёт в сторону меньших адресов).
Освобождение — прибавкой.

Полный код:
#include <p32xxxx.h>
.text
send_buffer:
	# a0 = buffer, a1 = cnt
	move	t2,zero		# Устанавливаем текущий символ на начало буфера (t2) = 0
	lui	t0,%hi(U1STA)	# Базовый адрес блока UART теперь в t0
.retry:
	lw	t1,%lo(U1STA)(t0) # U1STA проверяем Busy Flag (BF)
	andi	t1,t1,0x200
	bne	t1,zero,.retry	# если предыдущий символ не отправлен, ждём

	addu	t3,a0,t2	# Посчитать какой адрес у текущего символа (начало буфера(a0)+t2)
	lbu	t4,0(t3)	# Считать текущий символ из памяти

	sw	t4,%lo(U1TXREG)(t0) # Записать текущий символ в COM порт (U1TXREG)

	addiu	t2,t2,1	# Увеличиваем индекс символа (t2), чтобы получить следующий
	slt	t4,t2,a1	# Проверяем t2 == a1, результат пишем в t4

	bne	t4,zero,.retry	# Начало обработки следующего символа совпадает с повтором проверки BF

	j	ra
.size	send_buffer, .-send_buffer

.globl uart_buf
.equ uart_buf.sz,	uart_buf+0x00
.equ uart_buf.buf,	uart_buf+0x04

outch:
	# помещаем 
	lui	t0,%hi(uart_buf.buf)
	addiu	t0,t0,%lo(uart_buf.buf)	# t0 = адрес первого символа буфера

	lui	t1,%hi(uart_buf.sz)		# старший разряд адреса
	lw	t2,%lo(uart_buf.sz)(t1)	# t2 = позиция после последнего символа в буфере
	addiu	t1,t1,%lo(uart_buf.sz)	# t1 = адрес количества символов в буфере

	addu	t0,t0,t2			# t0 = адрес нового символа в буфере

	sb	a0,0(t0)			# Пишем один байт в буфер
	addiu	t2,t2,1			# Увеличиваем размер буфера на 1
	sw	t2,0(t1)			# сохраняем новое значение в памяти

	j	ra
.size outch, .-outch

print_hello:
	addiu	sp,sp,-4
	sw	ra, 0(sp) 	# Мы будем вызывать другие функции, надо сохранить адрес возврата на стеке

	lui	t0,%hi(uart_buf.sz)
	addiu	t0,t0,%lo(uart_buf.sz)
	sw	zero,0(t0)	# очистить буфер

	li	a0, 'H'
	jal	outch
	li	a0, 'e'
	jal	outch
	li	a0, 'l'
	jal	outch
	li	a0, 'l'
	jal	outch
	li	a0, 'o'
	jal	outch
	li	a0, ' '
	jal	outch
	li	a0, 'W'
	jal	outch
	li	a0, 'o'
	jal	outch
	li	a0, 'r'
	jal	outch
	li	a0, 'l'
	jal	outch
	li	a0, 'd'
	jal	outch
	li	a0, '.'
	jal	outch
	li	a0, '\r'
	jal	outch
	li	a0, '\n'
	jal	outch

	lui	t0,%hi(uart_buf.buf)
	addiu	a0,t0,%lo(uart_buf.buf)
	lui	t0,%hi(uart_buf.sz)
	lw	a1,%lo(uart_buf.sz)(t0)
	jal	send_buffer

	lui	t0,%hi(uart_buf.sz)
	addiu	t0,t0,%lo(uart_buf.sz)
	sw	zero,0(t0)	# очистить буфер

	lw	ra, 0(sp) 	# Востаналивааем адрес возврата
	addiu	sp,sp,4
	j	ra
.size	print_hello, .-print_hello

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

init_serial:
 	# U1BRG = CPU_CLOCK / 16 / BAUD_RATE -1;
	# U1BRG = 80000000L / 16 / 9600 -1;
	lui	t2,%hi(U1BRG)
	li	t3,519
	sw	t3,%lo(U1BRG)(t2)
	sw	zero,%lo(U1STA)(t2) 	# U1STA
	li	t3,0x8000
	sw	t3,%lo(U1MODE)(t2) # U1MODE
	li	t3,0x1400	# U1STASET bit 10 & bit 12
	sw	t3,%lo(U1STASET)(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 порт
# Включаем ножку как ножку выхода
	lui	s0,%hi(TRISFCLR)
	li	s1,1
	sw	s1,%lo(TRISFCLR)(s0)
.loop:
# зажигаем ножку
        sw      s1,%lo(LATFSET)(s0)
	jal	delay_1s	# ждём одну секунду
# гасим ножку
        sw      s1,%lo(LATFCLR)(s0)
	jal	delay_1s	# ждём одну секунду
	jal	print_hello
	j    	.loop
	.size	main, .-main

# Сюда положим данные:
	.section	.bss
	.size	uart_buf, 1028
uart_buf:
	.space	1028

  • +1
  • 13 марта 2014, 08:48
  • ihanick
  • 1
Файлы в топике: uno32-asm-serial-buffer.zip

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

RSS свернуть / развернуть
PIC32 и ассемблер MIPS детально разглянуты в офигительной книжке Digital design and Computer Architecture от Харрисов. Наверное лучший учебник из существующих.
0
Было бы неплохо еще и линк на нее приложить :)
0
+1
косяк с именем ссылки, короче, вы поняли…
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.