Подключение микроконтроллера к локальной сети: UDP-сервер

В этой части мы напишем простенький стек протоколов для работы с UDP в роли сервера и приложение, работающее с компом по UDP.

Краткое содержание:

  • Полезные штуки
  • Стек протоколов
  • Протокол Ethernet
  • ARP
  • IP
  • ICMP
  • UDP
  • Пишем приложение
  • Заключение

Что пригодится


При написании сетевых приложений для МК накосячить просто, а найти баг — не всегда. Так что, есть смысл для отладки использовать различные прикольные штуки, отладку общегчающие:

  • JTAG-адаптер для AVR'ок — поможет заглянуть в память и регистры МК, потрейсить прошивку. Поддерживается AVR Studio 4. Жутко тормозной, но найти многие баги с ним всё же намного быстрее, чем без него.
  • Wireshark — хороший кроссплатформенный сниффер. Покажет данные, передаваемые через сетевой адаптер компа. Умеет анализировать протоколы. Покажет в каком месте пакета что-то не так (битая чексумма, неверная длина пакета, etc.). Настраивается методом научного тыка)
  • netcat — удобная утилитка, которая поможет отправлять и принимать данные по сети. Под Windows можно взять нативную сборку, либо установить из cygwin'а.

Стек протоколов


Вот так будет выглядеть стек протоколов, который мы реализуем:

Стек протоколов

При прохождении пакета вниз по стеку, к нему прикрепляются заголовки протоколов. Например, вот так будет выглядеть UDP-пакет при передаче по сети Ethernet.

Пример пакета

В соответствии с моделью OSI, уровни должны быть изолированы друг от друга. Но у нас будет не совсем так, все протоколы будут работать с одним и тем же пакетом, но обращаться к своим заголовкам. Что позволит сэкономить память и такты микроконтроллера. Для такого простого стека это вполне нормально.

Неприятная особенность IP-стека — все поля пакетов всех протоколов закодированы в этом чудовищном атавизме, big endian'е. Придётся перекодировать в нормальный формат и обратно. Для этого пригодятся макросы:

// Перекодирование word'а
#define htons(a)            ((((a)>>8)&0xff)|(((a)<<8)&0xff00))
#define ntohs(a)            htons(a)

// Перекодирование dword'а
#define htonl(a)            ( (((a)>>24)&0xff) | (((a)>>8)&0xff00) |\
                                (((a)<<8)&0xff0000) | (((a)<<24)&0xff000000) )
#define ntohl(a)            htonl(a)


Для работы стека, устройству нужно задать MAC-адрес и IP-адрес.

// Макрос для IP-адреса
#define inet_addr(a,b,c,d)    ( ((uint32_t)a) | ((uint32_t)b << 8) |\
                                ((uint32_t)c << 16) | ((uint32_t)d << 24) )

// MAC-адрес
uint8_t mac_addr[6] = {0x00,0x13,0x37,0x01,0x23,0x45};

// IP-адрес
uint32_t ip_addr = inet_addr(192,168,0,222);



Ethernet


Вот формат Ethernet-фрейма:

Ethernet-фрейм

Контрольная сумма рассчитывается и проверяется ENC28J60, так что для нас остяются видимы только 4 поля:

  • Адрес получателя. Тут может находиться конкретный MAC-адрес, широковещательный адрес ff:ff:ff:ff:ff:ff или Multicast-адрес. В Multicast-адресе установлен бит 40 (01:00:00:00:00:00). Обрати на это внимание. Если ты установишь своему девайсу адрес, например, 01:23:45:67:89:ab, получишь кучу проблем, источник которых вовсе не очевиден. Когда будешь придумывать MAC-адрес, лучше вообще обнули старший байт.
  • Адрес отправителя. Тут MAC-адрес, с которого отправлен фрейм.
  • По стандарту, здесь может находиться длина поля данных фрейма (без выравнивания), либо идентификатор протокола. Если бит 8 (0x0800) установлен, поле идентифицирует протокол. В случае протоколов IP-стека (IP, ARP, etc.), здесь находится именно идентификатор протокола и ничего другого.
  • Поле данных. Здесь будет полезная нагрузка, например, IP-пакет. В нормальном фрейме данных должно быть от 60 до 1500 байт. Если реальных данных меньше, поле выравнивается нулями до 60 байт.

Вот пример работы с Ethernet-фреймом:

#define ETH_TYPE_ARP        htons(0x0806)
#define ETH_TYPE_IP            htons(0x0800)

// Ethernet-фрейм
typedef struct eth_frame {
    uint8_t to_addr[6]; // адрес получателя
    uint8_t from_addr[6]; // адрес отправителя
    uint16_t type; // протокол
    uint8_t data[];
} eth_frame_t;

// Отправка ответа на Ethernet-фрейм
//  (подходит для серверного приложения -
//  получили запрос, обменяли местами адрес отправителя и получателя,
//  отправили назад)
void eth_reply(eth_frame_t *frame, uint16_t len)
{
    memcpy(frame->to_addr, frame->from_addr, 6);
    memcpy(frame->from_addr, mac_addr, 6);
    enc28j60_send_packet((void*)frame, len + 
        sizeof(eth_frame_t));
}

// Обработчик получаемых Ethernet-фреймов
void eth_filter(eth_frame_t *frame, uint16_t len)
{
    // Проверяем длину фрейма
    // Проверять адрес получателя не будем,
    //  положимся на фильтр пакетов ENC28J60
    if(len >= sizeof(eth_frame_t))
    {
        switch(frame->type)
        {
        // Получен ARP-пакет, вызываем обработчик ARP-пакетов
        case ETH_TYPE_ARP:
            arp_filter(frame, len - sizeof(eth_frame_t));
            break;
        
        // Получен IP-пакет, вызываем обработчик IP-пакетов
        case ETH_TYPE_IP:
            ip_filter(frame, len - sizeof(eth_frame_t));
            break;
        }
    }
}


ARP


ARP (Address Resolution Protocol) — вспомогательный протокол, позволяющий получить MAC-адрес узла по IP-адресу.

Узел, желающий получать IP-пакеты, должен отвечать на ARP-запросы, чтобы другие узлы могли найти его в локальной сети по IP-адресу.

