Современные информационные технологии/Компьютерная инженерия

Мясищев А.А., Полозова В.М.

Хмельницкий национальный университет, Украина

Простой UDP сервер на базе atmega328p и enc26j60

         Для управления удаленными устройствами, удаленного считывания данных с датчиков можно использовать микроконтроллеры, подключенные к сети Интернет. Однако микроконтроллеры не имеют достаточно памяти для размещения полноценного стека протокола  TCP/IP.  Поэтому часто используют упрощенные  ("облегченные ") версии стека протоколов.

         Рассмотрим для удаленного управления по сети Интернет устройство, собранное на микроконтроллере ATmega328p, который установлен на плате Arduino UNO[1]. Для связи его с сетью Ethernet используется модуль в основе которого контроллер ENC28J60[2]. Для связи между  контроллерами используется шина SPI. В собранном устройстве на порт PB0 установлен  светодиод, который  имитирует удаленное исполнительное устройство. Схема подключений представлена на рисунке 1.

ardu1.jpg

Рис.1. Схема подключения контроллеров по шине SPI

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

1. Размер поля данных пакета Ethernet не должен превышать 576 байт для исключения сегментации пакета при переходе через  промежуточные  локальные сети.

2. Управление сервером выполняется по протоколу UDP.

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

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

4.Заголовок пакета IP имеет минимальную фиксированную длину 20 байт.

5. Не рассматривается программирование контроллера  enc28j60. В работе  используется отлаженная библиотека enc28j60.c, представленная на сайте http://www.tuxgraphics.org/electronics/.  Здесь используется лишь обращение к двум функциям этой библиотеки : uint16_t  enc28j60PacketReceive(uint16_t maxlen, uint8_t* data)  - получение Ethernet пакета в буфер data[ ]  и  void enc28j60PacketSend(uint16_t len, uint8_t* data) - передача пакета из буфера  data[].

         Рассмотрим последовательность пакетов и их взаимодействие для программирования UDP сервера.

         При обращении к функции enc28j60PacketReceive() выполняется чтение пакета Ethernet в буфер data[]. Структура пакета представлена на рисунке 2. Поле контрольной суммы здесь не рассматривается, поскольку оно формируется контроллером ENC28J60.

 

ris1.png

Рис.2.Структура пакета  Ethernet

 

         Здесь заголовок имеет фиксированную длину 14байт (data[0], ... data[13]), а поле данных начинается с data[14] и имеет размер в зависимости от вложенных туда пакетов. Для сети Интернет заголовок пакета  Ethernet имеет формат Ethernet II у которого в 2-х байтном поле data[12], data[13] находится код пакета, который вложен в поле данных. Например, если это пакет IP то код - 0x0800 (data[13]=0x08, data[14]=0x00), если ARP - то его код 0x0806 (data[13]=0x08, data[14]=0x06) . Таким образом, программное обеспечение (протокол), которое работает с пакетами, определяет тип пакета, находящийся в поле данных.

         Пример программы разборки пакета Ethernet при его чтении.

uint8_t  to_mac[6];  // MAC адрес назначения

uint8_t  fr_mac[6];   // MAC адрес источника

uint8_t  prot_eth[2]; // Код пакета, помещенного в пакет Ethernet

uint8_t  data[590];   // Max длина пакета Ethernet

 

void eth_read(void)  // Чтение пакета Ethernet.

// data[0...13] - заголовок пакета ethernet (14 байт)

{

i=0; while(i<6) {  to_mac[i]=data[i];i++;}   // Читаем mac адрес

//назначения  (адрес сервера или broadcast)

i=0; while(i<6) {  fr_mac[i]=data[i+6];i++;}  // и mac адрес

// источника (адрес пославшего пакет узла)

i=0; while(i<2) {  prot_eth[i]=data[i+12];i++;}   // Читаем код

// протокола в пакете ethernet

  if (prot_eth[0]==0x08 && prot_eth[1]==0x00)  // Если получен пакет IP

   ip_read();     // то разбираем  IP

 if (prot_eth[0]==0x08 && prot_eth[1]==0x06)  // Если получен пакет ARP

   arp_read_send();     // то разбираем ARP

}                                                                                                 

         Обычно при первом обращении клиентского узла к серверу, клиент должен узнать MAC адрес по известному IP адресу сервера.  Это потому, что в сети Ethernet пакеты передаются по MAC адресам. Поэтому при выполнении клиентом, например команды

nc  -u  192.168.1.178 15444

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

ris3.png

Рис.3. Пакет Ethernet с вложенным пакетом ARP

