SULI COMPANY | Научно-технический сайт Сулико Асабашвили » Электроника » Подключение микроконтроллера к локальной сети. (Часть 1)
Информация к новости
  • Просмотров: 8257
  • Автор: sulicompany
  • Дата: 16-02-2013, 17:19
 (голосов: 0)
16-02-2013, 17:19

Подключение микроконтроллера к локальной сети. (Часть 1)

Категория: Электроника


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

Тем более, существует микросхема, позволяющая микроконтроллеру прекрасно чувствовать себя в сети Ethernet. А именно, , разработка вражеской компании Microchip. Об её использовании мы и поговорим.

Вообще-то, Ethernet — достаточно обширная тема. В один пост явно не поместится. Так что частей будет несколько.

Эта первая часть, и как обычно, — вводная и обзорная.

А зачем это нужно?


С точки зрения микроконтроллера, локальная сеть — лишь ещё один интерфейс «наружу». Зачем использовать именно его, а не простейший RS-232, скажем?

  • Это удобно. Любая нормальная операционная система имеет нативную поддержку сети и API для работы с ней. Много стандартного софта умеет работать в сети и сможет обращаться к нашему девайсику.
  • Девайс будет доступен как минимум с любого компа, подключенного к локальной сети, а максимум — вообще с любого компа, имеющего доступ в интернет. В том числе, по беспроводному соединению — к девайсу можно будет обращаться, например, с ноутбука, не привязывая себя к девайсу проводами.
  • Девайс сам сможет выходить в интернет и получать различную информацию — от точного времени до прогноза погоды и последних новостей (хм, ну конечно если памяти у МК хватит на всё это).
  • Это быстро. 10 мбит/с — более чем прилично для микроконтроллерного девайсика.
  • Девайс больше не привязан к компу проводами — максимальные 100 м для витой пары не идут ни в какое сравнение с «классическими» интерфейсами.
  • Это просто. Нет, правда! IP-стек мне кажется куда проще и логичнее чем, скажем, USB.


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

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

Интересно? Поехали! :)

Disclaimer


Стоит сразу сказать что здесь подразумевается под Ethernet и что это есть на самом деле.

Ethernet — группа стандартов , описывающая различные технологии локальных сетей. В этих стандартах определён общий канальный уровень и набор технологий физического уровня (использующих для передачи данных оптоволокно, витую пару, коаксиал, с разными скоростями, etc.).

Здесь же под Ethernet будет подразумеваться конкретный стандарт IEEE 802.3i, который и поддерживается ENC28J60. В этом стандарте физический уровень — это , 10 мбит/с по витой паре (Twisted pair).

The Plan


Вот, в общем виде, структурная схема девайса:

The Plan

Здесь вроде-бы всё просто. ENC28J60 содержит в себе физический уровень (PHY) и канальный уровень (MAC) Ethernet. Микроконтроллеру остаётся только подтаскивать пакеты для отправки и забирать принятые через SPI.

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

Более подробно обо всём этом будет в следующих частях.

Модель OSI


А что это за уровни, о которых я всё говорю? Если ты знаешь, эту часть можно пропустить.

 — полезная вещь для понимания того, как работает сеть. Здесь о ней очень кратенько.

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

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

OSI Model

Всего есть 7 уровней модели OSI.

OSI Model Layers

У нас будет только 5 уровней.

Физический уровень


У нас это 10BASE-T. Физический уровень реализован в ENC28J60. Сюда входит витая пара и всё, что нужно для передачи данных по ней (драйверы, трансформаторы).

Канальный уровень


Канальный уровень Ethenet (MAC) тоже реализован в ENC28J60. Канальный уровень отвечает за передачу Ethernet-фреймов между узлами локальной сети (адресацию, проверку контрольной суммы, разрешение коллизий, etc.).

Проще говоря, канальный уровень — это то, что позволяет посылать фреймы другим узлам локальной сети и принимать фреймы от них.

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

Сетевой уровень


Сетевой уровень отвечает за передачу пакетов. У нас это IP. IP-пакет спокойно может пройти через несколько разных сетей с различными технологиями физического и канального уровня.

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

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

Транспортный уровень


Транспортный уровень отвечает за связь узлов. Скажем, приложение хочет отправит данные другому узлу. Транспортный уровень представляет эти данные в виде пакета сетевого уровня (или пакетов) и отправляет. Если используется протокол с установкой соединения, транспортный уровень занимается также установкой и контролем соединения. UDP и TCP — протоколы транспортного уровня.

Прикладной уровень

 


А тут может быть всё, что нам вздумается. Это то, ради чего всё затевалось. Та самая задача, обменивающаяся с внешним миром данными по какому-то своему или стандартному протоколу.

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

 

Подключение микроконтроллера к локальной сети: работаем с ENC28J60

Эта часть полностью посвящена работе с ENC28J60.

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

Но  всё равно может пригодится. А так же .

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

  • Включение ENC28J60
  • Архитектура ENC28J60
  • Обмен данными по SPI
  • Инициализация
  • Отправка пакетов
  • Приём пакетов
  • Заключение


Примеры кода написаны под AVR. Впрочем из платформенно-зависимых вещей тут только работа со SPI.

ENC28J60 — Ethernet-адаптер (проще говоря, «сетевая карточка») на одном чипе, разработанный вражеской компанией Microchip. Микросхемка не требует для работы много обвязки из внешних компонентов, к МК подключается с помощью SPI. Полностью соответствует спецификации IEEE 802.3 и, кроме того, поддерживает много дополнительных прикольных фич (например, аппаратную фильтрацию пакетов).

А теперь, немного о грустном. Количество багов в ENC28J60 трудно описать печатными словами. Из-за них половина фич либо работает нестабильно, либо может нарушать работу других важных модулей. Хотя, главное, что принимать и отправлять пакеты девайс всё-таки умеет. :)

Подключаемся


Микросхемка выпускается в 28-ножечных DIP, SOIC и QFN корпусах. Попадаются и готовые модули со всей обвязкой и разъёмом для сетевого кабеля.

Вот стандартная схема включения ENC28J60 (распиновка для DIP корпуса):





Питание — 3.3 В. Но входы микросхемы совместимы с 5-вольтовыми TTL уровнями.

Потребляет микросхемка прилично — 250 мА. Нужно столько для питания драйверов передатчика. Есть режим «пониженного энергопотребления», когда вся «силовая» часть отключается.

VCAP — выход встроенного преобразователя на 2,5 В (именно такое напряжение используется при передачи данных по сетевому кабелю). К этому выводу нужно подключить конденсатор на 10 мкф. Даташит не рекомендует питать от этого вывода что-то ещё.

R7 (RBIAS) — резистор для какой-то балансировки. В даташите указан номинал 2 кОм с допуском 1%. Однако в ENC28J60 есть баг, из-за которого в ревизиях микросхемы 1 и 4 нужно использовать резистор на 2,7 кОм. А в ревизиях 5 и 7 — на 2,32 кОм. Иначе выходной сигнал не будет соответствовать спецификации IEEE. Западло, однако. Ревизию можно определить только чтением соответствующего регистра. Мне попалась ревизия 7 — судя по всему, последняя. Так что, стоит запаять резистор на 2,32 кОм, прочитать ревизию, и, если вдруг попадётся другая ревизия, резистор заменить. Где взять резистор на такой необычный номинал? Можно бегать по магазинам и искать, или просто взять горсточку обычных неточных резисторов на 2,2 кОм и методом тыка выбрать наиболее близкий к требуемому номиналу. Впрочем, когда я только начинал экспериментировать с ENC28J60, об этом баге не знал и ставил резистор на 2 кОм. Проблем не было. :)

