Русский
Русский
English
Статистика
Реклама

Делаем из ENC28J60 внешнюю USB сетевую карту

ENC28J60 - простой Ethernet контроллер, который может выступать в роли внешней сетевой карты у одноплатных компьютеров с GPIO (для raspberry есть даже готовый драйвер) и прочих ардуин. У моего лэптопа GPIO не выведены, попробуем исправить этот недостаток и прикрутить к нему ENC28J60 посредством STM32F103 и шнурка USB.

Давайте посмотрим, как это можно сделать.

Нам понадобится:

  • ENC28J60

  • Отладочная плата с STM32 с поддержкой USB device (например, вот такая):

  • Компьютер с Linux (я использую Ubuntu 16)

  • Второй компьютер с Ethernet для тестирования соединения (у меня raspberry pi), подключенный по wi-fi (и в одной локальной сети с первым)

Настройку STM32F103 описывать не буду, мануалов в сети хватает. Как и программирование самого ENC28J60 (в своем примере я использовал вот этот код c минимальными модификациями). Подключение ENC28J60 к STM32F103 по SPI1 описано там же.

Соединение

Компьютер (usb) -> stm32(SPI) -> ENC28J60(Ethernet кабель) -> raspberry

Как все работает

Обойдемся без написания драйверов ядра, будем работать в user space. На компьютере создадим виртуальный сетевой tap интерфейс (второго уровня, с заголовками Ethernet фреймов, есть еще tun интерфейсы, которые работают только с ip пакетами), к которому подсоединим нашу программу (назовем ее tap_handler.c). Чтобы фрейм попал в сетевой стек Linuxа, tap_handler'у достаточно записать его в tap интерфейс. Ну и наоборот, пакеты, адресованные tap интерфейсу, будут читаться tap_handler'ом, который может с ними что-то дальше сделать. В итоге, tap_handler бегает в бесконечном цикле и ждет появления данных либо со стороны tap интерфейса, либо со стороны /dev/ttyACM0 (это представление нашего USB девайса в Linuxе). В первом случае полученные данные пишем в /dev/ttyACM0, во втором в tap интерфейс.

Более подробно про работу с виртуальными интерфейсами написано здесь (для демонстрации работы vpn). Отсюда же я взял код, отвечающий за работу с виртуальными интерфейсами.

На STM32 при помощи CubeMX подключаем библиотеки для работы с USB CDC (virtual com port). После подключения SMT32 к компу Linux создаст файл /dev/ttyACM0 (ну или другой номер). Из этого файла можно прочитать данные, которые нам отправил STM32, а если записать туда данные, их сможет прочитать STM32.

Прошивка STM32 работает аналогично. В бесконечном цикле читаем данные с компа (функция CDC_Receive_FS в файле usbd_cdc_if.c) и записываем их в ENC28J60, который уже передает их дальше в сеть, а также считываем фреймы из ENC28J60 и направляем их в комп функцией CDC_Transmit_FS.

Насколько я понимаю, CDC должен передавать данные без ошибок. Однако я столкнулся с тем, что ошибки все же есть. Более того, первый пакет обычно всегда дублируется (причину я не нашел, дубляж виден в том числе в wireshark при прослушивании шины usb). Гуглеж показал, что у кого-то это происходило из-за использования неподабающего генератора частоты на STM32, что вряд ли является причиной проблемы в моем случае, т.к. я просто использовал внешний кварц. Поэтому и пришлось городить огород с метками.

Работа со стороны компа

Сетевой интерфейс создаем командой:

sudo openvpn --mktun --dev tap0

Присваиваем ip адрес:

sudo ifconfig tap0 10.0.0.1/24 up

tap_handler.с

Для правильной работы с /dev/ttyACM0 нужно передавать данные в raw виде и не давать драйверу терминала вносить в них изменения (например, считать нетекстовые данные управляющими символами и пр.). Настройка терминала:

char cdc_name[20]="/dev/ttyACM0";int tty_fd = open(cdc_name, O_RDWR | O_NOCTTY); struct termios portSettings;tcgetattr(tty_fd, &portSettings);cfmakeraw(&portSettings);tcsetattr(tty_fd, TCSANOW, &portSettings);tcflush(tty_fd, TCOFLUSH);