ARP работет следующим образом:

  • Узел, который хочет узнать MAC-адрес другого узла, посылает широковещательный запрос с IP-адресом искомого узла.
  • Узел с указанным IP-адресом отсылает ответ первому узлу.
  • Первый узел добавляет адрес найденного узла в ARP-кэш, затем время от времени посылает новые запросы, чтобы убедиться, что второй узел никуда не делся.

Пример кода:

#define ARP_HW_TYPE_ETH        htons(0x0001)
#define ARP_PROTO_TYPE_IP    htons(0x0800)

#define ARP_TYPE_REQUEST    htons(1)
#define ARP_TYPE_RESPONSE    htons(2)

// ARP-пакет
typedef struct arp_message {
    uint16_t hw_type; // протокол канального уровня (Ethernet)
    uint16_t proto_type; // протокол сетевого уровня (IP)
    uint8_t hw_addr_len; // длина MAC-адреса =6
    uint8_t proto_addr_len; // длина IP-адреса =4
    uint16_t type; // тип сообщения (запрос/ответ)
    uint8_t mac_addr_from[6]; // MAC-адрес отправителя
    uint32_t ip_addr_from; // IP-адрес отправителя
    uint8_t mac_addr_to[6]; // MAC-адрес получателя, нули если неизвестен
    uint32_t ip_addr_to; // IP-адрес получателя
} arp_message_t;

// Обработчик ARP-пакетов
void arp_filter(eth_frame_t *frame, uint16_t len)
{
    arp_message_t *msg = (void*)(frame->data);

    // Проверяем длину пакета
    if(len >= sizeof(arp_message_t))
    {
        // Проверяем тип протокола
        if( (msg->hw_type == ARP_HW_TYPE_ETH) &&
            (msg->proto_type == ARP_PROTO_TYPE_IP) )
        {
            // ARP-запрос и наш IP-адрес?
            if( (msg->type == ARP_TYPE_REQUEST) && 
                (msg->ip_addr_to == ip_addr) )
            {
                // Отправляем ответ
                msg->type = ARP_TYPE_RESPONSE;
                memcpy(msg->mac_addr_to, msg->mac_addr_from, 6);
                memcpy(msg->mac_addr_from, mac_addr, 6);
                msg->ip_addr_to = msg->ip_addr_from;
                msg->ip_addr_from = ip_addr;

                eth_reply(frame, sizeof(arp_message_t));
            }
        }
    }
}


IP


IP (Internet Protocol) — протокол сетевого уровня. А заодно и сердце наших интернетов. Служит он для абстрагирования от технологии локальной сети, для маршрутизации пакетов между сетями, etc.

Формат IP-пакета:

