nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore: инициализация SD-карточки

Чего-то затянулось у меня это безобразие сильнее, чем хотелось бы, пора уже доводить до кондиции.
Итак, мы долго и упорно изучали особенности информационного обмена по SPI с SD-карточкой. Потом нашли ошибку в модуле памяти и исправили её. Когда попытались сделать заточенный под это дело контроллер SPI - он внезапно вышел дуплексным, более универсальным, тогда как узкоспециализированный вариант получился бы существенно сложнее, как это ни странно.

Потеряв полнедели на отладку, решили чуть-чуть упростить себе жизнь - лучше день потерять и за полчаса долететь!

В пятницу уже что-то начало крутиться, но очередная шлея под хвост попала, не просто вызывать процедуру одной командой, но и умудриться в той же команде передать ей один аргумент!

Сейчас пора отладить фрагмент программы, отвечающий за инициализацию SD-карточки. И разобраться, как в QuatCore можно делать "обработку исключений" - как всегда, без поллитры не разберёшься!




Приведём код программы:

;учимся возиться с SD-карточкой через SPI.
;хотим получить полный лог инициализации, т.е какие команды давали какой ответ
;проще всё посылать по UART, чтобы была "простыня"
;ЖК в этот раз без надобности
;и числа с фикс. точкой не нужны, хватит целых, а может вообще HEX забабахать?
%include "QuatCoreConsts.inc"
%include "Win1251.inc"
.rodata
ORG 0
	R1Idle	EQU	0x0001	
	R1Illegal	EQU	0x0004

	CMD0		dw	5,0x40,0x00,0x00,0x00,0x00,0x95
	CMD8		dw	5,0x48,0x00,0x00,0x01,0xAA,0x87
	Str0		dw	'Обращаемся к SD-карточке',13,0x800A
	CMD0str	dw	'Send CMD0',13,0x800A
	CMD8str	dw	'Send CMD8',13,0x800A
	Timeoutstr	dw	'Timeout, restarting...',13,0x800A
	R1failstr	dw	'R1 incorrect: '
	OKCMD0	dw	'CMD0 success',13,0x800A	
.data
	Stack		dw	?,?,?,?,?
.rodata
ORG -64
	StackAddr	dw	Stack
	Str0Adr	dw	Str0
	CMD0strAdr	dw	CMD0str
	CMD8strAdr	dw	CMD8str
	TimeoutstrAdr dw	Timeoutstr
	R1failstrAdr	dw	R1failstr
	OKCMD0Adr	dw	OKCMD0
.code
	main proc
				SP		StackAddr
				SP		[SP]
				;надо дать ему 74 такта SCK, или 148 тактов наших. Один уже прошёл :)
				;напишем что-нибудь по UART :)
				X		Str0Adr
				CALL		print
				;26 символов, по 10 бит, на 460800 бит/с даст 564 мкс, это 1128 тактов SCK - сойдёт :)
				;(точнее, 25 символов, т.к последнего мы не дожидаемся, но тоже хватит!)
				
				;ПОСЫЛАЕМ CMD0
		InitSD:		X		CMD0strAdr
				CALL		print
				X		CMD0
				CALL		SDsend
				;ответ получили, это должен быть 0x01, в случае таймаута нас уже вернули на InitSD. 
				CALL		checkR1
				;фух, первую команду осилили!
				X		OKCMD0Adr
				CALL		print
				
				;ПОСЫЛАЕМ CMD8
				X		CMD8strAdr
				CALL		print
				X		CMD8
				CALL		SDsend
				;должны получить R1=0x01, а затем "эхо" нашей команды
				CALL		checkR1
				;и по-хорошему ещё проверить "эхо", а для начала получить его!
				i		3
		@@echo:	
				
				
		@@endless:	JMP		@@endless
	main endp
	
	
	;посылает строку конкретно в UART. Длина строки не более 32 символов.
	;X указывает на начало строки текста
	;конец обозначается отрицательным числом (у нас НЕТ ФЛАГА НУЛЯ!!!)
	;меняет значение регистра Acc и i.
	print proc
				;[SP++]	i
				SIO		UART
				i		0
				X		[X+i] ;косвенная адресация
		@@start:	OUT		[X+i]
				Acc		[X+i]
				i++		0
				SUB		0
				JGE		@@start
				;i		[--SP]
				JMP		[--SP]
	print endp
	
	;более универсальный вариант, здесь любые 16-битные "символы" допустимы. Поэтому в начале строки лежит количество символов
	;считываем его и потом спокойненько отправляем!
	;сейчас затираем регистры Y,i,C. Если они нужны, надо раскомментировать соотв. строки
	;в конце получаем i=0
	;ответный 1 символ лежит по Acc, а также в C. 
	SDsend proc
				;[SP++]	Y
				;[SP++]	i
				SIO		SPI
				Y		SP
				SP		X
				i		[SP++]	;передавать само количество байт мы не должны!
		@@start:	OUT		[SP++]	
				iLOOP		@@start
				SP		Y
				;i		[--SP]
				;Y		[--SP]
				;теперь получаем ответ. Даём SD-карточке 8 байт на "подумать"
				i		7
				OUT		-1	;0xFF по сути
		@@waitR1:	C		IN	;здесь мы, увы, не можем терять ни одного такта
				OUT		-1
				Acc		C
				SUB		0	;проверяем знак (если старший бит 1, значит так ничего и не приняли - "таймаут")				
				JL		@@proceed				
				JMP		[--SP]
		@@proceed:	iLOOP		@@waitR1
				;если дошли досюда, значит, ответа нормального не получили. Объявим таймаут и начнём по-новой...
				X		TimeoutstrAdr
				CALL		print