На рисунке 4 показана побайтная структура пакета ARP, хранящаяся в массиве data[].

ris2.png

Рис.4. Структура пакета ARP

Здесь можно выделить следующие поля:

1 - data[14], data[15] - код протокола канального уровня. Для Ethernet data[14]=0x00, data[15]=0x01;

2 - data[16], data[17] - код протокола сетевого уровня. Для IP data[16]=0x08, data[17]=0x00;

3 - data[18] - длина адреса канального уровня. Для Ethernet data[18]=6;

4 - data[19] - длина адреса сетевого уровня. Для IP data[18]=4;

5 - data[20], data[21] - тип сообщения. Для ответа data[20]=0x00, data[21]=0x02;

6 - data[22], ... data[27] - MAC адрес отправителя пакета(при чтении ARP - это адрес клиента, при ответе - адрес сервера);

7 - data[28],...data[31] - IP адрес отправителя пакета(при чтении ARP - это адрес клиента, при ответе - адрес сервера);

8 - data[32], ... data [37] - MAC адрес получателя пакета(при запросе клиент устанавливает здесь нули, при ответе - адрес клиента);  

9 - data[38], ... data[41] - IP адрес получателя пакета(при чтении ARP - это адрес сервера, при ответе - адрес клиента);

         Пример программы по работе с ARP пакетом.

uint8_t  ip_my[4] = {192,168,1,178}; //IP адрес сервера

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

 

void arp_read_send(void) // Чтение и передача пакета ARP.

// data[14...41] - заголовок пакета ARP (28 байт)

{

if((data[20]==0x00 && data[21]==0x01) && (data[38]==ip_my[0] && \

data[39]==ip_my[1] && data[40]==ip_my[2] && data[41]==ip_my[3] )) // Если это

// ARP - запрос и IP адрес соответствует адресу  сервера то отправляем ответ

  {

data[20]=0x00; //Отправляем

data[21]=0x02; //тип сообщения (ответ)

i=0; while(i<6) {  data[32+i]=data[22+i];i++;} //mac-адрес отправителя

// переписываем в mac адрес получателя

i=0; while(i<6) {  data[22+i]=my_mac[i];i++;}  //mac-адрес получателя

// переписываем в mac адрес отправителя

i=0; while(i<4) {  data[38+i]=data[28+i];i++;} //IP-адрес отправителя

// переписываем в IP адрес получателя

i=0; while(i<4) {  data[28+i]=ip_my[i];i++;}   //IP-адрес получателя

// переписываем в IP адрес отправителя

  }

  memcpy(to_mac,fr_mac,6); // Заполняем адресные поля пакета Ethernet.

  // MAC адрес назначения  соответствует адресу, откуда пакет пришел

  memcpy(fr_mac,my_mac,6); // MAC адрес источника - это адрес сервера

eth_send(28); //Выполняем посылку пакета Ethernet.

}

         Так как в этой функции необходимо выполнить передачу пакета Ethernet  клиенту, ниже представлен фрагмент программы посылки Ethernet - пакета:

 

void eth_send( uint16_t len) // Посылка пакета Ethernet

// data[0...13] - заголовок пакета Ethernet (14 byte)

{

i=0;    while(i<6) {  data[i]=to_mac[i];i++;}   // Устанавливаем MAC адреса

i=6;    while(i<12){  data[i]=fr_mac[i-6];i++;} // в поля пакета Ethernet

i=12;  while(i<14){  data[i]=prot_eth[i-12];i++;}  //Устанавливаем тип пакета,

// который записан в поле данных пакета Ethernet

    enc28j60PacketSend(len + 14,data); // Посылаем пакет

    _delay_ms(250);

}

         После определения MAC адреса сервера  клиентом в поле данных пакета Ethernet вставляется пакет IP, а в поле данных пакета IP - пакет UDP (рисунок 4).  Обмен пакетами выполняется по MAC адресам.

ris4.png

Рис.4.Пакет Ethernet с вложенными в него пакетами IP и UDP

         На рисунке 5 показана побайтная структура заголовка пакета IP, хранящаяся в массиве data[].

ris5.png

Рис.5. Структура заголовка пакета IP

         Здесь можно выделить следующие поля:

1 - data[14] - версия и размер заголовка. Пакет IPv4 минимальной длины имеет версию 4, и размер заголовка - 5 32-х битных слов(20байт). Следовательно data[14]=0x45;

2 - data[15] - поле типа сервиса. Для рассматриваемой задачи data[15]=0;

3 - data[16], data[17] - общая длина пакета IP(сумма длин пакета UDP и заголовка пакета IP);