TR1 и TR2 — не абы какие трансформаторы, а специальные Ethernet-фильтры (Ethernet magnetics). Представляют собой систему из нескольких катушек на ферритовых колечках. Обычно выпускаются в виде готовых сборок (оба фильтра в одном корпусе, совместимом с DIP-16). Нужны они, судя по всему, для развязки, защиты от статики, etc. (сетевой кабель может иметь длину до 100, а то и 300 м — представь какой статический потенциал может быть между девайсами на таком расстоянии). Где их взять? Вариант номер раз — взять мёртвую/ненужную (или хотя-бы дешёвую) сетевую карточку, и выпаять из неё. Кстати, в ней же найдутся точные резисторы на 50 Ом, розеточка для сетевого кабеля и, если повезёт, кварц на 25 МГц. Вариант номер два — использовать сетевой разъём с уже встроенными фильтрами («MagJack»). Деталька редкая и дорогая. Впрочем, достать её можно из трупика материнской платы — обычно там используются именно они. Правда, чтобы выпаять такую массивную деталь из толстенной платы с кучей слоёв меди, понадобится паяльная лампа или что-то в этом роде. К тому же часто деталька идёт в виде неразделимого блока с USB-A розеточками, которые будут вносить неприятную энтропию в конструкцию (если, конечно, в ней не будет USB-хоста, хе-хе).

Катушка L1 — ферритовое колечко диаметром 5мм с несколькими витками проволоки. Если колечка под рукой нет, можно поставить дроссель, например, на 100 мкГн.

ENC28J60 автоматически определяет полярность подключенных светодиодов. Причём полярность светодиода, подключенного к выводу LEDB влияет на дуплексный режим работы микросхемы. Если светодиод подключен как показано на схеме, катодом к микрухе — ENC28J60 инициализируется в полнодуплексном режиме. Соответственно, если подключен анодом — то в полудуплексном. Если светодиод не подключен — состояние не определено. Впрочем, дуплексный режим можно изменить при инициализации.

Конденсатор C2 на 2 кВ служит для разрядки статики при подключении кабеля. Естественно, можно поставить конденсатор и на меньшее напряжение. Ни на что это не повлияет, разве что твой девайс не будет формально соответствовать стандарту.

Вот так я запаял

Вход RESET уже подтянут к питанию внутри микрухи, так что его можно оставить болтаться — ENC28J60 поддерживает и «мягкий» сброс.

Как выяснилось, вход RESET у ENC28J60, несмотря на то, что сказано в даташите, не подтянут! Его обязательно нужно соединить с питанием, иначе микруха может сброситься в самый неудачный момент из-за любой наводки.

Выходы прерываний использовать не обязательно, и, как мне кажется, и не нужно. Забрать принятый пакет или загрузить пакет для отправки — слишком длительная процедура, чтобы выполнять её в обработчике прерывания. Лучше делать это из главного цикла.

С выхода CLKOUT можно снимать тактовый сигнал (с настраиваемым делителем). Правда, из-за бага, при входе в спящий режим сигнал пропадает (хотя главные часики продолжают тикать). Блин, инженеры из Microchip совсем забивают на тестирование?!

Таким образом, для связи с микроконтроллером можно использовать только 4 провода — стандартную шину SPI.

Архитектура ENC28J60


На этой картинке я нарисовал основные блоки ENC28J60:

Архитектура ENC28J60

  • PHY — физический уровень. Приёмник, передатчик, драйверы, etc. В общем, всё, что необходимо для работы с определённой средой передачи данных (medium). В данном случае — с витой парой, по стандарту 10BASE-T. Доступ к PHY происходит исключительно через MII — Medium Independent Interface. MII задуман так, чтобы следующий (канальный) уровень мог абстрагироваться от типа среды передачи данных. PHY имеет свой набор 16-битных регистров (специфичных для среды передачи данных), доступ к которым осуществляется через MII. Не нужно пугаться аббревиатуры MII — это всего лишь набор регистров, через которые управляется PHY.
  • MAC (Medium Access Controller) — канальный уровень. В него входит вся логика, необходимая для отправки и приёма пакетов в сети Ethernet. MAC занимается адресацией, рассчётом контрольной суммы, фильтрацией принимаемых пакетов, разрешением коллизий (в полудуплескном режиме), etc. Обменивается со следующим, сетевым уровнем готовыми пакетами, а с физическим — отправляемыми и принимаемыми «сырыми» байтами.
  • Управляющая логика занимается всем остальным. В том числе, обслуживает буфер, из которого MAC берёт отправляемые данные и складывает принятые. Управляет режимами энергопотребления, etc.


Вся память в ENC28J60 делится на буфер для данных, управляющие регистры и регистры PHY.

Буфер для данных

В ENC28J60 есть буфер размером 8 КБ. Часть этого буфера обычно выделяется для приёма пакетов, остальное можно использовать как угодно. Например, для отправляемых данных.

Управляющие регистры

Управляющие регистры делятся на 4 банка (ну нравится Microchip'овским инженерам сегментированное адресное пространство). Каждый банк имеет размер в 32 регистра, причём последние 5 ячеек (0x1b..0x1f) всегда мапятся на одни и те же регистры, вне зависимости от того, какой банк выбран.

Страшно?

Карта регистров

Пугаться количества не нужно. Сейчас всё структурируем, и станет просто.

Основная часть регистров имеет префикс E (Ethernet). Регистры MAC — с префиксом MA, регистры MII — с префиксом MI.

Регистры можно разделить на функциональные группы.

Карта регистров

Основное

Назначение отдельных бит, как правило, понятно из их названий. :)
Здесь я опишу лишь то, что нам реально понадобится. Остальные биты описаны в даташите. Однако перед использованием той или иной фичи, нужно заглянуть в еррату и проверить нет ли с данной фичей проблем. Например, DMA (биты DMAST и CSUMEN) еррата использовать не рекомендует вообще. Так-то!

ECON1

  • BSEL1:BSEL0 — выбор банка регистров.
  • RXEN — разрешает приём данных.
  • TXRTS — разрешает отправку пакета (автоматически сбрасывается после того, как отправка пакета будет завершена).


ECON2

  • VRPS — разрешает перевод стабилизатора питания в экономичный режим при включении режима пониженного энергопотребления (бит PWRSV). Данный бит можно установить при инициализации и забыть про него.
  • PWRSV — включает режим пониженного энергопотребления. Прежде чем устанавливать этот бит, следует запретить приём новых пакетов и убедиться что приём данных завершён. После выхода из режима пониженного энергопотребления, нужно подождать 1 мс, чтобы PHY вошёл в рабочий режим.
  • PKTDEC — при установке этого бита значение счётчика пакетов уменьшается на 1.
  • AUTOINC — включает автоматическое инкрементирование указателей чтения и записи буфера (для удобства последовательного чтения и записи данных). Этот бит установлен после сброса, и трогать его ни к чему.


ESTAT

  • TXABRT — флаг завершения передачи с ошибкой.
  • RXBUSY — признак работы приёмника (установлен, если принимаются данные).


Регистр EPKTCNT — счётчик принятых пакетов. Автоматически инкрементируется при успешно принятом пакете. Уменьшается вручную, установкой бита PKTDEC в регистре ECON2. Вручную записывать в этот регистр ничего нельзя, т.к. все операции с ним должны выполняться атомарно.

Указатели буфера

Регистры, указывающие куда приёмник будет складывать данные, откуда данные будет брать передатчик, etc. Каждый указатель занимает два регистра. Например, младший байт ERDPT хранится в регистре ERDPTL, а старший — в ERDPTH.

ERDPT и EWRPT — указатели чтения и записи буфера. Указывают по какому адресу данные будут считываться из буфера или записываться в буфер микроконтроллером.

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

Раскладка буфера

ETXST и ETXND — начало и конец отправляемого пакета. Например, если мы хотим отравить пакет размером 256 байт, лежащий в буфере по адресу 0x1800, устанавливаем ETXST в 0x1800 и ETXND в 0x18ff.

ERXST и ERXND — начло и конец кольцевого буфера, в который будут приниматься пакеты. Из-за бага в ENC28J60, в ERXST можно записывать только 0. Например, если мы хотим выделить 4096 байт под приём пакетов, пишем в ERXST 0, а в ERXND 0x0fff. Когда приём пакетов разрешён, трогать эти регистры нельзя.

ERXRDPT и ERXWRPT — указатели кольцевого буфера. Доходя до конца буфера (ERXND), указатель затем перемещается на начало (ERXST).

ERXWRPT — указывает на место, куда приёмник положит следующий принятый пакет. Этот указатель доступен только для чтения. Он автоматически инициализируется вместе с ERXST и автоматически обновляется после приёма пакета.

