Подключение микроконтроллера к локальной сети: TCP и HTTP (продолжение)

В этой части речь пойдёт в основном про отдачу относительно жирной статики по HTTP.

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

  • Еррата
  • Отдача статики, соображения
  • Пробуем отдавать картинку
  • TCP-ретрансмиссии
  • Пишем простенький HTTP-сервер
  • Заключение

Сорри за такую большую задержку)

Еррата


В то время, пока я писал и отлаживал всё то, о чём пойдёт речь в этой части, выяснилась ещё одна прикольная особенность микрухи ENC28J60. Несмотря на то, что сказано в даташите, вход сброса у ENC28J60 не подтянут ни к чему. Так что, если оставить вход болтаться, ENC28J60 может сброситься от любой наводки в неинициализированное состояние в самый неудачный момент. А в остальное время работать идеально. Очень подлый баг. И в еррате, кстати, тоже ни слова)

Отдача тяжёлой статики, соображения


В предыдущей части мы отдавали по HTTP небольшие странички, помещавшиеся в 1 пакет. Выглядело это примерно так:

картинко

Проще говоря, алгоритм сводился к следующему:

  1. Дождаться, пока от клиента придёт какой-нибудь пакет
  2. В зависимости от того, какой пакет пришёл и текущего состояния, отправить соответствующий ответ

Всё это работало весьма неплохо и быстро. Можно предположить, что подобный метод можно применить и для отдачи файлов побольше.

картинко

  1. Ждём, пока клиент установит соединение
  2. Отдаём файл следующим образом:
    1. Дожидаемся от клиента ACK
    2. Отправляем пакет с данными
  3. Закрываем соединение

В принципе, работать такая штука будет, но, к сожалению, очень уж медленно.

Допустим, от сервера к клиенту и обратно пакет идёт около 25 мс (вполне нормальная задержка для интернетов). В таком случае, даже в идеальных условиях, мы не сможем передать больше 40 пакетов с данными в секунду. Если в каждом пакете будет, скажем, по 512 байт данных, нехитрый рассчёт подсказывает максимально возможную скорость — 20 кб/с.

Более того, как правило, узел не отправляет ACK на каждый пакет сразу. Поскольку в TCP применяется куммулятивное подтверждение (подтверждаются не отдельные пакеты, а весь поток, до определённой позиции), узел обычно посылает подтверждения через несколько пакетов, либо, после определённого таймаута, если поток приостанавливается. Этот таймаут обычно составляет около 100 мс. Так что, при использовании нашего алгоритма, мы, в лучшем случае, сможем отправить всего лишь 10 пакетов с данными в секунду. А значит не получим скорости больше 5 кб/с. А в реальной жизни выходит ещё меньше — где-то 1-3 кб/с.

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

картинко

Теперь всё работает немного побыстрее. Но всё ещё остаётся проблемка с таймаутами. Удалённый узел запросто может прислать подтверждение за 1 пакет до конца пачки, которое мы проигнорируем, а затем наивно дожидаться от нас новых пакетов. Подтверждение на последний пакет пачки, естественно, пришлёт только через таймаут.

картинко

Такая штука, всё же, работает немного побыстрее чем предыдущий алгоритм, но скорость, всё равно, выходит около 20 кб/с.

К счастью, данная трабла решается доволно просто. Достаточно в последнем пакете каждой пачки установить флаг PSH. На такой пакет подтверждение отправляется немедленно.

картинко

Остался ещё один баг, который нужно пофиксить. Нормальная реализация TCP отправляет пакеты более-менее равномерно. У нас же пакеты уходят залпами, почти вплотную. Увидев столько пакетов одновременно, свитч может офигеть, и какой-нибудь из них потерять. Единственное решение — вставить тупую, дедовскую задержку перед отправкой пакета. Скажем, 1,5 мс. Тогда всё работает отлично)

Пробуем отдавать картинку


Попробуем применить всё это на деле и научить наш стек более-менее прилично отдавать статические файлы.

Большая часть кода знакома нам по предыдущим частям, так что тут я опишу лишь наиболее важные изменения)

Добавим несколько полей в контекст TCP-соединения:

typedef struct tcp_state {
    
    // Код состояния
    tcp_status_code_t status;
    
    // Время последнего получения пакета
    //    (для таймаутов)
    uint32_t event_time;
    
    // Указатель потока
    //    (номер следующего отправляемого пакета)
    uint32_t seq_num;
    
    // Указатель подтверждения
    //    (докуда мы получили поток от удалённого узла)
    uint32_t ack_num;
    
    // Адрес и порт удалённого узла и наш порт
    uint32_t remote_addr;
    uint16_t remote_port;
    uint16_t local_port;
} tcp_state_t;


Добавили указатель потока и указатель подтверждения.

Отправка одного TCP-пакета.


// Режим отправки пакета
//    используется в основном для оптимизации отправки
typedef enum tcp_sending_mode {
    // Отправляем новый пакет.
    // При этом резолвим MAC-адрес получателя,
    //    выставляем все поля пакета, 
    //    залиыаем данные,
    //    считаем чексумму.
    // Самый медленный способ отправки.
    TCP_SENDING_SEND,

    // Только что получили пакет от удалённого узла и
    //    отправляем ему пакет в ответ.
    // При этом обмениваем в нём адреса получателя/отправителя,
    //    обновляем указатели seq/ack,
    //    заливаем новые данные,
    //    пересчитываем чексумму.
    //    Затем тупо пинаем пакет туда, откуда он пришёл
    TCP_SENDING_REPLY,
    
    // Только что отправляли пакет узлу и 
    //    отправляем следом ещё один.
    // При этом заливаем новые данные,
    //    обновляем указатели seq/ack,
    //    пересчитываем чексумму,
    //    все адреса оставляем те же.
    // Самый быстрый способ отправки.
    // После отправки любого пакета, этот режим 
    //    активируется автоматически.
    TCP_SENDING_RESEND
} tcp_sending_mode_t;

tcp_sending_mode_t tcp_send_mode;

// Признак отправки подтверждения
//    устанавливается при отправке пакета с установленым флагом ACK.
uint8_t tcp_ack_sent;

