środa, 6 grudnia 2017

[33.1] STM32F4 - ENC28J60, serwer UDP

Ten post chciałbym poświęcić na opisanie sposobu konfiguracji układu ENC28J60 za pomocą układu STM32F4 oraz bibliotek HAL'a. Po oprogramowaniu poprzednich funkcji przedstawię prostą aplikację serwera UDP.

[Źródło: http://www.st.com/en/evaluation-tools/stm32f4discovery.html]

Przygotowanie projektu CubeMx:


Tutaj przygotowanie projektu jest niezwykle łatwe. Należy uruchomić układ UART do debugowania, RCC, SWD oraz SPI do obsługi układu ENC28J60. Po tych operacjach zostały jeszcze do uruchomienia pin CS, który będzie sterowany programowo.


Konfiguracja zegara:


Ustawienie SPI:


Podłączenie układu ENC wygląda następująco:


  • 5V - niepodłączony
  • GND - GND;
  • LNT - PA2;
  • CLK - niepodłączony;
  • SO - PA6
  • WOL -  niepodłączony;
  • SCK - PA5;
  • ST - PA7;
  • RST - 3V;
  • CS - PA4;
  • Q3 - zasilanie układu pod 3,3V;
  • GND - GND;

Dodatkowo w0 oraz ykorzystywany jest UART 2 ze standardowymi ustawieniami. Jest on podłączony do pinu PA3(RX) oraz PD5(TX)

Opis funkcji:


Na samym początku komunikacja z układem za pomocą interfejsu SPI.

Podstawowa funkcja służąca do przesyłania danych do ENC oraz odbiera dane od układu:

  1. static uint8_t SPI_WriteRead(uint8_t Byte)
  2. {
  3.     uint8_t receivedbyte = 0;
  4.    
  5.     if(HAL_SPI_TransmitReceive(&hspi1,(uint8_t*) &Byte,(uint8_t*) &receivedbyte,1,0x2000)!=HAL_OK){
  6.         Error();
  7.     }
  8.    
  9.     return receivedbyte;
  10. }

Powyższa funkcja do komunikacji wykorzystuje bibliotekę hala do przesłania danych.

Funkcja przesyłająca oraz odbierająca dane od SPI:

  1. static void SPI_SendByte(uint8_t bt)
  2. {
  3.     SPIx_WriteRead(bt);
  4. }
  5. static uint8_t SPI_ReceiveByte(void)
  6. {
  7.     uint8_t bt = SPIx_WriteRead(0xFF);
  8.     return bt;
  9. }

Funkcja odpowiedzialna za wybranie banku pamięci w układzie:

  1. static void enc28j60SetBank(uint8_t addres)
  2. {
  3.     if((addres & BANK_MASK) != ENC28J60_Bank)
  4.     {
  5.         enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR,ECON1, ECON1_BSEL1|ECON1_BSEL0);
  6.         ENC28J60_Bank = addres & BANK_MASK;
  7.         enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ENC28J60_Bank>>5);
  8.     }
  9. }

Wpisanie bufora danych do pamięci

  1. static void enc28j60WriteBuffer(uint8_t* buffer, uint16_t bufferLength)
  2. {
  3.     ENC28J60_CS_SEL();
  4.    
  5.     SPI_SendByte(ENC28J60_WRITE_BUF_MEM);
  6.    
  7.     while(bufferLength--)
  8.     {
  9.         SPI_SendByte(*buffer++);
  10.     }
  11.     ENC28J60_CS_DEL();
  12. }

Odczytanie danych z pamięci:

  1. static void enc28j60ReadBuffer(uint8_t* buffer, uint16_t bufferLength)
  2. {
  3.     ENC28J60_CS_SEL();
  4.     SPI_SendByte(ENC28J60_READ_BUF_MEM);
  5.     while(bufferLength--)
  6.     {
  7.         *buffer++=SPI_WriteRead(0x00);
  8.     }
  9.     ENC28J60_CS_DEL();
  10. }

