Individální projekty MPOA

Mikroprocesory s architekturou ARM

Uživatelské nástroje

Nástroje pro tento web


2015:plc-st7580

Rozbor zadání

Úkolem tohoto projektu je demonstrovat datový tok po silovém vedení a to ve standardní napájecí síti 230 VAC/50 Hz. Zvolený komunikační kmitočet spadá do kategorie úzkopásmových technologií, tedy frekvenční rozsah je 3-500 kHz. Pro realizaci byl vybrán vhodný integrovaný obvod ST7580 v pozici PLC modemu, s programovatelnou nosnou frekvencí do 250 kHz. Obvod mimo jiné odpovídá Evropské normě CENELEC (EN 50065), tedy tento obvod je možné nasadit i do veřejných napájecích sítí. Čip vyžaduje obsluhu externím hostitelem přes rozhraní UART. Do této pozice byl vybrán mikrokontrolér architektury ARM, typu STM32 řady F0. V rámci projektu byl vytvořen obslužný software základních funkcí obvodu ST7580, které umožňují funkční přenos po uvedeném silovém vedení.

Princip řešení

Je vytvořená jednoduchá komunikační síť s dvěma uzly, které tvoří jednotky master a slave, podle obrázku níže. Konkrétní obvodové zapojení jednotky master je tvořeno vývojovým kitem EVALKITST7580-1 jako PLC modem a kitem STM32F0Discovery jako jednotka hostitele. Toto zapojení je totožné i pro slave. Obslužný software je tedy implementován v hostitelských MCU, který realizuje jak komunikaci s modemem, tak komunikaci po elektrickém vedení. Vytvořený software tvoří jednotlivé funkce, které je pak v rámci aplikace možné použít jako knihovní. V textu jsou popsány jen klíčové funkční části, bez nastavení procesoru, periferií nebo některých deklarací a definicí. Zběžně je přiblížen princip komunikace s modemem a vytvořené řešení. Výstupem projektu je řízení aktoru (RGB dioda), který je součást jednotky slave, a výpisu jeho stavu na LCD displej. Ovládání řeší uživatelské tlačítko na jednotce master.


Vytvořený software realizuje tyto funkce:

  • lokální komunikace
  • konfigurace modemu
  • vytvoření korektní zprávy pro PLC modem
  • odeslání dat do elektrické sítě
  • příjem dat z elektrické sítě
  • rozpoznání a zpracování přijatých dat
  • předání potřebných dat externí funkci pro řízení periferií


Lokální komunikace (hostitel – PLC modem)

Lokální komunikaci master zahájí funkcí _ST7580sendCommand, splňující požadavky lokálního rámce PLC modemu. Na straně jednotky slave je to funkce ST7580poll Pro celé řízení systému jsou zásadní příkazové kódy, které definují typ zprávy, např. žádost o konfiguraci modemu, odesílání dat do el. sítě, indikace přijatých zpráv, nebo chybová hlášení.
Pole data určené právě pro přenos uživatelských dat, je implementuje rovněž vytvořený aplikační protokol pro řízení výstupů hostitelského MCU na jednotce slave. Lokální a aplikační rámec je zobrazen na obrázku níže.


Argumenty funkce _ST7580sendCommand jsou právě hodnoty polí lokálního rámce.

