![]() |
![]() |
|||||||||||||||
Реализуем COM-порт на STM32F3Discovery с CMSIS
Автор: isx, a13428713@yandex.ru Приветствую форумчан! Давно собирался написать статью для форума, но никак не получалось - то времени нет, то нужного настроя. Сильно не пинаем - статья первая :) . Но сначала поздравим виновника всей этой суеты, которая сейчас происходит на форуме. 11 лет - это серьезный возраст. От себя пожелаю дальнейшего развития, творческих успехов и новых пользователей. Ну и колбасы конечно, куда ж без нее :) . Хочу поделиться результатами своих долгих мучений (успех был достигнут спустя 3 месяца после начала работ). Еще со времен работы с AVR я планировал запилить USB, но руки так и не доходили. Со временем прогресс меня настиг и я слез с 8-ми битных контроллеров на 32-битные ARM, а точнее – STM32. Аппаратная реализация USB на камне долгое время манила меня, но как всегда – не хватало времени. В конце концов, я плюнул и подцепил шнур к пользовательскому разъему. Так и началось практическое знакомство с данной шиной. В этой статье не будет использоваться HAL и SPL (да простят меня любители сих творений, но для них примеров и так много :) ) и в ней будет содержаться «топорный» код от которого у многих завсегдатых может случиться нервный тик, но сделано это специально – для полной наглядности. Не смотря на то, что в сети довольно много информации по данной теме, разобраться мне, как начинающему, было не просто. Многие скажут: «Зачем было изобретать велосипед, если есть готовые библиотеки?». Да, они есть, но, к примеру, в той же STM-овской черт ногу сломит, но самое важное это то, что я не хотел при каждой компиляции проекта с USB ждать тучу времени, пока все это соберется в единую кучу. К тому же, я предпочитаю знать точно, как работает мой код (при отладке тестовых версий устройства это очень важно). Итак, лирическое отступление окончено, переходим к делу :) . Вдаваться в подробности структур пакетов, транзакций и пр. мы не будем, так как это прекрасно и с картинками расписано в сети. Да и информация эта нам не понадобится (за исключением немногих моментов, которые мы все же здесь затронем). Проект построен на CMSIS, железо – STM32F3Discovery. Процесс настройки USB можно условно разделить на два этапа – железный и аппаратный. В одной из статей я наткнулся на информацию о том, что нужно сделать подтяжку пина USB установкой бита USB_PU, но как я не пытался его найти – Reference Manual (далее - RM) не позволял мне этого сделать. Дело оказалось в том, что в моем микроконтроллере он тупо не предусмотрен и подтягивается нужный пин припаянным на плате резистором в 1.5К. Кто не в курсе – поясняю, что подтяжка 3.3V к D+ сообщает хосту, что подключено FULL или HIGH speed устройство, а если к D- то устройство Low-Speed. Если нет подтяжки, то можно хоть повеситься на проводе – хост будет нас слать куда подальше :) . Припаиваем разъем если его нет и на этом можно сказать, что работа с железом завершена. Программная часть включает в себя настройку тактирования, пинов, Flash, прерываний и самого USB. Ну, чтож, поехали. Для начала инклудим элемент CMSIS: #include <stm32f30x.h> Позже мы еще вернемся сюда. Как вы наверное знаете, по умолчанию вся периферия в рассматриваемом семействе микроконтроллеров отключена. Запускаем тактирование от внешнего генератора: RCC->CFGR &= ~RCC_CFGR_SW; //Очистка битов источника тактового сигнала RCC->CR |= RCC_CR_HSEON; // Тактирование от внешнего генератора while((RCC->CR & RCC_CR_HSERDY)==0) {} //Ждем готовности внешнего генератора Тактировать USB лучше от внешнего кварца, так как от внутреннего он может тупо не завестись даже с рабочим кодом. USB имеет строго определенные тайминги, а частота внутреннего генератора сильно завит от температуры, фазы Луны и прочих факторов - от этого и все проблемы. Далее отложим тактирование и перейдем к Flash. Flash в STM32 не может работать на высоких скоростях (24МГц предел, насколько я помню), поэтому для этой периферии предусмотрен свой предделитель частоты. Настраиваем его: FLASH->ACR |= FLASH_ACR_LATENCY_1; // Пропускаем по два такта (работаем с каждым третьим) FLASH->ACR |= FLASH_ACR_PRFTBE; // включаем упреждающее чтение while((FLASH->ACR & FLASH_ACR_PRFTBS)==0) {} // ожидаем установки бита Собственно и все. Возвращаемся к тактированию и включим PLL (умножитель частоты, если кто не в курсе :) ) : RCC->CFGR |= RCC_CFGR_PLLSRC; //Источник сигнала для PLL - HSE (внешний генератор) RCC->CR &= ~RCC_CR_PLLON; //Отключаем генератор PLL RCC->CFGR &= ~RCC_CFGR_PLLMULL; //Очищаем биты предделителя RCC->CFGR |= RCC_CFGR_PLLMULL_0 | RCC_CFGR_PLLMULL_1 | RCC_CFGR_PLLMULL_2; //Коефициент умножения 9 (при кварце на 8 МГц будет 72 МГЦ) RCC->CFGR |= RCC_CFGR_PPRE1_2; // Претделитель низкоскоростной шины APB1 на 2 RCC->CR |= RCC_CR_PLLON; //Включаем PLL while((RCC->CR & RCC_CR_PLLRDY)==0) {} //Ждем готовность PLL //Переключаемся на тактирование от PLL RCC->CFGR |= RCC_CFGR_SW_1 ; //Источник тактового сигнала - PLL while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_1) {} //Ждем переключения на PLL Описывать здесь я больше ничего не буду, так как в комментариях кода все разложено по полочкам. Переходим к тактированию необходимой периферии: RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOEEN; // Включаем тактирование портов А и Е (здесь у нас пины USB и подпаянные на плату светодиоды) //RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; // Если Ваш контроллер имеет внутренний резистор подтяжки USB, то возможно будет нужно включить тактирование SYSCFG Настраиваем Порт А: GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR11 | GPIO_OSPEEDER_OSPEEDR12; // Скорость пинов 50 МГц GPIOA->MODER |= GPIO_MODER_MODER11_1 | GPIO_MODER_MODER12_1; // Режим альтернативной функции для пинов USB GPIOA->AFR[1] |= 0x000EE000; // Номер и пины альтернативной фунции (у нас пины 11 и 12 для альтернативной функции номер 14 - USB) Настраиваем Порт Е (здесь сидят светодиоды): GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR8 | GPIO_OSPEEDER_OSPEEDR9 | GPIO_OSPEEDER_OSPEEDR10 | GPIO_OSPEEDER_OSPEEDR11 | GPIO_OSPEEDER_OSPEEDR12 | GPIO_OSPEEDER_OSPEEDR13 | GPIO_OSPEEDER_OSPEEDR14 | GPIO_OSPEEDER_OSPEEDR15; // Скорость для указанных пинов 50 МГц GPIOE->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER9_0 | GPIO_MODER_MODER10_0 | GPIO_MODER_MODER11_0 | GPIO_MODER_MODER12_0 | GPIO_MODER_MODER13_0 | GPIO_MODER_MODER14_0 | GPIO_MODER_MODER15_0; // Указанные пины на выход Настраиваем вектор прерывания для USB (в разных моделях строка в скобках может сильно отличаться) NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 8); Делаем вызов функции, в которой производится настройка регистров USB, так как если делать все в основной функции, то получится каша. Для кота оно, конечно, хорошо, а вот работать с этим жутко не удобно. MyUSBinit(); Разрешим прерывания глобально: __enable_irq (); И делаем бесконечный цикл: while(1) { } Ну все, начало положено, теперь можно и переходить к регистрам самого USB. Как я говорил, все регистры мы будем отрабатывать в отдельной функции, а находиться она будет в отдельном файле, поэтому создадим их. Создадим отдельную папку (так будет удобнее), и в ней два файла (я обозвал их MyUSBsetting.h и MyUSBsetting.c). В том, что имеет расширение .h пишем чудовищно сложный код (слабонервным не смотреть!) : void MyUSBinit(void); и не забываем включить файл с определением нашей функции в САМОМ НАЧАЛЕ ОСНОВНОГО кода: #include <MyUSBsetting.h> Все. Вся дальнейшая работа будет проводиться в файле с расширением .c . Переходим в него и снова инклудим stm32f30x.h: #include «stm32f30x.h» Создаем переменные, которые нам понадобятся: /*Необходимы для тестирования*/ uint64_t RXBuff[100] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; uint8_t test1 = 0; // Для установки брейкпоинта uint32_t iTemp = 0; // Для счетчиков uint8_t EnterFuncCountRX = 0x00; // счетчик "посещений" прерывания по успешному приему данных uint8_t EnterFuncCountTX = 0x00; // счетчик "посещений" прерывания по успешной передаче данных uint8_t NumOfRepeatTX = 0x00; // с этой переменной в отладчике устанавливается брейкпоинт на необходимом "посещении" прерывания (успешная передача данных) по счету (3-е прерывание, 2-е и т.д.) uint8_t NumOfRepeatRX = 0x00; // с этой переменной в отладчике устанавливается брейкпоинт на необходимом "посещении" прерывания (успешный прием данных) по счету (3-е прерывание, 2-е и т.д.) /*Не используются в данном проекте, но в будущем могут пригодиться*/ uint8_t DataSizeToRead = 0x00; // установка количества необходимых для считывания байт uint8_t DataSizeToSend = 0x00; // установка количества необходимых для записи байт /*Буферы для принятых и передаваемых данных*/ uint8_t DataReaded[64] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; uint8_t DataToSend[64] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; uint8_t AdressBuff = 0; // временное хранилище для присваиваемого нашему устройству адреса USB Далее добавим дескрипторы (они не мои, я их брал из сети и немного правил для себя). Первым будет дескриптор устройства: uint8_t Virtual_Com_Port_DeviceDexyinaor[] = { 0x12, // размер данного дескриптора 0x01, // тип данного дескриптора - device dexyinaor 0x00, 0x02, // 2 байта - версия usb 2.0 0x02, // класс устройства cdc 0x00, // подкласс 0x00, // протокол 0x40, // USB_MAX_PACKET0, // max размер пакета для нулевой конечной точки 0x83, 0x04, // 2 байта - VID 0x40, 0x57,// 2 байта - PID 0x00, 0x02,// 2 байта - версия (ревизия) устройства 0x01, // индекс строки с названием производителя 0x02, // индекс строки с названием устройства 0x03, // индекс строки с серийным номером устройства 0x01 // количество поддерживаемых конфигураций }; Дескриптор конфигурации: uint8_t Virtual_Com_Port_ConfigDexyinaor[] = { /* ============== CONFIGURATION 1 =========== */ /* Configuration 1 dexyinaor */ 0x09, // размердескриптораконфигурации 0x02, // тип дескриптора - configuration 0x43, // 2 байта - полный размер дескриптора, включая др. дескрипторы 0x00, // (CwTotalLength 2 EP + Control) 0x02, // количество интерфейсов (2 интерфейса для CDC - data и config) 0x01, // номер данной конфигурации (SET_CONFIGURATION) 0x00, // индекс строки описывающий данную конфигурацию 0xC0, // битовое поле, характеризующее конфигурацию /* Распределение бит: D7 – зарезервировано (установлено в 1); D6 – признак наличия собственного источника питания; D5 – признак разрешения сообщения хосту о выходе устройства из режима «сна»; D4...D0 – зарезервированы (сброшены в 0) */ 0x64, // max потребляемый ток от шины (половина от реального (50 = 100мА))
/* Communication Class Interface Dexyinaor Requirement */ 0x09, // длина дескриптора 0x04, // тип дескриптора - интерфейс 0x00, // номер данного интерфейса 0x00, // номер альтернативной установки для интерфейса 0x01, // количество точек для данной альтернативной установки в данном интерфейсе 0x02, // код класса(USB-IF) 0x02, // код подкласса(USB-IF) 0x00, // код протокола(USB-IF) 0x01, // индекс строки, описывающей данную альтернативную установку данного интерфейса
/* Header Functional Dexyinaor */ 0x05, // bFunction Length 0x24, // bDexyinaor type: CS_INTERFACE 0x00, // bDexyinaor subtype: Header Func Desc 0x10, // bcdCDC:1.1 0x01,
/*Call Management Functional Dexyinaor */ 0x05, /* bFunctionLength */ 0x24, /* bDexyinaorType: CS_INTERFACE */ 0x01, /* bDexyinaorSubtype: Call Management Func Desc */ 0x00, /* bmCapabilities: device handles call management */ 0x01, /* bDataInterface: CDC data IF ID */
/* ACM Functional Dexyinaor */ 0x04, // bFunctionLength 0x24, // bDexyinaor Type: CS_INTERFACE 0x02, // bDexyinaor Subtype: ACM Func Desc 0x02, // bmCapabilities
/* Union Functional Dexyinaor */ 0x05, // bFunctionLength 0x24, // bDexyinaorType: CS_INTERFACE 0x06, // bDexyinaor Subtype: Union Func Desc 0x00, // bMasterInterface: Communication Class Interface 0x01, // bSlaveInterface0: Data Class Interface
/* Endpoint 1 dexyinaor */ 0x07, // размердескриптора 0x05, // типдескриптора - endpoint 0x81, // битовое поле адреса точки // IN /* D7 – направление передачи данных точкой (1 – IN, 0 – OUT); D6...D4 – зарезервированы (сброшены в 0); D3...D0 – адрес точки */ 0x03, // битовое поле, характеризующее точку /* D7, D6 – зарезервированы (сброшены в 0); D5, D4 – функция, выполняемая точкой: 00 – точка данных; 01 – точка обратной связи; 10 – точка данных с неявной обратной связью; 11 – зарезервировано D3, D2 – тип синхронизации хоста и точки: 00 – без синхронизации; 01 – асинхронный; 10 – адаптивный; 11 – синхронный D1, D0 – тип обмена данными: 00 – контрольный; 01 – изохронный; 10 – bulk; 11 – interrupt */
64, // 2 байта - битовое поле // характеризующее размер пакета передаваемых данных
0x00, /* D15…D13 – зарезервированы (сброшены в 0); D12, D11 – количество дополнительных передач: 00 – нет дополнительных передач; 01 – 1 дополнительная передача (всего 2 передачи), 10 – 2 дополнительные передачи (всего 3 передачи); 11 – зарезервировано. D10...D0 – размер пакета в байтах */ 0xFF, // интервал готовности точки к обмену данными
/* Data Class Interface Dexyinaor Requirement */ 0x09, // bLength 0x04, // bDexyinaorType 0x01, // bInterfaceNumber 0x00, // bAlternateSetting 0x02, // bNumEndpoints 0x0A, // bInterfaceClass 0x00, // bInterfaceSubclass 0x00, // bInterfaceProtocol 0x04, // iInterface
/* Endpoint 2 dexyinaor */ 0x07, // bLength 0x05, // bDexyinaorType 0x02, // bEndpointAddress, Endpoint 01 - OUT 0x02, // bmAttributes BULK 64, // wMaxPacketSize 0x00, 0x00, // bInterval
/* Endpoint 3 dexyinaor */ 0x07, // bLength 0x05, // bDexyinaorType 0x83, // bEndpointAddress, Endpoint 02 - IN 0x02, // bmAttributes BULK 64, // wMaxPacketSize 0x00, 0x00 // bInterval };
Строковые дескрипторы:
/* USB String Dexyinaors */ uint8_t Virtual_Com_Port_StringLangID[] = { 0x04, // длина дескриптора 0x03, // тип дескриптора - string desc 0x09, // N байт индетификатор языка 0x04 /* LangID = 0x0409: U.S. English */ };
uint8_t Virtual_Com_Port_StringVendor[] = { 12, // длинадескриптра 0x03, // типдескриптора - string desc /* имя */ 'T', 0, 'E', 0, 'S', 0, 'T', 0, 'V', 0 };
uint8_t Virtual_Com_Port_StringProduct[] = { 12, // длинадескриптра 0x03, // типдескриптора - string desc /* имя */ 'T', 0, 'E', 0, 'S', 0, 'T', 0, 'P', 0 };
uint8_t Virtual_Com_Port_StringSerial[] = { 12, // длинадескриптра 0x03, // типдескриптора - string desc /* имя */ 'T', 0, 'E', 0, 'S', 0, 'T', 0, 'S', 0 }; И специальный дескриптор CDC: uint8_t Virtual_Com_Port_GET_LINE_CODING[] = { 0x00, 0xC2, 0x01, 0x00, // - Эти 4 байта определяют скорость обмена(dwDTERate): 0х0001С200 =115200 бит/сек 0x00, // - интервал стоп-бита(bCharFormat): (0=1; 1=1.5; 2=2) 0x00, // - паритет(bParityType): (0 - None; 1=Odd; 2=Even; 3=Mark; 4=Space) 0x08 // -длина символа(bDataBits): (5; 6; 7; 8 или 16) байт }; Страшно? Не стоит бояться. Это вещь типа «Write only», поэтому, если нет особого желание менять проект, то можно и не вникать в суть, однако, я бы советовал таки пробежаться одним глазом. На что нужно обратить особое внимание. Дескриптор конфигурации отправляется одной пачкой – все 67 байта (за исключением случая, когда от нас просят ровно 9 байт). Остальное все расписано с хорошими комментариями (в большинстве случаев – не моими :) ), поэтому вдаваться в подробности сейчас не будем. В сети есть статьи и книги, в которых можно изучить структуру дескрипторов подробнее. Мы же начнем рассматривать то, что нам сейчас интересно :) . Далее по коду идут фунции по работе с битами регистра EPn. Если вы начнете понимать, что ничего не понимаете, то можете пропустить этот раздел и вернуться к нему позже, так как рассматривать все в отдельности дальше не получиться. Тут все взаимосвязано, и перечитать придется несколько раз, но я постараюсь описать все максимально просто. ВНИМАНИЕ!!! В файле stm32f30x.h могут содержаться определения для работы с регистрами EPn USB. Записывать значения битов через них можно ТОЛЬКО когда регистр EPn равен НУЛЮ, однако, чтобы не запутаться, лучше использовать их ТОЛЬКО для ЧТЕНИЯ состояния битов. Для начала немного углубимся в теорию. Для задания состояния контрольных точек в регистре EPn предусмотрены биты STAT_TX (передача) и STAT_RX (прием). Каждый из них может находиться в одном из четырех состояний: Disable – контрольная точка отключена. Пытаться отправить или принять что-либо от нее бесполезно. (Стоит по умолчанию). Stall – произошла ошибка, которая требует вмешательства пользователя. Эти два состояние в проекте не используются (мне она не пригодились, но работать с ними можно). NAK – точка в режиме ожидания. Она просто ждет пока закончится какой-то процесс и мы захотим работать с ней дальше. VALID – точка готова к приему или отправке данных. Для тех, кто не понимает, что такое точка и в принципе не в курсе структуры пересылки приведу аналогию. К примеру, у нас есть интернет магазин. Приходит к нам заказ с просьбой прислать товар и квитанцией об оплате. В данном случае покупатель это ХОСТ - мы можем ему отправить товар и получить деньги, только если он от нас это попросит. Если мы просто так что-либо отправим, то фиг нам чего в ответ вернется :) . Наш интернет-магазин это НАШЕ УСТРОЙСТВО – мы принимаем заказ, обрабатываем, и если все оплачено, то отправляем товар хосту. Если нужно отправить текст (к примеру лицензионный ключ), то мы можем отправить конверт через почтовое отделение. В отделении конверт отправят на склад, с которого его потом погрузят в машину и отвезут в почтовое отделение адресата. В этом случае, склад – это БУФЕР USB, почтовое отделение – КОНТРОЛЬНАЯ ТОЧКА № 0, а машина – ТРАНЗАКЦИЯ КОНТРОЛЬНОЙ ТОЧКИ №0. Внутри транзакции (нашей машины) едет письмо – ПАКЕТ ТРАНЗАКЦИИ КОНТРОЛЬНОЙ ТОЧКИ №0, внутри которого код – ДАННЫЕ ПАКЕТА ТРАНЗАКЦИИ КОНТРОЛЬНОЙ ТОЧКИ №0. На стороне получателя все точно также. Далее другой пример. Нам нужно отправить уже не код, а вагон угля. Если мы выгрузим уголь в почтовое отделение, в которое отправляли письмо (КОНТРОЛЬНУЮ ТОЧКУ №0), то сотрудники почтового отделения скажут, что у нас проблемы с головой. Или позвонят людям, с комплектом смирительных рубашек :) . Соответственно, уголь мы повезем в компанию, которая занимается ж/д перевозками (КОНТРОЛЬНАЯ ТОЧКА 1). Там наш контейнер отвезут его на нужное место на складе (Тот же БУФЕР USB, но в другом месте). Затем наш контейнер погрузят на поезд (ТРАНЗАКЦИЯ КОНТРОЛЬНОЙ ТОЧКИ №1) и наш контейнер (ПАКЕТ ТРАНЗАКЦИИ КОНТРОЛЬНОЙ ТОЧКИ №1) с углем (ДАННЫЕ ПАКЕТА ТРАНЗАКЦИИ КОНТРОЛЬНОЙ ТОЧКИ №1) увезут куда положено (то есть ХОСТУ). На стороне получателя все точно также. Собственно, зачем я рассказал вторую историю? Да просто так, увлекся. На самом деле нет. USB имеет одну или несколько контрольных точек. Нулевая контрольная точка производит начальную настройку устройства (ТОЛЬКО нулевая). Затем настраиваются и подключаются другие точки. Разные точки производят разные действия по разному – некоторые передают (принимают или и то и другое) служебную информацию, другие большие объемы данных (типа угля), третьи небольшие данные (типа писем) и т.д. (подробнее о типах контрольных точек читаем в интернетах). Если мы пошлем служебную информацию в точку, которая занимается пересылкой данных, то передать ничего не получиться. Поэтому важно направлять данные именно той точке, которая настроена на обработку информации такого типа. Как отправить данные по USB? Загружаем в нужное место в буфере (в нужную компанию) информацию (письмо/уголь) и выставляем STAT_TX в VALID (оплачиваем отправку). После того, как придет время данные отправятся в путь. Ожидаем подтверждения успешной отправки (прерывание по событию CTR_TX), после чего STAT_TX будет находиться в состоянии NAK и ожидать от нас дальнейших действий. Очищаем флаг успешной отправки. Как принять данные по USB? Ждем прерывания (извещения от нужной компании) о получении ожидаемых данных (писем/угля). Очищаем флаг успешного приема. После этого состояние STAT_RX будет NAK и устройство перестанет принимать другие данные.Определяем от кокой точки пришли данные и забираем их из буфера. Выставляем STAT_RX в VALID и теперь мы готовы к приему новых данных. Общая схема такая, но на практике мы ее немного изменим для удобства. Ну теперь вернемся к железу :) . В регистрах EPn все сделано через то самое место, возле которого прорастает хвост братьев наших меньших, но я думаю, что в ST так сделали не специально, поэтому будем приспосабливаться. Для начала теория. Биты регистров EPn делятся на 4 типа: R/W – это стандартные биты, которые можно прочитать либо перезаписать своими значениями. Read only – только чтение. Ну тут все ясно. Это флаги событий. Write 0 – их можно только сбросить записью нуля. Это тоже флаги событий. Toogle – а это самый сок :) . Эти биты нельзя просто так сбросить или установить (просто так можно только прочитать). Состояние этих битов можно только ИНВЕРТИРОВАТЬ записью единицы. Запись нуля не оказывает никакого воздействия. Их то, мы и рассмотрим подробно. Возьмем, к примеру, биты STAT_RX. Допустим, их состояние в данный момент 00 , тогда если мы запишем значение 01, то их состояние станет 01. Вроде все как обычно. Но попробуем теперь перевести их из состояния 01 в 11. Для этого нужно записать 10. После этой запись первый бит останется неизменным, а нулевой изменит свое значение (логическое) на противоположное. Чтобы нормально работать со всеми этими битами, нужно сделать маски для каждой группы. Подробно останавливаться на этом не будем (про маски битов можно почитать в интернетах, там все есть :) ), а посмотрим практическую реализацию работы перевода битов STAT_TX из состояния STALL в любое другое: void SetTX (uint8_t EPNum, uint8_t EPStatus) { uint16_t buff; uint8_t stat; buff = ((uint16_t*)0x40005C00)[EPNum*2]; //0x40005C00 базовый адрес регистров юсб buff = ((uint16_t*)0x40005C00)[EPNum*2]; ………………………. case 1: //STALL switch (EPStatus) { case 0: buff^=0x0010; buff&=0x8F9F; break; //TX case 1: break; case 2: buff^=0x0030; buff&=0x8FBF; break; //TX case 3: buff^=0x0020; buff&=0x8FAF; break; //TX } ……………………………….. ((uint16_t*)0x40005C00)[EPNum*2]=buff; Переменная EPStatus показывает, в какое состояние мы хотим переключиться. Если EPStatus = 0 - то в состояние DISABLE, если 1 - то в STALL, если 2 – то в NAK и если 3 – то VALID. EPNum – это выбор контрольной точки, значения битов которой, мы и хотим изменить. Допустимые значения переменной от 0 до 7 (мой МК поддерживает 8 контрольных точек - см. RM). 0x40005C00 в моем случае, это адрес памяти, с которого начинается регистр EP0. Умножение EPNum на 2 необходимо чтобы перенестись на 32 бита вперед по памяти (так как у нас (uint16_t*) ). С этого адреса начнутся биты регистра EP1). Вот вся функция обработки STAT_TX: void SetTX (uint8_t EPNum, uint8_t EPStatus) { uint16_t buff; uint8_t stat; buff = ((uint16_t*)0x40005C00)[EPNum*2]; //0x40005C00 базовый адрес регистров юсб
stat = (buff>>4) & 3; switch (stat) { case 0: //DISABLE switch (EPStatus) { case 0: break; case 1: buff^=0x0010; buff&=0x8F9F; break; //TX case 2: buff^=0x0020; buff&=0x8FAF; break; //TX case 3: buff^=0x0030; buff&=0x8FBF; break; //TX } break;
case 1: //STALL switch (EPStatus) { case 0: buff^=0x0010; buff&=0x8F9F; break; //TX case 1: break; case 2: buff^=0x0030; buff&=0x8FBF; break; //TX case 3: buff^=0x0020; buff&=0x8FAF; break; //TX } break;
case 2: //NAK switch (EPStatus) { case 0: buff^=0x0020; buff&=0x8FAF; break; //TX case 1: buff^=0x0030; buff&=0x8FBF; break; //TX case 2: break; case 3: buff^=0x0010; buff&=0x8F9F; break; //TX
} break;
case 3: //VALID switch (EPStatus) { case 0: buff^=0x0030; buff&=0x8FBF; break; //TX case 1: buff^=0x0020; buff&=0x8FAF; break; //TX case 2: buff^=0x0010; buff&=0x8F9F; break; //TX case 3: break; } break; default: break; } ((uint16_t*)0x40005C00)[EPNum*2]=buff; }
и STAT_RX:
void SetRX (uint8_t EPNum, uint8_t EPStatus) { uint16_t buff; uint8_t stat; buff = ((uint16_t*)0x40005C00)[EPNum*2]; //0x40005C00 базовый адрес регистров юсб stat = (buff>>12) & 3; switch (stat) { case 0: //DISABLE switch (EPStatus) { case 0: break; case 1: buff^=0x1000; buff&=0x9F8F; break; //RX case 2: buff^=0x2000; buff&=0xAF8F; break; //RX case 3: buff^=0x3000; buff&=0xBF8F; break; //RX } break;
case 1: //STALL switch (EPStatus) { case 0: buff^=0x1000; buff&=0x9F8F; break; //RX case 1: break; case 2: buff^=0x3000; buff&=0xBF8F; break; //RX case 3: buff^=0x2000; buff&=0xAF8F; break; //RX } break;
case 2: //NAK switch (EPStatus) { case 0: buff^=0x2000; buff&=0xAF8F; break; //RX case 1: buff^=0x3000; buff&=0xBF8F; break; //RX case 2: break; case 3: buff^=0x1000; buff&=0x9F8F; break; //RX } break;
case 3: //VALID switch (EPStatus) { case 0: buff^=0x3000; buff&=0xBF8F; break; //RX case 1: buff^=0x2000; buff&=0xAF8F; break; //RX case 2: buff^=0x1000; buff&=0x9F8F; break; //RX case 3: break; } break; } ((uint16_t*)0x40005C00)[EPNum*2]=buff; }
default: можно обработать и иначе, например чтоб выявлять ошибки в программе. Код «топорный», но зато все по шагам можно отследить и понять. Идем дальше. Флаги прерываний успешного приема и передачи данных. Тут все тоже, поэтому просто приведу код: void CTR_RXTXClear (uint8_t EPNum, uint16_t RXBitClear, uint16_t TXBitClear) // EPNum - номер контрольной точки, с флагами которой хотим работать. //RXBitClear – значение 0 ничего не меняет, значение 1 сбрасывает флаг CTR_RX //TXBitClear– значение 0 ничего не меняет, значение 1 сбрасывает флаг CTR_TX { uint16_t buff; buff=((uint16_t*)0x40005C00)[EPNum*2]; //0x40005C00 базовый адрес регистров юсб buff^=((uint16_t) (RXBitClear << 15) | (uint16_t) (TXBitClear << 7)); buff&=0x8F8F; ((uint16_t*)0x40005C00)[EPNum*2]=buff; } Дальше рассмотрим функцию MyUSBinit. Как мы говорили ранее, основная, так сказать, предстартовая настройка регистров USB будет происходить здесь. Поехали: void MyUSBinit(void) { //здесь нужно включить бит подтяжки резистора (но на плате STM32F3Discovery он припаян, см. RM)
RCC->CFGR &= ~RCC_CFGR_USBPRE; // Настраиваем частоту USB (здесь частота тактирования USB = частота тактирования ядра / 1.5) RCC->APB1ENR |= RCC_APB1ENR_USBEN; // Включаем тактирование USB от шины APB1 USB_CNTR &= ~USB_CNTR_PDWN; // Подаем питание на аналоговую часть USB (да да, именно сбросом бита) // делаем небольшую паузу iTemp = 0; while (iTemp < 1000000) {iTemp++;} iTemp = 0;
USB_CNTR |= USB_CNTR_FRES; // устанавливаем бит сброса (установлен по умолчанию, но на всякий случай сделаем это) USB_CNTR &= ~USB_CNTR_FRES; // сбрасываем, при этом произойдет RESET USB_ISTR = 0; // на всякий случай очистим флаги NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn); Включаем прерывания USB (на самом деле это прерывание не только от USB, но нам остальное сейчас не важно) USB_CNTR |= USB_CNTR_RESETM | USB_CNTR_CTRM | 0; // включаем прерывания по RSEST и по успешной транзакции (успешному приему или передаче данных) } С MyUSBinit все. Дальше работаем с прерыванием по USB_LP_CAN1_RX0_IRQHandler. Внутри будет обработка прерываний по двум событиям: void USB_LP_CAN1_RX0_IRQHandler (void) { uint8_ti = 0; // Временная переменная, которая нам пригодится
if (USB_ISTR & USB_ISTR_CTR) // прерывания по успешной транзакции (успешному приему или передаче данных) { … } if (USB_ISTR & USB_ISTR_RESET) // прерывания по RSEST { … } } Первым у нас сработает RESET, поэтому его и рассмотрим. Поехали: if (USB_ISTR & USB_ISTR_RESET) { USB_ISTR &= ~USB_ISTR_RESET; // сбрасываем флаг прерывания по RESET USB_BTABLE = 0; // таблица начинается с 0x0000 USB (или 0x40006000 Flash) *(__IO uint16_t*)(0x40006000) = (uint16_t) 0x0040; // начальный адрес USB_ADDR0_TX. Такой адрес позволяет в дальнейшем добавить все 8 возможных контрольных точек в начале памяти USB (каждая имеет размер 2 байта) *(__IO uint16_t*)(0x40006004) = (uint16_t) 0x0040; // размер исходящих данных - 64 байта USB_COUNT0TX *(__IO uint16_t*)(0x40006008) = (uint16_t) 0x0080; // начальный адрес USB_ADDR0_RX *(__IO uint16_t*)(0x4000600C) = (uint16_t) 0x8400; // 64 байта входящих данных USB_ USB_COUNT0RX_BL_SIZE
*(__IO uint16_t*)(0x40006010) = (uint16_t) 0x00C0; // начальный адрес USB_ADDR1_TX *(__IO uint16_t*)(0x40006014) = (uint16_t) 0x0040; // размер исходящих данных - 64 байта USB_COUNT1TX *(__IO uint16_t*)(0x40006018) = (uint16_t) 0x0100; // начальный адрес USB_ADDR1_RX *(__IO uint16_t*)(0x4000601C) = (uint16_t) 0x8400; // 64 байта входящих данных USB_ USB_COUNT1RX_BL_SIZE
*(__IO uint16_t*)(0x40006020) = (uint16_t) 0x0140; // начальный адрес USB_ADDR2_TX *(__IO uint16_t*)(0x40006024) = (uint16_t) 0x0040; // размер исходящих данных - 64 байта USB_COUNT2TX *(__IO uint16_t*)(0x40006028) = (uint16_t) 0x0180; // начальный адрес USB_ADDR2_RX *(__IO uint16_t*)(0x4000602C) = (uint16_t) 0x8400; // 64 байта входящих данных USB_ USB_COUNT2RX_BL_SIZE
*(__IO uint16_t*)(0x40006030) = (uint16_t) 0x01C0; // начальный адрес USB_ADDR3_TX *(__IO uint16_t*)(0x40006034) = (uint16_t) 0x0040; // размер исходящих данных - 64 байта USB_COUNT3TX *(__IO uint16_t*)(0x40006038) = (uint16_t) 0x0200; // начальный адрес USB_ADDR3_RX *(__IO uint16_t*)(0x4000603C) = (uint16_t) 0x8400; // 64 байта входящих данных USB_ USB_COUNT3RX_BL_SIZE
USB_EP0R |= USB_EP_CONTROL; // нулевая контрольная точка в режим CONTROL SetRX(0, 3); // нулевая контрольная точка STAT_RX в VALID - готовы принимать данные SetTX(0, 2); // нулевая контрольная точка STAT_TX в NAK - ждем данные на отправку
USB_EP1R |= 0x0601; // контрольная точка 1 в режим CONTROL USB_EP2R = 2; // контрольная точка 2 в режим BULK USB_EP3R = 3; // контрольная точка 3 в режим BULK
SetRX(1, 3); // контрольная точка 1 STAT_RX в VALID - готовы принимать данные SetTX(1, 2); // контрольная точка 1 STAT_TX в NAK - ждем данные на отправку SetRX(2, 3); // контрольная точка 2 STAT_RX в VALID - готовы принимать данные SetTX(2, 2); // контрольная точка 2 STAT_TX в NAK - ждем данные на отправку SetRX(3, 3); // контрольная точка 3 STAT_RX в VALID - готовы принимать данные SetTX(3, 2); // контрольная точка 3 STAT_TX в NAK - ждем данные на отправку USB_DADDR |= USB_DADDR_EF; // Включаем USB транзакции return; } Наверное, всем стало интересно, что за непонятная куча с адресами вывалена в 4-х блоках. Попытаюсь объяснить. «Кучи» у нас четыре, так как пользоваться мы будем четыремя конечными точками (нулевой для начальной настройки нашего девайса, первой для работы с прерываниями, второй для приема и третьей для передачи данных). Рассмотрим первую «кучу», позже. Для начала познакомимся с особенностью памяти буфера USB. Открываем даташит (не RM!). Мой МК, согласно таблице памяти периферии, имеет 512 байт USB SRAM, которая расположена по адресу 0x4000 6000 - 0x4000 63FF. Однако, судя по адресам размер должен составлять 3FF, то есть 1024 байта. Фишка в том, что каждый 2-й и 3-й байт адреса недоступен – им просто напросто нельзя пользоваться, поэтому половины памяти у нас по факту нет. Эту особенность нужно учесть при обращение к буферу USB. Вот так, примерно выглядит эта память (слева – со стороны Flash, справа со стороны USB). Теперь вернемся к нашим баранам… брр… то есть «кучам». Каждой контрольной точке мы должны задать еще 4 параметра (заполнить таблицу адресов) – USB_ADDR0_TX - адрес начала для буфера передачи данных, USB_COUNT0TX - максимальный размер этих данных, USB_ADDR0_RX - адрес начала буфера для приема данных и USB_COUNT0RX - адрес, в котором будет определяться размер буфера для приема данных и отображаться размер принятых данных. Описание каждого из параметров занимает 2 байта, соответственно для всех параметров – 8 байт. Вот что рисует нам RM: Контрольных точек у меня может быть максимум 8, поэтому для описания всех нужно будет 64 байта. Поскольку ранее мы писали, что USB_BTABLE = 0 , то данные таблицы адресов будут располагаться в начале буфера, то есть по адресу USB - 0x0000 (по адресу Flash - 0x40006000). Буфер для передаваемых нулевой контрольной точкой данных мы расположим сразу за данными таблицы адресов, то есть с 65-го байта (первые 64 байта мы отдали под таблицу, помним об этом), а значит с адреса 0x0040 по исчислению USB, т.е 0x40006080 по Flash (помним, что половину байт мы использовать не можем, поэтому и адрес в два раза больше). Так родилась первая строка нашей «кучи»: *(__IO uint16_t*)(0x40006000) = (uint16_t) 0x0040; // начальный адрес USB_ADDR0_TX (такой адрес позволяет в дальнейшем добавить все 8 возможных контрольных точек Далее запишем в таблицу, какой размер мы зарезервировали в буфере для передаваемых данных: *(__IO uint16_t*)(0x40006004) = (uint16_t) 0x0040; // размер исходящих данных - 64 байта USB_COUNT0TX Почему именно 64 байта? Все просто - это максимальный размер для класса CDC. Едем дальше, а дальше у нас USB_ADDR0_RX - адрес начала буфера для приема данных. Он у нас расположен сразу после буфера передачи данных, т.е 0x0040 + 0x40: *(__IO uint16_t*)(0x40006008) = (uint16_t) 0x0080; // начальный адрес USB_ADDR0_RX Дальше USB_COUNT0RX - адрес, в котором будет определяться размер буфера для приема данных (с 10-го по 15-й биты) и отображаться размер принятых данных (с нулевого по 9-й биты). Вписать размер буфера простым числом тут не получится. Нужно обратиться к таблице в RM: Обращаем внимание, что с BLSIZE = 0 одни значения, а с BLSIZE = 1 другие. Размер принятых данных будем подглядывать тогда, когда начнем обмениваться с хостом. Последняя строка нашей кучи: *(__IO uint16_t*)(0x4000600C) = (uint16_t) 0x8400; // 64 байта входящих данных USB_ USB_COUNT0RX_BL_SIZE Фухх. Чуть пальцы не стер пока писал. Сделайте перерыв и покормите наконец-то кота :) . Покормили? Тогда едем дальше. Теперь рассмотрим функции записи и считывания данных из буферов. Читаем: void ReadBuff (uint8_t DescArray[] ,uint8_t BiteNumToRead, uint8_t NumEP) { // DescArray[] - наш массив для считанных данных с буфера USB //BiteNumToRead - сколько байт нужно считать //NumEP - к буферу какой контрольной точки обращаемся uint8_t i = 0; for (i = 0; i < BiteNumToRead; i+=2) { DescArray[i] = *(__IO uint8_t*)(0x40006100 + i*2+(NumEP*0x100)); DescArray[i+1] = *(__IO uint8_t*)(0x40006100 + i*2+1+(NumEP*0x100)); } }; Обращаем внимание, что данная функция считывает данные ТОЛЬКО с начала буфера USB и работать будет с буфером контрольной точки ТОЛЬКО если ее размер и размеры всех буферов предыдущих контрольных точек равны 64 байтам. Мне пока другого и не надо было :) . Вызов функции: ReadBuff (DataReaded, *(__IO uint8_t*)(0x4000602C), 2); Кто понял что это за адрес *(__IOuint8_t*)(0x4000602C) ? Кто понял – молодец, кто не понял – объясняю. По этому адресу хранится размер принятых второй контрольной точкой данных. uint8_t позволяет нам оставить только первые 8 бит из десяти, чего нам вполне хватит (8 бит могут отобразить размер в 256 байт, а наш буфер имеет всего 64 байта). Пишем в буфер: void SendBuff (uint8_t DescArray[],uint8_t BiteNumToSend,uint8_t NumEP) { // DescArray[] - наш массив для данных, которые записываем в буфер USB //BiteNumToSend - сколько ПЕРВЫХ массива байт нужно записать в буфер USB //NumEP - к буферу какой контрольной точки обращаемся uint16_t TXBuff = 0; uint8_t i = 0; for (i = 0; i < BiteNumToSend; i+=2) { TXBuff = DescArray[i] + (DescArray[i+1] << 8); *(__IO uint16_t*)(0x40006080 + i*2+(NumEP*0x100)) = TXBuff; } *(__IO uint16_t*)(0x40006004 + (NumEP*0x10)) = (uint16_t) BiteNumToSend; }; Обращаем внимание, что данная функция записывает данные ТОЛЬКО с начала буфера USB и работать будет с буфером контрольной точки ТОЛЬКО если ее размер и размеры всех буферов предыдущих контрольных точек равны 64 байтам. Вызов функции: SendBuff (Virtual_Com_Port_DeviceDexyinaor, (*(__IO uint8_t*)(0x4000610C)),0); Здесь мы отправляем дескриптор устройства, а размер берем из последних полученных данных от хоста. *Глубокий выдох*, кажется на сегодня достаточно :) . Так как объем материала большой, то статья будет разбита на 2 части. В следующий раз мы рассмотрим работу с прерываниями по CTR и отправим что-нибудь в наш МК, а потом дождемся ответа от него. Принимается любая конструктивная критика (которая будет учтена при написании второй части), пожелания и замечания. Ну и еще раз поздравим Кота с его 11-ым Днем Рождения!!! :)
Все вопросы в Форум.
|
|
|||||||||||||||
![]() |
![]() |
![](/templates/radiokot_classic_neew/images/datasheet.png)
![](/templates/radiokot_classic_neew/images/dorogi.png)
![]() |
![]() |
|||
|
||||
![]() |
![]() |