Реклама ⓘ
Главная » Микроконтроллеры
Призовой фонд
на апрель 2024 г.
1. 100 руб.
От пользователей

Реклама ⓘ

USB FLASH. Часть 2 - Периферия USB в STM32F0

В 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.

Схема тактирования STM32F04x, STM32F07x и STM32F09x
Рисунок 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 пакетом, далее происходят транзакции, состоящие из запросов, пакетов данных и пакетов подтверждения.

Формат передачи данных через USB

Рисунок 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. Control — управляющие посылки, используются для команд получения состояния устройства, и процесса энумерации (определения устройства хостом). Максимальная длина полезной нагрузки пакетов данных для Full Speed устройств составляет 8, 16, 32 или 64 байта. При control передаче в запросе поле PID будет установлено в SETUP.
  2. — передача массивов. Используются для передачи больших объемов данных на высокой скорости с гарантией доставок и проверкой CRC. Максимальная длина полезной нагрузки пакетов данных для Full Speed устройств составляет 8, 16, 32 или 64 байта.
  3. — передача по прерыванию. Обычно используются для передачи небольших объемов информации через заданные промежутки времени с гарантией целостности пакетов. Максимальная длина полезной нагрузки пакетов данных для Full Speed устройств составляет 64 байта.
  4. — изохорные передачи. Используются для передачи больших объемов информации, без гарантии доставки, например видео или звук. Максимальная длинна полезной нагрузки пакетов даных для 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] – флаги готовности приема и передачи. Могут принимать следующие значения:

  • 00DISABLED – Буфер (RX или TX) не используется;
  • 01STALL – Буфер не работает для текущего запроса, и повторять его бессмысленно;
  • 10NAK – Буфер временно не готов, но нужно опрашивать повторно;
  • 11VALID – Буфер готов.

Установка флага готовности означает установку статуса 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;
	}
}

Начинается процесс энумерации. В общем случае он проходит в следующей последовательности:

  1. Первый сброс порта. Как только хост обнаружил, что в USB-порт что-то вставлено, он пошлет на шину сигнал RESET;
  2. Первый запрос дескриптора устройства (запрос GET_DESCRIPTOR для типа дескриптора DEVICE) используя адрес 0. Для Full Speed устройств размер запрашиваемого дескриптора 64 байта, но мы должны послать столько байт, сколько занимает наш дескриптор устройства. Этот запрос дескриптора используется исключительно с целью получить корректный максимальный размер пакета для конечной точки по умолчанию (default control endpoint - 0), размер пакета указан в поле bMaxPacketSize0 дескриптора устройства по смещению 7;
  3. Второй сброс порта;
  4. Установка адреса устройства. Хост выделяет устройству уникальный адрес, и выдает его устройству с помощью запроса SET_ADDRESS;
  5. Второй запрос дескриптора устройства. Выполняется уже по новому адресу;
  6. Запрос дескриптора конфигурации (запрос GET_DESCRIPTOR для типа дескриптора CONFIGURATION). В качестве длинны, хост укажет 255 байт, но нам надо послать столько, сколько занимает дескриптор конфигурации;
  7. Возможные запросы строковых дескрипторов;
  8. Посылка запроса 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 в следующей статье.

Прикрепленные файлы:

Теги:

Опубликована: Изменена: 14.09.2017 0 0
Я собрал 0 5
x

Оценить статью

  • Техническая грамотность
  • Актуальность материала
  • Изложение материала
  • Полезность устройства
  • Повторяемость устройства
  • Орфография
0

Средний балл статьи: 5 Проголосовало: 5 чел.

Комментарии (24) | Я собрал (0) | Подписаться

0
smack #
Спасибо! Очень толковая статья!
Ответить
0
BARS_ #
Замечательная статья. Про F1 не планируете сделать подобное?
Ответить
+1

[Автор]
sobs #
В F1 практически идентично, отличия только в тактировании и формировании таблицы конечных точек. Про F1 есть информация здесь http://mcu.goodboard.ru/viewtopic.php?id=40
В нескольких словах, отличие в том, что доступ к полям таблицы только 32-битный, это значит, что адрес в периферии отличается от фактического адреса в 2 раза.
Ответить
0
BARS_ #
Спасибо. Попробую на досуге на F103 камне USB пондять. Пробовал через HAL, но там мало что понятно.
Ответить
0
smack #
Скажите, сколько под Ваш проект нужно флэш мк?
У меня для F103C8 на основе библиотеки от стм USB-FS 4.0.0 под прошивку нужно около 8 кбайт. (Проект под кейл).
Ответить
0

[Автор]
sobs #
Без оптимизации около 6 кбайт, но при желании можно еще уменьшить, но код запутанней станет.
Ответить
0
влад #
Понятно, много! У меня под F103C8 на основе библиотеки от стм USB-FS 4.0.0 занимает чуть больше 7-8кб. Но все равно много. Понятно дело, что 32бит и так далее, но помню на атмеге 8 прошивка 2 кб занимала, правда там и связь была только по нулевой конечно точке.
Ответить
0

[Автор]
sobs #
2 КБ реализация mass storage? Можете показать?
Ответить
0
smack #
Нет, не мас сторе. На на нулевой точке типа контрл, до этапа инициализации других точек, т е по управлению.
Сам уже точно сказать не могу, уже года два как с авр дел не имел.
Ответить
0