// Отправка пакета
//    Поле tcp.flags должно быть установлено вручную
uint8_t tcp_xmit(tcp_state_t *st, eth_frame_t *frame, uint16_t len)
{
    uint8_t status = 1;
    uint16_t temp, plen = len;

    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);

    // Отправляем новый пакет?
    if(tcp_send_mode == TCP_SENDING_SEND)
    {
        // Устанавливаем поля в заголовке
        ip->to_addr = st->remote_addr;
        ip->from_addr = ip_addr;
        ip->protocol = IP_PROTOCOL_TCP;
        
        tcp->to_port = st->remote_port;
        tcp->from_port = st->local_port;
    }

    // Отправляем пакет в ответ?
    if(tcp_send_mode == TCP_SENDING_REPLY)
    {
        // Меняем местами порты отправителя/получателя
        temp = tcp->from_port;
        tcp->from_port = tcp->to_port;
        tcp->to_port = temp;
    }

    if(tcp_send_mode != TCP_SENDING_RESEND)
    {
        // Устанавливаем поля в заголовке
        tcp->window = htons(TCP_WINDOW_SIZE);
        tcp->urgent_ptr = 0;
    }

    // Если отправляем SYN, добавляем опцию MSS
    //        (максимальный размер сегмента)
    if(tcp->flags & TCP_FLAG_SYN)
    {
        tcp->data_offset = (sizeof(tcp_packet_t) + 4) << 2;
        tcp->data[0] = 2;//MSS
        tcp->data[1] = 4;//длина
        tcp->data[2] = TCP_SYN_MSS>>8;
        tcp->data[3] = TCP_SYN_MSS&0xff;
        plen = 4;
    }
    else
    {
        tcp->data_offset = sizeof(tcp_packet_t) << 2;
    }

    // Устанавливаем указатели потока
    tcp->seq_num = htonl(st->seq_num);
    tcp->ack_num = htonl(st->ack_num);

    // Вычисляем контрольную сумму
    plen += sizeof(tcp_packet_t);
    tcp->cksum = 0;
    tcp->cksum = ip_cksum(plen + IP_PROTOCOL_TCP,
        (uint8_t*)tcp - 8, plen + 8);

    // Отправляем всю эту фигню в виде IP-пакета
    switch(tcp_send_mode)
    {
    case TCP_SENDING_SEND:
        status = ip_send(frame, plen);
        tcp_send_mode = TCP_SENDING_RESEND;
        break;
    case TCP_SENDING_REPLY:
        ip_reply(frame, plen);
        tcp_send_mode = TCP_SENDING_RESEND;
        break;
    case TCP_SENDING_RESEND:
        ip_resend(frame, plen);
        break;
    }

    // Обновляем наш указатель потока
    st->seq_num += len;
    if( (tcp->flags & TCP_FLAG_SYN) || (tcp->flags & TCP_FLAG_FIN) )
        st->seq_num++;

    // Устанавливаем флаг отправки подтверждения
    if( (tcp->flags & TCP_FLAG_ACK) && (status) )
        tcp_ack_sent = 1;

    return status;
}


Здесь добавились режимы отправки, позволяющие, например, посылать несколько пакетов подряд.

Приложение будет отправлять данные с помощью вот такой штуки:

// Отправить блок с установленным флагом PSH
#define TCP_OPTION_PUSH            0x01
// Отправить блок и закрыть соединение
#define TCP_OPTION_CLOSE        0x02

// Отправляет данные по TCP
void tcp_send(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t options)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    tcp_state_t *st = tcp_pool + id;
    uint8_t flags = TCP_FLAG_ACK;

    // Если соединение не устновлено, выходим - защита от дурака)
    if(st->status != TCP_ESTABLISHED)
        return;
    
    // Если нужно, добавляем флаг PUSH
    if(options & TCP_OPTION_PUSH)
        flags |= TCP_FLAG_PSH;

    // Если закрываем соединения, добавляем флаг FIN
    //    и начинаем ждать, пока удалённый 
    //            узел тоже пришлёт FIN/ACK
    if(options & TCP_OPTION_CLOSE)
    {
        flags |= TCP_FLAG_FIN;
        st->status = TCP_FIN_WAIT;
    }

    // Пинаем пакет куда нужно
    tcp->flags = flags;
    tcp_xmit(st, frame, len);
}


Здесь у нас добавились опции отправки.

А вот обработчик приходящих пакетов:

// Обработчик TCP-пакетов
void tcp_filter(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    tcp_state_t *st = 0, *pst;
    uint8_t id, tcpflags;

    // Пришло не на наш адрес (например, на широковещательный) - выбрасываем
    // Кто будет посылать TCP-пакеты на широковещательный адрес?
    // ХЗ, подстрахуемся на всякий случай))
    if(ip->to_addr != ip_addr)
        return;

    // Отнимаем размер заголовка и получаем количество данных в пакете
    len -= tcp_head_size(tcp);

    // Отрезаем для удобства флаги SYN, FIN, ACK и RST
    //    другие флаги нам не интересны
    tcpflags = tcp->flags & (TCP_FLAG_SYN|TCP_FLAG_ACK|
        TCP_FLAG_RST|TCP_FLAG_FIN);

    // Переключаем tcp_xmit в режим отправки пакетов "в ответ"
    tcp_send_mode = TCP_SENDING_REPLY;
    tcp_ack_sent = 0;

    // Ищем к какому из активных соединений относится
    //    полученный пакет
    for(id = 0; id < TCP_MAX_CONNECTIONS; ++id)
    {
        pst = tcp_pool + id;

        if( (pst->status != TCP_CLOSED) &&
            (ip->from_addr == pst->remote_addr) &&
            (tcp->from_port == pst->remote_port) &&
            (tcp->to_port == pst->local_port) )
        {
            st = pst;
            break;
        }
    }

    // Не нашли такого соединения - пакет является запросом
    //    на соединение или просто каким-то заблудившимся пакетом
    if(!st)
    {
        // Получили запрос на соединение?
        if( tcpflags == TCP_FLAG_SYN )
        {
            // Ищем свободный слот, куда добавить новое соединение
            for(id = 0; id < TCP_MAX_CONNECTIONS; ++id)
            {
                pst = tcp_pool + id;

                if(pst->status == TCP_CLOSED)
                {
                    st = pst;
                    break;
                }
            }

            // Нашли слот, и приложение подтверждает это соединение
            if(st && tcp_listen(id, frame))
            {
                // Добавляем новое соединение
                st->status = TCP_SYN_RECEIVED;
                st->event_time = gettc();
                st->seq_num = gettc() + (gettc() << 16);
                st->ack_num = ntohl(tcp->seq_num) + 1;
                st->remote_addr = ip->from_addr;
                st->remote_port = tcp->from_port;
                st->local_port = tcp->to_port;

                // И пинаем обратно SYN/ACK
                tcp->flags = TCP_FLAG_SYN|TCP_FLAG_ACK;
                tcp_xmit(st, frame, 0);
            }
        }
    }

    // Полученный пакет относится к известному соединению
    else
    {
        // Получили RST (сброс), значит прибиваем соединение
        if(tcpflags & TCP_FLAG_RST)
        {
            if( (st->status == TCP_ESTABLISHED) ||
                (st->status == TCP_FIN_WAIT) )
            {
                tcp_closed(id, 1);
            }
            st->status = TCP_CLOSED;
            return;
        }

        // Проверяем в пакете указатели потока
        //    и подтверждения.
        // Пакет должен находиться в потоке строго
        //    после предыдущего полученного,
        // и подтверждать последний отправленный 
        //    нами пакет
        if( (ntohl(tcp->seq_num) != st->ack_num) ||
            (ntohl(tcp->ack_num) != st->seq_num) ||
            (!(tcpflags & TCP_FLAG_ACK)) )
        {
            return;
        }

        // Обновляем наш указатель подтверждения,
        //    чтобы следующим отправленным пакетом
        //    подтвердить только что полученный пакет
        st->ack_num += len;
        if( (tcpflags & TCP_FLAG_FIN) || (tcpflags & TCP_FLAG_SYN) )
            st->ack_num++;

        // Сбрасываем счётчик таймаута
        st->event_time = gettc();

        // Смотрим, в каком состоянии мы сейчас, и в зависимости
        //    от этого реагируем на полученный пакет
        switch(st->status)
        {

        // Состояние:
        //     Мы отправили SYN
        //     Теперь ждём SYN/ACK
        case TCP_SYN_SENT:

            // Получили что-то, но не SYN/ACK
            if(tcpflags != (TCP_FLAG_SYN|TCP_FLAG_ACK))
            {
                // Прибиваем соединение
                st->status = TCP_CLOSED;
                break;
            }

            // Отправляем ACK
            tcp->flags = TCP_FLAG_ACK;
            tcp_xmit(st, frame, 0);

            // Соединение установлено
            st->status = TCP_ESTABLISHED;

            // Дёргаем коллбэк, в котором
            //    прога может посылать свои данные
            tcp_read(id, frame);

            break;

        // Состояние:
        //    Мы получили SYN
        //    И отправили SYN/ACK
        //    Теперь ждём ACK
        case TCP_SYN_RECEIVED:

            // Если получили не ACK,
            if(tcpflags != TCP_FLAG_ACK)
            {
                // то прибиваем соединение
                st->status = TCP_CLOSED;
                break;
            }

            // Соединение установлено
            st->status = TCP_ESTABLISHED;

            // Приложение может отправлять данные
            tcp_read(id, frame, 0);

            break;

        // Состояние:
        //    Соединение установлено
        //    Ждём ACK или FIN/ACK
        case TCP_ESTABLISHED:

            // Получили FIN/ACK
            if(tcpflags == (TCP_FLAG_FIN|TCP_FLAG_ACK))
            {
                // Отдаём данные приложению
                if(len) tcp_write(id, frame, len);

                // Отправляем FIN/ACK
                tcp->flags = TCP_FLAG_FIN|TCP_FLAG_ACK;
                tcp_xmit(st, frame, 0);

                // Соединение закрыто
                st->status = TCP_CLOSED;
                tcp_closed(id, 0);
            }

            // Получили просто ACK
            else if(tcpflags == TCP_FLAG_ACK)
            {
                // Отдаём данные приложению
                if(len) tcp_write(id, frame, len);

                // Приложение может отправлять данные
                tcp_read(id, frame);

                // Если приложение не отправило ничего,
                //    но нам приходили данные,
                //    отправляем ACK
                if( (len) && (!tcp_ack_sent) )
                {
                    tcp->flags = TCP_FLAG_ACK;
                    tcp_xmit(st, frame, 0);
                }
            }

            break;

        // Состояние:
        //    Мы отправили FIN/ACK
        //    Ждём FIN/ACK или ACK
        case TCP_FIN_WAIT:

            // Получили FIN/ACK
            if(tcpflags == (TCP_FLAG_FIN|TCP_FLAG_ACK))
            {
                // Отдаём приложению данные
                if(len) tcp_write(id, frame, len);

                // Отправляем ACK
                tcp->flags = TCP_FLAG_ACK;
                tcp_xmit(st, frame, 0);

                // Соединение закрыто
                st->status = TCP_CLOSED;
                tcp_closed(id, 0);
            }

            // Получили данные и ACK
            // (типа удалённый узел отдаёт данные из буфера)
            else if( (tcpflags == TCP_FLAG_ACK) && (len) )
            {
                // Отдаём данные приложению
                tcp_write(id, frame, len);

                // Отправляем ACK
                tcp->flags = TCP_FLAG_ACK;
                tcp_xmit(st, frame, 0);
            }

            break;

        default:
            break;
        }
    }
}


Здесь изменилось лишь то, что вместо одного коллбэка tcp_data сделали 2 коллбэка — tcp_write и tcp_read, наподобие того, как сделано в либе AVR-USB)

Попробуем теперь отдать какой-нибудь файлик по HTTP размером в несколько кб.

// Пул соединений
typedef struct httpd_state {
    const prog_char *data;
    uint16_t numbytes;
} httpd_state_t;

httpd_state_t httpd_pool[TCP_MAX_CONNECTIONS];

// HTML-страничка вместе с HTTP-заголовком
const prog_char index_page[] =
    "HTTP/1.0 200 OK\r\n"
    "Content-Type: text/html; charset=windows-1251\r\n"
    "Connection: close\r\n"
    "\r\n"
    "<pre>Восьмибитный превед!\r\n\r\n</pre>\r\n"
    "<img src='i.gif'>";

// Страничка 404
const prog_char error_page[] =
    "HTTP/1.0 404 Not Found\r\n"
    "Content-Type: text/html; charset=windows-1251\r\n"
    "Connection: close\r\n"
    "\r\n"
    "<h1>404 - Not Found</h1>";