ERXRDPT — указывает на то место, откуда микроконтроллер будет забирать принятые пакеты.

Если микроконтроллер долго не будет забирать пакеты из буфера, ERXWRPT может «догнать» ERXRDPT. В таком случае приёмнику некуда будет складывать данные и приходящие данные начнут выбрасываться. Чтобы освобождать место в буфере, микроконтроллер, после того, как заберёт пакет из буфера, должен перемещать ERXRDPT к следующему пакету.

DMA

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

Фильтрация пакетов

ENC28J60 отлично умеет фильтровать пакеты. Это важно, особенно, если сеть на хабах. :)

Правила фильтрации пакетов устанавливает регистр ERXFCON.

ERXFCON

  • UCEN включает фильтр Unicast-пакетов. Пакет проходит фильтр, если адрес получателя в нём равен нашему MAC-адресу.
  • MCEN включает фильтр Multicast-пакетов. Пакет проходит фильтр, если является Multicast-пакетом.
  • BCEN включает фильтр Broadcast-пакетов. Пакет проходит фильтр, если является широковещательным.
  • MPEN включает фильтр волшебных пакетов. Пакет проходит фильтр, если является волшебным и направлен на наш MAC-адрес.
  • PMEN включает фильтрацию по шаблону.
  • HTEN включает фильтрацию по хэш-таблице.
  • ANDOR — группировка фильтров. Если бит установлен — пакет принимается только при прохождении всех выбраных фильтров. Если сброшен — прохождения одного фильтра достаточно.
  • CRCEN разрешает проверку контрольной суммы. Если установлен, принимаются пакеты только с корректной контрольной суммой.


Фильтрация по шаблону заключается в следующем. Из принятого пакета, по смещению, записанному в регистрах EPMO, берётся окно размером 64 байта. Из этого окна выбираются байты по маске, записанной в регистрах EPMM (например, если бит 0 в регистре EPMM0 установлен, выбирается байт 0 из окна, etc.). От выбранных байт рассчитывается контрольная сумма. Если она совпадает со значением в регистрах EPMCS, фильтр пройден.

При фильтрации по хэш-таблице, рассчитывается хэш от адреса получателя, указанного в заголовке пакета. Берётся соответствующий бит из регистров EHT. Например, если хэш равен 0x5, берётся бит 5 из регистра EHT0. Если бит установлен, фильтр пройден.

MAC-адрес

Те самые 6 байт, которые будут идентифицировать наш девайс в локальной сети. Нужны ENC28J60 для фильтрации входящих пакетов. Хранятся в обратном порядке, т.е. для адреса 01:23:45:67:89:ab в MAADR0 пишем 0xab, в MAADR1 — 0x89, etc.

Регистры MAC

MACON1

  • MARXEN — разрешить MAC принимать пакеты.
  • TXPAUS, RXPAUS — включают аппаратное управление потоком.


MACON3

  • FULLDPX — включить полнодуплексный режим. Дуплексный режим PHY (PHCON1.PDPXMD) должен быть таким же. Значение после сброса зависит от полярности подключения светодиода к ножке LEDB.
  • FRMLNEN — включить автоматическую проверку длины принимаемых и отправляемых фреймов.
  • TXCRCEN — включить автоматическое добавление контрольной суммы к фрейму.
  • PADCFG2:PADCFG0 — настройка паддинга фреймов:

    • 001 — выравнять пакет нулями до 60 байт, затем добавить контрольную сумму (4 байта). Бит TXCRCEN также должен быть установлен.
    • 000 — не выравнивать пакеты.


Регистры MAMXF — максимальная длина принимаемого и отправляемого пакета. Обычно 1518 байт или меньше. Ставим столько, сколько сможет утащить наш МК. Пакеты большего размера будут отбрасываться.

MABBIPG, MAIPGL и MAIPGH — задержка (gap) между отправляемыми пакетами. Стандартные значения:

  • MABBIPG — 0x15 (в полнодуплексном режиме) или 0x12 (в полудуплексном).
  • MAIPGL — 0x12.
  • MAIPGH — 0x0c.


MACLCON1 и MACLCON2 — настройка задержки и ретрансмиссий при возникновении коллизии. Оставляем по умолчанию.

Регистры MII

Регистры MII служат для доступа к регистрам PHY. Во как!

Регистры MII

Для чтения регистра PHY:

  1. Выставляем его адрес в регистре MIREGADR.
  2. Устанавливаем бит MICMD.MIIRD.
  3. Ждём, пока MISTAT.BUSY очистится.
  4. Вручную очищаем MICMD.MIIRD.
  5. Забираем данные из регистров MIRD.


Для записи в регистр PHY:

  1. Выставляем его адрес в регистре MIREGADR.
  2. Записываем данные в регистры MIWR. Сначала MIWRL, затем MIWRH.
  3. Ждём, пока MISTAT.BUSY очистится.


Управление ножкой CLKOUT

В ENC28J60 можно брать тактовый сигнал (с делителем) с ножки CLKOUT. Из-за бага, сигнал может пропадать при входе в режим пониженного энергопотребления.

ECOCON

Биты ECOCON2:0 устанавливают делитель:

  • 000 — ножка CLKOUT подтянута к земле.
  • 001 — делитель на 1 (25 МГц).
  • 010 — делитель на 2 (12,5 МГц).
  • 011 — делитель на 3 (8,333333 МГц).
  • 100 — делитель на 4 (6,25 МГц).
  • 101 — делитель на 8 (3,125 МГц).


Регистры PHY

Регистры PHY раположены в отдельном адресном пространстве. Получить к ним доступ можно через регистры MII. Размер адресного пространства — 32 регистра, всего заюзано 9 адресов.

Регистры PHY

Регистры PHY 16-битные. Используются для различных настроек PHY. Целый регистр выделен под настройки светодиодов. Эстетика!

PHCON1

  • PDPXMD — дуплексный режим PHY. Должен соответствовать дуплексному режиму MAC (MACON3.FULLDPX). Начальное значение зависит от полярности светодиода, подключенного к ножке LEDB.


PHCON2

  • HDLDIS — запрещает «заворот назад» (loopback) отправляемых данных в полудуплексном режиме.


PHSTAT1

  • LLSTAT — «асинхронный» бит сосотяния линка. Читается как 1 если линк есть и не пропадал с момента предыдущего чтения этого бита.


PHSTAT2

  • LSTAT — состояние линка. Бит установлен, если линк есть.


PHLCON

Управление светодиодиками.

  • STRCH — разрешает «растягивание» событий. Если бит включен, события будут отмечаться вспышкой светодиода определённой длительности. Если выключен — светодиод будет зажигаться только во время события (передача/приём данных, etc.).
  • LFRQ — длительность вспышки светодиода:

    • 00 — 40 мс.
    • 01 — 73 мс.

    • 10 — 139 мс.
  • LACFG и LBCFG — выбираем, что именно будут показывать светодиоды, подключенные к ножкам LEDA и LEDB:

    • 0001 — передача.
    • 0010 — приём.

    • 0100 — состояние линка.
    • 0101 — дуплексный режим.
    • 0111 — приём и передача.
    • 1000 — включен.
    • 1001 — выключен.
    • 1100 — приём и состояние линка.
    • 1101 — приём, передача и состояние линка.


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

SPI


Обмен по SPI ведётся в режиме 0 (CPOL=0, CPHA=0). ENC28J60 поддерживает скорость передачи данных по SPI до 10 мбит/с.

// Указываем как у нас подключено
#define ENC28J60_SPI_DDR    DDRB
#define ENC28J60_SPI_PORT    PORTB
#define ENC28J60_SPI_CS        (1<<PB4)
#define ENC28J60_SPI_MOSI    (1<<PB5)
#define ENC28J60_SPI_MISO    (1<<PB6)
#define ENC28J60_SPI_SCK    (1<<PB7)

#define enc28j60_select() ENC28J60_SPI_PORT &= ~ENC28J60_SPI_CS
#define enc28j60_release() ENC28J60_SPI_PORT |= ENC28J60_SPI_CS