IP-пакет

  • Версия — 4 для IPv4.
  • Длина заголовка — 5 (dword'ов) для стандартного пакета.
  • Длина пакета — суммарная длина заголовка и поля данных. Важна, поскольку точный размер пакета мы не знаем из-за выравнивания Ethernet-фреймов.
  • Время жизни — это поле уменьшается после каждого хопа (прохождения пакета через роутер). Если оно доходит до нуля, пакет прибивается. Нужно для отлавливания «заблудившихся» пакетов. При отпраке пакета пишем в него, например, 64.
  • Принцип рассчёта контрольной суммы описан ниже, в примере кода. При рассчёте контрольной суммы заголовка в данном поле должен быть 0.

Для каждой локальной сети определён MTU (Maximum Transmission Unit) — максимальный размер пакета, который она может протащить через себя. Например, для Ethernet — 1500 байт, максимальный размер поля данных в фрейме. Если пакет переходит в сеть, через которую он не может пролезть целиком, IP фрагментирует пакет. В каждом фрагменте устанавливается идентификатор и смещение фрагмента. Это значит, что, теоретически, пакет может прийти нам по кусочкам.

Впрочем, минимально допустимый MTU для IP — 576 байт. Пакеты до такого размера фрагментироваться не будут. Если мы не собираемся посылать и принимать пакеты больше минимального MTU, на фрагментирование можно просто забить.

Пример кода:

// Коды протоколов
#define IP_PROTOCOL_ICMP    1
#define IP_PROTOCOL_TCP        6
#define IP_PROTOCOL_UDP        17

// IP-пакет
typedef struct ip_packet {
    uint8_t ver_head_len; // версия и длина заголовка =0x45
    uint8_t tos; //тип сервиса
    uint16_t total_len; //длина всего пакета
    uint16_t fragment_id; //идентификатор фрагмента
    uint16_t flags_framgent_offset; //смещение фрагмента
    uint8_t ttl; //TTL
    uint8_t protocol; //код протокола
    uint16_t cksum; //контрольная сумма заголовка
    uint32_t from_addr; //IP-адрес отправителя
    uint32_t to_addr; //IP-адрес получателя
    uint8_t data[];
} ip_packet_t;


// Отправка IP-пакета в ответ - для сервера
void ip_reply(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t *packet = (void*)(frame->data);

    // Заполняем заголовок
    packet->total_len = htons(len + sizeof(ip_packet_t));
    packet->fragment_id = 0;
    packet->flags_framgent_offset = 0;
    packet->ttl = IP_PACKET_TTL;
    packet->cksum = 0;
    packet->to_addr = packet->from_addr;
    packet->from_addr = ip_addr;
    packet->cksum = ip_cksum(0, (void*)packet, sizeof(ip_packet_t));

    // Заворачиваем в Ethernet-фрейм и отправляем
    eth_reply((void*)frame, len + sizeof(ip_packet_t));
}

// Обработчик IP-пакетов
void ip_filter(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t *packet = (void*)(frame->data);
    
    // Можно не проверять - минимальное поле данных Ethernet-фрейма
    //  всегда больше размера IP-заголовка
    //if(len >= sizeof(ip_packet_t))
    //{
        // Проверяем версию протокола и адрес получателя
        if( (packet->ver_head_len == 0x45) &&
            (packet->to_addr == ip_addr) )
        {
            // Вычисляем длину поля данных
            len = ntohs(packet->total_len) - 
                sizeof(ip_packet_t);

            switch(packet->protocol)
            {
            // Протокол = ICMP, вызываем обработчик пакетов
            case IP_PROTOCOL_ICMP:
                icmp_filter(frame, len);
                break;
            
            // Протокол = UDP, вызываем обработчик пакетов
            case IP_PROTOCOL_UDP:
                udp_filter(frame, len);
                break;
            }
        }
    //}
}

// Рассчёт контрольной суммы для IP (и других протоколов)
uint16_t ip_cksum(uint32_t sum, uint8_t *buf, size_t len)
{
    // Рассчитываем сумму word'ов блока (big endian)
    // (блок выравнивается на word нулём)
    while(len >= 2)
    {
        sum += ((uint16_t)*buf << 8) | *(buf+1);
        buf += 2;
        len -= 2;
    }

    if(len)
        sum += (uint16_t)*buf << 8;

    // Складываем старший и младший word суммы
    // пока не получим число, влезающее в word
    while(sum >> 16)
        sum = (sum & 0xffff) + (sum >> 16);

    // Снова конвертируем в big endian и берём дополнение
    return ~htons((uint16_t)sum);
}


ICMP


ICMP (Internet Control Message Protocol) — вспомогательный протокол сетевого уровня, работающий «рядом» с IP. ICMP служит, в том числе, и для диагностики сети. Всем известная утилита ping использует ICMP Echo-запросы. Если мы хотим, чтобы девайс пинговался, можно добавить поддержку ICMP Echo-запросов.

ICMP-сообщение заворачивается в IP-пакет. Echo-запрос и ответ выглядят вот так:

Ping-пакет

  • Тип пакета. Запрос (8) или ответ (0).
  • Код пакета — 0 для Echo-запроса и ответа.
  • Контрольная сумма заголовка рассчитыватся также, как и для заголовка IP-пакета.

Остальные поля устанавливаются на усмотрение хоста. Ответ должен содержать те же значения.

Пример кода:

// Тип пакета
#define ICMP_TYPE_ECHO_RQ    8
#define ICMP_TYPE_ECHO_RPLY    0

// ICMP Echo-пакет
typedef struct icmp_echo_packet {
    uint8_t type;
    uint8_t code;
    uint16_t cksum;
    uint16_t id;
    uint16_t seq;
    uint8_t data[];
} icmp_echo_packet_t;

// Обработчик ICMP-пакета
void icmp_filter(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t *packet = (void*)frame->data;
    icmp_echo_packet_t *icmp = (void*)packet->data;

    // Проверяем длину пакета
    if(len >= sizeof(icmp_echo_packet_t) )
    {
        // Получили Echo-запрос
        if(icmp->type == ICMP_TYPE_ECHO_RQ)
        {
            // Меняем тип пакета на ответ
            icmp->type = ICMP_TYPE_ECHO_RPLY;
            
            // Обновляем контрольную сумму,
            // мы изменили только одно поле в пакете,
            // так что пересчитывать полностью не обязательно
            icmp->cksum += 8;
            
            // Отправляем пакет назад
            ip_reply(frame, len);
        }
    }
}


UDP


UDP (User Datagram Protocol) — простейший протокол транспортного уровня. UDP позволяет узлам обмениваться небольшими сообщениями, называемыми датаграммами.

Тут мы реализуем UDP-сервер — информация будет отправляться только в ответ на запрос.

Чтобы датаграмма точно пролезла в MTU сети без фрагментации IP-пакета, количество полезных данных в ней не должно превышать 512 байт.

UDP называется ненадёжным протоколом — при потере IP-пакета, датаграмма теряется. Впрочем, современные сети вполне себе надёжны — в хорошо работающей локальной сети пакеты не теряются почти никогда. Но, естественно, приложение должно адекватно реагировать, если датаграмма всё же потеряется.

UDP отлично подходит для передачи данных в реальном времени, нечувствительных к потерям. Скажем, с помощью UDP удобно забирать показания каких-нибудь датчиков или отправлять инфомрацию на дисплей.

UDP-пакет заворачивается в IP-пакет. Формат UDP-пакета:

UDP-пакет

Контрольная сумма тут рассчитывается необычно. Не от самого пакета, а от пакета с псевдозаголовком, который выглядит вот так:

UDP-пакет с псевдозаголовком

Часть полей берётся из заголовка IP-пакета (который, кстати, и сам с контрольной суммой). Мда)

Рассчитаная от этой штуки контрольная сумма записывается уже в нормальный UDP-пакет.

Пример кода:

// UDP-пакет
typedef struct udp_packet {
    uint16_t from_port;
    uint16_t to_port;
    uint16_t len;
    uint16_t cksum;
    uint8_t data[];
} udp_packet_t;

// Обработчик UDP-пакета
void udp_filter(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t *ip = (void*)(frame->data);
    udp_packet_t *udp = (void*)(ip->data);

    // Проверяем длину заголовка
    if(len >= sizeof(udp_packet_t))
    {
        // Отдаём пакет приложению
        udp_packet(frame, ntohs(udp->len) - 
            sizeof(udp_packet_t));
    }
}

// Ответ на UDP-пакет
void udp_reply(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t *ip = (void*)(frame->data);
    udp_packet_t *udp = (void*)(ip->data);
    uint16_t temp;

    // Рассчитываем длину всего пакета
    len += sizeof(udp_packet_t);

    // Меняем местами порт отправителя и получателя
    temp = udp->from_port;
    udp->from_port = udp->to_port;
    udp->to_port = temp;

    // Длина пакета
    udp->len = htons(len);

    // Рассчитываем контрольную сумму от псведозаголовка + данных
    // Псведозаголовок = длина пакета+протокол+IP адреса+нормальный udp-заголовок
    // длину пакета+протокол передаём как начальное значение для 
    // рассчёта контрольной суммы
    // ip адреса берём из заголовка IP-пакета (udp-пакет - 8)
    udp->cksum = 0;
    udp->cksum = ip_cksum(len + IP_PROTOCOL_UDP, 
        (uint8_t*)udp-8, len+8);

    ip_reply(frame, len);
}


Приложение


Чтобы придать всему этому смысл, напишем простенькое приложение, работающее по UDP. Например, «переходник» Ethernet-UART.

// Цепляем библиотеку для буферизованной работы с UART
#include "buart.h"

// При приёме UDP-пакета
void udp_packet(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t *ip = (void*)(frame->data);
    udp_packet_t *udp = (void*)(ip->data);
    uint8_t *data = udp->data;
    uint8_t i, count;

    // Отпавляем данные в UART
    for(i = 0; i < len; ++i)
        uart_write(data[i]);

    // Возвращаем компу то, что наприходило 
    //  с момента последнего обмена
    count = uart_rx_count();
    if(count)
    {
        for(i = 0; i < count; ++i)
            data[i] = uart_read();
        udp_reply(frame, count);
    }
}