// Принимаем подключения на порт 80
uint8_t tcp_listen(uint8_t id, eth_frame_t *frame)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    return (tcp->to_port == htons(80));
}

// Принимаем данные по TCP
void tcp_write(uint8_t id, eth_frame_t *frame, uint16_t len)
{
    httpd_state_t *st = httpd_pool + id;
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    char *request = (void*)tcp_get_data(tcp);
    char *url, *p;

    // Получен HTTP-запрос?
    if(!st->data)
    {
        // Выковыриваем из запроса URL
        url = request + 4;
        if( (!memcmp_P(request, PSTR("GET "), 4)) &&
            (p = strchr(url, ' ')) )
        {
            *p = 0;

            // Запросили главную страничку?
            if(!strcmp_P(url, PSTR("/")))
            {
                st->data = index_page;
                st->numbytes = sizeof(index_page) - 1;
            }
            
            // Запросили картинку?
            else if(!strcmp_P(url, PSTR("/i.gif")))
            {
                st->data = imagedata;
                st->numbytes = sizeof(imagedata);
            }
            
            // Запросили что-то другое?
            else
            {
                st->data = error_page;
                st->numbytes = sizeof(error_page) - 1;
            }
        }
    }
}

// Отправка данных
void tcp_read(uint8_t id, eth_frame_t *frame)
{
    httpd_state_t *st = httpd_pool + id;
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    char *buf = (void*)(tcp->data);
    uint8_t i, options;
    uint16_t blocksize;
    
    // Идёт отправка данных?
    if(st->numbytes)
    {
        // Отправляем пучок пакетов, до 4 штучек за раз
        for(i = 4; i; --i)
        {
            // До 512 байт в 1 пакете
            blocksize = 512;

            options = 0;
        
            // Если данных осталось <= 512 байт,
            //    отправляем сколько осталось 
            //    и закрываем соединение
            if(st->numbytes <= blocksize)
            {
                options = TCP_OPTION_CLOSE;
                blocksize = st->numbytes;
            }
        
            // В посдеднем пакете пучка 
            //    выставляем флаг PSH
            else if(i == 1)
            {
                options = TCP_OPTION_PUSH;
            }
        
            // Заполняем пакет данными
            memcpy_P(buf, st->data, blocksize);
            st->data += blocksize;
            st->numbytes -= blocksize;
        
            // И отправляем
            tcp_send(id, frame, blocksize, options);
        
            // Если данных больше нету, выходим из цикла
            if(options == TCP_OPTION_CLOSE)
                break;
        }
    }
}

// Соединение закрыто/оборвалось
void tcp_closed(uint8_t id, uint8_t hard)
{
    httpd_state_t *st = httpd_pool + id;
    st->data = 0;
    st->numbytes = 0;
}


Здесь imagedata — тупо gif-картинка с заранее прицепленным HTTP-заголовком:

HTTP/1.0 200 OK
Content-Type: image/gif
Content-Length: 7316
Connection: close



картинко

Пора проверить что у нас получилось)

картинко

Вот так выглядит отдача картинки:

картинко

Красным обозначены пакеты с данными.

Загружается картинка почти мгновенно.

картинко

Видимо, у тебя возник вопрос — а нафига вообще посылать пакеты пачками, а не реализовать нормальное скользящее окно? Тут есть 2 причины. Во-первых, так быстрее. Просто меняем местами адреса/порты получателя и отправителя и пинаем пакеты назад, откуда они пришли. Во-вторых, чтобы добавить возможность ретрансмиссий без буферизации данных)

TCP-ретрансмиссии


В предыдущей части, когда мы отдавали HTML-странички, влезающие в 1 пакет, мы просто забили на возможность потери пакетов. Посколкьку десяток пакетов, скорее всего, дойдут без проблем. Другое дело, если мы хотим передать файл размером в пару гигов или хотя бы тяжёлую HTML-страничку с кучей картинок. Конечно, сейчас в интернетах пакеты практически не теряются. То есть, можно целый день пролазить по интернетам и так и не потерять ни одного пакета. В локальных сетях же пакеты вообще не теряются почти никогда. Но происходит это в благоприятных условиях, в хорошо настроенной сети. Но ещё бывают сети на дешёвых хабах, кое как обжатые и настроенные. Соседский халявный вайфай на пределе видимости. Котэ, точащие зубы об сетевой кабель. Радиолюбители, колхозящие на коленках, с позволения сказать, TCP/IP стеки. В таких условиях пакеты могут теряться ещё как)

Как происходят ретрансимиссии потеряных пакетов?

картинко

Допустим, первый комп (скажем, сервер, хотя это не принципиально) отправил клиенту 3 пакета (допустим, каждый по 2 байта — для простоты) — с позициями в потоке, соответственно, 16, 18 и 20. Допустим, пакет 18 потерялся и клиент получил только пакеты 16 и 20. Клиент немного подождал, но видя, что поток приостановился и пакетов больше нету, посылает ACK, в котором указывает, что он успешно получил поток только до позиции 18. Сервак видит это и заново посылает пакеты 18 и 20.

Кстати за это и ругаются на TCP, типо пакет 20 и так получен, зачем его посылать ещё раз, и выдумывают разные примочки — селективные ACK'и, etc.

Но компу хорошо — он может сохранять в памяти все отправленные, но не подтверждённые пакеты. У нас же в микроконтроллере памяти и так едва хватает.

Но зато мы можем быстро регенерировать всю пачку пакетов. Скажем состояние «сервера» сохраняется у нас в виде такой структуры:

typedef struct httpd_state {
    const prog_char *data;
    uint16_t numbytes;
} httpd_state_t;


Ничто не мешает нам перед отправкой пакетов где-нибудь сохранить эти переменные, а при необходимости отправить пакеты повторно, вернуть их на место. После чего, отправится всё тоже самое, что и в предыдущий раз)

Получается вот такая схема:

картинко

Например, мы отправили пакеты 16, 18 и 20. И ждём от клиента подтверждения потока до позиции 22. А его всё нету и нету, потому что клиент наш пакет 18 почему-то не получил. После того, как у нас дотикает таймаут мы тупо «откатываем» состояние сервера назад, на сохранённое состояние и, таким образом, просто повторяем последнее действие. При этом вся пачка из пакетов 16, 18 и 20 отправляется повторно. На этот раз клиент получает пакеты и всё продолжается в штатном режиме.