// Инициализация ENC28J60
void enc28j60_init()
{
   
// Настроим ножки
    ENC28J60_SPI_DDR
|= ENC28J60_SPI_CS|ENC28J60_SPI_MOSI|ENC28J60_SPI_SCK;
    ENC28J60_SPI_DDR
&= ~ENC28J60_SPI_MISO;
    enc28j60_release
();
   
   
// Максимальная скорость SPI (CLK/2)
    SPCR
= (1<<SPE)|(1<<MSTR);
    SPSR
|= (1<<SPI2X);

   
// Остальная инициализация
   
// ...
}

// Передача данных через SPI
uint8_t enc28j60_rxtx
(uint8_t data)
{
    SPDR
= data;
   
while(!(SPSR & (1<<SPIF)))
       
;
   
return SPDR;
}



Обмен данными с ENC28J60 выполняется транзакциями. Транзакция начинается с отправки микроконтроллером команды. Затем идут опциональные данные (приём или передача). Завершается транзакция «поднятием» ножки CS. 

Чтение:

Чтение

При чтении данных уровень на линии MOSI не имеет значения.

Запись:

Запись

При записи линия MISO находится в Z-состоянии (т.е. не подключена ни к чему).

#define enc28j60_rx() enc28j60_rxtx(0xff)
#define enc28j60_tx(data) enc28j60_rxtx(data)



Команда состоит из опкода и аргумента. При чтении или записи регистра, аргумент содержит адрес регистра.

Таблица команд

Операции с регистрами

Для чтения регистра контроллер отправляет ENC28J60 команду чтения регистра и забирает значение. При чтении регистров MAC или MII, контроллер должен пропустить 1 «ложный» байт, затем прочитать значение.

// Операция чтения
uint8_t enc28j60_read_op
(uint8_t cmd, uint8_t adr)
{
    uint8_t data
;

   
// Низкий уровень на CS
    enc28j60_select
();
   
   
// Отправляем команду
    enc28j60_tx
(cmd | (adr & 0x1f));
   
   
// При необходимости, пропускаем "ложный" байт
   
if(adr & 0x80)
        enc28j60_rx
();
       
   
// Читаем данные
    data
= enc28j60_rx();
   
   
// Высокий уровень на ножке CS
    enc28j60_release
();
   
return data;
}

// Операция записи
void enc28j60_write_op(uint8_t cmd, uint8_t adr, uint8_t data)
{
    enc28j60_select
();
   
   
// Отправляем команду
    enc28j60_tx
(cmd | (adr & 0x1f));
   
   
// Отправляем значение
    enc28j60_tx
(data);
   
    enc28j60_release
();
}



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

Также, нам понадобится заголовочный файл с определениями регистров. Для удобства, в определение регистра можно включить также адрес банка и признак регистра MII/MAC.

// С этого адреса начинаются глобальные для всех банков регистры
#define ENC28J60_COMMON_CR    0x1B

// Банк 0
#define ERDPTL                 0x00
#define ERDPTH                 0x01
#define ERDPT                ERDPTL
//...


// Банк 1
#define EHT0                 (0x00 | 0x20)
#define EHT1                 (0x01 | 0x20)
//...

// Банк 2, регистры MAC/MII
#define MACON1                 (0x00 | 0x40 | 0x80)
//...

// Банк 3
#define EREVID                 (0x12 | 0x60)
//...



Кстати, быстро превратить таблицы из даташита в заголовочный файл поможет любой офисный пакет с редактором электронных таблиц. :)

#define ENC28J60_SPI_RCR    0x00
#define ENC28J60_SPI_WCR    0x40
#define ENC28J60_SPI_BFS    0x80
#define ENC28J60_SPI_BFC    0xA0

uint8_t enc28j60_current_bank
= 0;

// Выбор банка регистров
void enc28j60_set_bank(uint8_t adr)
{
    uint8_t bank
;

   
// Регистр относится к определённому банку?
   
if( (adr & ENC28J60_ADDR_MASK) < ENC28J60_COMMON_CR )
   
{
       
// Получаем номер банка
        bank
= (adr >> 5) & 0x03; //BSEL1|BSEL0=0x03
       
       
// Если выбран "не тот" банк
       
if(bank != enc28j60_current_bank)
       
{
           
// Выбираем банк
            enc28j60_write_op
(ENC28J60_SPI_BFC, ECON1, 0x03);
            enc28j60_write_op
(ENC28J60_SPI_BFS, ECON1, bank);
            enc28j60_current_bank
= bank;
       
}
   
}
}

// Чтение регистра
uint8_t enc28j60_rcr
(uint8_t adr)
{
    enc28j60_set_bank
(adr);
   
return enc28j60_read_op(ENC28J60_SPI_RCR, adr);
}

// Чтение пары регистров (L и H)
uint16_t enc28j60_rcr16
(uint8_t adr)
{
    enc28j60_set_bank
(adr);
   
return enc28j60_read_op(ENC28J60_SPI_RCR, adr) |
       
(enc28j60_read_op(ENC28J60_SPI_RCR, adr+1) << 8);
}

// Запись регистра
void enc28j60_wcr(uint8_t adr, uint8_t arg)
{
    enc28j60_set_bank
(adr);
    enc28j60_write_op
(ENC28J60_SPI_WCR, adr, arg);
}

// Запись пары регистров (L и H)
void enc28j60_wcr16(uint8_t adr, uint16_t arg)
{
    enc28j60_set_bank
(adr);
    enc28j60_write_op
(ENC28J60_SPI_WCR, adr, arg);
    enc28j60_write_op
(ENC28J60_SPI_WCR, adr+1, arg>>8);
}

// Очистка битов в регистре (reg[adr] &= ~mask)
void enc28j60_bfc(uint8_t adr, uint8_t mask)
{
    enc28j60_set_bank
(adr);
    enc28j60_write_op
(ENC28J60_SPI_BFC, adr, mask);
}

// Установка битов в регистре (reg[adr] |= mask)
void enc28j60_bfs(uint8_t adr, uint8_t mask)
{
    enc28j60_set_bank
(adr);
    enc28j60_write_op
(ENC28J60_SPI_BFS, adr, mask);
}



Чтение и запись буфера

Для чтения буфера отправляем команду чтения, затем читаем столько байт, сколько нам нужно. Завершается операция поднятием линии CS.

#define ENC28J60_SPI_RBM    0x3A

// Чтение данных из буфера (по адресу в регистрах ERDPT)
void enc28j60_read_buffer(uint8_t *buf, uint16_t len)
{
    enc28j60_select
();
    enc28j60_tx
(ENC28J60_SPI_RBM);
   
while(len--)
       
*(buf++) = enc28j60_rx();
    enc28j60_release
();
}



Запись происходит аналогично. Команда, передача данных, поднятие CS.

#define ENC28J60_SPI_WBM    0x7A

// Запись данных в буфер (по адресу в регистрах EWRPT)
void enc28j60_write_buffer(uint8_t *buf, uint16_t len)
{
    enc28j60_select
();
    enc28j60_tx
(ENC28J60_SPI_WBM);
   
while(len--)
        enc28j60_tx
(*(buf++));
    enc28j60_release
();
}



Мягкий сброс

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

#define ENC28J60_SPI_SC        0xFF

void enc28j60_soft_reset()
{
   
// Отправляем команду
    enc28j60_select
();
    enc28j60_tx
(ENC28J60_SPI_SC);
    enc28j60_release
();

   
// Ждём, пока ENC28J60 инициализируется
    _delay_ms
(1);

   
// Не забываем про банк
    enc28j60_current_bank
= 0;
}



Инициализация