4 - data[18], data[19] - идентификатор фрагмента. Так как пакеты не фрагментируются, здесь устанавливается 0;

5 - data[20], data[21] - смещение фрагмента. Устанавливаются в 0, как и в предыдущем случае;

6 - data[22] - время жизни пакета, принимаем равное 64. При прохождении через очередной маршрутизатор это время уменьшается на 1. Когда время будет равно 0, пакет удаляется маршрутизатором;

7 - data[23] - код протокола, который разместил свой пакет в поле данных пакета IP. В нашем случае размещен пакет  UDP, data[23]=17;

8 - data[24], data[25] - контрольная сумма заголовка пакета IP. Считается с помощью функции checksum() из источника[3]. Считается обязательно при отправке пакета клиенту - компьютеру. В противном случае пакет будет отброшен клиентом, который использует стандартный стек  TCP/IP;

9 - data[26], ... data[29] - IP адрес источника пакета;

10 - data[30], ... data[33] - IP адрес получателя пакета.

         Пример программы разборки заголовка пакета IP при его чтении.

 

#define IP_PROTO_UDP_V 17 // Код протокола UDP

void ip_read(void)// Чтение пакета IP. Выбираем только нужные данные

// data[14...33] - заголовок пакета IP (20 байт)

{

len_ip[0] = data[16]; //Длина пакета IP (старший байт)

len_ip[1] = data[17]; //Длина пакета IP (младший байт)

prot_ip=data[23]; // Код протокола, который разместил

// пакет в поле данных пакета IP

i=0; while(i<4) { from_ip[i]=data[26+i];i++; }//Чтение поля  IP адреса источника

i=0; while(i<4) { to_ip[i]=data[30+i];i++; }//Чтение поля  IP адреса назначения

if (prot_ip==IP_PROTO_UDP_V) // Если в пакете IP находится пакет UDP, то

    udp_read(); // читаем его

}

         На рисунке 6 показана побайтная структура заголовка пакета UDP, хранящаяся в массиве data[]. Как указывалось выше пакет UDP вкладывается в поле данных пакета IP.

ris6.png

Рис.6. Побайтная структура пакета UDP

         Здесь выделены следующие поля:

1 - data[34], data[35] - порт отправителя пакета;

2 - data[36], data[37] - порт получателя пакета;

3 - data[38], data[39] - общая длина пакета UDP;

4 - data[40], data[41] - контрольная сумма  пакета UDP. Считается с помощью функции checksum() из источника[3]. Считается обязательно при отправке пакета клиенту - компьютеру. В противном случае пакет будет отброшен клиентом, который использует стандартный стек  TCP/IP;

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

void udp_read(void)// Чтение пакета UDP

// data[34...41] - заголовок пакета UDP (8 byte)

{

from_port[0]=data[34];//Порт от источника(старший байт)

from_port[1]=data[35];//Порт от источника(младший байт)

to_port[0]=data[36];//Аналогично для порта приемника пакета

to_port[1]=data[37];

len_udp[0]=data[38];//Полная длина пакета UDP(старший байт)

len_udp[1]=data[39];//Полная длина пакета UDP(младший байт)

}

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

1. Формируется массив данных заданной длины (например dat[]="Led on - 1")

2. Эти данные передаются функции, которая формирует пакет UDP. Ниже представлен фрагмент этой программы:

uint16_t  fr_p; //Порт источника

uint16_t  to_p; //Порт приемника

uint8_t  cksudp[2]={0x0,0x0};//Поле контрольной суммы всего пакета UDP

uint8_t  *buf1; //Буфер хранения данных для расчета контрольной суммы

uint8_t  prot_ip; //Указывает, какой пакет находится в поле данных пакета IP

void udp_send( uint8_t *dat, uint16_t len) // Посылка пакета UDP

// data[34...41] - заголовок пакета UDP (8 байт)

