STM32 USB НАЧАЛО.
На сегодняшний день, даже самые дешевые МК имеют в себе аппаратный usb и в тоже время этот интерфейс является стандартом для подключения периферийных устройств к ПК. Это понимание пришло ко мне не сегодня, но usb достаточно сложен и я долгое время не знал как к нему подойти.
Проблема в том, что за счет того, что обмен идет непрерывно на большой скорости, привычным способом, устанавливая breakpoint, его отлаживать не получиться. Да и сам процесс обмена достаточно сложен и желательно перед глазами иметь картинку для лучшего понимания происходящего.
Оказалось, что китайский salea logic решает эти проблемы, он записывает обмен данными между устройством и компом декодируя при этом посылки.
Про физическую часть скажу всего пару слов, сигнал передается по дифференциальной паре, что помогает достигать высоких скоростей. В зависимости от того, какая из двух линий притянута к питанию устройство будет работать как HS или FS.
Картинка ниже очень наглядно показывает, как программно реализован usb.
Из нее понятно, что после того как мы подключили устройство обмен данными происходит с помощью конечных точек.
Конечную точку можно представить себе как почтовый ящик, почтальон знает, что в него надо кидать письма, а Вы знаете, что из него можно забирать письма. В случае с usb почтовым ящиком является конечная точка.
Очень важный момент работы usb, что общение всегда инициирует хост, а устройство может лишь отвечать на запросы, которые отправил хост. Давайте создадим нулевую конечную точку, которая предназначена для обмена служебной информацией.
Для этого надо понимать как происходит общение:
Давайте оформим это в виде кода.
Инициализируем usb.
Тут может возникнуть вопрос, что такое таблица конечных точек?
Так как посылки у usb могут быть разной длины и большого размера, разработчики МК не стали делать для получаемых/принимаемых данных отдельный регистр, для каждой конечной точки. Они выделили для этой цели кусок памяти!!! А для того, чтобы МК понимал, где заканчивается одна точка и начинается другая, в начале этой памяти расположена таблица.
То есть если мы хотим отправить данные, используя нулевую точку, мы записываем данные по нужному адресу. Остальное МК сделает за нас.
Описываем прерывание.
Создаем конечную точку.
Теперь когда конечная точка создана можно обмениваться данными.
Что касается дескриптора устройства, оставлю тут табличку в которой расписано назначение каждого байта.
Тут начинается самое интересное, для начала расширим код нашего прерывания, для случая когда МК успешно принял данные.
Теперь добавим, функции записи и чтения конечных точек.
У этой статьи обязательно будет продолжение, но пока выложу её в том виде, что есть.
Проблема в том, что за счет того, что обмен идет непрерывно на большой скорости, привычным способом, устанавливая breakpoint, его отлаживать не получиться. Да и сам процесс обмена достаточно сложен и желательно перед глазами иметь картинку для лучшего понимания происходящего.
Оказалось, что китайский salea logic решает эти проблемы, он записывает обмен данными между устройством и компом декодируя при этом посылки.
Про физическую часть скажу всего пару слов, сигнал передается по дифференциальной паре, что помогает достигать высоких скоростей. В зависимости от того, какая из двух линий притянута к питанию устройство будет работать как HS или FS.
Картинка ниже очень наглядно показывает, как программно реализован usb.
Из нее понятно, что после того как мы подключили устройство обмен данными происходит с помощью конечных точек.
Конечную точку можно представить себе как почтовый ящик, почтальон знает, что в него надо кидать письма, а Вы знаете, что из него можно забирать письма. В случае с usb почтовым ящиком является конечная точка.
Очень важный момент работы usb, что общение всегда инициирует хост, а устройство может лишь отвечать на запросы, которые отправил хост. Давайте создадим нулевую конечную точку, которая предназначена для обмена служебной информацией.
Для этого надо понимать как происходит общение:
- Устройство подтягивает к питанию одну из линий передачи данных , таким образом хост узнает о его подключении.
- Host подает сигнал сброса в шину. Для устройства это значит, что хост собирается с ним общаться и необходимо создать конечную точку.
Давайте оформим это в виде кода.
Инициализируем usb.
void USB_Init(void)
{
//включаем тактирование порта и альтернативных функций
RCC -> APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
//настраиваем порты
GPIOA->CRH &= ~GPIO_CRH_CNF11;
GPIOA->CRH |= GPIO_CRH_CNF11_1;
GPIOA->CRH |= GPIO_CRH_MODE11;
GPIOA->CRH &= ~GPIO_CRH_CNF12;
GPIOA->CRH |= GPIO_CRH_CNF12_1;
GPIOA->CRH |= GPIO_CRH_MODE12;
//выключаем предделитель usb
RCC->CFGR &= ~RCC_CFGR_USBPRE;
//включаем тактирования usb
RCC -> APB1ENR |= RCC_APB1ENR_USBEN;
//выходим из power down
USB->CNTR &=~ USB_CNTR_PDWN;
//небольшая задержка
uint32_t temp;
for ( temp=30000; temp != 0; temp--);
//сбрасываем бит, который ресетит usb
USB->CNTR &=~ USB_CNTR_FRES;
//разрешаем прерывания по RESET и CTRM(correct transfer interrupt mask)
USB -> CNTR |= USB_CNTR_RESETM | USB_CNTR_CTRM;
//очищаем флаги прерывания
USB -> ISTR = 0;
//таблица конечных точек начинается с адреса 0x40006000
USB -> BTABLE = 0;
//подтягиваем вывод D+ к питанию
USB_PULL_UP
//разрешаем прерывание по приему данных
NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
}
Тут может возникнуть вопрос, что такое таблица конечных точек?
Так как посылки у usb могут быть разной длины и большого размера, разработчики МК не стали делать для получаемых/принимаемых данных отдельный регистр, для каждой конечной точки. Они выделили для этой цели кусок памяти!!! А для того, чтобы МК понимал, где заканчивается одна точка и начинается другая, в начале этой памяти расположена таблица.
То есть если мы хотим отправить данные, используя нулевую точку, мы записываем данные по нужному адресу. Остальное МК сделает за нас.
Описываем прерывание.
void USB_LP_CAN1_RX0_IRQHandler()
{
uint8_t n;
if (USB -> ISTR & USB_ISTR_RESET)
{
//создаем 0 конечную точку, типа CONTROL
EP_Init(0, EP_TYPE_CONTROL, 128, 192);
//создаем 1 конечную точку, типа BULK
EP_Init(1, EP_TYPE_BULK, 256, 320);
//обнуляем адрес устройства
USB -> DADDR = USB_DADDR_EF;
//присваиваем состояние DEFAULT STATE
USB_Dev.USB_Status = USB_DEFAULT_STATE;
}
}
Создаем конечную точку.
void EP_Init(uint8_t number, uint8_t type, uint32_t addr_tx, uint32_t addr_rx)
{
USB -> EPnR[number] = (type << 9) | (number & USB_EPnR_EA);
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 + 2*addr_tx);
endpoints[number].rx_buf = (uint8_t *)(USB_BTABLE_BASE + 2*addr_rx);
}
Теперь когда конечная точка создана можно обмениваться данными.
- Host шлет запрос GET_STATUS. Это своеобразное приветствие, чтобы понять, что конечная точка настроена, устройство понимает хост и дальнейшее общение имеет смысл. Он ожидает от устройства статус default state.
- Host шлет запрос GET_DESCRIPTOR для получения основной информации о устройстве, в том числе максимальном размере пакета для нулевой конечной точки.
Что касается дескриптора устройства, оставлю тут табличку в которой расписано назначение каждого байта.
Тут начинается самое интересное, для начала расширим код нашего прерывания, для случая когда МК успешно принял данные.
if (USB -> ISTR & USB_ISTR_CTR)
{
//определяем номер конечной точки, которая вызвала прерывание
n = USB -> ISTR & USB_ISTR_EP_ID;
//копируем количество принятых байт
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;
//очищаем флаги приема и передачи
endpoints[n].status = CLEAR_CTR_RX_TX(endpoints[n].status);
USB -> EPnR[n] = endpoints[n].status;
}
Теперь добавим, функции записи и чтения конечных точек.
void EP_Write(uint8_t number, uint8_t *buf, uint16_t size){
uint8_t i;
uint32_t timeout = 1000000;
uint16_t status = USB -> EPnR[number];
if (size > 64) size = 64;
uint32_t *pv = (uint32_t*)(endpoints[number].tx_buf);
/*
* ВНИМАНИЕ КОСТЫЛЬ
* Из-за ошибки записи в область USB/CAN SRAM с 8-битным доступом
* пришлось упаковывать массив в 16-бит, сообветственно размер делить
* на 2, если он был четный, или делить на 2 + 1 если нечетный
*/
uint16_t temp = (size & 0x0001) ? (size + 1) / 2 : size / 2;
for(i = 0; i < temp; i++, buf += 2)
{
*pv++ = *((uint16_t *)buf);
}
//Количество передаваемых байт
USB_BTABLE -> EP[number].USB_COUNT_TX = size;
status = KEEP_STAT_RX(status); //RX в NAK
status = SET_VALID_TX(status); //TX в VALID
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;
}
}
/*
* Функция чтения массива из буфера конечной точки
* number - номер конечной точки
* *buf - адрес массива куда считываем данные
*/
void EP_Read(uint8_t number, uint8_t *buf){
uint32_t timeout = 100000;
uint16_t status, i, n;
uint32_t *pm = (uint32_t *)endpoints[number].rx_buf;
uint16_t *p = (uint16_t *)buf;
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;
}
n = (endpoints[number].rx_cnt & 0x01) ? (endpoints[number].rx_cnt + 1) / 2 : endpoints[number].rx_cnt / 2;
for (i = 0; i < n; i++){
p[i] = (uint16_t)pm[i];
}
}
У этой статьи обязательно будет продолжение, но пока выложу её в том виде, что есть.
Похожие статьи