static int _ST7580sendCommand(ST7580_STX_Typedef stx,
                             uint8_t len,
                             ST7580_CmdCode_Typedef cmdCode,
                             uint8_t* dataIn,
                             uint8_t  maxOutLen,
                             uint8_t* dataOut,
                             uint8_t* lenOut)
{
    int i;
    uint16_t  checksum = 0;
    uint8_t   ackNack;
    uint8_t   isReady[2] = {0, 0};
    uint8_t   respStx = 0, respLen = 0, respCmd = 0;
    uint16_t  respChecksum = 0;
    uint8_t   dataReqConfig = 0x24;
 
    if(!dataIn) return ST7580_ERR;
    if((!dataOut)&&(maxOutLen!=0)) return ST7580_ERR;
 
    RingBuffer_flush((RingBuffer*)ST7580_RB_U1);
 
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET);
    RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)isReady, 2);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_SET);
 
    if((isReady[0] != ST7580_READY_B0) || (isReady[1] != ST7580_READY_B1))
    {
        goto ERROR;
    }
 
    /* checksum consists of len, CMD, payload */
    for(i = 0; i < len; i++)
    {
        checksum += dataIn[i];
    }
    checksum += cmdCode;
 
    switch(cmdCode)
    {
        default:
            checksum += len;
            HAL_UART_Transmit(&huart1, (uint8_t*)&stx, 1, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)&len, 1, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)&cmdCode, 1, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)dataIn, len, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)&checksum, 2, ST7580_ACK_TIMEOUT);
            break;
        case DL_DataRequest:
            checksum += dataReqConfig;
            len++;
            checksum += len;
            HAL_UART_Transmit(&huart1, (uint8_t*)&stx, 1, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)&len, 1, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)&cmdCode, 1, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)&dataReqConfig, 1, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)dataIn, len-1, ST7580_ACK_TIMEOUT);
            HAL_UART_Transmit(&huart1, (uint8_t*)&checksum, 2, ST7580_ACK_TIMEOUT);
            break;
    }
 
    /* receive the ACK */
            RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&ackNack, 1);
 
    if(ackNack != ST7580_ACK)
    {
             goto ERROR;
    }
 
    /* receive the STX and Lenght */ 
            RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&respStx, 1);
 
    if(respStx != stx)
    {
        goto ERROR;
    }
 
            RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&respLen, 1);
 
    if(respLen != maxOutLen)
    {
        goto ERROR;
    }
 
    if(lenOut != NULL)
    {
        *lenOut = respLen; 
    }
 
 
    /* receive the CMD code */
            RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&respCmd, 1);
 
 
    if(respCmd != (cmdCode+1))
    {
            goto ERROR;
    }
 
    /* receive the payload */
    RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)dataOut, respLen);
    RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&respChecksum, 2);
 
    checksum = 0;
 
    for(i = 0; i < respLen; i++)
    {
            checksum += dataOut[i];
    }
    checksum += respLen;
    checksum += respCmd;
 
    if(checksum == respChecksum)
    {
        /* send the ACK */
        ackNack = ST7580_ACK;
        HAL_UART_Transmit(&huart1, (uint8_t*)&ackNack, 1, ST7580_ACK_TIMEOUT); 
        return ST7580_OK;
    }
ERROR:
 /* send the NACK */
ackNack = ST7580_NACK;
HAL_UART_Transmit(&huart1, (uint8_t*)&ackNack, 1, ST7580_ACK_TIMEOUT);
return ST7580_ERR; 
}

Po skončení komunikace s hostitel-modem, funkce vrací stavy ST7580_OK nebo ST7580_ERR.

Konfigurace modemu

Princip konfigurace modemu je definován několika deklarovanými sktrukturami, které definují potřebnou konfiguraci.

typedef struct
{
#define ST7580_MODEM_CONFIG_BASE (0x00)
#define ST7580_MODEM_CONFIG_LEN  (0x02)
    uint8_t modemConfigIndex;      /*Index of the MIB object (0x00)*/
    uint8_t modemConfig;         /*Actual value of the MIB object*/
}ST7580modemConfigTypedef;
 
typedef struct
{
#define ST7580_PHY_CONFIG_BASE (0x01)
#define ST7580_PHY_CONFIG_LEN  (0x0F)
    uint8_t phyConfigIndex;      /* Index of the MIB object (0x01) */
    uint8_t DlFreqPair[6];       /* Frequency Pair Byte 0 - 5*/
    uint8_t DlRXControl;         /*RX Control Byte 6 */
    uint8_t Gain;                /* Gain Byte 7*/
    uint8_t DlZeroCrossing[2];   /*Zero Crossing Byte 8-9*/ 
    uint8_t D1PSKPreamLen;       /*PSK Preamble Length Byte 10*/
    uint8_t DlFSKConf;           /*FSK Conf Byte 11*/
    uint8_t FSKunicWord1;        /*FSK Unic Word 1*/
    uint8_t FSKunicWord2;        /*FSK Unic Word 2*/
}ST7580phyConfigTypedef;

Po resetu MCU proběhne výchozí konfigurace voláním funkce _ST7580sendInitSequence, v rámci inicializace.