Типичная последовательность инициализации ENC28J60 выглядит примерно так:

  • Настраиваем размер FIFO для приёма данных (ERXST, ERXND), инициализируем указатель для чтения данных из FIFO (ERXRDPT).
  • Настраиваем фильтрацию входящих пакетов. По умолчанию, ENC28J60 пропускает пакеты, приходящие на наш MAC-адрес и широковещательные пакеты. В принципе, можно так и оставить.
  • Настраиваем MAC:

    • Очищаем MACON2.MARST чтобы снять сброс MAC.
    • Устанавливаем MACON1.MARXEN чтобы разрешить приём данных MAC.

    • Устанавливаем MACON1.RXPAUS и MACON1.TXPAUS для включения аппаратного упралвения потоком.
    • Настраиваем биты PADCFG, TXCRCEN в MACON3. Для большинства приложений подойдёт выравнивание пакета до 60 байт и автоматическое добавление контрольной суммы.
    • Устанавливаем максимальный размер фрейма в регистрах MAMXF.
    • Устанавливаем размер промежутка между фреймами в регистрах MABBIPG, MAIPGL и MAIPGH.
    • Устанавливаем MAC-адрес в регистрах MAADR.
  • Настраиваем PHY:

    • Включаем бит PHCON2.HDLDIS, если не хотим получать свои пакеты обратно в полудуплексном режиме.
    • Выбираем как на различные события будут реагировать светодиоды LEDA и LEDB в регистре PHLCON.

  • Настраиваем дуплексный режим, если хотим переопределить значение, определяемое полярностью светодиода LEDB. Для включения полного дуплекса устанавливаем биты PHCON1.PDPXMD и MACON3.FULDPX.
  • Разрешаем приём пакетов




#define ENC28J60_SPI_DDR    DDRB
#define ENC28J60_SPI_PORT    PORTB
#define ENC28J60_SPI_CS        (1<<PB4)
#define ENC28J60_SPI_MOSI    (1<<PB5)
#define ENC28J60_SPI_MISO    (1<<PB6)
#define ENC28J60_SPI_SCK    (1<<PB7)

#define ENC28J60_BUFSIZE    0x2000
#define ENC28J60_RXSIZE        0x1A00
#define ENC28J60_MAXFRAME    1500

#define ENC28J60_RXSTART    0
#define ENC28J60_RXEND        (ENC28J60_RXSIZE-1)
#define ENC28J60_TXSTART    ENC28J60_RXSIZE
#define ENC28J60_BUFEND        (ENC28J60_BUFSIZE-1)

uint16_t enc28j60_rxrdpt
= 0;

void enc28j60_init(uint8_t *macadr)
{
   
// Настраиваем SPI
    ENC28J60_SPI_DDR
|= ENC28J60_SPI_CS|ENC28J60_SPI_MOSI|ENC28J60_SPI_SCK;
    ENC28J60_SPI_DDR
&= ~ENC28J60_SPI_MISO;
    enc28j60_release
();

    SPCR
= (1<<SPE)|(1<<MSTR);
    SPSR
|= (1<<SPI2X);

   
// Выполняем сброс
    enc28j60_soft_reset
();

   
// Настраиваем размер буфера для приёма пакетов
    enc28j60_wcr16
(ERXST, ENC28J60_RXSTART);
    enc28j60_wcr16
(ERXND, ENC28J60_RXEND);

   
// Указатель для чтения принятых пакетов
    enc28j60_wcr16
(ERXRDPT, ENC28J60_RXSTART);
    enc28j60_rxrdpt
= ENC28J60_RXSTART;

   
// Настраиваем MAC
    enc28j60_wcr
(MACON2, 0); // очищаем сброс
    enc28j60_wcr
(MACON1, MACON1_TXPAUS|MACON1_RXPAUS| // включаем управление потоком
        MACON1_MARXEN
); // разрешаем приём данных
    enc28j60_wcr
(MACON3, MACON3_PADCFG0| // разрешаем паддинг
        MACON3_TXCRCEN
| // разрешаем рассчёт контрольной суммы
        MACON3_FRMLNEN
| //разрешаем контроль длины фреймов
        MACON3_FULDPX
);// включаем полный дуплекс
    enc28j60_wcr16
(MAMXFL, ENC28J60_MAXFRAME); // устанавливаем максимальный размер фрейма
    enc28j60_wcr
(MABBIPG, 0x15); // устанавливаем промежуток между фреймами
    enc28j60_wcr
(MAIPGL, 0x12);
    enc28j60_wcr
(MAIPGH, 0x0c);
    enc28j60_wcr
(MAADR5, macadr[0]); // устанавливаем MAC-адрес
    enc28j60_wcr
(MAADR4, macadr[1]);
    enc28j60_wcr
(MAADR3, macadr[2]);
    enc28j60_wcr
(MAADR2, macadr[3]);
    enc28j60_wcr
(MAADR1, macadr[4]);
    enc28j60_wcr
(MAADR0, macadr[5]);

   
// Настраиваем PHY
    enc28j60_write_phy
(PHCON1, PHCON1_PDPXMD); // включаем полный дуплекс
    enc28j60_write_phy
(PHCON2, PHCON2_HDLDIS); // отключаем loopback
    enc28j60_write_phy
(PHLCON, PHLCON_LACFG2| // настраиваем светодиодики
        PHLCON_LBCFG2
|PHLCON_LBCFG1|PHLCON_LBCFG0|
        PHLCON_LFRQ0
|PHLCON_STRCH);

   
// разрешаем приём пакетов
    enc28j60_bfs
(ECON1, ECON1_RXEN);
}



Отправка пакетов


Для отправки пакета, записываем указатели на его начало и конец в регистры ETXST и ETXND. Перед пакетом должен находиться управляющий байт, в котором можно переопределить некоторые настройки MAC для отправки этого пакета. После того, как пакет будет отправлен, после его конца будет записан блок, содержащий статус передачи.

Раскладка отправляемого пакета

Если хотим отправить пакет с настройками по-умолчанию, младший бит (POVERRIDE) управляющего байта должен быть сброшен.


void enc28j60_send_packet(uint8_t *data, uint16_t len)
{
   
// Ждём готовности передатчика
   
while(enc28j60_rcr(ECON1) & ECON1_TXRTS)
       
;
       
   
// Записываем пакет в буфер
    enc28j60_wcr16
(EWRPT, ENC28J60_TXSTART);
    enc28j60_write_buffer
((uint8_t*)"\x00", 1);
    enc28j60_write_buffer
(data, len);

   
// Устанавливаем указатели ETXST и ETXND
    enc28j60_wcr16
(ETXST, ENC28J60_TXSTART);
    enc28j60_wcr16
(ETXND, ENC28J60_TXSTART + len);

   
// Разрешаем отправку
    enc28j60_bfs
(ECON1, ECON1_TXRTS);
}



Правда, в ENC28J60 есть баг, из-за которого бит TXRTS может не сбрасываться при серьёной ошибке передачи пакета. Соответственно, готовности передатчика мы не дождёмся. Еррата рекомендует проверять бит EIR.ERIF, и, если он установлен, выполнять сброс передатчика. Для этого изменим код вот так:


   
//...
   
while(enc28j60_rcr(ECON1) & ECON1_TXRTS)
   
{
       
// При ошибке, сбрасываем передатчик
       
if(enc28j60_rcr(EIR) & EIR_TXERIF)
       
{
            enc28j60_bfs
(ECON1, ECON1_TXRST);
            enc28j60_bfc
(ECON1, ECON1_TXRST);
       
}
   
}
   
//...



Приём пакетов


ENC28J60 записывает пакеты в кольцевой буфер в виде связанного списка:

Раскладка принятого пакета

Адрес первого непрочитанного пакета храниться в регистрах ERXRDPT. Забрав пакет, микроконтроллер записывает в ERXRDPT адрес следующего пакета. После этого место, которое занимал пакет считается свободным и ENC28J60 может использовать его для приёма новых пакетов.

Статус приёма — длина пакета (2 байта) и различные флаги (тоже 2 байта). Из флагов нас интересует только бит 7 — приём успешно завершён.

Все принятые пакеты ENC28J60 записывает в буфер с выравниванием на 2 байта. Таким образом, адрес пакета всегда чётный.

Для того, чтобы забрать принятый пакет, микроконтроллер делает следующее:

  • Смотрит сколько принято пакетов (в регистре EPKTCNT).
  • Читает пакет из буфера (по адресу ERXRDPT).
  • Записывает в ERXRDPT адрес следующего пакета.
  • Уменьшает значение счётчика пакетов установкой бита ECON2.PKTDEC.