Чтобы добавить эту фичу в наш стек, достаточно дописать функцию обработки таймаутов, которая будет не просто прибивать соединение, а откатывать состояние назад и повторять последнее действие.

В контекст TCP-соединения добавим поля для сохранения состояния:

typedef struct tcp_state {

    // Тут всё по-прежнему
    tcp_status_code_t status;
    uint32_t event_time;
    uint32_t seq_num;
    uint32_t ack_num;
    uint32_t remote_addr;
    uint16_t remote_port;
    uint16_t local_port;

    // Соединение закрывается?
    uint8_t is_closing;
    
    // Количество выполненных ретрансмиссий
    uint8_t rexmit_count;
    
    // Сохранённый указатель потока
    uint32_t seq_num_saved;
} tcp_state_t;


При получении подтверждения, перед отправкой новой пачки пакетов, сохраняем состояние:

st->seq_num_saved = st->seq_num;
st->rexmit_count = 0;


А вот эту штуку выполняем периодически, для обработки таймаутов:

// Периодическое событие
void tcp_poll()
{
    eth_frame_t *frame = (void*)net_buf;
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);

    uint8_t id;
    tcp_state_t *st;

    // Проходимся по всем соединениям
    for(id = 0; id < TCP_MAX_CONNECTIONS; ++id)
    {
        st = tcp_pool + id;

        // Есть таймаут?
        if( (st->status != TCP_CLOSED) &&
            (gettc() - st->event_time > TCP_REXMIT_TIMEOUT) )
        {
            // Если количество ретрансмиссий превысило
            //    допустимый предел, прибиваем соединение
            if(st->rexmit_count > TCP_REXMIT_LIMIT)
            {
                st->status = TCP_CLOSED;
                tcp_closed(id, 1);
            }

            // Иначе...
            else
            {
                // Сбрасываем счётчик таймаута
                st->event_time = gettc();

                // Увеличиваем счётчик ретрансмиссий
                st->rexmit_count++;

                // Загружаем предыдущее состояние
                st->seq_num = st->seq_num_saved;

                // Будем отпаврялт пакеты "с нуля",
                // принятых пакетов у нас в буфере нету
                tcp_send_mode = TCP_SENDING_SEND;
                tcp_ack_sent = 0;

                // В зависимсоти от состояния, 
                //    производим ретрансмиссию
                switch(st->status)
                {
                // Состояние:
                //    Посылали SYN, но не 
                //        получили в ответ ничего
                case TCP_SYN_SENT:
                    // Посылаем SYN ещё раз
                    tcp->flags = TCP_FLAG_SYN;
                    tcp_xmit(st, frame, 0);
                    break;

                // Состояние:
                //    Получили SYN,
                //    Отправили SYN/ACK,
                //    Но не получили ответа
                case TCP_SYN_RECEIVED:
                    // Посылаем SYN/ACK ещё раз
                    tcp->flags = TCP_FLAG_SYN|TCP_FLAG_ACK;
                    tcp_xmit(st, frame, 0);
                    break;

                // Состояние:
                //    Посылали FIN/ACK,
                //    либо посылали ACK в состоянии FIN_WAIT,
                //    но не получили ответа
                case TCP_FIN_WAIT:

                    // Мы посылали ACK в состояниии FIN_WAIT
                    // (st->is_closing означает, что уже получено
                    //    подтверждение на отправленный нами FIN/ACK)
                    if(st->is_closing)
                    {
                        // Посылаем ACK ещё раз
                        tcp->flags = TCP_FLAG_ACK;
                        tcp_xmit(st, frame, 0);
                        break;
                    }

                    // Мы посылали FIN/ACK в состоянии ESTABLISHED,
                    // после чего перешли в состояние FIN_WAIT,
                    // но не получили ответа на FIN/ACK
                    // Возвращаемся к состоянию ESTABLISHED
                    st->status = TCP_ESTABLISHED;

                // Состояние:
                //    Мы посылали данные в сост. ESTABLISHED
                //    или ACK в состоянии ESTABLISHED
                //    или FIN/ACK в сост. ESTABLISHED
                case TCP_ESTABLISHED:
                    // Просим приложение 
                    //    повторить последнее действие
                    //    (последний параметр у tcp_read
                    //        указывает на это)
                    tcp_read(id, frame, 1);
                    
                    // Посылаем ACK, если нужно
                    if(!tcp_ack_sent)
                    {
                        tcp->flags = TCP_FLAG_ACK;
                        tcp_xmit(st, frame, 0);
                    }
                    break;

                default:
                    break;
                }
            }
        }
    }
}


У tcp_read добавился дополнительный параметр re, означающий, что необходимо повторить последнее действие:

void tcp_read(uint8_t id, eth_frame_t *frame, uint8_t re);


Раздача статики по HTTP


Мы научились отдавать файлы по HTTP и перепосылать потерянные пакеты. Теперь можем сделать сервак, надёжно раздающий большие файлы. Тут нам пригодится SD-карточка и либа FatFs от ChaN'а. Хе, и ATmega32, поскольку в мегу16 это всё уже не влезает))

// Подключаем FatFs
#include "ff/ff.h"

// Настройки сервера
#define HTTPD_PORT            htons(80)        // Порт
#define HTTPD_PACKET_BULK    10                // Макисмальный пучок пакетов
#define HTTPD_MAX_BLOCK        512                // Максимальный блок данных
#define HTTPD_NAME            "ATmega32"        // Название сервера
#define HTTPD_INDEX_FILE    "/index.htm"    // Индексная страница

// Состояние соединения
typedef enum httpd_state_code {
    HTTPD_CLOSED,        // закрыто
    HTTPD_INIT,            // только что открыто
    HTTPD_READ_HEADER,    // получение заголовка запроса
    HTTPD_WRITE_DATA    // отправка данных
} httpd_state_code_t;