Подключаем tap_handler к tap0:

/* в dev передаем имя уже созданного интерфейса tap0*/int tun_alloc(char *dev, int flags) {    struct ifreq ifr;    int fd, err;    char *clonedev = "/dev/net/tun";    /* отталкиваемся от устройства  /dev/net/tun */    if( (fd = open(clonedev , O_RDWR)) < 0 ) {        perror("Opening /dev/net/tun");        return fd;    }        memset(&ifr, 0, sizeof(ifr));    ifr.ifr_flags = flags;        /* используем уже созданный tap0 */    if (*dev) {        strncpy(ifr.ifr_name, dev, IFNAMSIZ);    }    /* подсоединяемся к интерфейсу */    if( (err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0 ) {        perror("ioctl(TUNSETIFF)");                close(fd);        return err;    }        strcpy(dev, ifr.ifr_name);    return fd;}

Вызываем эту функцию в main.c следующим образом:

strcpy(tun_name, "tap0");int tap_fd = tun_alloc(tun_name, IFF_TAP | IFF_NO_PI);

Флаг IFF_TAP указывает на тип интерфейса (tap). Флаг IFF_NO_PI нужен, чтобы ядро не добавляло префиксные байты перед началом пакета.

Проверяем наличие данных в tap0 и в /dev/ttyACM0. Пока данных нет, tap_handler находится в блокирующем select:

while(1) {    int ret;    fd_set rd_set;    FD_ZERO(&rd_set);    /* tap_fd - tap inteface descriptor */    FD_SET(tap_fd, &rd_set);    /* tty_fd - /dev/ttyACM0 descriptor */    FD_SET(tty_fd, &rd_set);    ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);

После получения данных проверяем источник. При получении фрейма от tap0 tap_handler формирует пакет для STM32 (структура пакета: пакет начинается с метки - определенных 4 байт, чтобы идентифицировать начало фрейма, следующие 2 байта - длина фрейма, затем уже сам фрейм) и записывает его в /dev/ttyACM0. Затем небольшая задержка, чтобы данные прошли успешно:

if(FD_ISSET(tap_fd, &rd_set)) {        uint16_t nread = cread(tap_fd, buffer, BUFSIZE);       uint8_t buf[6];    *(uint32_t *)buf = PACKET_START_SIGN;    *(uint16_t *)(buf + 4) = nread;        cwrite(tty_fd,(char *)buf,6);        cwrite(tty_fd, buffer, nread);    delay_micro(delay_m);    }

Если есть данные в /dev/ttyACM0, убеждаемся что они начинаются с правильной метки (те же 4 байта), затем считываем длину фрейма, и потом сам фрейм. Полученный фрейм записываем в tap интерфейс:

if(FD_ISSET(tty_fd, &rd_set)) {    uint32_t sign;    /* считываем метку */    int nread = read_n(tty_fd, (char *)&sign, sizeof(sign));    /* дескриптор закрыт, выходим из программы */    if(nread == 0) {              break;    }    /* если не совпадает, пытаемся найти подпись в следующих 4 байтах */    if(sign != PACKET_START_SIGN){             continue;    }    /* читаем длину фрейма */    nread = read_n(tty_fd, (char *)&plength, 2);    if(nread == 0) {              break;    }    if (nread != 2){              continue;    }  /* здесь обрабатываем ситуацию, когда после запуска программы первый пакет дублируется */    if(flag){      flag = 0;      nread = cread(tty_fd, buffer, sizeof(buffer));      if(nread != 6){                continue;      }    }  /* слишком большая длина пакета, заканчиваем программу */    if(plength > BUFSIZE){            break;    }    /* читаем фрейм (plength байт)  и пишем его в tap interface*/    nread = read_n(tty_fd, buffer, plength);                if (nread != 0){                    cwrite(tap_fd, buffer, nread);                    delay_micro(delay_m);    }  }

Со стороны STM32

Кроме USB CDC в CubeMX подключим также HAL драйверы для работы с SPI1 и светодиодом.

Прием данных выполняется в callback'е CDC_Receive_FS (файл usbd_cdc_if.c), запуск которого инициируется прерываниями в библиотеке USB. В этом месте нельзя ставить долгоиграющие операции, поэтому пришедшие данные копируем в кольцевой буфер, из которого забираем их в основном цикле и отправляем наружу через ENC28J60. При риске переполнения буфера данные отбрасываются:

/* USB_POINTERS_ARRAY_SIZE - размер array_pos *//* MAX_FRAMELEN - максимальная длина фрейма *//* USB_BUFSIZE - размер кольцевого буфера */extern uint8_t usb_buf[]; /* кольцевой буфер для полученных от компа данных */extern uint32_t pos_int; /* индекс для размещения следующего пакета в кольцевом буфере */extern uint32_t array_pos[]; /* кольцевой массив из индексов, которые указывают на полученные пакеты в кольцевом буфере */extern uint32_t p_a; /* индекс следующей записи в array_pos для CDC_Receive_FS*/extern uint32_t pl_a;/* индекс следующей записи в array_pos у основного потока *//* USB_POINTERS_ARRAY_SIZE - размер array_pos *//* MAX_FRAMELEN - максимальная длина фрейма *//* USB_BUFSIZE - размер кольцевого буфера */static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len){  int8_t memok = 1;  /* отбрасываем пришедшие данные, если не хватает места в кольцевом буфере */  if( pl_a !=0 && p_a !=0){    int32_t mem_lag = array_pos[(p_a - 1) % USB_POINTERS_ARRAY_SIZE] - array_pos[(pl_a - 1) % USB_POINTERS_ARRAY_SIZE];    if(mem_lag > USB_BUFSIZE - MAX_FRAMELEN)      memok = 0;  }  /* Копируем поступившие данные в кольцевой буфер,    обновляем массив индексов пришедших пакетов (array_pos) */  if(*Len < USB_BUFSIZE && *Len != 0 && memok){    uint16_t offset = pos_int % USB_BUFSIZE;    uint16_t new_pos = offset + *Len;    uint8_t split = 0;    if (new_pos > USB_BUFSIZE){      split = 1;    }    if(split){      int len1 = USB_BUFSIZE - offset;      int len2 = *Len - len1;      memcpy(usb_buf + offset, Buf, len1);      memcpy(usb_buf, Buf + len1, len2);    }    else      memcpy(usb_buf + offset, Buf, *Len);    pos_int += *Len;    array_pos[p_a % USB_POINTERS_ARRAY_SIZE] = pos_int;    p_a++;  }  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);  USBD_CDC_ReceivePacket(&hUsbDeviceFS);  return (USBD_OK);}