Ну и последний на сегодня баг ENC28J60 — при записи чётного значения в регистр ERXRDPT, ENC28J60 может повредить данные в буфере (кстати, адрес пакета всегда чётный из-за выравнивания). Еррата рекомендует записывать в ENC28J60 всегда нечётное значение. Стоп, а как же мы узнаем откуда брать новый пакет? Придётся хранить это значение в памяти микроконтроллера. Но ERXRDPT мы всё равно должны записывать, чтобы ENC28J60 знал сколько памяти доступно для приёма пакетов. Только записывать будем не адрес следующего пакета, а на адрес на 1 байт выше.

// "Правильное" значение ERXRDPT
uint16_t enc28j60_rxrdpt
= 0;

uint16_t enc28j60_recv_packet
(uint8_t *buf, uint16_t buflen)
{
    uint16_t len
= 0, rxlen, status, temp;

   
// Есть ли принятые пакеты?
   
if(enc28j60_rcr(EPKTCNT))
   
{
       
// Считываем заголовок
        enc28j60_wcr16
(ERDPT, enc28j60_rxrdpt);

        enc28j60_read_buffer
((void*)&enc28j60_rxrdpt, sizeof(enc28j60_rxrdpt));
        enc28j60_read_buffer
((void*)&rxlen, sizeof(rxlen));
        enc28j60_read_buffer
((void*)&status, sizeof(status));

       
// Пакет принят успешно?
       
if(status & 0x80)
       
{
           
// Выбрасываем контрольную сумму
            len
= rxlen - 4;
           
           
// Читаем пакет в буфер (если буфера не хватает, пакет обрезается)
           
if(len > buflen) len = buflen;
            enc28j60_read_buffer
(buf, len);    
       
}

       
// Устанавливаем ERXRDPT на адрес следующего пакета - 1
        temp
= (enc28j60_rxrdpt - 1) & ENC28J60_BUFEND;
        enc28j60_wcr16
(ERXRDPT, temp);

       
// Уменьшаем счётчик пакетов
        enc28j60_bfs
(ECON2, ECON2_PKTDEC);
   
}

   
return len;
}



Заключение

 


Готовую библиотеку можно взять .

Уфф, ну вот вроде и всё про ENC28J60 :)

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

update: Микрочип время от времени обновляет даташит. Последнюю версию можно найти . Статья написана на основе документа .

 

 

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

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

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

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


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


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

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


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


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

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

При прохождении пакета вниз по стеку, к нему прикрепляются заголовки протоколов. Например, вот так будет выглядеть 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.) или это всё и так понятно?

 

 

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

 

 

В этой части мы продолжим писать наш стек протоколов. Добавим возможность отправлять UDP-пакеты на любой IP-адрес и научимся получать данные с удалённого сервера.

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

  • Введение в роутинг
  • ARP-ресолвер
  • Отправка пакетов
  • Пример работы со стеком
  • Заключение


Маршрутизация в Internet


Немножко теории. Рассмотрим основы роутинга в Internet. Думаю, большинство читателей могут эту часть спокойно пропустить)

Итак, возьмём, для примера, мою домашнюю сеть.

Домашняя сеть

Допустим, комп знает IP-адрес девайса и хочет отправить ему пакет. При этом происходит следующее:

  • Комп убеждается, что девайс находится в той же локальной сети, что и он сам.
  • С помощью ARP, комп определяет MAC-адрес девайса.
  • Комп заворачивает IP-пакет в Ethernet-фрейм и отрпавляет на MAC-адрес девайса.
  • Девайс получает фрейм, видит в нём IP-пакет, в котором в качестве адреса получателя указан его собственный IP-адрес.
  • Полученный IP-пакет передаётся протоколу транспортного уровня (UDP, etc.) и, затем, приложению.


Как комп определяет, что девайс находится с ним в одной локальной сети? Любой IP-адрес состоит из адреса подсети и адреса хоста. Именно адрес подсети определяет принадлежность IP-адреса к конкретной локальной сети.

IP-адрес и маска подсети

Для выделения адреса подсети из IP-адреса служит маска подсети. Накладываем на IP-адрес маску и получаем адрес подсети.

В моей домашней сетке маска равна 255.255.255.0. Соответственно, адрес подсети равен 192.168.0.0, а допустимые IP-адреса — от 192.168.0.1 до 192.168.0.254 (первый и последний адреса зарезервированы). Видя, что адрес девайса относится к этому промежутку, комп и понимает — девайс находится с ним в одной локальной сети.

Но что, если мы хотим отправить пакет узлу, который находится не в нашей локальной сети, а вообще неизвестно где? Рассмотрим вот такую сеть (случай не то, чтобы очень жизненный, зато наглядный):

Пример сети

Запись вроде 192.168.3.0/24 означает подсеть с адресом 192.168.3.0 и маской 255.255.255.0 (24 бита установлено).

Допустим, 192.168.0.33 хочет отправить пакет узлу 192.168.3.3, находящемуся в сети 192.168.3.0. Происходит следующее:

  • 192.168.0.33 записывает в IP пакет адрес отправителя 192.168.0.33 (свой), а адрес получателя — 192.168.3.3. Ничего необычного.
  • Заворачивает пакет в Ethernet-фрейм и отправляет его на MAC-адрес узла 192.168.0.22.
  • 192.168.0.22 получает фрейм, видит в нём пакет, предназначеный 192.168.3.3. Поскольку он подключен к обоим локальным сетям, он просто пересылает пакет узлу 192.168.3.3.


Как 192.168.0.33 узнает кому пересылать пакет? Для этого у него есть таблица роутов. Выглядит она примерно так:

Таблица роутов

Когда узел хочет отправить пакет в другую сеть, он просматривает свою таблицу роутов. В ней он находит адрес узла (гейт), на который нужно переслать пакет, чтобы он попал в нужную сеть. Последняя запись — роут по-умолчанию (default route). Она определяет основной гейт (default gateway) — узел, на который пересылаются все пакеты, роут для которых не прописан в явном виде.

Вернёмся к нашей сети. Все пакеты, выходящие за пределы сети проходят через роутер (192.168.0.1), он-то и является основным гейтом. Дополнительные роуты нам прописывать ни к чему, для отправки пакетов наружу достаточно знать адрес основного гейта.

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

ARP-ресолвер


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

Алгоритм работы нашего ARP-ресолвера будет следующий:

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


Добавлять узлы в ARP-кэш будем по кругу (т.е при добавлении новой записи, самая старая будет затираться).

Проверять валидность записей кэша мы не будем — врядли MAC-адрес какого-либо узла внезапно изменится.

// Размер ARP-кэша
#define ARP_CACHE_SIZE      3

// ARP-кэш
typedef struct arp_cache_entry {
   uint32_t ip_addr
;
   uint8_t mac_addr
[6];
} arp_cache_entry_t;

uint8_t arp_cache_wr
;
arp_cache_entry_t arp_cache
[ARP_CACHE_SIZE];

// Поиск в ARP-кэше
uint8_t
*arp_search_cache(uint32_t node_ip_addr)
{
    uint8_t i
;
   
for(i = 0; i < ARP_CACHE_SIZE; ++i)
   
{
       
if(arp_cache[i].ip_addr == node_ip_addr)
           
return arp_cache[i].mac_addr;
   
}
   
return 0;
}

// ARP-ресолвер
// Если MAC-адрес узла известен, возвращает его
// Неизвестен - посылает запрос и возвращает 0
uint8_t
*arp_resolve(uint32_t node_ip_addr)
{
    eth_frame_t
*frame = (void*)net_buf;
    arp_message_t
*msg = (void*)(frame->data);
    uint8_t
*mac;

   
// Ищем узел в кэше
   
if((mac = arp_search_cache(node_ip_addr)))
       
return mac;

   
// Отправляем запрос
    memset
(frame->to_addr, 0xff, 6);
    frame
->type = ETH_TYPE_ARP;

    msg
->hw_type = ARP_HW_TYPE_ETH;
    msg
->proto_type = ARP_PROTO_TYPE_IP;
    msg
->hw_addr_len = 6;
    msg
->proto_addr_len = 4;
    msg
->type = ARP_TYPE_REQUEST;
    memcpy
(msg->mac_addr_from, mac_addr, 6);
    msg
->ip_addr_from = ip_addr;
    memset
(msg->mac_addr_to, 0x00, 6);
    msg
->ip_addr_to = node_ip_addr;

    eth_send
(frame, sizeof(arp_message_t));
   
return 0;
}

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

   
// Проверяем длину пакета
   