int main()
{
    uint8_t len;
    static uint8_t net_buf[576];
    eth_frame_t *frame = (void*)net_buf;

    // Инициализируем ENC28J60
    enc28j60_init(mac_addr);
    
    // Инициализируем UART
    uart_init();
    sei();

    // Ловим приходящие пакеты
    while(1)
    {
        if((len = enc28j60_recv_packet(net_buf, sizeof(net_buf))))
            eth_filter(frame, len);
    }

    return 0;
}


Работать с переходником просто. Чтобы вывести данные в UART, отправляем девайсу UDP-пакет на любой порт. Если с момента последнего обмена на UART приходили данные, девайс возвращает пакет с этими данными. Чтобы постоянно получать данные, комп должен с некоторым интервалом посылать девайсу пакеты (хотя бы пустые). В принципе, типичный способ общения с сервером.

Тест

Девайс этот полезен тем, что позволяет подключить к сети другие, уже сделанные девайсы. Ну и поэкспериментировать с сетью.

Заключение


Взять проект целиком можно здесь.

В этой части мы научились отвечать на UDP-пакеты. В следующей узнаем как самим отпрвить пакет, чтобы получить данные с другого сервера.

зы. Нужна ли статья по общению с сетевыми девайсами со стороны компа (ну и «выводе» девайсов в интернет, etc.) или это всё и так понятно?


Все статьи цикла

Комментарии (105)

RSS свернуть / развернуть
Наконец-то до меня дошло почему всю эту беду стеком обозвали :)
0
О, так в IP только заголовок КС защищен?
0
  • avatar
  • Vga
  • 31 марта 2011, 09:27
Ага. IP предполагает, что транспортный протокол сам может захотеть обработать ошибки или дать их обработать приложению. Есть, например, UDP Lite — вариант UDP, в котором контрольная сумма проверяется от заголовка и части датаграммы — полезно для приложений, допускающих небольшие помехи (VoIP, etc.)
0
" Нужна ли статья по общению с сетевыми девайсами со стороны компа (ну и «выводе» девайсов в интернет, etc.) или это всё и так понятно?"
Конечно нужна. Хотелось бы знать от и до.
Спасибо за информацию, за труды.
0
Вообще-то, инфы по этой части, в том числе и на русском — дофига. Искать по «сетевое программирование» и «сокеты» на программерских ресурсах.
0
Инфа действительно есть. Но мне нравится как подается здесь материал. И я хочу читать статьи Lifelover'а на эту тему и дальше.
0
У нас тут программеров минимум :) В основном железячники, так что надо.
0
канечно пиши
у тебя класно получается
например я некоторых нюансов не знал
пока не прочел твои статьи
0
Спасибо за статью. Сеть довольно прикольная штука. Вот сворганил небольшое продолжение UDP сервера под IAR + UDP клиент acvarif.info/progmk/progmk6.html Может кому пригодится.
0
Нужна ли статья по общению с сетевыми девайсами со стороны компа (ну и «выводе» девайсов в интернет, etc.)
А кстати, какая здесь связь? Девайсы, в принципе, уже готовы к выходу в инет — достаточно их подключить к нему, это задача сисадминская и изрядно зависит от железа. Правда, прежде, чем их выставлять голой жопой в инет — стоит позаботиться о защите (и, возможно, веб-морде) — но это все со стороны МК.
А вот общение со стороны компа — задача чисто программерская и довольно простая, благо стек в ОС уже полностью готов и используется через вполне стандартный интерфейс.
То есть, на мой взгляд это довольно разные вопросы.
0
  • avatar
  • Vga
  • 31 марта 2011, 10:41
Не а, это один но очень разноплановый вопрос. Чтобы сделать готовый продукт надо вникнуть во все нюансы. Как МК будет взаимодействовать с компьютером или как компьютер будет взаимодействовать с МК. МК и интернет, интернет и МК. Чем больше взаимосвязанной инфы будет в одном месте, тем проще будет вникнуть в суть вопроса. Особенно начинающим.
0
Ну, первые три статьи — тем более один вопрос. Разве что — если оба вопроса короткие и сгруппировать их в одну статью по принципу «это вне контроллера».
0
Конечно нужна. Прекрасная подача и разжевывание материала как в теоретической, так и в практической частях прекрасно располагает к чтению и пониманию.
(Поймал себя на мысли, что жду следующей части, какой-бы она не была)
0
Аффтар, пешы ысчо!!!111 Жызненна!!111одинодин

Наконец-то я разобрался, как оно работает! И про ПК тоже стОит. По крайней мере, я за.
0
  • avatar
  • _YS_
  • 31 марта 2011, 13:45
Спасибо, отличная статья, продолжай в том же духе :)
0
Еще бы чего нибудь про http сервер, чтобы можно было без проги на компе…
0
Ну это ближе к концу будет)
0
Добивающим ударом может стать связь МК-МК через инет :) Или хотя бы эзернет.
0
В принципе, при использовании протокола IP — это одно и то же. Нужно только провесить канал до инета и выделить IP девайсу. Ну или спрятать за NAT, выделив порт.
А вот HTTP — он, хотя и не сложный, но зато текстовый (причем не W 10 20 45 как конфигурирование всяких девайсов по COM), так что нужно достаточно много и ОЗУ (под разбираемые тексты), и ПЗУ (под контент).
0
По сему и интересно.
0
Классная подача материал. С удовольствием прочитал. Для полного счасть не хватает CGI сценариев и думаю, Ваши статьи дадут толчек многим к разработке множества интересных модулей на МК.