{

uint16_t cks;

len=len+8; // полная длина пакета UDP

// (длина поля данных(len) + длина заголовка UDP(8байт))

i=0; while(i<4) { data[26+i]=from_ip[i];i++; } //До расчета контрольной

//суммы пакета UDP

i=0; while(i<4) { data[30+i]=to_ip[i];i++; }   //заполняем поле IP адреса

data[34] = fr_p/256;  //Преобразование порта источника

// из 2-х байтового числа (старший  байт)

data[35] = (fr_p<<8)/256; // в два однобайтовых (младший байт)

data[36] = to_p/256;      // Тоже и для порта назначения - старший  байт

data[37] = (to_p<<8)/256; // - младший байт

data[38] = len/256;      //Преобразование полной длины UDP пакета в старший

data[39] = (len<<8)/256; // и младший байт

data[40] = cksudp[0]; // Перед расчетом контрольной суммы вначале задаем

data[41] = cksudp[1]; // ее значение  равным  0

i=0; while(i<len-8) { data[42+i]=dat[i];i++;} //Добавляем к заголовку пакета

// пользовательские данные

buf1=&data[26]; //Формирование набора данных для расчета

//контрольной суммы. Расчет контрольной суммы

// выполняется с полей IP адресов, заголовка UDP пакета

// и завершается полем пользовательских данных

cks = checksum(buf1,len+8,1); //Расчет контрольной суммы.

// Здесь len+8 это добавление к полной длине пакета UDP полей IP адресов

data[40] =  cks/256; // Вставляем в пакет UDP поле контрольной суммы

data[41] = (cks<<8)/256; //после ее расчета

prot_ip=IP_PROTO_UDP_V; //В пакет IP помещаем пакет UDP

ip_send(len); // Передаем длину пакета UDP функции формирования пакета IP

}

3. На следующем этапе формируем заголовок пакета IP. Это выполняет фрагмент программы, представленный ниже.

uint8_t ver_head_len=0x45; //Версия и длина заголовка пакета IP

uint8_t tos=0; // Тип обслуживания пакета

uint8_t frag_id[2]={0x0,0x0}; // Идентификатор пакета

uint8_t offset[2]={0x0,0x0}; // Смещение фрагмента

uint8_t ttl = 64; //Время жизни пакета

uint8_t cksum[2]={0x0,0x0}; //Контрольная сумма заголовка пакета IP

uint8_t from_ip[4];// IP адрес источника

uint8_t to_ip[4];  // IP адрес назначения

uint8_t prot_eth[2]; // Код пакета, помещенного в пакет Ethernet

void ip_send(uint16_t  len)  // Посылка пакета IP

// data[14...33] - заголовок пакета IP (20 байт)

// Заполнение полей пакета IP

{

uint16_t cks;

len=len+20; // Полная длина пакета IP

// Заполняем заголовок пакета IP

data[14] = ver_head_len;

data[15] = tos;

data[16] = len/256;        //Преобразование 2-х байтовой длины (старший байт)

data[17] = (len<<8)/256;//в два однобайтовых числа (младший байт)

data[18] = frag_id[0];

data[19] = frag_id[1];

data[20] = offset[0];

data[21] = offset[1];

data[22] = ttl;

data[23] = prot_ip;

data[24] = cksum[0]; //До расчета контрольной суммы устанавливаем

data[25] = cksum[1]; // ее поля равным нулю

i=0; while(i<4) { data[26+i]=from_ip[i];i++; }//Заполнение поля  IP адреса источника

i=0; while(i<4) { data[30+i]=to_ip[i];i++; }  //Заполнение поля  IP адреса назначения

buf1=&data[14]; //Формирование набора данных для расчета

// контрольной суммы заголовка IP

cks = checksum(buf1,20,0); //Расчет контрольной суммы

// для заголовка пакета IP (20 байт)

data[24] = cks/256;        // Замена поля контрольной суммы

data[25] = (cks<<8)/256;   //после ее расчета

prot_eth[0]=0x08;//Пакету Ethernet указываем, что в нем находится пакет IP

prot_eth[1]=0x00;

eth_send(len);// Передаем длину пакета IP функции отправки пакета Ethernet

}

4. Формируем заголовок (eth_send()) пакета Ethernet ()и передаем весь пакет функции enc28j60PacketSend().

         Рассмотрим само приложение(функцию main()). Приложение должно получать пакеты, анализировать содержимое поля данных пакета UDP. Если первый символ поля данных имеет символ 1, то светодиод, подключенный к нулевому биту PORTB,  должен загореться.  Если первый символ 0, то светодиод гаснет. Для выполнения этой части достаточно использовать функции чтения пакета и посылки только пакета Ethernet.

         Если первым символом поля данных будет символ клавиши Enter,  то клиенту должен быть послан пакет с набором  опций работы сервера ("меню"). Если первым символом будет 2, то сервером будет передан пакет, информирующий состояние светодиода("Led On" или "Led Off"). Для выполнения этой части сервер должен формировать пакеты UDP, IP, рассчитывать контрольные суммы.

         Ниже представлен фрагмент программы, выполняющие описанные действия.

void my_address(void)