Odczytanie rejestru kontrolnego:

  1. static uint8_t enc28j60ReadOp(uint8_t operation, uint8_t addres)
  2. {
  3.     uint8_t status = 0;
  4.     ENC28J60_CS_SEL();
  5.     SPI_SendByte(operation|(addres & ADDR_MASK));
  6.     SPI_SendByte(0x00);
  7.     if(addres & 0x80)
  8.     {
  9.         SPI_ReceiveByte();
  10.     }
  11.     status = SPI_ReceiveByte();
  12.     ENC28J60_CS_DEL();
  13.     return status;
  14. }

Wpisanie danych do odpowiedniego rejestru:

  1. static void enc28j60WriteOp(uint8_t operation, uint8_t addres, uint8_t dataToWrite)
  2. {
  3.     ENC28J60_CS_SEL();
  4.    
  5.     SPI_SendByte(operation|(addres&ADDR_MASK));
  6.     SPI_SendByte(dataToWrite);
  7.    
  8.     ENC28J60_CS_DEL();
  9. }

Wprowadzenie danych do rejestrów PHY:

  1. static void enc28j60WritePhy(uint8_t addres, uint16_t data)
  2. {
  3.     /* set PHY register adress */
  4.     enc28j60WriteToRegByte(MIREGADR, addres);
  5.     /* write PHY data */
  6.     enc28j60WriteToRegByte(MIWR,     data);
  7.     /* wait before PHY complete task */
  8.     while(enc28j60ReadByteFromReg(MISTAT) & MISTAT_BUSY)
  9.     {
  10.         HAL_Delay(1);
  11.     }
  12. }

Wpisanie danych do rejestru:

  1. static void enc28j60WriteToReg(uint8_t destAdress, uint16_t dataToWrite)
  2. {
  3.     /* writes uint16_t need two operation, first LSB last MSB */
  4.     enc28j60WriteToRegByte(destAdress, dataToWrite & 0xff);
  5.     enc28j60WriteToRegByte(destAdress + 1, dataToWrite >> 8);
  6. }

Funkcja powyżej wykonuje wpisanie zmiennej uint16_t. Jest ona wysyłana dwa razy po 8 bitów od wartości najmniej znaczącej. 

Kolejna funkcja odpowiada za odczytanie danych z podanego rejestru:

  1. static uint8_t enc28j60ReadByteFromReg(const uint8_t destAdress)
  2. {
  3.     /* Set bank adress */
  4.     enc28j60SetBank(destAdress);
  5.     /* Read data*/
  6.     return enc28j60ReadOp(ENC28J60_READ_CTRL_REG, destAdress);
  7. }

Wpisanie pojedynczej wartości do rejestru:

  1. static void enc28j60WriteToRegByte(const uint8_t destAddres, const uint8_t dataToWrite)
  2. {
  3.     enc28j60SetBank(destAddres);
  4.     enc28j60WriteOp(ENC28J60_WRITE_CTRL_REG, destAddres, dataToWrite);
  5. }