Большое СПАСИБО.
0
Прикрутил я enc28j60 к PinBoard. Всё работает.
Прям радость на душе :-)
0
хе. а пробовал мои сорцы собирать-запускать. рабоатет что нить?)
0
Угу)) Вот из этой статьи откомпилил, единственное что сделал так это подправил в проекте частоту МК на 8 мгц. Все заработало без проблем.
Спасибки тебе!
0
ну если попробуеш что то ещё (особенно мя волнует dhcp), отпишись работает или нет)
а то я тестил ведь только у ся. мало ли какие там баги)
0
Ок, я все твои статьи перепробую и еще схемы подключения к PinBoard с фотками сделаю.
0
О, было бы здорово :)
0
Спасибо за статью, пока единственная настолько разжевана и на русском.
Вопрос к автору каким образом можно послать пинг на пк и узнать мак адрес сетевой карты.
0
Проект настоился и откомпилился (Build succeeded with 0 Warnings…
) под mega128 (AvrStudio 4.13 Build 528). Но при попытке симуляции застревает на void enc28j60_soft_reset() В железе наверняка работать на будет. Оригинальный проект (mega16) симулируется легко. Не пойму, что не так
0
Ой, наверняка вечная трабла со SPI. Ножка SS (PB0 в м128) в режиме мастера должна быть или настроена на выход (DDRB |= 1<<PB0) или подтянута снаружи к земле.
Оригинальный проект (mega16) симулируется легко.
И даже пакеты принимает и отправляет?)
0
дефайны для меги128
#define ENC28J60_SPI_DDR	DDRB
#define ENC28J60_SPI_PORT	PORTB
#define ENC28J60_SPI_CS		(1<<PB0)
#define ENC28J60_SPI_MOSI	(1<<PB2)
#define ENC28J60_SPI_MISO	(1<<PB3)
#define ENC28J60_SPI_SCK	(1<<PB1)

Потом PB0 всеравно инициализируется на выход
// Initialize SPI
ENC28J60_SPI_DDR |= ENC28J60_SPI_CS|ENC28J60_SPI_MOSI|ENC28J60_SPI_SCK;

Теперь симулируется легко (имеется ввиду симулятор, а не jtag), но железо вроде работает но на запросы не отвечает.
На железе:
ss — отрицательные импульсы длительность 4мкс период 15мкс
sck — по две ВЧ пачки (по положению внутри ss) период между пачками как и ss
mosi — по 2 отрицательных импулься (по положению внутри ss) период тот же
0
А, я думал ты под протеусом гонял.
0
Пока пытаюсь на AvrStudio.
Я так понимаю, что железо работает правильно (судя по импульсам), тоесть ожидает пакеты?
Похоже я не допираю как на enc28 подать пакет с сообщением.
Подключил все к внутренней сети (у нас сетка 192.168.2.1, себе выставил в проекте IP 192.168.2.15). Запускаю утилиту nc и пишу nc -u 192.168.2.15 12345 test
Я так понимаю, что железо должно поймать это и перетранслировать на ком. Но ком гад молчит как рыба об лед.
0
я делал на этом стэке переходник Ethernet — com RS-485 на Atmega128a если интересно могу поделиться проектом и платой
0
Конечно интересно. Буду признателен. Может и у меня заработает…
Ящик ygusin (собака) gmail.com
0
я сделаю не так- тисну маленькую статейку и там все выложу ok?
+1
О, давай!
0
Как все же правильно подключить к сети данный UDP сервер?
Через хаб или к сетевухе компа. Если к сетевой карте компа то как ее настроить? Проект в железе вроде крутится, а увидеть это воочию не удается.
Как в проекте сделать признак, что enc28… проинициализировалась правильно (все регистры получили, что надо)?
Как можно увидеть, что утилита nc посылает пакет на нужный IP?
Как в проекте сделать какой либо признак о том, что пакет принят?
Вопросы вроде простые, но небольшой опыт работы с сеткой не дает ответа на эти вопросы. Ответьте кому не лень…
0
В прошивке настроить айпи, подключать к хабу прямым кабелем.
0
Сделал пока так:
void lan_poll()
{
	uint16_t len = 0;
	eth_frame_t *frame = (void*)net_buf;
	
	while((len = enc28j60_recv_packet(net_buf, sizeof(net_buf))))
	{
		DDRC |= (1 << DDC1);                          
   		PORTC |= (1 << PC1);          
   		_delay_ms(1);                   
   		PORTC &= ~(1 << PC1);          
		eth_filter(frame, len);
	}
}

Если enc принимает пакеты (любые) то на PC1 будут появляться 1мс импульсы. Это действительно происходит, поскольку в сети всегда чего нибудь твориться. Кроме того, поскольку импульсы имеются, значит enc работает и что-то принимает. Я прав?
Теперь сделал так:
void ip_filter(eth_frame_t *frame, uint16_t len)
{
	ip_packet_t *packet = (void*)(frame->data);
	
	//if(len >= sizeof(ip_packet_t))
	//{
		if( (packet->ver_head_len == 0x45) &&
			(packet->to_addr == ip_addr) )
		{
			len = ntohs(packet->total_len) - 
				sizeof(ip_packet_t);

			switch(packet->protocol)
			{
#ifdef WITH_ICMP
			case IP_PROTOCOL_ICMP:
				icmp_filter(frame, len);
				break;
#endif
			case IP_PROTOCOL_UDP:
				udp_filter(frame, len);
				break;
			}
		    
			DDRC |= (1 << DDC0);                          
    		PORTC |= (1 << PC0);          
    		_delay_ms(1);                   
    		PORTC &= ~(1 << PC0);          
		}
	//}
}