// Контекст соединения
typedef struct httpd_state {

    // состояние
    httpd_state_code_t status;

    // заголовок
    uint8_t statuscode;                // код ответа
    const prog_char *type;            // content type
    uint32_t numbytes;                // content length

    // если отправляем данные из флеша
    const prog_char *data;            // указатель
    
    // если отправляем данные из файла
    uint8_t file_opened;            // файл открыт
    FIL fs;                            // файл

    // сохранённое состояние
    uint8_t statuscode_saved;        // код ответа
    uint32_t numbytes_saved;        // content length

    const prog_char *data_saved;    // указатель на данные
} httpd_state_t;

// всякие строки
const prog_char http_200[] = "HTTP/1.0 200 OK\r\n";
const prog_char http_404[] = "HTTP/1.0 404 Not Found\r\n";
const prog_char http_server[] = "Server: "HTTPD_NAME"\r\n";
const prog_char http_content_type[] = "Content-Type: ";
const prog_char http_content_length[] = "Content-Length: ";
const prog_char http_connection_close[] = "Connection: close\r\n";
const prog_char http_linebreak[] = "\r\n";
const prog_char http_header_end[] = "\r\n\r\n";
const prog_char http_not_found[] = "<h1>404 - Not Found</h1>";

const prog_char http_text_plain[] = "text/plain";
const prog_char http_text_html[] = "text/html";
const prog_char http_text_js[] = "text/javascript";
const prog_char http_text_css[] = "text/css";
const prog_char http_image_gif[] = "image/gif";
const prog_char http_image_png[] = "image/png";
const prog_char http_image_jpeg[] = "image/jpeg";

const prog_char http_ext_txt[] = "txt";
const prog_char http_ext_htm[] = "htm";
const prog_char http_ext_html[] = "html";
const prog_char http_ext_js[] = "js";
const prog_char http_ext_css[] = "css";
const prog_char http_ext_gif[] = "gif";
const prog_char http_ext_png[] = "png";
const prog_char http_ext_jpg[] = "jpg";
const prog_char http_ext_jpeg[] = "jpeg";

// табличка MIME-типов
const prog_char* PROGMEM mime_type_table[][2] =
{
    {http_ext_txt, http_text_plain},
    {http_ext_htm, http_text_html},
    {http_ext_html, http_text_html},
    {http_ext_js, http_text_js},
    {http_ext_css, http_text_css},
    {http_ext_gif, http_image_gif},
    {http_ext_png, http_image_png},
    {http_ext_jpg, http_image_jpeg},
    {http_ext_jpeg, http_image_jpeg}
};

// пул соединений
httpd_state_t httpd_pool[TCP_MAX_CONNECTIONS];

// эта хрень возвращает длину массива в элементах
#define len(some) (sizeof(some)/sizeof(some[0]))

// прототипы функций
void fill_buf(char **buf, char *str);
void fill_buf_P(char **buf, const prog_char * str);

// определяет MIME-тип по имени файла
const prog_char *httpd_get_mime_type(char *url)
{
    const prog_char *t_ext, *t_type;
    char *ext;
    uint8_t i;

    if((ext = strrchr(url, '.')))
    {
        ext++;
        strlwr(ext);

        for(i = 0; i < len(mime_type_table); ++i)
        {
            t_ext = (void*)pgm_read_word(mime_type_table[i] + 0);

            if(!strcmp_P(ext, t_ext))
            {
                t_type = (void*)pgm_read_word(mime_type_table[i] + 1);
                return t_type;
            }
        }
    }
    return 0;
}

// обработчик HTTP-запросов
void httpd_request(httpd_state_t *st, char *url)
{
    // вместо / подставляем имя индексного файла
    if(!strcmp_P(url, PSTR("/")))
        url = HTTPD_INDEX_FILE;

    // пробуем открыть файл
    if(!f_open(&(st->fs), url, FA_READ))
    {
        st->statuscode = 2;                        // код ответа - 200
        st->data = 0;                            // данные из файла
        st->numbytes = st->fs.fsize;            // размер файла
        st->type = httpd_get_mime_type(url);    // тип контента
        st->file_opened = 1;                    // файл открыт
    }
    
    // при ошибке возвращаем 404
    else
    {
        st->statuscode = 4;                        // код ответа - 404
        st->data = http_not_found;                // данные - страничка 404
        st->numbytes = sizeof(http_not_found)-1;// размер странички
        st->type = http_text_html;                // тип - text/html
    }

    // сохраняем состояние
    st->statuscode_saved = st->statuscode;
    st->data_saved = st->data;
    st->numbytes_saved = st->numbytes;
}

// заливает HTTP-заголовок ответа в буфер
void httpd_header(httpd_state_t *st, char **buf)
{
    char str[16];

    // статусная строка
    if(st->statuscode == 2)
        fill_buf_P(buf, http_200);
    else
        fill_buf_P(buf, http_404);

    // Content-Type
    if(st->type)
    {
        fill_buf_P(buf, http_content_type);
        fill_buf_P(buf, st->type);
        fill_buf_P(buf, http_linebreak);
    }

    // Content-Length
    if(st->numbytes)
    {
        ltoa(st->numbytes, str, 10);
        fill_buf_P(buf, http_content_length);
        fill_buf(buf, str);
        fill_buf_P(buf, http_linebreak);
    }

    // Server
    fill_buf_P(buf, http_server);

    // Connection: close
    fill_buf_P(buf, http_connection_close);

    // конец заголовка (\r\n)
    fill_buf_P(buf, http_linebreak);
}

// принимаем подключения на порт 80
uint8_t tcp_listen(uint8_t id, eth_frame_t *frame)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    return (tcp->to_port == HTTPD_PORT);
}