Procedura uruchamiająca układ ENC28J60:

  1. void enc28j60Initialize(void)
  2. {
  3.     /* System Reset */
  4.     enc28j60WriteOp(ENC28J60_SOFT_RESET,ENC28J60_SOFT_RESET,0);
  5.     HAL_Delay(20);
  6.     while(!enc28j60ReadOp(ENC28J60_READ_CTRL_REG,ESTAT)&ESTAT_CLKRDY);
  7.     /* start RX */
  8.     enc28j60WriteToReg(ERXST,RXSTART_INIT);
  9.     /* set receive pointer address */
  10.     enc28j60WriteToReg(ERXRDPT,RXSTART_INIT);
  11.     /* RX end */
  12.     enc28j60WriteToReg(ERXND,RXSTOP_INIT);
  13.     /* TX start */
  14.     enc28j60WriteToReg(ETXST,TXSTART_INIT);
  15.     /* TX end */
  16.     enc28j60WriteToReg(ETXND,TXSTOP_INIT);
  17.     //ERXFCON_BCEN
  18.     enc28j60WriteToRegByte(ERXFCON,enc28j60ReadByteFromReg(ERXFCON)|(ERXFCON_UCEN|ERXFCON_CRCEN|ERXFCON_PMEN|ERXFCON_BCEN));
  19.     /* Enable MAC receive */
  20.     enc28j60WriteToRegByte(MACON1, MACON1_MARXEN|MACON1_TXPAUS|MACON1_RXPAUS);
  21.     /* Bring MAC out of reset */
  22.     enc28j60WriteToRegByte(MACON2, 0x00);
  23.     /*  enable automatic padding to 60bytes and CRC operations */
  24.     enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, MACON3, MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN);
  25.     /* set inter-frame gap (non-back-to-back) */
  26.     enc28j60WriteToReg(MAIPG,INTER_FRAME_GAP);
  27.     /* set inter-frame gap (back-to-back) */
  28.     enc28j60WriteToRegByte(MABBIPG,0x12);
  29.     /* Set maximum packet size that controller will accept, this value
  30.      * cant be larger than MAX_FRAMELEN
  31.      */
  32.     enc28j60WriteToReg(MAMXFL,MAX_FRAMELEN);
  33.     /* Write mac adres to banc 3. MAC Adress in
  34.      * ENC28J60 is write in byte bacward order*/
  35.     enc28j60WriteToRegByte(MAADR5,macaddr[0]);
  36.     enc28j60WriteToRegByte(MAADR4,macaddr[1]);
  37.     enc28j60WriteToRegByte(MAADR3,macaddr[2]);
  38.     enc28j60WriteToRegByte(MAADR2,macaddr[3]);
  39.     enc28j60WriteToRegByte(MAADR1,macaddr[4]);
  40.     enc28j60WriteToRegByte(MAADR0,macaddr[5]);
  41.     /* no loopback of transmitted frames */
  42.     enc28j60WritePhy(PHCON2,PHCON2_HDLDIS);
  43.     enc28j60WritePhy(PHLCON,PHLCON_LACFG2|
  44.                         PHLCON_LBCFG2|PHLCON_LBCFG1|PHLCON_LBCFG0|
  45.                         PHLCON_LFRQ0|PHLCON_STRCH);
  46.     /* switch to bank 0 */
  47.     enc28j60SetBank(ECON1);
  48.     /* Enable interrupt  */
  49.     enc28j60WriteOp(ENC28J60_BIT_FIELD_SET,EIE,EIE_INTIE|EIE_PKTIE);
  50.     /* enable packet receptions */
  51.     enc28j60WriteOp(ENC28J60_BIT_FIELD_SET,ECON1,ECON1_RXEN);
  52.     HAL_Delay(15);
  53. }

Obsługa odbierania pakietów:

  1. uint16_t enc28j60ReceiveDataPacket(uint8_t* buffer, const uint16_t bufferLen)
  2. {
  3.     uint16_t length=0;
  4.     uint16_t dataBuffer[3] = {0};// 0 nextPacket 1 byteCount 2 status
  5.     /*  Check if packet was received and buffered */
  6.     if(enc28j60ReadByteFromReg(EPKTCNT)>0)
  7.     {
  8.         /* Set the read pointer to the start of the received packet */
  9.         enc28j60WriteToReg(ERDPT, glob_NextPacket);
  10.         /* read next data */
  11.         /* This function reads data and writes it to buffer
  12.          * first value will be nextPacket pointer
  13.          * next will be packet length
  14.          * last is receive status */
  15.         enc28j60ReadBuffer((uint8_t*)&dataBuffer, sizeof(dataBuffer));
  16.         glob_NextPacket = dataBuffer[0];
  17.         /* read the packet length */
  18.         length =  dataBuffer[1] - 4;
  19.         /* check packet length */
  20.         if(length>bufferLen)    {   length=bufferLen;   }
  21.         /* Check CRC and symbol Errors */
  22.         if((dataBuffer[2]&0x80)==0) {   length=0;   }
  23.         else{
  24.             /* copy data packet */
  25.             enc28j60ReadBuffer(buffer, length);
  26.         }
  27.         buffer[length]=0;
  28.         if(((glob_NextPacket-1) > RXSTOP_INIT) || ((glob_NextPacket) - 1 < RXSTART_INIT))
  29.         {
  30.             enc28j60WriteToReg(ERXRDPT, RXSTOP_INIT);
  31.         }
  32.         else
  33.         {
  34.             enc28j60WriteToReg(ERXRDPT, glob_NextPacket-1);
  35.         }
  36.         enc28j60WriteOp(ENC28J60_BIT_FIELD_SET,ECON2,ECON2_PKTDEC);
  37.     }
  38.     else
  39.     {
  40.         return 0;
  41.     }
  42.     /* return packet length */
  43.     return length;
  44. }

