Сегодня мы начинаем писать программу для микроконтроллера, реализующую низкоскоростную версию интерфейса USB. И писать её мы начнём с самого низкого уровня — с физики и приёма пакетов.
Ранее (в предыдущей части, когда схему делали) мы решили:
- что информационные линии у нас будут заводиться на ноги PB0 (D+) и PB1 (D-),
- что определять начало передачи мы будем по переднему фронту на линии D+ (в состоянии IDLE у нас на D+ ноль, а на D- — единица), которую для максимально быстрой реакции мы дополнительно завели на ногу INT0 (для которой возможно установить прерывание по переднему фронту),
- что длительность одного информационного бита у нас будет составлять 8 тактов контроллера (кварц на 12 МГц).
- Кроме того, мы решили, что наше устройство будет управлять тремя светодиодами, которые подключены к ногам PB2, PB3, PB4.
Теперь давайте думать, как мы будем определять значения отдельных битов и принимать пакеты. Для этого давайте вспомним, что мы вообще об этом знаем.
Знаем мы следующее:
- все пакеты начинаются с передачи синхропоследовательности SYNC=0b10000000,
- все данные, кроме CRC передаются младшим битом вперёд (то есть SYNC передаётся в виде: 0b00000001),
- данные передаются в NRZI-кодированном виде, то есть при передачи нуля состояние шины (уровни на линиях D+/D-) меняется на противоположное, а при передаче единицы — остаётся неизменным.
Исходя из этих знаний, а также начального уровня на линии D+ в состоянии IDLE (ноль), можно сделать вывод о том, какие данные на линии D+ мы должны видеть в начале каждого пакета. Закодировав SYNC в NRZI относительно D+, получаем: 0b10101011.
Вспомнив, что по спецификации некоторая часть первоначальных бит синхропоследовательности может потеряться (но мы не знаем сколько именно бит потеряно), остаётся только один способ определения начала пакета, — первые две подряд единицы на линии D+ после выхода из состояния IDLE.
Таким образом, задача определения начала пакета сводится к обнаружению в прерывании INT0 первых двух последовательно передаваемых единиц на линии D+. Следующий за этими двумя единицами бит — это уже первый информационный бит пакета. Плюс нужно учесть, что до того, как мы встретим две подряд единицы, — не должно встречаться два подряд нуля.
Причём, для этого первого информационного бита предыдущее состояние линии D+ будет 1, то есть для него ноль на линии D+ будет соответствовать передаче нулевого бита (состояние линии меняется на противоположное), а единица на D+ — передаче единичного бита (состояние линии не меняется). То есть, мы можем просто записывать через битовые интервалы состояние линии D+, а потом уже декодировать эту последовательность, исходя из правил NRZI (все данные у нас для этого есть, — набор состояний линии и знание, что первое состояние соответствует значению передаваемого бита).
Осталось определить конец пакета. Определить его мы можем по сигналу SE0 (в течении двух битовых интервалов на обоих информационных линиях ноль).
Кроме того, здесь же давайте учтём следующий факт: если достаточно долго ничего не происходит и обе линии при этом притянуты к нулю, — значит мы видим DISCONNECT и наш USB интерфейс нужно перезагрузить (сбросить адрес, текущее состояние и всё прочее).
Далее, обдумаем сразу ещё один момент, — нужно ли принимать весь пакет целиком или, увидев неправильный PID или адрес, остаток пакета можно игнорировать? Рассуждаем так, — если мы выйдем из прерывания не дождавшись конца пакета, то мы сразу же опять попадём в прерывание, поскольку положительные фронты будут случаться на D+ всё то время, пока идут какие-то данные. Но теперь уже мы попадём не в начало пакета, а в середину. И не факт, что из данных, которые мы получим из середины, не сложится какой-то пакет, который будет похож на адресованный нам и даже такой, у которого окажется правильный CRC. Да и вообще, зачем рисковать, если прерывание в любом случае будет срабатывать и нас всё равно будут отвлекать. Поэтому, придётся нам в любом случае принимать все пакеты целиком, независимо от того, можем ли мы сразу опознать, что пакет предназначен не нам или нет, а потом уже решать, что с этим делать.
Следом сразу всплывает ещё одна проблема. «Сырые» данные мы будем складывать в буфер в оперативе, но поскольку контроллер у нас мелкий, то и оператива не резиновая. Тут можно схитрить. Давайте договоримся, что наши полезные данные могут быть размером максимум 8 байт (они по-моему на LS и так могут быть максимум 8 байт, не помню, надо спеки смотреть). Это значит, что, без учёта поля Sync, максимальный размер предназначенного для нас пакета может быть 1(PID+CHECK)+8+2(CRC16) байт. Сколько в таком сообщении может быть битовых вставок? С учётом того, что вставка делается каждые 6 подряд единичных бит, получаем максимум 11*8/6=14 вставок. Это при условии, что абсолютно все биты будут единицами (реально нужно было бы хотя бы PID+СHECK отсюда исключить). То есть нам нужен буфер для входящих данных максимум на 13 байт (маркер-пакеты и пакеты подтверждения в любом случае короче). А если входящих данных будет больше? Не будем записывать лишнее, да и дело с концом. Это точно не нам, так чего напрягаться, — будем просто дожидаться конца пакета.
Всё, вот теперь, учитывая всё вышесказанное, давайте напишем начало программы:
.device ATtiny2313 .include "tn2313def.inc" .list ;--- определяем выводы портов --- .equ InputPort = PINB ; отсюда читаем .equ OutputPort = PORTB ; сюда пишем .equ Direction = DDRB ; выбор направления .equ USBDPlus = 0 ; PB0 - DATA+ .equ USBDMinus = 1 ; PB1 - DATA- .equ LED0 = 2 .equ LED1 = 3 .equ LED2 = 4 ;--- вспомогательные константы --- .equ USBPinMask = ~((1<<USBDMinus)|(1<<USBDPlus)) ; 0b11111100 ;--------------------------------- ;--- распределение памяти -------- .equ MaxUSBBytes = 13 ; максимальный размер буфера для "сырых" данных .equ StackTop = RAMEND ; вершина стека .equ InputBuffer = RAMEND-127 ; начало буфера "сырых" входных данных USB (0) ;------------------------------------------------------ .def temp0 = r16 ; temporary register .def temp1 = r17 ; temporary register .def InputReg = r18 ; входной регистр (сюда читаем значения линий) .def ShiftReg = r19 ; сдвиговый регистр (сюда копим принимаемые биты) .def USBBufPtrXL = r26 ; XL - указатель на буфер USB .def USBBufPtrXH = r27 ; XH - указатель на буфер USB ;****************************************************** ;-- начало программного кода .cseg .org 0 rjmp Init ; переход на начало программы (вектор сброса) ;-- дальше идут вектора прерываний rjmp IRQ_INT0 ; внешнее прерывание INT0 .org WDTaddr+1 ; программа начинается за таблицей векторов ;----------------------------------------------------------- ;-- начало программы (инициализация портов и переменных) --- Init: ldi temp0, StackTop out SPL,temp0 ; инициализируем стек ldi temp0,(1<<LED0)+(1<<LED1)+(1<<LED2) out Direction,temp0 ; линии светодиодов - выходы ldi temp0,0b11111011 out PORTD,temp0 ; включаем подтяжки на PORTD, кроме PD2(INT0) rcall USBReset ; обнуление адресов, сброс состояний и т.д. ldi temp0,0x0F ; INT0 - прерывание по переднему фронту out MCUCR,temp0 ldi temp0,1<<INT0 ; включаем внешнее прерывание INT0 out GIMSK,temp0 sei ; разрешаем немаскированные прерывания ;--- Основной цикл --- General_loop: sbis InputPort,USBDminus ; если D- = 0, то возможно это Disconnect, rjmp CheckUSBReset ; в случае которого мы будем делать Reset rjmp General_loop ; если D- = 1, то считаем, что это IDLE ;--- Проверяем, не случился ли Disconnect --- CheckUSBReset: ldi temp0,255 ; будем отсчитывать 255 циклов ; если за это время не изменится состояние D- (так и останется 0), то ; считаем, что случился Disconnect и нужен Reset WaitUSBReset: sbic InputPort,USBDminus ; если D- всё ещё ноль - пропустить rjmp General_loop dec temp0 ; уменьшаем счётчик brne WaitUSBReset ; прыгаем, если не ноль rcall USBReset rjmp General_loop ;--- Конец основного цикла --- ;********************************************************** ;--- Сброс USB (обнуление адресов, сброс состояний ...) --- USBReset: ret ;********************************************************** ;--- Внешнее прерывание INT0 (положительный фронт на D+) --- IRQ_INT0: ldi temp0,2 ; готовимся отсчитывать количество одинаковых битов ldi temp1,2 ; определяем момент смены бита CheckDMOne: sbis InputPort,USBDMinus rjmp CheckDMOne ;--- теперь ждём единичный бит (в синхропоследовательности это в ;--- обязательном порядке будет состояние, когда D+ = 1) ;--- момент обнаружения этого бита будем использовать для синхронизации ;--- (от него начинаем отсчёт битовых интервалов, поскольку дальнейшие куски ;--- кода должны быть строго выверены по количеству тактов в отладчике) CheckDPOne: sbis InputPort,USBDPlus ; самое начало единичного бита rjmp CheckDPOne ; 2-й такт DetectSyncEnd: sbis InputPort,USBDPlus ; 3,4-й такты (D+=1) или 3-й такт (D+=0) rjmp TestBit0 ; -/- 4,5-й такты (D+=0) TestBit1: ldi temp0,2 ; 5-й такт (сбрасываем счётчик нулей) dec temp1 ; 6-й такт nop ; 7-й такт (задержка, чтобы получить 8 тактов на бит) breq USBBeginPacket ; 8-й такт при temp1>0 или 8-й и 1-й при temp1=0 rjmp DetectSyncEnd ; 1,2-й такты TestBit0: ldi temp1,2 ; 6-й такт (сбрасываем счётчик единиц) dec temp0 ; 7-й такт nop ; 8-й такт brne DetectSyncEnd ; 1,2-й такты при temp0>0 ;--- сюда попадаем, если два подряд бита равны нулю --- ;--- считаем, что это был глюк и просто выходим --- ExitFromIRQ: reti ;--- начинаем принимать информационные биты --- USBBeginPacket: ; сюда мы попадаем после 1-го такта первого информационного бита ;--- читаем первый информационный бит байта --- in ShiftReg,InputPort ; и сразу пишем его в сдвиговый регистр nop ; 3-й такт USBLoopBegin: ldi temp0,6 ; счётчик битов (для следующих 6 битов) ldi temp1,MaxUSBBytes ; счётчик байтов ldi USBBufPtrXL,InputBuffer ; 6-й такт nop ; 7-й такт USBLoopByte: nop ; 8-й такт ;--- читаем 2-7 информационные биты --- USBLoop27: in InputReg,InputPort ; читаем значение битов D+/D- | 1-й такт cbr InputReg,USBPinMask ; сбрасываем все биты порта, кроме значений D+/D- breq EndPacket ; если оба нули - конец пакета | 3-й такт, если не ноль ror InputReg ; вытесняем D+ в CF | 4-й такт rol ShiftReg ; и пишем его в сдвиговый регистр | 5-й такт dec temp0 ; прочитали 7 битов? | 6-й такт brne USBLoop27 ; если нет - читаем (7,8-й такты) nop ; 8-й такт ;--- читаем последний бит байта --- USBLoop8: in InputReg,InputPort ; 1-й такт cbr InputReg,USBPinMask ; сбрасываем все биты порта, кроме значений D+/D- breq EndPacket ; если оба нули - конец пакета | 3-й такт, если не ноль ror InputReg ; вытесняем D+ в CF | 4-й такт rol ShiftReg ; и пишем его в сдвиговый регистр | 5-й такт ldi temp0,7 ; настраиваем счётчик на приём остальных 7 битов st X+,ShiftReg ; сохраняем в буфер принятый байт | 7,8-й такт ;--- читаем первый бит очередного байта --- USBLoop1: in ShiftReg,InputPort ; сразу пишем его в сдвиговый регистр cbr InputReg,USBPinMask ; сбрасываем все биты порта, кроме значений D+/D- breq EndPacket ; если оба нули - конец пакета | 3-й такт, если не ноль dec temp0 ; 4-й такт dec temp1 ; 5-й такт brne USBLoopByte ; 6,7-й такт, если не 0 | 6-й такт, если 0 ;--- если буфер переполнен - остальные биты просто не записываем, ;--- (наш пакет по-любому меньше, но конца пакета нужно дождаться) BufferOverrange: nop ; 7-й такт nop ; 8-й такт in InputReg,InputPort ; 1-й такт cbr InputReg,USBPinMask ; сбрасываем все биты порта, кроме значений D+/D- breq EndPacket ; если оба нули - конец пакета | 3-й такт, если не ноль nop ; 4-й такт rjmp BufferOverrange ; 5,6-й такт ;--- закончили принимать пакет и начинаем его анализировать --- ;--- попадаем сюда на 5-м такте первого или второго битового --- ;--- интервала SE0, теперь нужно как можно быстрее понять чего --- ;--- от нас хотят и ответить (или не ответить, если хотят не от нас) --- EndPacket: cpi USBBufPtrXL,InputBuffer+3 ; приняли хотя бы 3 байта? brcs ExitFromIRQ ; если нет - просто выходим ;********************************************* ;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ;--- а вот если да - начинаем разбирать пакет reti |
Ну вот пока и всё. Приведённый выше код позволяет нам считывать с шины USB все low speed пакеты (целиком или первые 13 байт), сохранять принятые данные в SRAM по адресам 0x60-0x6C, а так же определять концы принимаемых пакетов. В следующей части нужно будет решить ещё больше интересных вопросов: как быстро отправить хосту подтверждение, как из «сырых» данных восстановить передаваемую информацию и что с этой информацией делать дальше.
- Часть 1. Основы.
- Часть 2. Как происходит передача данных по шине.
- Часть 3. Что должно уметь любое USB-устройство.
- Часть 4. Дескрипторы и классы.
- Часть 5. Программная реализация LS устройства USB. Схема.
- Часть 6. Программная реализация LS устройства USB. Физика и приём пакетов.
- Часть 7. Программная реализация LS устройства USB. Разбираем пакеты по типам.
- Часть 8. Программная реализация LS устройства USB. Передача по USB произвольного буфера и пакетов подтверждения.
- Часть 9. Программная реализация LS устройства USB. Продолжаем разбираться с принятыми пакетами.