if(len >= sizeof(arp_message_t))
   
{
       
// Ethernet <> IP и наш IP-адрес
       
if( (msg->hw_type == ARP_HW_TYPE_ETH) &&
           
(msg->proto_type == ARP_PROTO_TYPE_IP) &&
           
(msg->ip_addr_to == ip_addr) )
       
{
           
switch(msg->type)
           
{
           
// ARP-запрос, посылаем ответ
           
case ARP_TYPE_REQUEST:
                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));
               
break;
           
           
// ARP-ответ, добавляем узел в кэш
           
case ARP_TYPE_RESPONSE:
               
if(!arp_search_cache(msg->ip_addr_from))
               
{
                    arp_cache
[arp_cache_wr].ip_addr = msg->ip_addr_from;
                    memcpy
(arp_cache[arp_cache_wr].mac_addr, msg->mac_addr_from, 6);
                    arp_cache_wr
++;
                   
if(arp_cache_wr == ARP_CACHE_SIZE)
                        arp_cache_wr
= 0;
               
}
               
break;
           
}
       
}
   
}
}



Отправка пакетов


Теперь мы готовы отправлять IP-пакеты любым узлам, подключенным к интернету. Для начала, нам понадобится определить наш IP-адрес, маску подсети и основной гейт.

uint32_t ip_addr = inet_addr(192,168,0,222);
uint32_t ip_mask
= inet_addr(255,255,255,0);
uint32_t ip_gateway
= inet_addr(192,168,0,1);



Алгоритм отправки IP-пакета будет простой:

  • Определяем IP-адрес узла, на который будем отправлять Etheret-фрейм, содержащий пакет. Если пакет пересылается в пределах локальной сети, сразу посылаем его нужному узлу. Иначе будем отправлять фрейм основному гейту.
  • Ресолвим MAC-адрес узла.
  • Заворачиваем IP-пакет в Ethernet-фрейм и посылаем.


// Отправка Ethernet-фрейма
// Должны быть установлены следующие поля:
//    - frame.to_addr - MAC-адрес получателя
//    - frame.type - протокол
// len - длина поля данных фрейма
void eth_send(eth_frame_t *frame, uint16_t len)
{
    memcpy
(frame->from_addr, mac_addr, 6);
    enc28j60_send_packet
((void*)frame, len +
       
sizeof(eth_frame_t));
}

// Отправка IP-пакета
// Следующие поля пакета должны быть установлены:
//    ip.to_addr - адрес получателя
//    ip.protocol - код протокола
// len - длина поля данных пакета
// Если MAC-адрес узла/гейта ещё не определён, функция возвращает 0
uint8_t ip_send
(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t
*ip = (void*)(frame->data);
    uint32_t route_ip
;
    uint8_t
*mac_addr_to;

   
// Если узел в локалке, отправляем пакет ему
   
//    если нет, то гейту
   
if( ((ip->to_addr ^ ip_addr) & ip_mask) == 0 )
        route_ip
= ip->to_addr;
   
else
        route_ip
= ip_gateway;

   
// Ресолвим MAC-адрес
   
if(!(mac_addr_to = arp_resolve(route_ip)))
       
return 0;

   
// Отправляем пакет
    len
+= sizeof(ip_packet_t);

    memcpy
(frame->to_addr, mac_addr_to, 6);
    frame
->type = ETH_TYPE_IP;

    ip
->ver_head_len = 0x45;
    ip
->tos = 0;
    ip
->total_len = htons(len);
    ip
->fragment_id = 0;
    ip
->flags_framgent_offset = 0;
    ip
->ttl = IP_PACKET_TTL;
    ip
->cksum = 0;
    ip
->from_addr = ip_addr;
    ip
->cksum = ip_cksum(0, (void*)ip, sizeof(ip_packet_t));

    eth_send
(frame, len);
   
return 1;
}



Ну и отправка UDP-пакета.

// Отправляет UDP-пакет
// Должны быть установлены следующие поля:
//    ip.to_addr - адрес получателя
//    udp.from_port - порт отрпавителя
//    udp.to_port - порт получателя
// len - длина поля данных пакета
// Если MAC-адрес узла/гейта ещё не определён, функция возвращает 0
uint8_t udp_send
(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t
*ip = (void*)(frame->data);
    udp_packet_t
*udp = (void*)(ip->data);

    len
+= sizeof(udp_packet_t);

    ip
->protocol = IP_PROTOCOL_UDP;
    ip
->from_addr = ip_addr;

    udp
->len = htons(len);
    udp
->cksum = 0;
    udp
->cksum = ip_cksum(len + IP_PROTOCOL_UDP,
       
(uint8_t*)udp-8, len+8);

   
return ip_send(frame, len);
}



Пишем приложение


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

NTP реализуем самым тупым способом — отрпавляем серверу запрос, получаем ответ с точным временем. Для правильной работы NTP, локальный UDP-порт не должен равняться UDP-порту сервера (123). Также, нужно учитывать, что NTP возвращает неправильный timestamp — количество секунд, прошедших с 1 января 1900 года. Чтобы получить нормальный timestamp, считающийся с 1 января 1970, года, нужно отнять от NTP-timestamp'а ровно 2208988800 секунд.

// Порт NTP-сервера
#define NTP_SRV_PORT        htons(123)

// Локальный порт
#define NTP_LOCAL_PORT        htons(14444)

// Фомат времени в NTP - время в секундах с 1 января 1900 г.
//  В формате fixed point 32:32
typedef struct ntp_timestamp {
    uint32_t seconds
; // целая часть
    uint32_t fraction
; // дробная часть
} ntp_timestamp_t;

// Формат NTP-сообщения
typedef struct ntp_message {
   
// информация о пакете
    uint8_t status
;
   
   
// информация об эталонных часах (тип, точность, etc.)
    uint8_t type
;
    uint16_t precision
;
    uint32_t est_error
;
    uint32_t est_drift_rate
;
    uint32_t ref_clock_id
;
   
   
// информация о времени
    ntp_timestamp_t ref_timestamp
; // установки эталонных часов
    ntp_timestamp_t orig_timestamp
; // отправки пакета клиентом
    ntp_timestamp_t recv_timestamp
; // получения пакета сервером
    ntp_timestamp_t xmit_timestamp
; // отправки пакета сервером
} ntp_message_t;

// Отправка запроса на NTP-сервер
uint8_t ntp_request
(uint32_t srv_ip)
{
    eth_frame_t
*frame = (void*)net_buf;
    ip_packet_t
*ip = (void*)(frame->data);
    udp_packet_t
*udp = (void*)(ip->data);
    ntp_message_t
*ntp =(void*)(udp->data);

    ip
->to_addr = srv_ip;
    udp
->to_port = NTP_SRV_PORT;
    udp
->from_port = NTP_LOCAL_PORT;

   
// ntp.status = 8
   
// остальные поля заполняем нулями
    memset
(ntp, 0, sizeof(ntp_message_t));
    ntp
->status = 0x08;

   
return udp_send(frame, sizeof(ntp_message_t));
}

// Обработка ответа NTP-сервера
uint32_t ntp_parse_reply
(void *data, uint16_t len)
{
    ntp_message_t
*ntp = data;
    uint32_t temp
;

   
// Проверяем длину пакета
   
if(len >= sizeof(ntp_message_t))
   
{
       
// Переводим в нормальный timestamp и возвращаем
        temp
= ntp->xmit_timestamp.seconds;
       
return (ntohl(temp) - 2208988800UL);
   
}
   
return 0;
}



С помощью NTP, будем запрашивать время каждые 12 часов.

// Цепляем библиотеку для HD44780
#include "hd44780.h"

// Адрес NTP-сервера
//  !!! Никогда не хардкодь адрес серевера в рабочий, не учебный девайс !!!
#define NTP_SERVER    inet_addr(62,117,76,142)

// Часовой пояс. Для простоты забьём его константой
#define TIMEZONE    7

// Счётчик времени с момента включения девайса
static volatile uint16_t ms_count;
static volatile uint32_t second_count;