Przesyłanie pakietów danych:

  1. void enc28j60PacketSend(uint8_t* buffer, uint16_t bufferLength)
  2. {
  3.     /* Check if there is no transmit in progress */
  4.     while(!enc28j60ReadOp(ENC28J60_READ_CTRL_REG,ECON1)&ECON1_TXRTS)
  5.     {
  6.         /* Reset transmit */
  7.         if(enc28j60ReadByteFromReg(EIR) & EIR_TXERIF)
  8.         {
  9.             enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1,ECON1_TXRST);
  10.             enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1,ECON1_TXRST);
  11.         }
  12.     }
  13.     /* Set write pointer to buffer arena start */
  14.     enc28j60WriteToReg(EWRPT, TXSTART_INIT);
  15.     /* TXND Pointer, write given packet size */
  16.     enc28j60WriteToReg(ETXND, TXSTART_INIT+bufferLength);
  17.     /* Use MACON3 settings */
  18.     enc28j60WriteOp(ENC28J60_WRITE_BUF_MEM, 0, 0x00);
  19.     /* Write data into transmit buffer */
  20.     enc28j60WriteBuffer(bufferLength,buffer);
  21.     /* send buffer into network */
  22.     enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);
  23. }

Funkcje powyżej odpowiadają za komunikacje z układem ENC. Teraz przejdę do opisu funkcji obsługujących połączenia sieciowe oraz ramki danych.

Obliczanie sumy kontrolnej ramki danych wygląda następująco:

  1. uint16_t checksum(uint8_t *dataPointer, uint16_t packetLength, protType_t type)
  2. {
  3.     uint32_t calculateCheckSum=0;
  4.     if(type == Prot_UPD)
  5.     {
  6.         calculateCheckSum += Prot_UPD;
  7.         calculateCheckSum += packetLength - 8;
  8.     }
  9.     while(packetLength > 1)
  10.     {
  11.         calculateCheckSum += (uint16_t) (((uint32_t)*dataPointer<<8)|*(dataPointer+1));
  12.         dataPointer+=2;
  13.         packetLength-=2;
  14.     }
  15.     if(packetLength)
  16.     {
  17.         calculateCheckSum+=((uint32_t)*dataPointer)<<8;
  18.     }
  19.     while(calculateCheckSum>>16)
  20.     {
  21.         calculateCheckSum=(uint16_t)calculateCheckSum+(calculateCheckSum>>16);
  22.     }
  23.     return ~CONVERT_16BIT((uint16_t)calculateCheckSum);
  24. }

Przesłanie otrzymanych danych od układu wygląda następująco:

  1. void ethernetSendData(enc28j60_frame_t *frame, uint16_t len)
  2. {
  3.     memcpy(frame->destMacAddress,frame->sourceMacAddress,6);
  4.     memcpy(frame->sourceMacAddress,macaddr,6);
  5.     enc28j60PacketSend((void*)frame,len+sizeof(enc28j60_frame_t));
  6. }

Najpierw kopiowane są adresy MAC po czym przesyłany jest otrzymany pakiet danych.

