В 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 типа транзакций:
- Control — управляющие посылки, используются для команд получения состояния устройства, и процесса энумерации (определения устройства хостом). Максимальная длина полезной нагрузки пакетов данных для Full Speed устройств составляет 8, 16, 32 или 64 байта. При control передаче в запросе поле PID будет установлено в SETUP.
- — передача массивов. Используются для передачи больших объемов данных на высокой скорости с гарантией доставок и проверкой CRC. Максимальная длина полезной нагрузки пакетов данных для Full Speed устройств составляет 8, 16, 32 или 64 байта.
- — передача по прерыванию. Обычно используются для передачи небольших объемов информации через заданные промежутки времени с гарантией целостности пакетов. Максимальная длина полезной нагрузки пакетов данных для Full Speed устройств составляет 64 байта.
- — изохорные передачи. Используются для передачи больших объемов информации, без гарантии доставки, например видео или звук. Максимальная длинна полезной нагрузки пакетов даных для Full Speed устройств составляет 1023 байта.
Пример управляющей транзакции: хост отправляет первый запрос дескриптора устройства:
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; } }
Начинается процесс энумерации. В общем случае он проходит в следующей последовательности:
- Первый сброс порта. Как только хост обнаружил, что в USB-порт что-то вставлено, он пошлет на шину сигнал RESET;
- Первый запрос дескриптора устройства (запрос GET_DESCRIPTOR для типа дескриптора DEVICE) используя адрес 0. Для Full Speed устройств размер запрашиваемого дескриптора 64 байта, но мы должны послать столько байт, сколько занимает наш дескриптор устройства. Этот запрос дескриптора используется исключительно с целью получить корректный максимальный размер пакета для конечной точки по умолчанию (default control endpoint - 0), размер пакета указан в поле bMaxPacketSize0 дескриптора устройства по смещению 7;
- Второй сброс порта;
- Установка адреса устройства. Хост выделяет устройству уникальный адрес, и выдает его устройству с помощью запроса SET_ADDRESS;
- Второй запрос дескриптора устройства. Выполняется уже по новому адресу;
- Запрос дескриптора конфигурации (запрос GET_DESCRIPTOR для типа дескриптора CONFIGURATION). В качестве длинны, хост укажет 255 байт, но нам надо послать столько, сколько занимает дескриптор конфигурации;
- Возможные запросы строковых дескрипторов;
- Посылка запроса SET_CONFIGURE.
Все запросы имеют одинаковую структуру:
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 Кб)
Комментарии (24) | Я собрал (0) | Подписаться
Для добавления Вашей сборки необходима регистрация
[Автор]
В нескольких словах, отличие в том, что доступ к полям таблицы только 32-битный, это значит, что адрес в периферии отличается от фактического адреса в 2 раза.
У меня для F103C8 на основе библиотеки от стм USB-FS 4.0.0 под прошивку нужно около 8 кбайт. (Проект под кейл).
[Автор]
[Автор]
Сам уже точно сказать не могу, уже года два как с авр дел не имел.
[Автор]
Полтора кб только инициализация мк будет занимать.
[Автор]
Выхлоп компилятора на скриншоте.
Вы не разобрались, как присваиваются идентификаторы сообщениям приема/передачи?
[Автор]
Посмотрите сами, в STM32F0x2 USB реализован аппаратно, по сути его нужно только настроить, а остальное это уже надстройки верхнего уровня. В атмеге 8 USB нет, он реализован программно, соответственно добавляется еще поддержка низкого уровня протокола.
Немного не понял про какие "идентификаторы сообщениям приема/передачи" идет речь? DATA0 и DATA1?
[Автор]
Использую stm32f072, настроен при помощи стандартной библиотеки как virtual com port. При подключении устройства к компьютеру оно однократно устанавливается и ему назначается номер порта. Возникает такая проблема - каждому новому устройству присваивается новый номер com порта, т.е. в каждом usb что-то уникальное и компьютер это видит. К компьютеру подключается всегда только одно устройство, но их 50шт, прошивки одинаковые. Возможно ли stm настроить так, что бы компьютер не видел разницы между этими устройствами.
[Автор]
Я уже долго не могу понять, как отправить пакет байт больше, чем размер эндпоинта. Использую STM32F103С8 интерфейс FF device class vendor specific. Размер 64 байта передает и принимает прекрасно. Во всех описаниях есть возможность переключать(чередовать) Data0 Data1 пакеты при запросе чтения. есть и указатель на смещение размером 2 байта в запросе чтения. Очевидно, что этот запрос должен сгенерировать хост. Но как это сделать. Подскажите мне пожалуйста. Опыт работы с AVR у меня имеется, а с STM32 только начал знакомится. С Уважением,Георгий.
Допустим я хочу использовать две платы связанные с между собой через uart 115200, одна плата host и одна device. К host прицепим мышь. А device должен будет транслировать usb транзации к мыши и ждать ответа от мыши. Думаю NAK потребует из-за задержки. Будет работать такая связка?