;				NOP		[--SP]	;адрес возврата не нужен! И не можем допустить бесконтрольного роста стека
;				JMP		InitSD				
				[--SP]	Call(InitSD)
	SDsend endp
	
	;после этой процедуры, JL означает "прыгнуть если РАВНО НУЛЮ", а JGE - "прыгнуть если НЕ РАВНО"
	IsZero proc
		ABS	Acc
		SUB	1
		JMP	[--SP]
	IsZero endp
	
	;в аккумуляторе и регистре C лежит однобайтовый ответ (НЕ 0xFF), хотим подтвердить, что это единичка.
	checkR1 proc
			SUB		R1Idle
			CALL		IsZero
			JGE		@@R1fail
			JMP		[--SP]
	@@R1fail:	X		R1failstrAdr
			CALL		print
			[--SP]	Call(InitSD)
	checkR1 endp


Обилие строк, как тех, что отправляются по UART для отладки, так и команд для SD-карточки, заставило хотя бы для первых из них ввести косвенную адресацию. Непосредственные значения, а значит и адреса, доступны нам в диапазоне от -64 до +63, и если строки разместить именно в этой области, они тупо не влезают. Приходится в самом начале памяти разместить указатели на строки, которые занимают не столь фешенебельные районы. Конечно, можно было бы упаковывать строки более плотно, всё же по 2 байта в слово, но это лишь продлит агонию.

Поэтому даже процедуру print мы чуть модифицировали, что кидаем ей в регистре X адрес УКАЗАТЕЛЯ, а она сама по нему уже переходит, строкой
  X   [X+i]

К счастью, теперь модуль QuatCoreMem у нас как шёлковый :)

Для этой программы логичнее было бы иметь не селектор ввода/вывода и общие для всех устройств адреса IN и OUT, а посадить бы каждое устройство на свой адрес, поскольку процедура print заведомо работает с UART, а процедура SendSD - с SPI. Выиграли бы 4 байта :) В принципе, не так уж сложно переделать, можно в модуль QuatCoreIOselector добавить ещё один параметр, но пока повременим.

В процедуре SDsend мы решили использовать другое представление строки: в первом слове лежит количество символов для отправки, а потом - сами символы. Для 16-битного модуля SPI это было бы необходимо, т.к все 16 бит отправляются на передачу, причём любые посылки допустимы, т.е каким-то образом выделить одну и обозначить как конец передачи нельзя.

Сейчас предполагается использование 8-битного SPI, самого что ни есть стандартного, для него можно было так не заморачиваться. Но оставили пока в таком виде - интересно же, насколько хорошо ложится на ассемблер те или иные решения. Здесь, с издевательством над стеком, легло неплохо! Внутренний цикл сократился до двух строк - отправить значение и сделать цикл по i.

Когда команда была отправлена, мы хотим получить 1 байт ответа - ведь 1 байт всегда должен прийти, иногда и больше. Для этого отправляем ещё один байт, весь из единичек, а затем принимаем ответ, который производится в этот же самый момент. И тут же, не раздумывая - заказываем передачу ещё одного байта. Такая уж специфика нашего дуплексного модуля SPI - между получением ответа и посылкой нового байта нельзя терять ни такта, по кр. мере на максимальной частоте SPI в половину частоты процессора. Если он работает медленнее, то времени на раздумья чуть больше...

И только отправив очередной байт, мы проверяем, а что нам пришло? Сейчас мы так модифицировали 8-битный модуль SPI, что он дублирует "старший" 7-й бит ещё и в 15-й бит, чтобы удобно было проверять знак. Любые ответы SD-карточки по SPI должны начинаться с нуля, если там единичка, значит, карточка ещё "громко думает".