void _ST7580sendInitSequence(void)
{
    ST7580phyConfigTypedef phyCfg = 
    {
        .phyConfigIndex = ST7580_PHY_CONFIG_BASE,
        .DlFreqPair = {0x01, 0x4F, 0xF0, 0x01, 0x19, 0x40},
        .DlRXControl = 0x0E,
        .Gain = 0x15,
        .DlZeroCrossing = {0x00, 0x00},
        .D1PSKPreamLen = 0x02,
        .DlFSKConf = 0x35,
        .FSKunicWord1 = 0x9B,
        .FSKunicWord2 = 0x58,
    };
 
    ST7580phyConfigTypedef rxPhyCfg;
 
    ST7580modemConfigTypedef modemCfg = 
    {
        .modemConfigIndex = ST7580_MODEM_CONFIG_BASE,
        .modemConfig = 0x15,
    };
 
    uint8_t readAddr;
 
    _ST7580sendCommand(STX_local,
                       ST7580_MODEM_CONFIG_LEN,
                       MIB_WriteRequest,
                       (uint8_t*)&modemCfg,
                       0,
                       NULL,
                       NULL);
 
    _ST7580sendCommand(STX_local,
                       ST7580_PHY_CONFIG_LEN,
                       MIB_WriteRequest,
                       (uint8_t*)&phyCfg,
                       0,
                       NULL,
                       NULL);
 
    readAddr = ST7580_PHY_CONFIG_BASE;
    _ST7580sendCommand(STX_local,
                       1,
                       MIB_ReadRequest,
                       &readAddr,
                       ST7580_PHY_CONFIG_LEN-1,                       
                       &rxPhyCfg.DlFreqPair[0], 
                       NULL);
    rxPhyCfg.phyConfigIndex = ST7580_PHY_CONFIG_BASE;
}

Tímto proběhne kompletní nastavení PLC, např. výběr linkové komunikační vrstvy, možnosti výpočtu CRC a další. Pro přehlednost jsou deklarovány výčtové typy enum, definující všechny možné příkazové kódy pro lokální komunikaci. Stejně jsou pak definovány pomocné stavy a další prvky nutné pro komunikaci hostitel-modem. Příklad níže.

/* STX */
typedef enum
{
    STX_local          = 0x02,
    STX_retransmit     = 0x03,
}ST7580_STX_Typedef; 
 
typedef enum
{
    /*** Request commands ***/
    BIO_ResetRequest    = 0x3C,
    MIB_WriteRequest    = 0x08,
    MIB_ReadRequest     = 0x0C,
    MIB_EraseRequest    = 0x10,
    PingRequest         = 0x2C,
    PHY_DataRequest     = 0x24,
    DL_DataRequest      = 0x50,
    SS_DataRequest      = 0x54,
 
    /*** Confirm commands ***/
    BIO_ResetConfirm    = 0x3D, 
    MIB_WriteConfirm    = 0x09,
    MIB_ReadConfirm     = 0x0D,
    MIB_EraseConfirm    = 0x11,
    PingConfirm         = 0x2D,
    PHY_DataConfirm     = 0x25,
    DL_DataConfirm      = 0x51,
    SS_DataConfirm      = 0x55,
 
    /*** Error commands ***/
    BIO_ResetError      = 0x3F,
    MIB_WriteError      = 0x0B,
    MIB_ReadError       = 0x0F,
    MIB_EraseError      = 0x13,         
    PHY_DataError       = 0x27,
    DL_DataError        = 0x53,
    SS_DataError        = 0x57,
    CMD_SyntaxError     = 0x36,
 
    /*** Indication commands ***/
    BIO_ResetIndication = 0x3E,
    PHY_DataIndication  = 0x26,
    DL_DataIndication   = 0x52,
    DL_SnifferIndication= 0x5A,
    SS_DataIndication   = 0x56,
    SS_SnifferIndication= 0x5E,
 
    CMD_END = 0xFF,
}ST7580_CmdCode_Typedef;



Odeslání dat do elektrické sítě masterem

Pro vysílání dat do elektrické sítě (na linkové vrstvě) je nutné zaslat žádost použitím příkazového kódu DL_DataRequest. K tomu je určená funkce ST7580sendPayload, která má jako argumenty užitečná dat (aplikační rámec) a jeho délku.

int ST7580sendPayload(uint8_t* payload, uint8_t len)
{
    uint8_t temperatures[5];
    return _ST7580sendCommand(STX_local,
                              len,
                              DL_DataRequest,
                              payload,
                              5,
                              temperatures,
                              NULL);
}

Tímto se provede další lokální komunikace s žádostí o vysílání dat to elektrické sítě. Po úspěšném zpracování, se vytvoří datový rámec (v tomto případě na linkové vrstvě), který se vyšle do sítě. Tuto proceduru samostatně obstarává PLC modem. Data jsou přenášena na nosném kmitočtu 148 kHz s modulací BPSK.

Příjem dat z elektrické sítě slavem