// Время следующего NTP-запроса
static volatile uint32_t ntp_next_update;

// Точное время (относительно момента second_count = 0)
static volatile uint32_t time_offset;

// Таймер на частоту 1 кГц
ISR
(TIMER0_COMP_vect)
{
   
if(++ms_count == 1000)
   
{
       
++second_count;
        ms_count
= 0;
   
}
}

// Обработчик получаемых 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);
    uint32_t timestamp
;

   
// Получили ответ от NTP-сервера?
   
if(udp->to_port == NTP_LOCAL_PORT)
   
{
       
if((timestamp = ntp_parse_reply(udp->data, len)))
       
{
           
// Запоминаем время
            time_offset
= timestamp - second_count;
           
           
// Следующее обновление через 12 часов
            ntp_next_update
= second_count + 12UL*60*60;
       
}
   
}
}

int main()
{
   
static uint8_t net_buf[512];
    eth_frame_t
*frame = (void*)net_buf;
    uint32_t display_next_update
= 0;
    uint32_t loctime
;
    uint8_t s
, m, h;
   
char buf[5];

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

   
// Инициализируем LCD
    _delay_ms
(30);
    hd44780_init
();
    hd44780_mode
(1,1,0);
    hd44780_clear
();

   
// Инициализируем Таймер 0 в режиме CTC, 1 кГц (при FCLK = 16 МГц)
    TCCR0
= (1<<WGM01)|(1<<CS01)|(1<<CS00);
    OCR0
= 250;
    TIMSK
|= (1<<OCIE0);
    sei
();

   
while(1)
   
{
       
// Ловим пакеты
       
if((len = enc28j60_recv_packet(net_buf, sizeof(net_buf))))
            eth_filter
(frame, len);
       
       
// Пора отправить NTP-запрос
       
if(second_count >= ntp_next_update)
       
{
           
// Пробуем отправить запрос на NTP-сервер
           
if(!ntp_request(NTP_SERVER))
           
{
               
// Пакет не отрпавлен - MAC-адрес гейта ещё не известен
               
// попробуем снова через 2 секунды
                ntp_next_update
= second_count+2;
           
}
           
else
           
{
               
// Пакет отправлен - если ответ не получим,
               
// попробуем снова через 60 секунд
                ntp_next_update
= second_count+60;
           
}
       
}

       
// Пора обновить данные на экране (точное время известно)
       
if((time_offset) && (second_count >= display_next_update))
       
{
           
// Вычисляем время
            loctime
= time_offset+second_count + 60UL*60*TIMEZONE;
            s
= loctime % 60;
            m
= (loctime/60)%60;
            h
= (loctime/3600)%24;

           
// Рисуем время на экране
            hd44780_clear
();
            itoa
(h,buf,10);
            hd44780_puts
(buf);
            hd44780_puts
(":");
            itoa
(m,buf,10);
            hd44780_puts
(buf);
            hd44780_puts
(":");
            itoa
(s,buf,10);
            hd44780_puts
(buf);

           
// Следующее обновление через 1 секунду
            display_next_update
= second_count+1;
       
}
   
}

   
return 0;
}



Потестируем то, что получилось.

 

Заключение

 


Скачать проект можно .

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

 

 

Подключение микроконтроллера к локальной сети: Широковещательные сообщения и DHCP

 

 

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

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

  • Широковещательные сообщения
  • Отправка и приём
  • DHCP
  • Заключение


Широковещательные сообщения


Иногда бывает полезно отправить сообщение сразу всем узлам локальной сети. Например, если к сети подключено несколько девайсов и хочется обратиться сразу ко всем. Особенно, если адреса девайсов неизвестны.

Для широковещательных пакетов используется последний адрес подсети. Скажем, для подсети 192.168.0.0 с маской 255.255.255.0 широковещательный адрес — 192.168.0.255.

Широковещательный адрес

Пакет, отправленный на этот адрес придёт всем узлам, подключенным к подсети и поддерживающим широковещательные пакеты.

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

Также, существует «широковещательный адрес для всего интернета» 255.255.255.255. Но чаще всего, этот адрес просто считается невалидным. В форточках он даже юзается в качестве индикатора ошибки (в возвращаемых значениях, etc.)

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


Чтобы научить наш стек отправлять и принимать широковещательные сообщения, достаточно дописать всего несколько строчек кода.

Во-первых, определим сам широковещательный адрес для нашей сети:

#define ip_broadcast (ip_addr | ~ip_mask)



Отправка пакетов:

// Отправка IP-пакета
uint8_t ip_send
(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t
*ip = (void*)(frame->data);
    uint32_t route_ip
;
    uint8_t
*mac_addr_to;

   
// Вот здесь мы допишем:
   
//  если адрес получателя - широковещательный
   
//  адрес нашей подсети, то юзаем широковещательный MAC
   
if(ip->to_addr == ip_broadcast)
   
{
       
// use broadcast MAC
        memset
(frame->to_addr, 0xff, 6);
   
}
   
   
// Иначе, делаем всё как обычно
   
else
   
{
       
// Смотрим, уходит ли пакет за пределы сети
       
//  если так - пересылаем его гейту
       
if( ((ip->to_addr ^ ip_addr) & ip_mask) == 0 )
            route_ip
= ip->to_addr;
       
else
            route_ip
= ip_gateway;

       
// Определяем MAC-адрес узла
       
if(!(mac_addr_to = arp_resolve(route_ip)))
           
return 0;
        memcpy
(frame->to_addr, mac_addr_to, 6);
   
}

   
// Отправляем IP-пакет
    frame
->type = ETH_TYPE_IP;

    len
+= sizeof(ip_packet_t);

    ip
->ver_head_len = 0x45;
    ip
->tos = 0;
    ip
->total_len = htons(len);
    ip
->fragment_id = 0;
    ip
->flags_framgent_offset = 0;
    ip
->ttl = IP_PACKET_TTL;
    ip
->cksum = 0;
    ip
->from_addr = ip_addr;
    ip
->cksum = ip_cksum(0, (void*)ip, sizeof(ip_packet_t));

    eth_send
(frame, len);

   
return 1;
}



Приём пакетов:

// Обработчик приходящих IP-пакетов
void ip_filter(eth_frame_t *frame, uint16_t len)
{
    uint16_t hcs
;
    ip_packet_t
*packet = (void*)(frame->data);
   
   
//if(len >= sizeof(ip_packet_t))
   
//{
       
// Здесь тоже мало изменилось
       
//  Добавили "|| (packet->to_addr == ip_broadcast)",
       
//  чтобы также принимать широковещательные пакеты
        hcs
= packet->cksum;
        packet
->cksum = 0;

       
if( (packet->ver_head_len == 0x45) &&
           
(ip_cksum(0, (void*)packet, sizeof(ip_packet_t)) == hcs) &&
           
((packet->to_addr == ip_addr) || (packet->to_addr == ip_broadcast)) )
       
{
            len
= ntohs(packet->total_len) -
               
sizeof(ip_packet_t);

           
switch(packet->protocol)
           
{
           
case IP_PROTOCOL_ICMP:
                icmp_filter
(frame, len);
               
break;

           
case IP_PROTOCOL_UDP:
                udp_filter
(frame, len);
               
break;
           
}
       
}
   
//}
}



Теперь можно посылать широковещательный пинг и смотреть как девайс(ы) на него отзываются)

Широковещательный пинг

DHCP


До этого момента мы прописывали IP-адрес и другие параметры, нужные для работы в сети в явном виде. Что очень неудобно — требуется отдельно конфигурировать каждый девайс перед использованием. Совершенно не годится если ты захочешь, например, подарить кому-нибудь готовый девайс.

К счастью, существует специальный протокол DHCP (Dynamic Host Configuration Protocol), основанный на широковещательных сообщениях, позволяющий узлу все нужные для конфигурирования самого себя данные получить с работающего в сети DHCP-сервера.

DHCP-сервер есть почти во всех сетях. В домашних роутерах он, как правило, включен по умолчанию, etc. То, что нужно!

Вот, в кратце, алгоритм получения узлом конфигурационных данных:

  • Узел посылает широковещательное сообщение DISCOVER, на которое откликаются все имеющиеся в сети DHCP-серверы.