Если принят пакет и в нем длина и IP что надо то появится 1 мс импульс на PC0. У меня после команды nc -u 192.168.2.15 12345 test ничего не происходит. Не допираю в чем секрет… как еще кроме nc можно посылать UDP пакеты? Может как-то можно это делать в цикле? С помощью чего?
0
Наконец заработало. Применил это www.hwgroup.cz/products/hercules/index_en.html
Согласование уровней для меги128а вроде не нужно. Похоже и так все понимает.
Спасибо автору.
0
Можно прокомментировать эту запись (подобные в проекте почти в каждой функци)?
void arp_filter(eth_frame_t *frame, uint16_t len){    arp_message_t *msg = (void*)(frame->data);

Создан указатель *msg на структуру arp_message_t, а дальше…
0
Перенес код на STM32F100
Сделал так:
while(1)
   {
     if((len = enc28j60_recv_packet(net_buf, sizeof(net_buf))))
     {
            eth_filter(frame, len);
     }
   }
Отправляю с ПК, через дабагер смотрю что заходит у функцию eth_filter(frame, len); дальше
case ETH_TYPE_ARP: 
arp_filter(frame, len - sizeof(eth_frame_t));
и вот на проверке
if( (msg->type == ARP_TYPE_REQUEST) && (msg->ip_addr_to == ip_addr) )
ip_addr_to =0 как такое может быть?

Основной шлюз 192.168.1.1
IP 192.168.1.100
В проекте поставил 192.168.1.101
0
  • avatar
  • VIC
  • 10 июня 2012, 17:08
прежде всего спасибо за отличный цикл статей! Просто все супер!
Но и не без вопроса конечно… Я натолкнулся на странную ситуацию. При реализации PING получаю следующее все вроде работает но ответы пинга я вижу только в сниффере а сама команда на них не реагирует… ПОЧТИ! Ладно б совсем не реагировала! Но времени от времени она ловит мой ответ. Но крайне редко и с огромной задержкой. При этом сниффер реагирует мгновенно. Может кто что подскажет? Пингую из под Windows
0
вы решили эту проблему? у меня похожая ситуация…
0
хочу поделится своим опытом доработки данного примера под STM32:
1) в виду проблем с паковкой все структуры данных следует упаковывать, иначе когда по структуре начинаешь «бегать» относительно ее начала попадаешь мимо полей нужных, т.к. компилятор выравнивает все структуры по dword? запретить ему это делать надо вот как:
typedef struct arp_message {
	uint16_t hw_type;
	uint16_t proto_type;
	uint8_t hw_addr_len;
	uint8_t proto_addr_len;
	uint16_t type;
	uint8_t mac_addr_from[6];
	uint32_t ip_addr_from;
	uint8_t mac_addr_to[6];
	uint32_t ip_addr_to;
} __attribute__ ((__packed__)) arp_message_t;

2)причину тут я не понял но видимо из за тормознутости enc28j60 на высоких скоростях без задержек у меня ничего не заработало, вернее работало только в дебагере, когда код выполнялся медленно, проблема решилась когда я перед обработкой очередного фрейма делал небольшую задержку, примерно 2 мсек:
void eth_filter(eth_frame_t *frame, uint16_t len)
{
	if(len >= sizeof(eth_frame_t))
	{
	    Delay_ms(2); // ВОТ ТУТ СТАВИМ ЗАДЕРЖКУ И ВУАЛЯ!!!!

		switch(frame->type)
		{
		case ETH_TYPE_ARP:
			arp_filter(frame, len - sizeof(eth_frame_t));
			break;
		case ETH_TYPE_IP:
			ip_filter(frame, len - sizeof(eth_frame_t));
			break;
		}
	}
}
0
стоит упомянуть что тестировалось и проверялось все на покупном модуле Ethernet и покупном STM32-Discovery:
0
Привет zwitch! Тисни название покупного модуля Ethernet и где его можно купить. И если не очень в лом дай пошире свой упомянутый опыт. Заранее благодарен.
0
Я тоже такую штуку непрофессионально заюзал, брал на ебае (ENC28J60 Ethernet LAN Network Module). Трудно поддаётся понимание материала, но чую структурированность кода (тяжело в ученье — легко на работе). Закурил я eth_232 код на Атмеге8@1MHz, со второго пинка пинг завёлся, чему я был несказанно рад, мерси за это автору. Размер данных больше 466 байтов не проходит — нет ответа (может памяти в кристалле мало) при этом задержка 35-40мс, пробовал заводить 5 пингов на енку по 466 байтов каждый, задержка увеличилась и стала нестабильной, так и должно быть.
0
Простите, что не совсем топик, но всё же.

Загрузил исходники по ссылке выше, создал проект в AVR Studio 4. Пытаюсь сделать build, но он пишет, что не может найти файл eth_232.elf. Я начинающий в деле программирования. Если можете, то подскажите в чем проблема и что мне надо поменять в коде или makefile'e?

Заранее спасибо
0
Убедитесь что в настройках проекта не стоит галочка на использовании внешнего makefile. проект очистите (clean). Затем пробуйте собирать…
0
Скачал проект из статьи, скомпилировал под внутренний кварц 8МГц, контроллер тот же, операционка XP. Скачал Netcat по первой ссылке (нативную сборку), закинул в system32.
Пишу в командной строке: nc -u 192.168.0.222 12345 test; в ответ получаю: invalid port test: NO_DATA. Как правильно пользоваться NetCat в данном случае?
0
устройство пингуется?
0
Подключил через свитч. Устройство в сети не появилось. Соответственно, на ping не отвечает: PING: сбой передачи. General failure. Но в то же время ENC пересылает контроллеру все сетевые пакеты от других приложений(браузер, антивирус, …)
0
Лампочка, надо полагать, на свитче горит?