// upstream callback
void tcp_read(uint8_t id, eth_frame_t *frame, uint8_t re)
{
    httpd_state_t *st = httpd_pool + id;
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    char *buf = (void*)(tcp->data), *bufptr;
    uint16_t blocklen;
    uint8_t i, close = 0;
    uint16_t sectorbytes;
    uint8_t options;

    // соединение только открыто
    if(st->status == HTTPD_CLOSED)
    {
        st->status = HTTPD_INIT;
    }

    // отправка данных
    else if(st->status == HTTPD_WRITE_DATA)
    {

        // нужен ретрансмит?
        if(re)
        {
            // загружаемся
            st->statuscode = st->statuscode_saved;
            st->numbytes = st->numbytes_saved;
            st->data = st->data_saved;

            // и откатываем указатель файла, если нужно
            if(st->file_opened)
                f_lseek(&(st->fs), st->fs.fsize - st->numbytes);
        }
        
        else
        {
            // сохраняемся перед отправкой пакета
            st->statuscode_saved = st->statuscode;
            st->numbytes_saved = st->numbytes;
            st->data_saved = st->data;
        }

        // посылаем пучок пакетов
        for(i = HTTPD_PACKET_BULK; i; --i)
        {
            blocklen = HTTPD_MAX_BLOCK;
            bufptr = buf;

            // добавляем HTTP заголовок
            if(st->statuscode != 0)
            {
                httpd_header(st, &bufptr);
                blocklen -= bufptr - buf;
                st->statuscode = 0;
            }

            // посылаем до 512 байт (минус заголовок)
            if(st->numbytes < blocklen)
                blocklen = st->numbytes;

            // заливаем данные в буффер...
            if(st->data)
            {
                memcpy_P(bufptr, st->data, blocklen);
                bufptr += blocklen;

                st->data += blocklen;
                st->numbytes -= blocklen;
            }

            // ... либо читаем файл в буффер
            else if(st->file_opened)
            {
                // стараемся вырывнивать чтение на размер сектора для производительности
                // (если блок пересекает конец сектора, обрезаем блок)
                sectorbytes = 512 - ((uint16_t)(st->fs.fptr) & 0x1ff);
                if(blocklen > sectorbytes)
                    blocklen = sectorbytes;

                // читаем файл
                f_read(&(st->fs), bufptr, blocklen, &blocklen);
                bufptr += blocklen;

                st->numbytes -= blocklen;
            }

            // пинаем пакет куда нужно
            if(!st->numbytes)
                options = TCP_OPTION_CLOSE;
            else if(i == 1)
                options = TCP_OPTION_PUSH;
            else
                options = 0;

            tcp_send(id, frame, bufptr - buf, options);

            if(close)
                break;
        }
    }
}

// обработчик получаемых данных
void tcp_write(uint8_t id, eth_frame_t *frame, uint16_t len)
{
    httpd_state_t *st = httpd_pool + id;
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    char *request = (void*)tcp_get_data(tcp);
    char *url, *p;

    request[len] = 0;

    // состояние INIT и получены данные?
    if(st->status == HTTPD_INIT)
    {
        // получен GET-запрос?
        //    выковыриваем URL
        url = request + 4;
        if( (!memcmp_P(request, PSTR("GET "), 4)) &&
            ((p = strchr(url, ' '))) )
        {
            *(p++) = 0;

            // обрабатываем запрос
            httpd_request(st, url);

            // ищем конец заголовка
            //    если не находим, ставим состояние READ_HEADER
            if(strstr_P(p, http_header_end))
                st->status = HTTPD_WRITE_DATA;
            else
                st->status = HTTPD_READ_HEADER;
        }
    }

    // состояние READ_HEADER и получены данные?
    else if(st->status == HTTPD_READ_HEADER)
    {
        // ищем конец заголовка
        if(strstr_P(request, http_header_end))
            st->status = HTTPD_WRITE_DATA;
    }
}

// обработчик закрытия соединения
void tcp_closed(uint8_t id, uint8_t hard)
{
    httpd_state_t *st = httpd_pool + id;

    // закрываем файл, если нужно
    if(st->file_opened)
    {
        f_close(&(st->fs));
        st->file_opened = 0;
    }
    
    // закрываем соединение
    st->status = HTTPD_CLOSED;
}

// всякие утилитки
void fill_buf(char **buf, char *str)
{
    uint16_t len = 0;
    char *p = *buf;

    while(*str)
        p[len++] = *(str++);
    *buf += len;
}

void fill_buf_P(char **buf, const prog_char * str)
{
    uint16_t len = 0;
    char *p = *buf, byte;

    while((byte = pgm_read_byte(str++)))
        p[len++] = byte;
    *buf += len;
}


Пора протестировать наш сервер и скачать какой-нибудь большой файл)

картинко

Скорости вполне хватает чтобы в реальном времени отдавать видео в средненьком качестве, либо mp3 в несколько потоков. Конечно, не 10 мбит/с, которые теоретически может прокачать ENC28J60, но тоже ничего, учитывая, что нам ещё приходится читать с SD-карточки, рассчитывать контрольные суммы пакетов, etc.

Правда, это в локальной сети. В интернетах скорость падает до 50-80 кб/с.

Заключение


Хотелось в этой части уже закончить с HTTP, но всё, что хотелось, уже не влезло бы, да и для мя трудно осилить такой кусок за раз)

В этой части мы научились раздавать статику, осталось ещё разобраться с динамическим содержимым. Об этом будет следующая часть)

Скачать проект сервера можно тут: httpd.zip.



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

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

RSS свернуть / развернуть
ну ты крут
жги дальше земляк
0
За тестовую страничку отдельное спасибо. Отдает вполне так неплохо http://visualbooster.com/share/20110424230600957.png.
Есть такой вопрос, какая предельная скорость для UDP подключения, с грубо говоря односторонней передачей? Как говорится «я бы и сам проверил, да пока плата мрийдет, пока я до неё доберусь», а так хоть будет известно чего ожидать :)
0
Хм, да я пока не проверял.
Сейчас померять не могу — микруха занята)
Ближе к концу выложу всю информацию по стеку — бенчмарки, описание апи, etc.
0
Ну в вашем апи мне не все нравится :) да и юзать я буду не мегу, но Си на то и Си что пофигу.
Сейчас и не обязательно. мне с китая причешет посыль тока через чесяц наверно, а сам сяду ещё месяц, так что не к спеху.
0
Да, в несколько потоков отдает то лучше http://visualbooster.com/share/20110425203429484.png, что-то я не подумал вчера об этом. Порядка 750кбит/с меня такая скорость устраивает в принципе то. Ну естественно всегда хочется большего :) Можно собирать девайс и тестить уже в боевых условиях.
0
Хм… только что дошло что у тя там скорость в мегабитах)
0
Вообще, скорость оч сильно зависит от пинга, ибо нормального окна нету.
Пинг несколько мс — всё летает. То, что у тя на графике показано — это предел.
А если больше — ползёт кое как.
0
Можешь мя попинговать (178.49.21.56) и выложить рез-т для интереса.
0
PING 178.49.21.56 (178.49.21.56): 56 data bytes
64 bytes from 178.49.21.56: seq=0 ttl=55 time=60.388 ms
64 bytes from 178.49.21.56: seq=1 ttl=55 time=60.234 ms
64 bytes from 178.49.21.56: seq=2 ttl=55 time=59.994 ms
64 bytes from 178.49.21.56: seq=3 ttl=55 time=59.983 ms