[Автор]
sobs #
А, ну если используя эту библиотеку сделать custom usb, то проект тоже около 2 КБ занимать будет. Правда для компа нужны будут драйвера.
Ответить
0
smack #
Сомневаюсь!
Полтора кб только инициализация мк будет занимать.
Ответить
0

[Автор]
sobs #
Развею ваши сомнения. Поскольку вы не привели программу, позволяющую оценить ее возможности, в качестве аргумента привожу программу, посылающую принятые данные обратно по USB. Этот код писался во время моего обучения работы с юсби
Выхлоп компилятора на скриншоте.
Прикрепленный файл: Screen.png
Прикрепленный файл: stm32f0_usb_v3.rar
Ответить
0
smack #
sobs, Почему тогда добавление еще одной конечной точки (к нулевой) приводит к 4х кратному увеличению размера прошивки?
Вы не разобрались, как присваиваются идентификаторы сообщениям приема/передачи?
Ответить
0

[Автор]
sobs #
Оно не приводит. В этом проекте (flash), кроме USB есть еще работа с микросхемой флеш памяти и реализация mass storage (протокол SCSI). Тот же проект, что в прошлом моем комментарии, это просто custom usb, для него нужен драйвер, поэтому столько и занимает. В нем, кстати, 2 конечные точки используются. Тот проект на AVR скорее всего так же и сделан.
Посмотрите сами, в STM32F0x2 USB реализован аппаратно, по сути его нужно только настроить, а остальное это уже надстройки верхнего уровня. В атмеге 8 USB нет, он реализован программно, соответственно добавляется еще поддержка низкого уровня протокола.
Немного не понял про какие "идентификаторы сообщениям приема/передачи" идет речь? DATA0 и DATA1?
Ответить
0
CoBa31Rus #
Спасибо отлично все описано!
Ответить
0
Kvanto #
Добрый день! Подскажите, в статье приводится описание подключения МК STM32 как запоминающего устройства Mass Storage и хостом будет выступать ПК, или же подключения запоминающего устройства Mass Storage(флешки) к МК как хосту.
Ответить
0

[Автор]
sobs #
Здравствуйте, хост - ПК.
Ответить
0
Николай #
Здравствуйте.
Использую stm32f072, настроен при помощи стандартной библиотеки как virtual com port. При подключении устройства к компьютеру оно однократно устанавливается и ему назначается номер порта. Возникает такая проблема - каждому новому устройству присваивается новый номер com порта, т.е. в каждом usb что-то уникальное и компьютер это видит. К компьютеру подключается всегда только одно устройство, но их 50шт, прошивки одинаковые. Возможно ли stm настроить так, что бы компьютер не видел разницы между этими устройствами.
Ответить
0

[Автор]
sobs #
Здравствуйте. По идее при одинаковых дескрипторах(обратите внимание на серийный номер, возможно куб пишет туда id микроконтроллера) должно все работать как вы хотите.
Ответить
0
Александр #
Приветствую. Может кто подсказать, как прошить или восстановить возможность вводить процессор стм32ф405 в DFU режим для программирования? Перепаял на полётном контроллере, и не вводится... прошивается через стлинк нормально, всё работает.. кроме этого режима. В программировании не силён, вот гуглю, ищу "знакомые буквы", может кто подскажет как и что.
Ответить
0
mixan23 #
Попробуйте переустановить драйвера с помощью Zadig (https://zadig.akeo.ie/).
Ответить
0
Александр #
Я перепробовал многое. И про задиг в курсе, но он не ставит ничего нового, т.к. проц просто не входит в режим программирования. Фирменные утилиты типа флашлоадердемонстратор пишет, что проблема с загрузчиком.
Ответить
0
Георгий #
Огромное спасибо за вашу статью!
Я уже долго не могу понять, как отправить пакет байт больше, чем размер эндпоинта. Использую STM32F103С8 интерфейс FF device class vendor specific. Размер 64 байта передает и принимает прекрасно. Во всех описаниях есть возможность переключать(чередовать) Data0 Data1 пакеты при запросе чтения. есть и указатель на смещение размером 2 байта в запросе чтения. Очевидно, что этот запрос должен сгенерировать хост. Но как это сделать. Подскажите мне пожалуйста. Опыт работы с AVR у меня имеется, а с STM32 только начал знакомится. С Уважением,Георгий.
Ответить
0
Валерий #
А сколько раз можно хосту на запрос отвечать nak?
Допустим я хочу использовать две платы связанные с между собой через uart 115200, одна плата host и одна device. К host прицепим мышь. А device должен будет транслировать usb транзации к мыши и ждать ответа от мыши. Думаю NAK потребует из-за задержки. Будет работать такая связка?
Ответить
Добавить комментарий
Имя:
E-mail:
не публикуется
Текст:
Защита от спама:
В чем измеряется электрическая мощность?
Файлы:
 
Для выбора нескольких файлов использйте CTRL

Pickit 2 - USB-программатор PIC-микроконтроллеров
Pickit 2 - USB-программатор PIC-микроконтроллеров
Конструктор - Гитарная педаль Remote Delay 2.5 Ручной фен 450 Вт с регулировкой температуры
вверх