Pokud má jednotka slave nakonfigurované klíčové parametry totožně jako master, např. nosná frekvence a další, slave příjme a zpracuje datový rámec do podoby lokálního rámce, který je odeslán modemem k hostiteli. Lokání rámec je definován příkazovým kódem DL_DataIndication, který v poli data obsahuje poslední přenesená data. Lokální rámec se pak člení s pomocí kruhového bufferu. Pro příjem a rozpoznání dat slouží kontinuální volání funkce ST7580poll.

int ST7580poll(void)
{
    int i;
    uint8_t   ackNack;
    uint8_t   stx = 0, len = 0, cmd = 0;
    uint16_t  checksum = 0, calcChecksum = 0;
    uint8_t	payload[256];
 
            while(!RingBuffer_empty((RingBuffer*)ST7580_RB_U1))
            {
            	/* receive the STX and Lenght */ 
            	RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&stx, 1);
 
             if(stx != STX_local)
            {
            	RingBuffer_flush((RingBuffer*)ST7580_RB_U1);
            	return ST7580_ERR;
            }
 
    RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&len, 1);  
 
    /* receive the CMD code */
    RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&cmd, 1);
 
 
    /* receive the payload and checksum */
    RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)payload, len);
    RingBufferReceive((RingBuffer*)ST7580_RB_U1, (uint8_t*)&checksum, 2);
 
    calcChecksum = 0;
 
    for(i = 0; i < len; i++)
    {
            calcChecksum += payload[i];
    }
    calcChecksum += len;
    calcChecksum += cmd;
 
	if(checksum == calcChecksum)
	{
            	/* send the ACK */
            	ackNack = ST7580_ACK;
            	HAL_UART_Transmit(&huart1, (uint8_t*)&ackNack, 1, ST7580_ACK_TIMEOUT); 
            	RingBuffer_flush((RingBuffer*)ST7580_RB_U1);
 
            	for(i = 0; ST7580rxTable[i].code != CMD_END; i++)
            	{
            	   	if(cmd == ST7580rxTable[i].code)
            	   	{
            	   	   	if(ST7580rxTable[i].pfunc != NULL)
            	   	   	ST7580rxTable[i].pfunc(payload, len);
            	   	   	break;
            	   	}
            	}
	}
		else
		{
			 /* send the NACK */
			ackNack = ST7580_NACK;
			HAL_UART_Transmit(&huart1, (uint8_t*)&ackNack, 1, ST7580_ACK_TIMEOUT);
			RingBuffer_flush((RingBuffer*)ST7580_RB_U1);
			return ST7580_ERR; 
		}
	}
    return ST7580_OK;
}

Tímto jsou přijatá data zpracována a také je identifikován typ zprávy. Jedná se tedy o přijata data s příkazovým kódem DL_DataIndication. Dále je zjištěna existence funkce pro tento příkazový kód. V tomto případě se jedná o funkci DL_DataIndicationFunc.

void DL_DataIndicationFunc(uint8_t* payload, uint8_t len)
{
    AppCmdParser(payload + 4, len - 4); 
}

Jedná se pouze o předání osamostatněného pole payload (aplikační rámec) ke zpracování požadovaných funkcí aplikačním protokolem a tvoří pak s délkou zprávy argument funkce AppCmdParser. Zároveň je určen počátek zprávy, tedy pole payload je indexováno od 0.

Funkce je koncipována podobně jako funkce ST7580poll, ale je přizpůsobena pro strukturu aplikačního rámce. V rámci zpracování se určí, jestli je zpráva určená pro daný slave. Dále se ověří hodnota adresy registru, která tvoří identifikátor povelu v poli struktur, které obsahují ukazatele na možné funkce dané jednotky slave. Požadované funkci (povelu) je pak argument obsah pole AppData (níže uveden příklad funkce AppLedStateChanged).