В основном потоке (main.c) смотрим на наличие данных в буфере и отправляем их через ENC28J60:

if(pl_a < p_a){      uint32_t prev = 0;  if(pl_a > 0)    prev = array_pos[(pl_a - 1) % USB_POINTERS_ARRAY_SIZE];  /* размер пакета (кусочек фрейма), принятого в CDC_Receive_FS */  int32_t n = array_pos[pl_a % USB_POINTERS_ARRAY_SIZE] - prev;//usb frame size  /* указатель на пакет в буфере */  uint8_t *from = usb_buf + prev % USB_BUFSIZE;  /* признак корректности пакета */  uint8_t right_n = 1;  if (n < 0 || n > MAX_FRAMELEN){    right_n = 0;  }  /* проверка на новый фрейм. В пакете должно быть минимум 6 байтов (подпись 4 байта и 2 байта длина) */  if((packet_len == 0) && packet_start && (n > 5) && right_n){    /* спец. функция для чтения из кольцевого буфера */    uint32_t sign = read32(from,usb_buf);     /* получаем указатель на данные через 4 байта */    uint8_t *next = next_usb_ptr(from,usb_buf,4);    /* читаем размер фрейма */    packet_size = read16(next,usb_buf);// 2 bytes after sign is packet length    /* отбрасываем неправильный пакет */    if (packet_size > MAX_FRAMELEN || sign != PACKET_START_SIGN){            packet_size = 0;    }    else{      /* копируем принятый пакет данных в буфер фрейма */      next = next_usb_ptr(from,usb_buf,6);      copy_buf(packet_buf, next, usb_buf, n - 6);            packet_len = n - 6;      packet_next_ptr = packet_buf + packet_len;      packet_start = 0;    }  }  /* обрабатываем последующие пакеты в фрейме */  else if(packet_len < packet_size && right_n){  /* копируем принятый пакет данных в буфер фрейма */        copy_buf(packet_next_ptr, from, usb_buf, n);        packet_len += n;    packet_next_ptr = packet_buf + packet_len;  }  /* отбрасываем ошибочный фрейм */  else if (packet_len > packet_size){        packet_len = 0;    packet_start = 1;  }  /* отправляем фрейм через enc28j60 */  if(packet_len == packet_size && packet_size > 0){        enc28j60_packetSend(packet_buf, packet_size);    packet_len = 0;    packet_start = 1;  }  pl_a++;}

а также проверяем наличие данных в ENC28J60 и при наличии отправляем их в USB и мигаем светодиодом:

len = enc28j60_packetReceive(net_buf,sizeof(net_buf));if (len > 0) {  *((uint16_t*)(sign_buf + 4)) = len;  while(CDC_Transmit_FS(sign_buf, sizeof(sign_buf)) == USBD_BUSY_CDC_TRANSMIT);  while(CDC_Transmit_FS(net_buf, len) == USBD_BUSY_CDC_TRANSMIT);  HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);}