Итак, если уже сформировался какой-то ответ - мы возвращаемся из процедуры SendSD, при этом не забываем, что в этот момент посылается очередной байт на SD-карточку, "все единицы", и параллельно записывается ответ карточки, и командой IN мы можем этот ответ принять.

На формирование ответа мы даём 8 "попыток", после чего начинается интересное - "исключение"!

А именно, процедура SendSD, поняв, что наступил таймаут, сама об этом "ругается" по UART, и вовсе не собирается возвращать выполнение в ту точку, откуда была вызвана. Вместо этого она начинает процедуру инициализации с самого начала. Делается это вот такой командой:

[--SP]	Call(InitSD)


Рядом виден закомментированный код, который делает то же самое, и более понятен, но он длиннее на слово и не такой прикольный:

	NOP		[--SP]	;адрес возврата не нужен! И не можем допустить бесконтрольного роста стека
	JMP		InitSD


ничего сложного - раз при нашем вызове в стек был занесён адрес возврата, то стек нужно сдвинуть назад, а то в стеке будет накапливаться мусор!
А потом спокойно переходим туда, куда хотели.

Строка выше, которая заменила эти две, делает то же самое. Просто мы вспомнили, что для прыжка хватит "половинки команды", только SrcAddr. А для сдвижки стека тоже хватит половинки команды, [--SP]. А то, что адрес возврата будет заменён теперь "адресом возврата" в процедуру SendSD - это так, случайное следствие, ни на что не влияющее.

Ровно такая же хитрость сделана в процедуре CheckR1 . Там же видно, как мы мучаемся без флага нуля. Нам хочется узнать, что ответ совершенно точно 0x01. Для этого мы вычитаем ту самую единичку, которую назвали романтично R1idle, а там бы сразу сделать что-то типа JZ или JNZ, но нет у нас такой радости :)

Так что мы вызываем процедуру IsZero. Она берёт модуль числа, лежащего в аккумуляторе, вычитает единичку - и всё. Только в одном случае результат мог получиться отрицательным - если изначально там был ноль. Так что теперь команда JL (Jump if Less) будет эквивалентна JZ (Jump if Zero), а JGE - команде JNZ.

Чем интересен такой подход - если мы имеем дело с "зашумлёнными данными" и хотим проверить примерное равенство, данная процедура легко модифицируется под любой "эпсилон". А у нас всё-таки процессор в первую очередь "математический", и в его "родных" задачах проверка на точное равенство - вещь невероятно редкая.

Как видно, отсутствие флага нуля усложняет программу, но не так уж сильно. Он стоит нам лишние 6 байт "единовременно" (процедура IsZero) и ещё по 2 байта каждый раз, когда нам нужна проверка на ноль. А если всё-таки осуществить свою авантюру, где процедура вызывается с аргументом, то и вовсе вместо

SUB R1idle
Call IsZero
JL  ...


можно будет написать

CMPZ R1idle
JZ ...

где JZ будет другим названием для JL, а CMPZ будет именем процедуры, которая в первую очередь вычитает из аккумулятора аргумент, а потом уже берёт модуль и определяет, ноль или нет.

Давайте посмотрим, как она работает на симуляции, с модулем SPI_dummy_slave, который просто транслирует MOSI в MISO. Сначала листинг программы, чтобы следить по PC, что у нас выполняется:
    main proc
00  FD40                  SP      StackAddr
01  FDFC                  SP      [SP]
02  CD41                  X       Str0Adr
03  F3B3                  CALL        print
04  CD42          InitSD: X       CMD0strAdr
05  F3B3                  CALL        print
06  CD00                  X       CMD0
07  F3B4                  CALL        SDsend
08  F3B0                  CALL        checkR1
09  CD46                  X       OKCMD0Adr
0A  F3B3                  CALL        print
0B  CD43                  X       CMD8strAdr
0C  F3B3                  CALL        print
0D  CD07                  X       CMD8
0E  F3B4                  CALL        SDsend
0F  F3B0                  CALL        checkR1
10  A003                  i       3
        @@echo: 
11  B811          @@endless:  JMP     @@endless
    main endp
    print proc
12  4000                  SIO     UART
13  A000                  i       0
14  CDC4                  X       [X+i] ;косвенная адресация
15  00C4          @@start:    OUT     [X+i]
16  80C4                  Acc     [X+i]
17  A400                  i++     0
18  8300                  SUB     0
19  B17C                  JGE     @@start
1A  B8FF                  JMP     [--SP]
    print endp
    SDsend proc