void AppCmdParser(uint8_t* payload, uint8_t len)
{
    uint8_t i, j, chs = 0, temp;
    int remaining;
    if((APP_EXTRACT_ADDRESS(payload[0]) == AppCmdSlaveAddress) ||
        (APP_EXTRACT_ADDRESS(payload[0]) == APP_ADDRESS_BROADCAST))
    {
        /* calculate the checksum */
        for(i = 0; i < (len-1); i++)
        {
            chs += payload[i];
        }
 
        if(chs == payload[len-1])
        {
            switch(APP_EXTRACT_REQUEST(payload[0])) // masked FF to 0x80
            {
                case APP_READ_REQUEST:
                for(i = payload[1], remaining = payload[2], j = 0; sensitivityTable[j].addr != 0xFF; j++)
                {
                    if(sensitivityTable[j].addr == i)
                    {
                        if(sensitivityTable[j].pfuncRead != NULL)
                        {
                            temp = sensitivityTable[j].pfuncRead(i, remaining);
                            if(!temp) temp = 1;
                            i += temp;
                            remaining -= temp;
                        }
                        if(!remaining) break;
                    }   
                }           
                break;
 
                case APP_WRITE_REQUEST:
                memcpy(AppCmdRegisters + payload[1], &payload[2], len-3);
                for(i = payload[1], remaining = len-3, j = 0; sensitivityTable[j].addr != 0xFF; j++)
                {
                    if(sensitivityTable[j].addr == i)
                    {
                        if(sensitivityTable[j].pfuncWrite != NULL)
                        {
                            temp = sensitivityTable[j].pfuncWrite(AppCmdRegisters + i, remaining);
                            if(!temp) temp = 1;
                            i += temp;
                            remaining -= temp;
                        }
                        if(!remaining) break;
                    }       
                } 
                break;
            }
        }
    }
}

Příklad možností povelu dané jednotky slave:

AppCmdParserRecordTypedef sensitivityTable[] = 
{
/* {cmd identifier, pointer to a write function, pointer to a read function} */
    {APP_ADDR_LED, AppLedStateChanged, AppLedReadRequested},
    {0xFF, NULL, NULL},
};



Předání přijatých dat externí funkci pro řízení periferií

Funkce vykonaná po přijetí ukázkové zprávy pro demonstraci komunikace po napájecím vedení:

uint8_t AppLedStateChanged(void* newValue, uint8_t remLen)
{
   uint8_t val = *((uint8_t*)newValue);
 
   HAL_GPIO_WritePin(GPIOC,GPIO_PIN_9, val);	
   HAL_GPIO_WritePin(GPIOC,GPIO_PIN_6, !val);	
   HAL_GPIO_WritePin(GPIOC,GPIO_PIN_7, !val);	
 
   if (val == 1)
   {
	LCD_Clear();
	LCD_Puts(0,0,(uint8_t*)"Stav aktoru:");
	LCD_Puts(0,1,(uint8_t*)"ZAPNUTO");
   }
   if (val == 0)
   {
	LCD_Clear();
	LCD_Puts(0,0,(uint8_t*)"Stav aktoru:");
	LCD_Puts(0,1,(uint8_t*)"VYPNUTO");		
   }
   if ((val != 0) && (val != 1))
   {
	LCD_Clear();
	LCD_Puts(0,0,(uint8_t*)"Stav aktoru:");
	LCD_Puts(0,1,(uint8_t*)"WRONG DATA");
	return 0;		
   }
 
   return 1;
}

Ukázková aplikace odesílání dat masterem v nekonečné smyčce hlavního souboru.

  while (1)
  {
	ST7580poll();
	//ST7580getFirmwareVersion();
 
      if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0))
      {
        request[2] = !request[2];
        request[3] = 0xFF & (request[0] + request[1] + request[2]);
        ST7580sendPayload(request, 4);
        HAL_Delay(600);
      }
}

Obsah hlavní smyčky jednotky slave tvoří pouze funkce ST7580poll, která plně obsluhuje lokální komunikaci.


Logická analýza lokální komunikace

Uvedené průběhy na sebe horizontálně navazují, první na druhý. Zachycují lokální komunikaci jednotek master (1-PLC) a slave (2-PLC). Jedná se o žádost mastera k odeslání zprávy do elektrické sítě, úspěšné provedení, a příjem lokálního rámce obsahující přenášená data pro daný slave.




Videoukázka

V následujícím videu je zachyceno řízení výstupního členu jednotky slave, uživatelským tlačítkem na Discovery kitu jednotky master.



Závěr

Pro vývoj tohoto software bylo použito aplikace STM32CubeMX v kooperaci s HAL knihovnami ve free verzi vývojového prostředí MDK. Navržený software implementovaný v hostitelském MCU STM32F0 pro obsluhu PLC modemu ST7580, demonstruje funkční úzkopásmovou komunikaci po napájecím vedení. Pro ukázku je zvolen k jednotce slave aktor, v podobě RGB diody a LCD displeje, který vypisuje stav. Podle dosažených výsledku, je zadání projektu splněno.

autor — Ján Sláčik 2016/01/16 23:01

2015/plc-st7580.txt · Poslední úprava: 2016/01/20 16:52 autor: Ján Sláčik