Zadaniem kolejnej funkcji jest przesłanie ramki danych IP do układu ENC:

  1. void ipSendData(enc28j60_frame_t *frame, uint16_t length)
  2. {
  3.     ip_frame_t  *ip_frame = (void*)(frame->dataBuffer);
  4.     ip_frame->packetLength=CONVERT_16BIT(length);
  5.     ip_frame->flags=0;
  6.     ip_frame->timeToLive=64;
  7.     ip_frame->controlType=0;
  8.     memcpy(ip_frame->destinIpAdress,ip_frame->sourceIpAdress,4);
  9.     memcpy(ip_frame->sourceIpAdress,ipaddr,4);
  10.     ip_frame->controlType=checksum((void*)ip_frame,sizeof(ip_frame_t),0);
  11.     ethernetSendData(frame, length);
  12.     free(ip_frame);
  13. }

Odczytanie i zdekodowanie pakietu ICMP:

  1. static void ICMPRead(enc28j60_frame_t *frame, uint16_t length)
  2. {
  3.     ip_frame_t *ip_frame = (void*)(frame->dataBuffer);
  4.     icmp_frame_t *icmp_pkt = (void*)(ip_frame->dataBuffer);
  5.     char bufferString[50] = {0};
  6.     if((length>=sizeof(icmp_frame_t)) && (icmp_pkt->type==ICMP_REQ))
  7.     {
  8.         icmp_pkt->type=ICMP_REPLY;
  9.         icmp_pkt->checkSum=0;
  10.         icmp_pkt->checkSum=checksum((void*)icmp_pkt,length,0);
  11.         ipSendData(frame, length+sizeof(ip_frame_t));
  12.         #ifdef DEBUG_NET_FIL
  13.         sprintf(bufferString,"%d.%d.%d.%d-%d.%d.%d.%d icmp request\r\n",
  14.                 ip_frame->destinIpAdress[0],ip_frame->destinIpAdress[1],
  15.                 ip_frame->destinIpAdress[2],ip_frame->destinIpAdress[3],
  16.                 ip_frame->sourceIpAdress[0],ip_frame->sourceIpAdress[1],
  17.                 ip_frame->sourceIpAdress[2],ip_frame->sourceIpAdress[3]);
  18.         HAL_UART_Transmit(&huart2,(uint8_t*)bufferString,strlen(bufferString),0x1000);
  19.         #endif
  20.     }
  21.     free(ip_frame);
  22.     free(icmp_pkt);
  23. }

Główna funkcja wywoływana podczas inicjalizacji ma za zadanie uruchomić układ ENC. W przyszłości w przypadku wykorzystywania innych protokołów sieciowych może ona zawierać dodatkowe implementacje.

  1. void netInitialize(void)
  2. {
  3.     HAL_UART_Transmit(&huart2,(uint8_t*)"Initialize net settings\r\n",8,0x1000);
  4.     enc28j60Initialize();
  5. }

Teraz zaprezentuje bezpośrednią obsługę serwera UDP.

Do obsługi danych od UDP przygotowałem dwie funkcje, pierwsza mniej potrzebna wyświetla ona otrzymane dane oraz informacje o nadawcy itp. Druga przygotowuje ramkę danych do przesłania.

  1. void udpReadRecFrame(enc28j60_frame_t *dataFrame, uint16_t dataLength)
  2. {
  3.     char buffer[60];
  4.     ip_frame_t *ip_frame = (void*)(dataFrame->dataBuffer);
  5.     udp_frame_t *udp_frame = (void*)(ip_frame->dataBuffer);
  6.     #ifdef DEBUG_UDP_FIL
  7.     sprintf(buffer,"Data from device[MAX] %02X:%02X:%02X:%02X:%02X:%02X UDP packet:%d \r\n",
  8.             dataFrame->sourceMacAddress[0],dataFrame->sourceMacAddress[1],dataFrame->sourceMacAddress[2],
  9.             dataFrame->sourceMacAddress[3],dataFrame->sourceMacAddress[4],dataFrame->sourceMacAddress[5],
  10.         dataLength);
  11.     HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen(buffer),0x1000);
  12.     sprintf(buffer,"From device: %d.%d.%d.%d To device: %d.%d.%d.%d udp request\r\n",
  13.             ip_frame->sourceIpAdress[0],ip_frame->sourceIpAdress[1],
  14.             ip_frame->sourceIpAdress[2],ip_frame->sourceIpAdress[3],
  15.             ip_frame->destinIpAdress[0],ip_frame->destinIpAdress[1],
  16.             ip_frame->destinIpAdress[2],ip_frame->destinIpAdress[3]);
  17.     HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen(buffer),0x1000);
  18.     sprintf(buffer,"Send port: %u Receive port: %u\r\n",
  19.             CONVERT_16BIT(udp_frame->sourcePort),
  20.             CONVERT_16BIT(udp_frame->destiPort));
  21.     HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen(buffer),0x1000);
  22.     sprintf(buffer,"Rec data:\r\n");
  23.     HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen(buffer),0x1000);
  24.     #endif
  25.     sprintf(buffer,"%s\r\n", udp_frame->dataBuffer);
  26.     #ifdef DEBUG_UDP_FIL
  27.     HAL_UART_Transmit(&huart2,(uint8_t*)buffer,strlen(buffer),0x1000);
  28.     #endif
  29.     /* Send data back with echo or normal */
  30.     udpReplyToData(dataFrame, dataLength, buffer, 1);
  31.     free(ip_frame);
  32.     free(udp_frame);
  33. }