поддержку ICMP включили, UPD настроили? Какая скорость передачи на SPI?
0
Да, лампочка горит. hex брал у DI в статье Pinboard II. Ethernet модуль на базе ENC28J60. Контроллер ATMega16, частота 8MHz. Проект даже не компилил. ICMP и UDP в проекте настроены. Скорость передачи по SPI 4Mbit.
0
IP адрес ENC в Вашей подсети? С маком не намудрили?
0
IP 192.168.2.222 MAC 00:13:37:01:23:45, как у всех.
0
А сама подсеть в которой ты работаешь, с каким IP?
0
Подключал через свитч (т.к. в наличии провода только с прямой обжимкой) просто к одному компу (192.168.7.100).
0
А как ты собираешься пинговать устройство в другой подсети? Не пробовал сделать компу адрес 192.168.2.100?
0
адрес компа поменял на 192.168.2.100, не подумал, спасибо. Только компьютер пишет то же самое: узел недоступен.
0
Значит настройка прошла не полностью, либо не прошла вообще, либо «эта хр… на не работает...». :) проверяй соединения, подключения, порты на соответствие. На какой платформе балуешься?
0
Пробовал на XP и семерке: результат один. Насчет «эта хр… на не работает...» есть сомнения, т.к. ENC ловила пакеты от оперы и вебера. Думаю подключить лог. анализатор и проверить все пакеты с ENC. Может там собака зарыта.
0
Да там как раз все ок… либо обмен идет и идет правильно, либо не идет…
0
И маску подсети не забудь настроить…
0
достаточно просто маску подсети выставить 255.255.0.0 — и сеть останется, и девайс будет виден.
0
Вопрос в том, адекватно ли написана библиотека — я не помню этого момента. )))
0
Библиотеку вообще не трогал, взял полностью весь проект.
0
пробовал, не помогло.
0
Еще прошу определиться — через свитч или напрямую? Свитчи уже давно автокроссом обладают, как и компы — тип кабеля пофигу.
0
через свитч DES-1005A.
0
Вот сейчас мне мысль пришла — взял я и выдернул кабель сетевой из компа и попробовал пингануть несуществующий хост — выдало «аппаратный сбой». Если кабель не вытаскивать и пингануть несуществующий узел — будет ответ «заданный узел недоступен». Чуешь к чему это я веду?
0
Пробовал выдернуть кабель, комп написал то же самое: заданный узел недоступен. К чему ведёшь не чую :)
0
к тому что ENC линк не подняла — читай не настроил твой МК эзернет либо разъем подключен неверно.
0
слушай, путаешься в показаниях — в начале у тебя был аппаратный сбой, теперь узел недоступен — ошибки разные и причины разные. Прошу срочно определиться!!!
0
Ошибки разные потому, что с разных компов. Где узел недоступен там комп с ХР, где general failure там семерка. Сегодняшние попытки были на ХР, сейчас сделал все тоже самое на семерке (настроил IP и маску) и все заработало. Почему на ХР не работает непонятно, может настройки где, а может и глючная винда.
0
винда в которой не работает примитивная часть сети — нет нет, Вы не правы ))) Скорее всего уж надо грешить на себя…
0
Как я выяснил на этом компе (с ХР) стоит специальная программа для работы, которая блокирует все чуждые сетевые соединения, удалить или заблокировать её не получается, а на семёрке всё ок. Вообщем подытожим: проблема была в неправильных настройках сетевых адресов компа.
0
Большое спасибо! :)
0
пжлста
0
Чет не могу понять. С приемом пакетов все нормально. На пинги отвечает, ARP тоже нормально резолвится. А вот с отправкой пакетов UDP — косяк. Шарк ругается на контрольную сумму IP пакета.
Вот ответ железки (отправляю 16 символов «1»)

0000  00 19 5b ea 7b ae 00 13  37 01 23 45 08 00 45 00   ..[.{... 7.#E..E.
0010  00 2d 00 00 00 00 40 11  <strong>b9 2b</strong> c0 a8 79 de c0 a8   .-....@. .+..y...
0020  79 65 4d bc fd 99 00 18  b6 49 31 31 31 31 31 31   yeM..... .I111111
0030  31 31 31 31 31 31 31 31  31 31 00 00               11111111 11..    

А шарк говорит, что Header checksum: 0xb92b [incorrect, should be 0x062c (may be caused by «IP checksum offload»?)]
0
Приветствую. Ну во-первых спасибо автору за статью, как раз то, что я искал. Заранее извиняюсь за дилетантские вопросы. Вот пытаюсь использовать этот проект для подключения enc28j60 к pic18f2455. Если я правильно понял, в папке проекта eth_232 все блоками, так как в основной файле buart.c, lan.c и enc28j60.c не присоединены. Попытался компилировать, ошибки полезли. Сначала регистры исправил, потом типы данных изменил (uint8_t на unsigned char, uint16_t на unsigned int и uint32_t на unsigned long). Да, и еще data заменил на Data, и code на Code, так как она в этой проге как-то не так воспринимаются. Вот теперь вылазит ошибка, никак не могу понять, чего надо исправить, так как с программированием у меня туго. Подскажите, кто может. Буду очень благодарен. Вот фото ошибки:
0
Во первых, не рекомендую mikroC. Я невысокого мнения о качестве их компилятора. Да и из всех доступных он похоже наименее соответсвует стандартам С.
Во вторых, что значит «присоединены»? Это С, никакого присоединения тут нет. Файлы компилируются независимо и затем линкуются в единую программу, причем ты сам должен указать из каких именно файлов нужно собирать программу. В современных средах это делается добавлением требуемых .c файлов в проект.
И в третьих, стек написан для компилятора AVR-GCC. Рекомендую в нем и работать, т.к. не похоже что ты обладаешь достаточными знаниями для портирования на другой компилятор.
В четвертых, если ты хочешь, чтобы кто-то таки ковырялся с твоим вариантом на mikroC — выложи весь проект на нем. Кто знает, что ты там наменял, на скрине все нормально.
0
Понятно. Ну переходить с pic на avr не хотелось бы, так как с ним я хоть более менее освоился. А вот по поводу проги, если mikroC так плох, что лучше использовать?
0
А, это еще и пик. Тогда вообще никаких гаранти2й, нужно портировать с пониманием того, как работает стек, МК и компилятор и в чем различия между платформами (angel5a вон, например, успешно портировал этот стек на STM8, рекомендую почитать его опыт).
По поводу компиляторов для PIC почти ничего не скажу. Для них вообще на удивление мало С-компилеров. Хорошим вроде считается HI-TECH PIC C для младших семейств, для старших обычно применяются C18/C30/C32 от самого микрочипа. Еще есть новый комплект компиляторов от микрочипа — XC8, XC16, XC32 (вроде так), из комплекта MPLAB X, но я ничего не знаю о их качестве. Вот только они все поголовно платные, в бесплатных версиях (если есть) как правило отключена оптимизация.
Ну и современные версии поделок микроэлектроники вроде уже не так лажают и косячат, как первые.
0
Ну и на это спасибо)
0
Ну и все-таки если кто может подсказать по поводу кода на mikroC, вот исходники
0
0
Бросай этот mikroC. Я пользовался, пока не стали вылезать непонятные глюки (типа страшных ошибок в пустой строке. Иногда лечилось просто копированием текста в новый файл).
Лучше уж CCS. Тоже не бесплатный… И тоже не без глюков. Но кого это останавливает?
0
Добрый день!
У меня есть предложение по ускорению расчета контрольной суммы заголовка)
Дело в том, что во время суммирования не обязательно переводить из Big-endian в Little-endian как у Вас:
0
Я прошу прощения за неполное сообщение, что-то пошло не так когда я писал пост, коммент отправился после нажатия Ctrl+V.