В CDC_Transmit_FS я немного изменил код, чтобы функцию можно было поместить в while. В этом месте CDC_Transmit_FS возвращаем новый статус USBD_BUSY_CDC_TRANSMIT вместо просто USBD_BUSY. Без изменений узнать приняты ли данные к отправке мне не удалось:

if (hcdc->TxState != 0){    return USBD_BUSY_CDC_TRANSMIT;}

В функции инициализации ENC28J60 enc28j60_ini нужно разрешить принимать и передавать любые фреймы, не только адресованные ему (promiscuous mode):

enc28j60_writeRegByte(ERXFCON,0);

Настройка и проверка

Raspberry

Поднимаем eth0 и устанавливаем ip адрес. Запускаем ping

sudo ifconfig eth0 up 10.0.0.2/24ping 10.0.0.2

В другом окне можно и tcpdump:

sudo tcpdump -i eth0

Комп

Подсоединяем прошитый STM32 к компу, соединяем ENC28J60 сетевым кабелем с raspberry. На STM32 начинает мигать светодиод, показывая приход arp / icmp пакетов (от ping). Убеждаемся, что появился /dev/ttyACM0:

ls /dev/ttyACM*

Компилируем и запускаем tap_handler:

gcc tap_handler.c -o tap_handler./tap_handler

tap_handler запускает всю цепочку - подхватывает приходящие пакеты от raspberry, адресует их tap0, получает от него фрейм в ответ, шлет его STM32, когда фрейм доходит до raspberry мы видим, что пинги начинают проходить.

Делаем интернет на компе

Если пинги проходят проверяем работу нашей сетевой карты.

Комп

Я подсоединился к raspberry по ssh через wi-fi и управляю им с компа, соединение нам терять не стоит, поэтому просто меняем default gateway. Браузер перестает работать, но соединение с raspberry не теряется:

sudo route del default gateway 192.168.1.1sudo route add default gateway 10.0.0.2

Может потребоваться корректировка DNS сервера (в /etc/resolv.conf нужно прописать в качестве nameserver, например, 8.8.8.8).

Raspberry

Разрешаем переход пакетов из eth0 в wlan0 и включаем NAT:

echo 1 | sudo tee -a /proc/sys/net/ipv4/ip_forwardsudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE

Проверяем комп. Интернет должен появиться, правда небыстрый (у меня 0.5 Мбит/с).

Можно не заморачиваться с raspberry, а напрямую подсоединить сетевой провод от рутера в ENC28J60 (нужно отключить wi-fi на компе и задать правильный адрес tap0). Но тестировать проще с raspberry, в tcpdump видно все что происходит.

Зачем все это

Использовать такую связку в жизни наверно не очень удобно (особенно при наличии недорогих usb ethernet адаптеров в продаже), но сделать ее было очень интересно. Спасибо за внимание. Ссылка на код (проект в Atollic TrueStudio).

Источник: habr.com
К списку статей
Опубликовано: 01.12.2020 12:23:05
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Настройка linux

Сетевые технологии

Программирование микроконтроллеров

Stm32

Tap

Usb

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru