В STM32F0 аппаратно реализован USB 2.0 Full Speed интерфейс, работающий на частоте 48 МГц и обеспечивающий скорость до 12 Мбит/с. Он обрабатывает все низкоуровневые операции – прием и передача пакетов в формате NRZI, подсчет и сравнение CRC, и раскладывает всю полезную нагрузку пакетов в соответствующие конечные точки (endpoints). Всего периферия предоставляет нам до восьми конечных точек, для которых доступно 1 Кбайт SRAM. Таким образом, нам нужно правильно настроить периферию USB и вся дальнейшая работа сведется к приему и отправке конечных точек.
Настройка тактирования USB
Как уже говорилось, для работы периферии USB, ее нужно затактировать частотой 48 МГц (рисунок 8). Тут у нас есть два варианта: использовать PLL или HSI48 RC – встроенный тактовый генератор на 48 МГц. Так как частота встроенного тактового генератора может плавать, инженеры ST Microelectronics предусмотрели модуль CRS (Clock Recovery System), который подстраивает выходную частоту HSI48, взяв за эталон частоты другой источник. Это может быть вход микроконтроллера, к которому подключен высокостабильный генератор, выход LSE или принятые USB SOF пакеты. SOF пакеты посылает хост каждую 1 мс ± 500нс (для Full Speed устройств). Остановим выбор на HSI48 и ядро затактируем от него.
//Запускаем HSI48
RCC -> CR2 |= RCC_CR2_HSI48ON;
//Ждем стабилизации частоты на выходе HSI48
while (!(RCC -> CR2 & RCC_CR2_HSI48RDY));
//Тактирование USB от HSI48
RCC -> CFGR3 &= ~RCC_CFGR3_USBSW;
/*
Согласовываем работу FLASH с частотой 48 МГц:
FLASH_ACR_PRFTBE – разрешаем буферизацию предварительной выборки
FLASH_ACR_LATENCY – 001, если 24 МГц < SYSCLK ≤ 48 МГц
*/
FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY;
Настройка модуля CRS. По умолчанию CRS настроен для работы USB, синхронизация по SOF пакетам. Так что нам нужно разрешить автоподстройку частоты и разрешить работу CRS.
Рисунок 1 – Схема тактирования STM32F04x, STM32F07x и STM32F09x
//Включаем тактирование CRS
RCC -> APB1ENR |= RCC_APB1ENR_CRSEN;
//Разрешает автоподстройку частоты
CRS -> CR |= CRS_CR_AUTOTRIMEN;
//Включаем CRS
CRS -> CR |= CRS_CR_CEN;
Назначаем в качестве SYSCLK HSI48:
RCC -> CFGR |= RCC_CFGR_SW;
Оформим это как функцию:
void SetClockHSI48(){
RCC -> APB1ENR |= RCC_APB1ENR_CRSEN;
RCC -> CR2 |= RCC_CR2_HSI48ON;
while (!(RCC -> CR2 & RCC_CR2_HSI48RDY));
RCC -> CFGR3 &= ~RCC_CFGR3_USBSW;
FLASH -> ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY;
CRS -> CR |= CRS_CR_AUTOTRIMEN;
CRS -> CR |= CRS_CR_CEN;
RCC -> CFGR |= RCC_CFGR_SW;
}
Общие сведения о работе USB
Обмен информацией по USB происходит в режиме master-slave. В качестве мастера выступает хост, в качестве слейва – микроконтроллер. Это означает, что мы можем лишь отвечать на запросы мастера, и по своей инициативе мы ничего послать не можем. С точки зрения программы, USB представляет собой набор конечных точек – буферов.
Когда хост хочет прочитать содержимое конечной точки, он отправляет запрос, с указанием номера конечной точки и направления IN. Приняв этот запрос, наша задача заполнить эту конечную точку данными и установить флаг готовности на передачу. Пока флаг готовности передачи не установлен, хост пытается забрать данные, но у него ничего не выходит, но как лишь мы его установим, данные отправятся к хосту и флаг готовности автоматически сбросится.
Когда хост хочет послать данные конечной точке, он отправит запрос с указанием номера конечной точки и направления OUT. Если у конечной точки установлен флаг готовности приема, микроконтроллер аппаратно примет пакет от хоста и автоматически сбросит флаг готовности приема. Если у конечной точки сброшен флаг готовности приема, хост периодически будет продолжать попытки передачи.
Передача данных через USB происходит кадрами (рисунок 2). Продолжительность кадра 1 мс. Каждый кадр начинается SOF пакетом, далее происходят транзакции, состоящие из запросов, пакетов данных и пакетов подтверждения.
Рисунок 2 — Формат передачи данных через USB
SOF пакеты информируют о начале нового кадра. Они имеют следующую структуру:
SYNC
PID
Номер кадра
CRC5
EOP
SYNC – поле синхронизации, все пакеты начинаются с него. Для Full Speed имеет длину 8 бит. Он используется для синхронизации тактов приемника с тактами передатчика.
PID – это поле используется для обозначения типа пакета, который сейчас отправляется. Оно имеет длину 8 бит, составленную из 4х бит типа пакета и их инверсии. Для SOF пакетов поле содержит 01011010.
Номер кадра – представляет собой 11-битное число, которое каждый кадр инкрементируется. При переполнении сбрасывается в 0.
CRC5 – контрольная сумма 5 бит.
EOP – конец пакета.
Запросы (маркер-пакет или token) от хоста имеют следующий вид:
SYNC
PID
ADDR
ENDP
CRC5
EOP
SYNC – поле синхронизации.
PID – для запросов поле PID может принимать следующие значения (первые 4 бита):
- 0001 – OUT (запрос на запись);
- 1001 – IN (запрос на чтение);
- 1101 – SETUP (используется для управляющих передач).
ADDR – это поле указывает какому из устройств, подключенных к USB предназначен пакет. Оно имеет размер 7 бит, что позволяет адресовать 127 устройств. Адрес 0 зарезервирован, он присваивается новым устройствам, до назначения им другого адреса.
ENDP – это поле указывает к какой конечной точке обращается хост. Поле имеет длину 4 бита, что позволяет 16 возможных контрольных точек.
CRC5 – контрольная сумма 5 бит.
EOP – конец пакета.
Важно отметить, что эти запросы обрабатываются аппаратно, и в программе их анализировать не надо.
Следом за запросом может идти пакет данных. Формат пакета данных имеет следующий вид:
SYNC
PID
DATA
CRC16
EOP
SYNC – поле синхронизации.
PID – для пакетов данных в режиме Full Speed, поле PID может принимать следующие значения (показаны первые 4 бита, оставшиеся 4 бита являются их инверсией):
- 0011 — DATA0;
- 1011 — DATA1.
Пакеты данных должны чередоваться DATA0 и DATA1. Если хост примет подряд 2 пакета с одинаковым полем PID, он посчитает это ошибкой и повторит транзакцию.
DATA – полезная нагрузка пакета, именно она запишется или считается из конечной точки. Для Full Speed устройств максимальная длинна поля составляет 1023 байта и ограничивается размером конечной точки. Если требуется передать или принять пакет размером больше размера конечной точки, то данные разбиваются на несколько пакетов данных, причем сначала идут полноразмерные пакеты, а в конце оставшиеся байты.
CRC – контрольная сумма 16 бит.
EOP – конец пакета.
Транзакция завершается пакетом подтверждения (handshake):
SYNC
PID
EOP
SYNC – поле синхронизации.
PID – для пакетов подтверждения, поле PID может принимать следующие значения (показаны первые 4 бита, оставшиеся 4 бита являются их инверсией):
- 0010 — ACK — пакет успешно принят;
- 1010 — NAK — устройство временно не может отправить или принять данные.
- 1110 — STALL — устройство требует вмешательства хоста. Обычно это означает ошибку в протоколе.
EOP – конец пакета.
Всего существуют 4 типа транзакций:
Пример управляющей транзакции: хост отправляет первый запрос дескриптора устройства:
1. Запрос
SYNC
PID: SETUP
ADDRESS: 0x00
ENDPOINT: 0x00
CRC OK
EOP
На этом этапе периферия МК проверит поле адреса, если адрес в запросе совпадает с адресом устройства (при включении все устройства имеют 0 адрес), то периферия прочтет номер конечной точки, к которой обращается хост и тип передачи (SETUP). Каждое устройство должно иметь нулевую конечную точку, именно через нее проходят управляющие передачи.
2. Следом за запросом следует пакет данных:
Если у конечной точки 0 был установлен флаг готовности приема, то выделенные 8 байт пакета данных скопируются в приемный буфер конечной точки, флаг готовности сбросится и произойдет запрос на прерывание.
3. В зависимости от того, был ли установлен флаг готовности приема у конечной точки, периферия отправляет пакет подтверждения с полем PID:ACK (если пакет успешно принят) или PID:NAK (если устройство не готово принять пакет).
SYNC
PID: ACK
EOP
Теперь наша задача проанализировать принятый запрос, скопировать запрашиваемый дескриптор в передающий буфер конечной точки, и установить флаг готовности передачи.
Хост тем временем пытается прочитать дескриптор.
4. Запрос:
SYNC
PID: IN
ADDRESS: 0x00
ENDPOINT: 0x00
CRC OK
EOP
5. Пока флаг готовности передачи сброшен, устройство будет отвечать пакетом подтверждения с полем PID: NAK, и хост периодически будет повторять запрос 4.
SYNC
PID: NAK
EOP
6. Как лишь мы установим флаг готовности передачи, в ответ на запрос 4 устройство отправит хосту пакет данных, и следом за ним пакет-подтверждение с PID: ACK.
SYNC
PID: DATA1
64 байта дескриптора
CRC OK
EOP
SYNC
PID: ACK
EOP
7. Для SETUP передач, когда хост успешно примет пакет(ы) данных, он ответит пустым пакетом данных с установленным полем PID: DATA1. И напротив, когда хост успешно передаст нам пакет(ы), мы должны отправить пустой пакет данных с установленным полем PID: DATA1.
SYNC
PID: OUT
ADDRESS: 0x00
ENDPOINT: 0x00
CRC OK
EOP
SYNC
PID: DATA1
CRC OK
EOP
SYNC
PID: ACK
EOP
Этим и завершается управляющая транзакция.
Регистры USB периферии STM32F0
Пару слов о среде программирования. Я пользуюсь coocox 1.7.8, и когда я начинал разбираться с USB, я был удивлен отсутствием описания регистров USB, поэтому по образу и подобию пришлось их написать (файл usb_defs.h). Так что, если кто-то пишет в других средах, названия регистров в них может отличаться.
Для создания буферов конечных точек в МК предусмотрено 1024 байта памяти, начиная с адреса 0x40006000. Доступ к этой памяти лишь побайтовый, или с помощью полуслов (16 бит). 32-битный доступ запрещен. Расположение и размеры буферов не фиксированы, и должны задаваться с помощью таблицы, расположенной в этой же области памяти. Адрес расположения таблицы задается с помощью регистра USB_BTABLE:
Размер поля таблицы составляет 8 байт, поэтому адрес, записанный в USB_BTABLE, должен быть выровнен по 8 байт (биты 2-0 зарезервированы, и должны быть равны нулю). Обычно в этот регистр записывают 0, тогда таблица располагается с адреса 0x40006000.
Таблица состоит из 4-х полуслов на каждую конечную точку:
Тип
Поле
Описание
uint16_t
USB_ADDR_TX
Адрес начала передающего буфера конечной точки
uint16_t
USB_COUNT_TX
Количество байт, которые нужно передать
uint16_t
USB_ADDR_RX
Адрес начала приемного буфера конечной точки
uint16_t
USB_COUNT_RX
Количество принятых байт
Всего периферия поддерживает 8 конечных точек, соответственно максимальный размер таблицы может быть 64 байта.
В файле usb_defs.h эта таблица описана следующим образом:
#define USB_BTABLE_BASE 0x40006000
#define USB_BTABLE ((USB_BtableDef *)(USB_BTABLE_BASE))
typedef struct{
__IO uint16_t USB_ADDR_TX;
__IO uint16_t USB_COUNT_TX;
__IO uint16_t USB_ADDR_RX;
__IO uint16_t USB_COUNT_RX;
} USB_EPDATA_TypeDef;
typedef struct{
__IO USB_EPDATA_TypeDef EP[8];
} USB_BtableDef;
Отдельно надо разобрать поле USB_COUNT_RX, которое представляет собой регистр вида:
Бит BLSIZE совместно с битами NUM_BLOCK[4:0] определяют размер приемного буфера следующим образом:
Биты COUNTn_RX[9:0] содержат количество принятых байт.
Обращение к полям таблицы происходит следующим образом, где number – номер конечной точки:
//Адрес передающего буфера 0x40006040
USB_BTABLE -> EP[number].USB_ADDR_TX = 0x40;
//Количество байт для передачи — 0
USB_BTABLE -> EP[number].USB_COUNT_TX = 0;
//Адрес приемного буфера 0x40006080
USB_BTABLE -> EP[number].USB_ADDR_RX = 0x80;
//Размер приемного буфера 64 байта (BL_SIZE = 1, NUM_BLOCK = 00001), 0 принятых байт
USB_BTABLE -> EP[number].USB_COUNT_RX = 0x8400;
Для управления конечными точками предназначены 8 регистров USB_EPnR, по одному на конечную точку:
Выделенные биты работают одинаково, лишь первая группа управляет приемником, а вторая передатчиком.
Биты CTR_RX, CTR_TX – флаги событий конечной точки, устанавливаются контроллером USB, если данные успешно приняты (RX) или переданы (TX). Режим доступа к этим битам rc_w0, это значит, что запись 0 сбросит бит, а запись 1 игнорируется.
Биты DTOG_RX, DTOG_TX – эти биты определяют тип ожидаемого (RX) или передающегося (TX) пакета данных. 0 означает пакет DATA0, 1 – DATA1. Эти биты меняют свои значения автоматически, но иногда (например, в начале транзакций энумерации следует установить их в 0, или в конце транзакций энумерации, когда мы ожидаем, или передаем пустой пакет данных, следует установить DTOG_RX = 1, или DTOG_TX = 1 соответственно). Режим доступа к этим битам t (toggle), это значит, что запись 1 инвертирует бит, а запись 0 игнорируется.
Биты STAT_RX[1:0], STAT_TX[1:0] – флаги готовности приема и передачи. Могут принимать следующие значения:
- 00 – DISABLED – Буфер (RX или TX) не используется;
- 01 – STALL – Буфер не работает для текущего запроса, и повторять его бессмысленно;
- 10 – NAK – Буфер временно не готов, но нужно опрашивать повторно;
- 11 – VALID – Буфер готов.
Установка флага готовности означает установку статуса VALID, а по окончании транзакции периферия сама сбросит статус в NAK. Режим доступа к этим битам t (toggle), это значит, что запись 1 инвертирует бит, а запись 0 игнорируется.
Биты EA[3:0] – задают адрес конечной точки. Это сделано для того, чтобы можно было эмулировать конечные точки, номера которых больше 7. К примеру, для конечной точки 1, установив эти биты в 1111, хост будет думать, что это конечная точка 15.
Биты EP_TYPE[1:0] – определяют тип конечной точки:
- 00 – Bulk;
- 01 – Control;
- 10 – Isochronous;
- 11 – Interrupt.
Про эти типы было написано ранее.
Бит SETUP – дополнительный флаг события конечной точки с нулевым адресом и типом control. Устанавливается совместно с CTR_RX, сбрасывается также совместно со сбросом CTR_RX.
Бит EP_KIND – служит для задания дополнительных состояний конечных точек в режиме двойной буферизации. Тут этот режим не рассматривается.
На мой взгляд, ST сделали не очень удобную вещь, замешав разряды с разными режимами доступа (rc_w0, t, rw) в один регистр. Для установки или сброса toggle-битов можно воспользоваться операцией исключающее-или так:
USB_EPnR ^= (нужные значения toggle-битов).
Тут есть недостаток – нужно явно указывать значения всех бит, оставить какой-либо из toggle-бит неизменным не получится. Особенно неудобно становится работа с битами DTOG_RX и DTOG_TX, которые сами аппаратно переключаются, а нам придется следить самим за их переключениями. Я предлагаю другой вариант управления битами, для этого я написал макросы:
- CLEAR_DTOG_RX(R) – Очистка бита DTOG_RX;
- SET_DTOG_RX(R) – Установка бита DTOG_RX;
- KEEP_DTOG_RX(R) – Оставить бит DTOG_RX без изменения;
- CLEAR_DTOG_TX(R) – Очистка бита DTOG_TX;
- SET_DTOG_TX(R) – Установка бита DTOG_TX;
- KEEP_DTOG_TX(R) – Оставить бит DTOG_TX без изменения;
- SET_VALID_RX(R) – Установить значения бит STAT_RX в 11;
- SET_NAK_RX(R) – Установить значения бит STAT_RX в 10;
- SET_STALL_RX(R) – Установить значения бит STAT_RX в 01;
- KEEP_STAT_RX(R) – Оставить значение STAT_RX неизменным;
- SET_VALID_TX(R) – Установить значения бит STAT_TX в 11;
- SET_NAK_TX(R) – Установить значения бит STAT_TX в 10;
- SET_STALL_TX(R) – Установить значения бит STAT_TX в 01;
- KEEP_STAT_TX(R) – Оставить значение бит STAT_TX неизменным;
- CLEAR_CTR_RX(R) – Сброс флага CTR_RX;
- CLEAR_CTR_TX(R) – Сброс флага CTR_TX;
- CLEAR_CTR_RX_TX(R) – Сброс флагов CTR_RX и CTR_TX.
Сброс или установка бит выполняются следующим образом:
//читаем значения регистра 0 конечной точки
uint16_t status = USB -> EPnR[0];
status = SET_VALID_RX(status);
status = SET_NAK_TX(status);
status = KEEP_DTOG_RX(status);
status = KEEP_DTOG_TX(status);
//запись полученного значения в регистр конечной точки
USB -> EPnR[0] = status;
Ограничения этого варианта – перед записью в USB_EPnR, надо обязательно вызывать по макросу на каждый toggle-бит. У этого варианта недостатков больше, чем у предыдущего (например, большущая неатомарность), но мне он показался удобнее, хоть и громоздок.
Я столкнулся с трудностью понимания работы с макросами, поэтому решил расжевать этот вопрос — pdf в конце статьи.
Регистр конфигурации USB_CNTR:
В этом регистре нас больше всего интересуют биты CTRM, RESETM, PWDN и FRES.
Бит CTRM – маска разрешения прерывания по завершению приема или передачи.
0 – прерывание запрещено;
1 – прерывание разрешено.
Бит RESETM – маска разрешения прерывания по событию сброса на шине.
0 – прерывание запрещено;
1 – прерывание разрешено.
Бит PWDN – режим пониженного энергосбережения.
0 – выйти из режима пониженного энергосбережения;
1 – войти в режим пониженного энергосбережения.
Бит FRES – принудительный сброс USB.
0 – выйти из состояния сброса;
1 – принудительно сбросить периферию USB, отправив сигнал сброса на шину. Периферия USB останется в состоянии сброса пока этот бит будет равен 1. Если разрешено прерывание по событию сброса на шине (RESETM = 1), сработает прерывание.
Регистр статуса USB_ISTR:
В этом регистре нас больше всего интересуют биты CTR, RESET, DIR и EP_ID.
Бит CTR – устанавливается аппаратно после завершения транзакции. Используйте биты DIR и EP_ID, чтобы определить направление и номер конечной точки, вызвавшей установку флага. Если разрешено прерывание по завершению приема или передачи (CTRM = 1), сработает прерывание.
Бит RESET – устанавливается после обнаружения сигнала RESET на шине USB. Если установлен бит RESETM в регистре USB_CNTR, сработает прерывание. После обнаружения события RESET заново переконфигурировать USB. Регистры конечных точек сбрасываются автоматически. Этот флаг сбрасывается записью 0.
Бит DIR – показывает, в какую сторону была транзакция:
Если DIR = 0, транзакция типа IN, в регистре USB_EPnR будет установлен бит CTR_TX.
Если DIR = 1, транзакция типа OUT, в регистре USB_EPnR будет установлен бит CTR_RX, либо оба CTR_TX и CTR_RX.
Биты EP_ID – содержат номер конечной точки, к которой относилась транзакция.
Регистр адреса устройства USB_DADDR:
Бит EF – установка в 1 разрешает работу USB.
Биты ADDR[6:0] – адрес устройства USB. При включении питания должен быть равен 0. Должен измениться после принятия запроса SET_ADDRESS во время энумерации.
Регистр детектора заряда батареи USB_BCDR:
В этом регистре нас интересует лишь бит DPPU – внутренний подтягивающий резистор на линии DP. Запись 1 включает его, и хост обнаруживает устройство и начинается процесс энумерации. Запись 0 выключает резистор, и хост думает, что устройство отсоединено.
Теперь можно писать код. Функция инициализации USB:
void USB_Init(){
//Включаем тактирование
RCC -> APB1ENR |= RCC_APB1ENR_USBEN;
RCC -> APB2ENR |= RCC_APB2ENR_SYSCFGEN;
RCC -> AHBENR |= RCC_AHBENR_GPIOAEN;
//Ремапим ноги с USB
SYSCFG -> CFGR1 |= SYSCFG_CFGR1_PA11_PA12_RMP;
//Разрешаем прерывания по RESET и CTRM
USB -> CNTR = USB_CNTR_RESETM | USB_CNTR_CTRM;
//Сбрасываем флаги
USB -> ISTR = 0;
//Адрес таблицы конечных точек с 0x40006000
USB -> BTABLE = 0;
//Включаем подтяжку на DP
USB -> BCDR |= USB_BCDR_DPPU;
//Включаем прерывание USB
NVIC_EnableIRQ(USB_IRQn);
}
После выполнения этого кода, хост сразу пошлет сигнал RESET, соответственно сработает прерывание по событию RESET, там мы и инициализируем конечные точки.
Конечные точки опишем следующей структурой:
typedef struct {
//Адрес передающего буфера
uint16_t *tx_buf;
//Адрес приемного буфера
uint8_t *rx_buf;
//Состояние регистра USB_EPnR
uint16_t status;
//Количество принятых байт
unsigned rx_cnt : 10;
//Флаг события успешной передачи
unsigned tx_flag : 1;
//Флаг события успешного приема
unsigned rx_flag : 1;
//Флаг-маркер управляющей транзакции
unsigned setup_flag : 1;
} ep_t;
И создаем описатели конечных точек:
//Количество конечных точек
#define MAX_ENDPOINTS 2
ep_t endpoints[MAX_ENDPOINTS];
Функция инициализации контрольных точек:
/* Инициализация конечной точки
* number — номер (0…7)
* type — тип конечной точки (EP_TYPE_BULK, EP_TYPE_CONTROL, EP_TYPE_ISO, EP_TYPE_INTERRUPT)
* addr_tx — адрес передающего буфера в периферии USB
* addr_rx — адрес приемного буфера в периферии USB
* Размер приемного буфера — фиксированный 64 байта
*/
void EP_Init(uint8_t number, uint8_t type, uint16_t addr_tx, uint16_t addr_rx){
//Записываем в USB_EPnR тип и номер конечной точки. Для упрощения номер конечной точки
//устанавливается равным номеру USB_EPnR
USB -> EPnR[number] = (type << 9) | (number & USB_EPnR_EA);
//Устанавливаем STAT_RX = VALID, STAT_TX = NAK
USB -> EPnR[number] ^= USB_EPnR_STAT_RX | USB_EPnR_STAT_TX_1;
//Заполняем таблицу для конечной точки
USB_BTABLE -> EP[number].USB_ADDR_TX = addr_tx;
USB_BTABLE -> EP[number].USB_COUNT_TX = 0;
USB_BTABLE -> EP[number].USB_ADDR_RX = addr_rx;
USB_BTABLE -> EP[number].USB_COUNT_RX = 0x8400; //размер приемного буфера
endpoints[number].tx_buf = (uint16_t *)(USB_BTABLE_BASE + addr_tx);
endpoints[number].rx_buf = (uint8_t *)(USB_BTABLE_BASE + addr_rx);
}
Структура, описывающая состояние устройства USB в целом:
//Статус и адрес соединения USB
typedef struct {
/*
Статус:
USB_DEFAULT_STATE – устройство не определено
USB_ADRESSED_STATE — устройство адресовано (получен новый адрес)
USB_CONFIGURE_STATE – устройство сконфигурировано
*/
uint8_t USB_Status;
//Адрес устройства
uint16_t USB_Addr;
} usb_dev_t;
Код обработчика прерывания USB:
void USB_IRQHandler(){
uint8_t n;
//Событие RESET
if (USB -> ISTR & USB_ISTR_RESET){
//Переинициализируем регистры
USB -> CNTR = USB_CNTR_RESETM | USB_CNTR_CTRM;
USB -> ISTR = 0;
//Создаем 0 конечную точку, типа CONTROL
EP_Init(0, EP_TYPE_CONTROL, 128, 256);
//Обнуляем адрес устройства
USB -> DADDR = USB_DADDR_EF;
//Присваиваем состояние в DEFAULT (ожидание энумерации)
USB_Dev.USB_Status = USB_DEFAULT_STATE;
}
//Событие по завершению транзакции
if (USB -> ISTR & USB_ISTR_CTR){
//Определяем номер конечной точки, вызвавшей прерывание
n = USB -> ISTR & USB_ISTR_EPID;
//Копируем количество принятых байт
endpoints[n].rx_cnt = USB_BTABLE -> EP[n].USB_COUNT_RX;
//Копируем содержимое EPnR этой конечной точки
endpoints[n].status = USB -> EPnR[n];
//Обновляем состояние флажков
endpoints[n].rx_flag = (endpoints[n].status & USB_EPnR_CTR_RX) ? 1 : 0;
endpoints[n].setup_flag = (endpoints[n].status & USB_EPnR_SETUP) ? 1 : 0;
endpoints[n].tx_flag = (endpoints[n].status & USB_EPnR_CTR_TX) ? 1 : 0;
//Очищаем флаги приема и передачи, оставляем DTOGи и STATы без изменений
endpoints[n].status = CLEAR_CTR_RX_TX (endpoints[n].status);
//Записываем новое значение регистра EPnR
USB -> EPnR[n] = endpoints[n].status;
}
}
Начинается процесс энумерации. В общем случае он проходит в следующей последовательности:
Все запросы имеют одинаковую структуру:
typedef struct {
uint8_t bmRequestType;
uint8_t bRequest;
uint16_t wValue;
uint16_t wIndex;
uint16_t wLength;
} config_pack_t;
Описание полей запроса:
Стандартом определены 8 стандартных запросов, которые должно поддерживать каждое устройство:
Запрос Get_Status отправленный на устройство вернет 2 байта:
Бит RemoteWakeup – если 1 – у устройства разрешена возможность удаленного пробуждения хоста от спячки или приостановки.
Бит SelfPowered – 1 – устройство имеет свой источник питания, 0 – устройство питается от USB.
Итак, нам нужна функция отправки данных:
/*
uint8_t number – номер конечной точки
uint8_t *buf – указатель на отправляемые данные
uint16_t size – длинна отправляемых данных
*/
void EP_Write(uint8_t number, uint8_t *buf, uint16_t size){
uint8_t i;
uint32_t timeout = 100000;
//Читаем EPnR
uint16_t status = USB -> EPnR[number];
//Ограничение на отправку данных больше 64 байт
if (size > 64) size = 64;
/* ВНИМАНИЕ КОСТЫЛЬ
* Из-за ошибки записи в область USB/CAN SRAM с 8-битным доступом
* пришлось упаковывать массив в 16-бит, собственно размер делить
* на 2, если он был четный, или делить на 2 + 1 если нечетный
*/
uint16_t temp = (size & 0x0001) ? (size + 1) / 2 : size / 2;
uint16_t *buf16 = (uint16_t *)buf;
for (i = 0; i < temp; i++){
endpoints[number].tx_buf[i] = buf16[i];
}
//Количество передаваемых байт
USB_BTABLE -> EP[number].USB_COUNT_TX = size;
//STAT_RX, DTOG_TX, DTOG_RX – оставляем, STAT_TX=VALID
status = KEEP_STAT_RX(status);
status = SET_VALID_TX(status);
status = KEEP_DTOG_TX(status);
status = KEEP_DTOG_RX(status);
USB -> EPnR[number] = status;
//Ждем пока данные передадутся
endpoints[number].tx_flag = 0;
while (!endpoints[number].tx_flag){
if (timeout) timeout—;
else break;
}
}
Функция отправки пустого пакета данных:
void EP_SendNull(uint8_t number){
uint32_t timeout = 100000;
uint16_t status = USB -> EPnR[number];
//Число байт для передачи = 0
USB_BTABLE -> EP[number].USB_COUNT_TX = 0;
//DTOG_TX = 1, STAT_TX = VALID
status = KEEP_STAT_RX(status);
status = SET_VALID_TX(status);
status = KEEP_DTOG_RX(status);
status = SET_DTOG_TX(status);
USB -> EPnR[number] = status;
//Ждем окончания передачи
endpoints[number].tx_flag = 0;
while (!endpoints[number].tx_flag){
if (timeout) timeout—;
else break;
}
}
Функция приема пустого пакета данных:
void EP_WaitNull(uint8_t number){
uint32_t timeout = 100000;
uint16_t status = USB -> EPnR[number];
status = SET_VALID_RX(status);
status = KEEP_STAT_TX(status);
status = KEEP_DTOG_TX(status);
status = SET_DTOG_RX(status);
USB -> EPnR[number] = status;
endpoints[number].rx_flag = 0;
while (!endpoints[number].rx_flag){
if (timeout) timeout—;
else break;
}
endpoints[number].rx_flag = 0;
}
Функция приема пакета данных:
/*
* Функция чтения массива из буфера конечной точки
* number — номер конечной точки
* *buf — адрес массива куда считываем данные
*/
void EP_Read(uint8_t number, uint8_t *buf){
uint32_t timeout = 100000;
uint16_t status, i;
status = USB -> EPnR[number];
status = SET_VALID_RX(status);
status = SET_NAK_TX(status);
status = KEEP_DTOG_TX(status);
status = KEEP_DTOG_RX(status);
USB -> EPnR[number] = status;
endpoints[number].rx_flag = 0;
while (!endpoints[number].rx_flag){
if (timeout) timeout—;
else break;
}
for (i = 0; i < endpoints[number].rx_cnt; i++){
buf[i] = endpoints[number].rx_buf[i];
}
}
Напишем «скелет» функции выполнения энумерации. Вызываться будет из главного цикла:
void Enumerate(uint8_t number){
//Чтобы удобнее обрабатывать запросы «натянем» приемный буфер на тип config_pack_t
config_pack_t *packet = (config_pack_t *)endpoints[number].rx_buf;
//Если пришел пакет данных
if ((endpoints[number].rx_flag) && (endpoints[number].setup_flag)){
//Тут обработка запросов. Из-за громоздкости функции, я далее буду описывать ее кусками
//Полный код функции находится в файле usb_lib.c
//TX = NAK, RX = VALID. Так как все транзакции на 0 конечную точку начинаются с
//DATA0, очищаем DTOGи
status = USB -> EPnR[number];
status = SET_VALID_RX(status);
status = SET_NAK_TX(status);
status = CLEAR_DTOG_TX(status);
status = CLEAR_DTOG_RX(status);
USB -> EPnR[number] = status;
endpoints[number].rx_flag = 0;
}
}
Первым приходит запрос дескриптора устройства.Наш дескриптор устройства выглядит следующим образом (файлы usb_descr.h, usb_descr.c):
//Длина дескриптора устройства в байтах
#define DEVICE_DESCRIPTOR_SIZE_BYTE 18
const uint8_t USB_DeviceDescriptor[] = {
0x12, //bLength
0x01, //bDescriptorType
0x10, //bcdUSB_L
0x01, //bcdUSB_H
0x00, //bDeviceClass
0x00, //bDeviceSubClass
0x00, //bDeviceProtocol
0x40, //bMaxPacketSize
0x83, //idVendor_L
0x04, //idVendor_H
0x11, //idProduct_L
0x57, //idProduct_H
0x01, //bcdDevice_Ver_L
0x00, //bcdDevice_Ver_H
0x00, //iManufacturer – для простоты не используем
0x00, //iProduct – для простоты не используем
0x03, //iSerialNumber – серийный номер
0x01 //bNumConfigurations
};
Все дескрипторы отправляются одинаково, поэтому я опишу код лишь для дескриптора устройства.
В функции Enumerate пишем:
switch (packet -> bmRequestType){
//bmRequestType = 0x80 – стандартный запрос устройству, направление от мк к хосту
case 0x80:
switch (packet -> bRequest){
// bRequest = GET_DESCRIPTOR – запрос дескриптора
case GET_DESCRIPTOR:
switch (packet -> wValue){
//Тип дескриптора – дескриптор устройства
case DEVICE_DESCRIPTOR:
//Если запрашиваемая длина больше размера дескриптора, передаем байты дескриптора
length = ((packet -> wLength < DEVICE_DESCRIPTOR_SIZE_BYTE) ? packet -> wLength : DEVICE_DESCRIPTOR_SIZE_BYTE);
//Передаем дескриптор
EP_Write(number, USB_DeviceDescriptor, length);
//Ожидаем пустой пакет-подтверждение от хоста
EP_WaitNull(number);
break;
…
Успешно выполнив эту последовательность, хост пошлет сигнал сброса. Следующий запрос – установка адреса:
switch (packet -> bmRequestType){
case 0x00:
//bmRequestType = 0x00 – стандартный запрос устройству, направление от хоста к мк
switch (packet -> bRequest){
case SET_ADDRESS:
//Сразу присвоить адрес в DADDR нельзя, потому что хост ожидает подтверждения
//приема со старым адресом
USB_Dev.USB_Addr = packet -> wValue;
//Отправляем пакет подтверждения 0 длины
EP_SendNull(number);
//Присваиваем новый адрес устройству
USB -> DADDR = USB_DADDR_EF | USB_Dev.USB_Addr;
//Устанавливаем состояние в «Адресованно»
USB_Dev.USB_Status = USB_ADRESSED_STATE;
break;
…
Следующим будет повторный запрос дескриптора устройства, лишь хост уже будет обращаться к устройству по новому адресу. Это у нас уже реализовано.
Далее хост посылает запрос дескриптора конфигурации. Ответ идентичен ответу на запрос дескриптора устройства. Стоит отметить, что дескриптор конфигурации отправляется вместе с дескрипторами интерфейса, конечных точек и дескриптором репорта (для HID). Дескриптор конфигурации выглядит следующим образом:
//Размер дескриптора конфигурации в байтах
#define CONFIG_DESCRIPTOR_SIZE_BYTE 32
const uint8_t USB_ConfigDescriptor[] = {
//Дескриптор конфигурации
0x09, //bLength
0x02, //bDescriptorType
CONFIG_DESCRIPTOR_SIZE_BYTE, //wTotalLength_L
0x00, //wTotalLength_H
0x01, //bNumInterfaces
0x01, //bConfigurationValue
0x00, //iConfiguration
0x80, //bmAttributes
0x32, //bMaxPower
//Дескриптор интерфейса
0x09, //bLength
0x04, //bDescriptorType
0x00, //bInterfaceNumber
0x00, //bAlternateSetting
0x02, //bNumEndpoints (один – передача, второй прием)
0x08, //bInterfaceClass (MASS STORAGE)
0x06, //bInterfaceSubClass (SCSI)
0x50, //bInterfaceProtocol (BULK ONLY)
0x00, //iInterface
//Дескриптор конечной точки 1 IN
0x07, //bLength
0x05, //bDescriptorType
0x81, //bEndpointAddress (EP1 IN)
0x02, //bmAttributes (BULK)
0x40, //wMaxPacketSize_L
0x00, //wMaxPacketSize_H
0x00, //bInterval
//Дескриптор конечной точки 1 OUT
0x07, //bLength
0x05, //bDescriptorType
0x01, //bEndpointAddress (EP1 OUT)
0x02, //bmAttributes (BULK)
0x40, //wMaxPacketSize_L
0x00, //wMaxPacketSize_H
0x00 //bInterval
};
Так как мы указали, что используем серийный номер (дескриптор устройства, поле iSerialNumber = 0x03), то следующим происходит запрос строкового дескриптора с нулевым индексом (строковый описатель), который содержит 16-битный код языка, на котором написаны следующие строковые дескрипторы.
const uint8_t USB_StringLangDescriptor[] = {
0x04, //bLength
0x03, //bDescriptorType
0x09, //wLANGID_L (U.S. ENGLISH)
0x04 //wLANGID_H (U.S. ENGLISH)
};
Следом произойдет запрос строкового дескриптора с индексом 3 (серийный номер). Не обязательно 3, просто условимся, что если используется Manufacturer ID, то поле iManufacturer = 0x01, если используется Product ID, по поле iProduct = 0x02, и если есть серийный номер, то поле iSerialNumber = 0x03.
Отправляем наш серийный номер (в формате Unicode).
const uint8_t USB_StringSerialDescriptor[STRING_SERIAL_DESCRIPTOR_SIZE_BYTE] =
{
STRING_SERIAL_DESCRIPTOR_SIZE_BYTE, // bLength
0x03, // bDescriptorType
‘1’, 0,
‘2’, 0,
‘3’, 0,
‘4’, 0,
‘5’, 0,
‘6’, 0,
‘7’, 0,
‘8’, 0
};
Если мы укажем, что наше устройство USB 2.0 или выше, и мы подключим его к порту USB 1.1, то произойдет запрос дескриптора-квалификатора.
const uint8_t USB_DeviceQualifierDescriptor[] = {
0x0A, //bLength
0x06, //bDescriptorType
0x00, //bcdUSB_L
0x02, //bcdUSB_H
0x00, //bDeviceClass
0x00, //bDeviceSubClass
0x00, //bDeviceProtocol
0x40, //bMaxPacketSize0
0x01, //bNumConfigurations
0x00 //Reserved
};
Но потому что мы указали, что наше устройство USB 1.1 (см. дескриптор устройства), то этого запроса не произойдет.
В завершении энумерации произойдет запрос SET_CONFIGURATION, на который мы должны ответить пустым пакетом подтверждения.
switch (packet -> bmRequestType){
…
case 0x00:
switch (packet -> bRequest){
…
case SET_CONFIGURATION:
//Устанавливаем состояние в «Сконфигурировано»
USB_Dev.USB_Status = USB_CONFIGURE_STATE;
EP_SendNull(number);
break;
…
Запрос GET_STATUS:
switch (packet -> bmRequestType){
case 0x80:
switch (packet -> bRequest){case GET_STATUS:
status = 0;
//отправляем состояние
EP_Write(0, (uint8_t *)&status, 2);
EP_WaitNull(number);
break;
…
Для класса Bulk-Only Mass Storage могут приходить ещё 2 классовых запроса:
1. Bulk—Only Mass Storage Reset
Насколько я понял, следом будут идти запросы состояния, и нам надо отвечать NAK, пока не происходит сброс устройства. Как лишь сброс завершится, отвечаем ACK. Мне этот запрос не приходил.
2. GET MAX LUN
Устройство может содержать несколько логических носителей (LUN), и в ответ на этот запрос мы должны его отправить в 1 байте. Наше устройство не поддерживает несколько носителей, поэтому отправляем 0.
Ну вот, теперь энумерация проходит успешно. Если прошить микроконтроллер, то в диспетчере устройств мы должны увидеть:
Рисунок 3 – Наше устройство в диспетчере устройств
Далее хост обращается уже к конечной точке 1, используя протокол SCSI. Потому, в обработчик прерывания USB по RESET добавим функцию инициализации конечной точки 1:
EP_Init(1, EP_TYPE_BULK, 384, 512);
Про SCSI в следующей статье.
Прикрепленные файлы:
- usb_lib.rar (6 Кб)
- Очистка битов EPnR.pdf (41 Кб)