1B  4002                  SIO     SPI
1C  DDFD                  Y       SP
1D  FDCD                  SP      X
1E  A0F3                  i       [SP++]  ;передавать само количество байт мы не должны!
1F  00F3          @@start:    OUT     [SP++]  
20  A87F                  iLOOP       @@start
21  FDDD                  SP      Y
22  A007                  i       7
23  007F                  OUT     -1  ;0xFF по сути
24  8A90          @@waitR1:   C       IN  ;здесь мы, увы, не можем терять ни одного такта
25  007F                  OUT     -1
26  8083                  Acc     C
27  8300                  SUB     0   ;проверяем знак (если старший бит 1, значит так ничего и не приняли - "таймаут")                
28  B002                  JL      @@proceed               
29  B8FF                  JMP     [--SP]
2A  A87A          @@proceed:  iLOOP       @@waitR1
2B  CD44                  X       TimeoutstrAdr
2C  F3B3                  CALL        print
2D  FFB1                  [--SP]  Call(InitSD)
    SDsend endp
    IsZero proc
2E  8480          ABS Acc
2F  8301          SUB 1
30  B8FF          JMP [--SP]
    IsZero endp
    checkR1 proc
31  8301              SUB     R1Idle
32  F3B2              CALL        IsZero
33  B102              JGE     @@R1fail
34  B8FF              JMP     [--SP]
35  CD45      @@R1fail:   X       R1failstrAdr
36  F3B3              CALL        print
37  FFB1              [--SP]  Call(InitSD)
    checkR1 endp


Начало нас не очень интересует, более-менее отработано. Смотрим с момента вызова процедуры SendSD, адрес 1B. Быстрее всего отследить по строке Char - это выход нашего "отладочного" UART. Там проскочит сначала набор чисел (так Quartus показывает русские буквы), затем фраза Send CMD0, затем символы перехода строки: 13, 10, и вот тогда мы возьмёмся за SPI:

waveform0.png

Проверяем работу 8-битного SPI. Действительно, передаются байт за байтом, "бесшовно", всё нормально.

Крутим дальше:


мы успешно передаём команду, состоящую из 6 байт: код команды, 4-байтный аргумент (для этой команды - ноль) и последний байт - CRC.

В тот момент, пока передаётся CRC, мы уже заготовили команду передачи "-1", то бишь все единицы. Именно она "бесшовно запускается", после чего мы переходим на команду
24  8A90          C       IN


В результате в регистр C поступает забавное значение 0x80FF - самый старший, 15-й бит дублирует 7-й бит, чтобы удобно было знак проверять. А в целом, мы получили значении 0xFF, что означает "молчание".

Видно, как за один такт до окончания передачи мы переходим с 24-го адреса на 25-й, это команда
25  007F                  OUT     -1

что заставляет нас "бесшовно" начать передавать очередной байт из всех единичек. Нас это не устраивает - и мы продолжаем приём. Тем временем, индексный регистр i уменьшается, отсчитывая оставшиеся попытки...

Смотрим, чем это заканчивается:


Мы вышли из цикла и вызвали процедуру print - ругаемся на таймаут. Отмечаем, что адрес возврата отправился в 0x73 - таково текущее значение SP. По 0x72 лежит адрес возврата в main. Далее мы видим, как начинает отображаться строка "Timeout...". Тем временем, SPI отправил последний байт, и nCS переключилась в единицу. То есть, наш 8-битный SPI ведёт себя корректно. Смотрим, что будет после вывода строки "Timeout":



Возвращаемся в конец процедуры SendSD, и там прыгаем по адресу InitSD, при этом стек возвращается в исходную позицию 0x72 - всё, как и задумано!


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

Сейчас наконец-то опробую на реальной SD-карточке...

UPD. Работает!

Понятно, только первые две команды корректно воспринялись, дальше ещё не программировал, но уже радостно. А если карточка не вставлена, то непрерывно говорит о таймауте и начинает по-новой.
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • А всё-таки есть польза от ковариаций

    Вчера опробовал "сценарий", когда варьируем дальность от 1 метра до 11 метров. Получилось, что грамотное усреднение - это взять с огромными весами…

  • Так есть ли толк в ковариационной матрице?

    Задался этим вопросом применительно к своему прибору чуть более 2 недель назад. Рыл носом землю с попеременным успехом ( раз, два, три, четыре),…

  • Big Data, чтоб их ... (4)

    Наконец-то стряхнул пыль с компьютерной модели сближения, добавил в неё код, чтобы мы могли определить интересующие нас точки, и выписать…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 5 comments