С роутера и компа результат одинаковый.
Ещё чуток поиздевался над серваком, походу он одновременно может отдавать только один экземпляр файла. во всяком случае второе окно с картинкой (любой) не начинает грузится пока первая не загрузилась. Так же выловил несколько упсов в картинках :)
На пинг загрузка никак не влияет, стабильный.
wget с роутера качает картинку на 500кбит/с
wget на 4 картинки дает в где-то среднем 1.15Мбит http://visualbooster.com/share/20110425231659347.png
0
Один файл отдавать в два потока — запросто)
Это похоже браузер не хочет дважды один файл качать (хочет закэшировать, но сервер, паразит last-modifed и expires не отдаёт:)) Поэтому докачав 1 раз браузер обижается и начинает снова качать. Как-то так))
0
PING 178.49.21.56 (178.49.21.56): 4096 data bytes
4104 bytes from 178.49.21.56: seq=0 ttl=55 time=63.962 ms
4104 bytes from 178.49.21.56: seq=1 ttl=55 time=63.621 ms
4104 bytes from 178.49.21.56: seq=2 ttl=55 time=63.544 ms
4104 bytes from 178.49.21.56: seq=3 ttl=55 time=63.509 ms

PING 178.49.21.56 (178.49.21.56): 64000 data bytes
64008 bytes from 178.49.21.56: seq=0 ttl=54 time=102.676 ms
64008 bytes from 178.49.21.56: seq=1 ttl=54 time=102.538 ms
64008 bytes from 178.49.21.56: seq=2 ttl=54 time=102.190 ms
64008 bytes from 178.49.21.56: seq=3 ttl=54 time=103.118 ms

Я думал большие пинг-пакеты вообще не пройдут :)
0
Хех, что бы им не пройти)
На них роутер отвечает)
Девайс внутри локалки, только 591 порт проброшен через нат.
0
тьфу ты, так не интерестно :)
0

c:\Users\admin>ping 192.168.0.222 -l 554

Pinging 192.168.0.222 with 554 bytes of data:

Reply from 192.168.0.222: bytes=554 time=3ms TTL=64
Reply from 192.168.0.222: bytes=554 time=3ms TTL=64
Reply from 192.168.0.222: bytes=554 time=3ms TTL=64
Reply from 192.168.0.222: bytes=554 time=3ms TTL=64

Ping statistics for 192.168.0.222:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 3ms, Maximum = 3ms, Average = 3ms

c:\Users\admin>ping 192.168.0.222 -l 555

Pinging 192.168.0.222 with 555 bytes of data:

Request timed out.
Request timed out.
Request timed out.
Request timed out.

Ping statistics for 192.168.0.222:
    Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),

c:\Users\admin>
0
Всё, последнее на сегодня издевательство
cat /dev/zero | telnet 178.49.21.56 591 > /dev/null

http://visualbooster.com/share/20110425234836121.png
0
Замечательный цикл статей. Спасибо.
<наглость>Осталось только ftpbootloader и счастью не будет предела.</наглость>
0
Хе, не влезет ведь в бут сектор)
0
Да легко — в рабочем режиме прошивка засасывается в чрево i2c EEPROM а когда надо перепрошиться, то бутлоадер оттуда ее перезаливает себе в мозги.
0
еще можно там хранить образ рабочей прошивки, и при трех неудачных запусках новой — грузить старую. а то чревато, если контроллер лежит не рядом.
0
Хорошая серия статей. Очень внимательно читаю, скоро предстоит всё это реализовывать. Спасибо!
0
да, офигенная. бери и делай
0
отличные статьи, я думаю автор достоин главного приза:-)
0
Поддерживаю…
0
+100500
афтар жгет
сектор приз на барабане
0
Осциллограф в студию! ))))
0
теперь di уговорите
0
Посчитаем потом по циферкам и решим.

Но в целом да, материал первоклассный, подробный и ничего подобного я не встречал. Чтобы от и до, на примерах и с разьяснением на пальцах.
0
Хватит мя хвалить))

В общем-то твоя идея с сообществом и сподвигла чего-нибудь такого написать. Иначе, наверное, так и не дошли бы руки до этой микрухи)
0
Тоже считаю что автор достоин главного приза. На мой взгляд самый полезный цикл статей, понятный и развернутый. Респект автору.
0
Молодец!!! Я тоже делал на енс и меге 8 но там был простой термометр.до такого не додумался, кстати неплохо бы приделать изменение адреса по web.
0
Хм… какого адреса?
0
Тот который ip
0
Есть же DHCP )
0
есть сети где пароли назначает администратор
0
так и сделать как во всех осях выбор: либо DHCP адрес, либо этот статический с маской, и ещё набор альясов :)
0
это я и хотел сказать
0
Ну так дефайнами в коде выбирается)
А юзать всё сразу как-то жирно для мк. Простота наше всё)
0
Плохо что Atmel не выпускает контроллеры с езернетом на борту, я было занялся разработкой связки енс мега, потом попался чип пик с езернетом на борту, стал юзать его, удобно с одной стороны езернет, с другой датчички, кнопочки, ручечки и все в одном флаконе.
0
А по цене он как?

Я бы щас сделал, но уже на арме. А то и W5100 поставил.
0
Кстати под авр есть и софтовое решение. Ну в смысле, MAC. Скорость до 170 кбайт/с. А вам бы всё арм ставить))
0
Да не очень дорого 195 р, так что получается дешевле чем мега и енс, на арме конечно круто для начинающих довольно проблематично.
0
ничем армы не сложнее авров. даже проще благодаля наличию библиотек перефирии от разработчиков.
0
Незнаю, мб кому будет актуально, а мб кто-то, где-то уже исправил. Для нормальной реализации f_write (chan fatfs) вместе с этим примером в файле mmc.c надо поправить ровно один символ (diff):

267c267
<                       if(!mmc_command(WRITE_BLOCK, sector))
---
>                       if(mmc_command(WRITE_BLOCK, sector))
0
Можно SD и ETH на один SPI присоединить?
0
Они на одном и висят.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.