{

    fr_p=MY_UDP_PORT;

to_p=256*from_port[0]+from_port[1];// Порт назначения должен равняться

                                                          // порту - источнику пакета(клиенту)

memcpy(to_ip,from_ip,4); // Присваиваем адресу назначения ip адрес,

     // откуда пришел пакет. Т.е. пакет должен быть отправлен обратно

 memcpy(from_ip,ip_my,4); // В качестве IP адреса источника 

    // устанавливаем адрес сервера

memcpy(to_mac,fr_mac,6); //Присваиваем адресу назначения mac адрес,

    // откуда пришел пакет

memcpy(fr_mac,my_mac,6); // В качестве mac адреса источника

    // устанавливаем адрес сервера

}

int main()

{

     enc28j60Init(fr_mac); // Инициализация enc28j60

  _delay_ms(200);

    DDRB |= (1<<PB0);    PORTB &= ~(1<<PB0);

    while(1)

    {

len_eth = enc28j60PacketReceive(sizeof(data), data); //Чтение приходящих пакетов

        if(len_eth)   // Если пакет есть, то

        eth_read();  // читать его как пакет Ethernet (и все остальные в нем пакеты)

int to_po=256*to_port[0]+to_port[1]; //Преобразование однобайтовых значений

                                                          // порта в двухбайтовое

if(to_po==MY_UDP_PORT)  // если портом назначения является порт сервера,

                                               // то запрос обрабатываем

{ 

  if(data[42]==0x0a) //При получении символа Enter формируем передачу "меню"

 {

uint8_t dat[]="Hello!\nLed Off - 0\nLed On  - 1\nStatus  - 2\nHelp    - Enter\n";

my_address();  //Формирование адресной информации для пересылки пакета

udp_send(dat, sizeof(dat)-1); // Формируем UDP пакет с полем данных на 1 меньше

 // длины строки(dat) для исключения символа 0x00 (конца строки)

}

if (data[42]==0x31) //Если самым первым символом поля данных

// пакета UDP является символ 1, то включаем светодиод

{  PORTB |= (1<<PB0);data[42]=0x00; }

 if (data[42]==0x30) //Если самым первым символом поля данных

 // пакета UDP является символ 0, то выключаем светодиод

 {  PORTB &= ~(1<<PB0);data[42]=0x00; }

if (data[42]==0x32) //Если самым первым символом поля данных пакета UDP

// является символ 2, то передаем на сервер состояние светодиода

{  if( (PORTB&0x01)==0x01) //Если нулевой бит порта B установлен,

// то передаем пакет "Led On\n"

{ uint8_t dat[]="Led On\n";  my_address();   udp_send(dat, sizeof(dat)-1); }

 else { uint8_t dat[]="Led Off\n";//В любом другом случае передаем пакет "Led Off\n"

my_address();   udp_send(dat, sizeof(dat)-1); }

      }

   }

 }

return 0;

}

         В качестве среды разработки использовался AVR Studio 4, ver.4.18. с компилятором AVR-GCC.  Для программирования микроконтроллера использовался программатор avrdude, входящий в программную среду Ардуино.  Пример командной строки:

C:\putty\my_ip_read1\default>avrdude -C avrdude.conf -patmega328p -carduino -PCOM4 -b115400 -D -V -Uflash:w:my_ip.hex:i

         Рабочий проект находится с источнике[4].

Для работы с сервером можно использовать командную строку

nc  -u  192.168.1.178 15444

Здесь nc - программа Netcat, -u  -  режим работы программы с пакетами UDP.

Выводы

1. Разработан простой сервер на микроконтроллере AVR и контроллере Ethernet  enc28j60 для удаленного управления устройствами по сети Интернет.

2. Представленный сервер также информирует клиента о состоянии удаленных устройств.

3. Для передачи данных между сервером и клиентом используется облегченный протокол UDP/IP. В качестве клиентской программы может быть использована популярная программа  Netcat (nc), которая работает также и с пакетами UDP.

Литература

1. Arduino.  Официальный  сайт. [Electronic resource]. -  Mode of access:    http://arduino.cc , 2014.

2. ENC28J60. Stand-Alone Ethernet Controller with  SPI™  Interface.  [Electronic resource]. -  Mode of access:

http://ww1.microchip.com/downloads/en/devicedoc/39662a.pdf, 2004. 

3. Guido Socher.   AVR microcontroller based Ethernet device.  [Electronic resource]. -  Mode of access:

http://www.tuxgraphics.org/electronics/200606/article06061.shtml, 2008.

4. Мясищев А.А. UDP сервер на модулях Arduino UNO и enc28j60.  [Electronic resource]. -  Mode of access: 

http://webstm32.sytes.net/stm32_web/avr_enc28_serv_udp.html, 2014.