Funkcja powyżej jest główną wywoływaną poprzez odebranie danych z UDP. Jej głównym zadaniem jest wyświetlenie otrzymanych danych i przekazanie ich do funkcji przygotowującej ramkę do wysłania.

  1. static void udpReplyToData(enc28j60_frame_t *frame, uint16_t len, char *array, uint8_t enableEcho)
  2. {
  3.     uint16_t port=0;
  4.     ip_frame_t *ip_frame = (void*)(frame->dataBuffer);
  5.     udp_frame_t *udp_frame = (void*)(ip_frame->dataBuffer);
  6.     port = udp_frame->destiPort;
  7.     udp_frame->destiPort = udp_frame->sourcePort;
  8.     udp_frame->sourcePort = port;
  9.     udp_frame->packLength = 0;
  10.     if(enableEcho == 0)
  11.     {
  12.         strcpy((char*)udp_frame->dataBuffer,"Dane odebrane przez serwer UDP\r\n");
  13.         len = strlen((char*)udp_frame->dataBuffer) + sizeof(udp_frame_t);
  14.     }
  15.     else
  16.     {
  17.         strcpy((char*)udp_frame->dataBuffer, array);
  18.         len = strlen((char*)udp_frame->dataBuffer) + sizeof(udp_frame_t);
  19.     }
  20.     udp_frame->packLength = CONVERT_16BIT(len);
  21.     udp_frame->contSum=0;
  22.     udp_frame->contSum=checksum((uint8_t*)udp_frame-8, len+8, Prot_UPD);
  23.     ipSendData(frame, len+sizeof(ip_frame_t));
  24.     free(ip_frame);
  25.     free(udp_frame);
  26. }

Funkcja udpReplayToData przygotowuje ramkę danych do wysłania. Jest ona wystawiana gdy przyjdzie wiadomości od klienta UDP.

Otrzymane dane można prześledzić w Terminalu poprzez UART. Dane przesyłane po UDP można podglądnąć np. w WireShark, SocketTest czy Hercules. Poniżej zrzuty ekranów prezentujące wykonane operacje.


Na zrzucie ekranu powyżej widać przesłaną ramkę danych jak i odpowiedź. Wysyłana jest przypadkowa ramka. W odpowiedzi sklejana jest przykładowa ramka odpowiedzi z danymi.


Powyżej widać ramkę danych z danymi z programu Terminal. Widać na nim dokładny proces komunikacji pomiędzy urządzeniami.

Program można pobrać z dysku Google pod tym linkiem. Po jego otwarciu należy wejść do folderu STM32 a następnie do STM32F4_Discovery.

W kolejnej części dodam kilka czujników i będę przesyłał dane między STM-em a komputerem. Klient będzie przesyłał zapytanie do płyty ona odczyta aktualnie zmierzone pomiary i prześle je poprzez UDP do programu prezentującego otrzymane informacje.