Добрый день!
У меня есть предложение по ускорению расчета контрольной суммы заголовка)
Дело в том, что во время суммирования не обязательно переводить из Big-endian в Little-endian как у Вас:
<code>sum += ((uint16_t)*buf << 8) | *(buf+1);</code>
Можно просто посчитать сумму(с поразярдным дополнением) 16-битных чисел заголовка в том порядке байтов, в котором они лежат в структуре ip_packet, поскольку в конце, перед записью в поле контрольной суммы заголовка его и так надо записывать в big-endian.
вот так:
<code>// Расчёт контрольной суммы для IP (и других протоколов)
uint16_t ip_cksum(uint32_t sum, uint16_t *buf, size_t len)
{
    // Рассчитываем сумму word'ов блока
    while(len >= 2)
    {
        sum += *buf;
        buf ++;
        len -= 2;
    }

    if(len)
        sum += *buf;

    // Складываем старший и младший word суммы
    // пока не получим число, влезающее в word
    while(sum >> 16)
        sum = (sum & 0xffff) + (sum >> 16);

    //Берём дополнение до единицы
    return ~((uint16_t)sum);
}</code>
0
Автору огромное спасибо за статьи, сижу прорабатываю)))) Соответственно к сожалению вопрос,
щас гоняю под протеусом, все нормально, пинг идет на ура, из уарта в удп передается все нормально,
а вот обратно из удп в уарт идти-то идет но почему-то сообщения дублируются, то есть если мы посылаем
удп пакет с содержимым 1 то в уарте мы увидим 11 причем и при передаче из уарта в удп и удпэшный
(отправленный в удп) пакет и уартовский(отправленный до этого по уарту) дублируются оба в уарт.
Это мне одному не повезло с протеусом или это на самом деле?
0
А-аа сорри доперло)) это видимо потому, что я вместо реальной сети использую заглушку(аппаратный лупбэк) для имитации нахождения в сети, а то протеус коннектится только к физическому интерфейсу, а локалки у меня под рукой нету. Надо будет проверить живьем))
0
проверил, все работает замечательно, надо было написать раньше, автору респект-)
0
Большое спасибо Автору за статью! Во всем инете даже нечего рядом поставить.
Можно разъяснить ситуацию, когда udp-server стоит дома, а я хочу получить от него данные в другом городе.
Какой IP нужно и можно назначить, так чтобы его издалека увидеть?
Может где почитать об этом?
0
IP выдается провайдером. Подключаешь к прову так же, как и любой другой девайс (т.е. как правило через роутер), при необходимости прокидываешь порты на роутере.
Если адрес динамический — можно использовать динамический DNS (noip.com, например). Если адрес за NAT'ом… Ну, тогда придется изучать вопрос покупки у провайдера более белого айпи, либо отказываться от сервера на девайсе (сервер можно расположить на хостинге в инете, а девайс будет сам периодически связываться с этим сервером и обмениваться данными).
0
Спасибо, все ясно!
0
Люди добрые и знающие, подскажите.
1) if((len = enc28j60_recv_packet(net_buf, sizeof(net_buf))))
Данное выражение означает, если >0
2) У меня есть возможность посмотреть любую переменную в микроконтроллере находу и нет uart. Как восстановить функцию udp_packet(), чтоб она просто этот самый udp принимала.
3) Никак не могу понять, в какой переменной мне посмотреть принятый udp_packet. Вот отправил я к примеру цифру 4 с компа, где мне ее увидеть?

4) ЧТо задать со стороны компа в настройках IP, маски подсети и основного шлюза? Я же так понимаю тут комп главный и задавать надо вручную
Не уверен, что все работает верно, но считать из регистра ENC записанный при инициализации MAC адрес удалось и он верный. Следовательно SPI работает правильно.
Заранее спасибо. Извиняюсь за дилетантский уровень вопросов
0
1) Данное выражение присваивает переменной len значение, возвращенное функцией enc28j60_recv_packet и проверяет его на неравенство нулю.
2) Убрать все содержимое функции udp_packet, начиная с "// Отпавляем данные в UART". Правда, тогда она вообще ничего полезного делать не будет и оптимизатор скорее всего выкинет все не нужные более действия вроде помещения указателя на payload в переменную data. Можно просто вернуть пакет как есть, заменив удаленное строчкой udp_reply(frame, len).
3) Полезные данные (payload) пакета можно посмотреть в переменной uint8_t *data в функции udp_packet.
4) Зависит от конфигурации твоей сети.
0
Спасибо за ответ, надеюсь найти его на данные простые для знающих вопросы.
1) Функция udp_reply(), как я понял, отправляет принятый UDP пакет обратно отправителю. А зачем это нужно в данном случае, когда пакет приходит в комп по uart. Или я понял чего не правильно. Зачем вообще эта функция нужна?
2) Данное объявление меня немного смущает
eth_frame_t *frame = (void*)net_buf;
То есть мы объявляем указатель типа eth_frame_t, в котором куча переменных и вкидываем в него просто массив net_buf. Как так они стыкуются. Просто по порядку? А что не влезло? Можно пояснить
3) Приведение к типу указателя (void*) какое имеет тут значение, зачем и почему применяется?
Заранее спасибо
0
1) Эта функция для отправки ответа на датаграмму. В приведенном автором коде оно отсылает принятое из UART'а.
2, 3 — учи си, мне лень устраивать ликбез по нему.
0
возможно, ошибка:

int main()
{
    uint8_t len;
    static uint8_t net_buf[576];
    eth_frame_t *frame = (void*)net_buf;
   …

   …
    // Ловим приходящие пакеты
    while(1)
    {
        if((len = enc28j60_recv_packet(net_buf, sizeof(net_buf))))
            eth_filter(frame, len);
    }

 …

разве len uint8_t? Ведь функция  enc28j60_recv_packet возвращает uint16_t.
Спасибо за статьи, они очень помогают!
+1
Если ктото использует IAR и stm32 для этого проекта

для типов используйте __packed например:
typedef __packed struct eth_frame {
	uint8_t to_addr[6];
	uint8_t from_addr[6];
	uint16_t type;
	//uint8_t *data;
} eth_frame_t;


и для указателей на поля данных eth, ip, udp используйте отдельные указатели uint8_t
придется добавить по строчке в каждый уровень например
void lan_poll()
{
	uint16_t len;
	eth_frame_t *frame = (eth_frame_t*)net_buf;
  eth_data_p = net_buf + sizeof(eth_frame_t);
	
	while((len = enc.recv_packet(net_buf, sizeof(net_buf))))
	{
		eth_filter(frame, len);
	}
}
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.