piątek, 26 lipca 2019

[3] STM32 - Biblioteka LL - USART

W tym poście chciałbym opisać obsługę UART za pomocą bibliotek LL.

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



Pominę tutaj opis uruchamiania UART'u za pomocą CubeMx ponieważ te operacje są dosyć proste i zostały już opisane w innych postach na tym blogu.

Na sam początek funkcja wysyłająca dane.

  1. #define CHECK_IF_DATA_SEND_UART_FINISHED(UART) LL_USART_IsActiveFlag_TXE(UART)
  2. void UsartSendData(USART_TypeDef* UART_PORT, uint8_t* framePtr, uint16_t frameSize)
  3. {
  4.   uint16_t dataCounter = 0;
  5.   while (dataCounter<frameSize)
  6.   {
  7.     while (!CHECK_IF_DATA_SEND_UART_FINISHED(UART_PORT)) { }
  8.     LL_USART_TransmitData8(UART_PORT,*(uint8_t*)(framePtr + (dataCounter++)));
  9.   }
  10. }

Wysyłanie danych jest dosyć proste najpierw należy sprawdzić czy zakończono przesyłanie poprzednich danych. Jeśli tak to nadajemy dane ponownie.

Funkcja przesyłająca dane czyli LL_USART_TransmitData8 wpisuje dane (jeden bajt) do odpowiedniego rejestru:

  1. __STATIC_INLINE void LL_USART_TransmitData8(USART_TypeDef *USARTx, uint8_t Value)
  2. {
  3.   USARTx->DR = Value;
  4. }

Funkcja odbierająca pojedyncze dane i zwracająca ją do obsługi. Pierwszy najprostszy przykład jest funkcją blokującą:

  1. uint8_t UartRecSingleChar(USART_TypeDef* UART_PORT)
  2. {
  3.   uint8_t recData = 0;
  4.   while (!CHECK_IF_DATA_RECEIVE(USART1)) {}
  5.   recData = (uint8_t)(USART1->DR & 0x00FF);
  6.   return recData;
  7. }

Jest ona średnio efektywna ponieważ czeka w pętli while aż uda się otrzymać dane i dopiero wtedy przechodzi dalej. Można do niej dołożyć opóźnienie do pętli tak aby po określonym czasie np. 20ms przechodziła do obsługi pozostałej części programu.

Lepszym rozwiązaniem jest zastosowanie licznika tak aby gdy po zadanym czasie nie zostanie otrzymana wiadomość to nastąpi wyjście z funkcji w celu wykonania innych operacji.

  1. uint16_t UartRecWholeBuffer(USART_TypeDef* UART_PORT, uint8_t* framePtr,
  2.                             uint16_t frameSize)
  3. {
  4.     uint16_t dataLoop = 0;
  5.     uint16_t receiveDataCounter = 0;
  6.     while(1)
  7.     {
  8.         volatile uint32_t timeout = 0;
  9.         while (!CHECK_IF_DATA_RECEIVE(UART_PORT)) {
  10.             timeout++;
  11.             if(timeout == REC_TIMEOUT) { break; }
  12.         }
  13.         if(timeout == REC_TIMEOUT) { break; }
  14.         else
  15.         {
  16.             *(framePtr + dataLoop) = (uint8_t)(UART_PORT->DR & 0x00FF);
  17.             dataLoop++;
  18.             receiveDataCounter++;
  19.             if(receiveDataCounter == frameSize)
  20.             {
  21.                 dataLoop = 0;
  22.             }
  23.         }
  24.     }
  25.     return receiveDataCounter;
  26. }

Po przekroczeniu czasu oczekiwania na zakończenie transmisji następuje wyjście z funkcji i zwrócenie informacji o ilości otrzymanych danych. W przypadku odbioru danych powyżej maksymalnego rozmiaru bufora następuje nadpisywanie otrzymanych danych.

Można tą funkcję rozbudować przez dodanie np. że odebranie pustego elementu, czyli wartości 0x00, powoduje wyjście z funkcji lub innej dowolnej wartości..

  1. uint16_t UartRecWholeBufferWithSpecValueAtBufEnd(USART_TypeDef* UART_PORT, uint8_t* framePtr,
  2.                             uint16_t frameSize)
  3. {
  4.     uint16_t dataLoop = 0;
  5.     uint16_t receiveDataCounter = 0;
  6.     while(1)
  7.     {
  8.         volatile uint32_t timeout = 0;
  9.         while (!CHECK_IF_DATA_RECEIVE(UART_PORT)) {
  10.             timeout++;
  11.             if(timeout == REC_TIMEOUT) { break; }
  12.         }
  13.         if(timeout == REC_TIMEOUT) { break; }
  14.         else
  15.         {
  16.             *(framePtr + dataLoop) = (uint8_t)(UART_PORT->DR & 0x00FF);
  17.             if(*(framePtr + dataLoop) == 0x00)
  18.             {
  19.                 break;
  20.             }
  21.             dataLoop++;
  22.             receiveDataCounter++;
  23.             if(receiveDataCounter == frameSize)
  24.             {
  25.                 dataLoop = 0;
  26.             }
  27.         }
  28.     }
  29.     return receiveDataCounter;
  30. }

Innym sposobem do przygotowania jest odbiór danych którego jest znany rozmiar np. podawany w transmisji danych:

  1. uint16_t UartRecWholeBufferWithSpecFrameSize(USART_TypeDef* UART_PORT, uint8_t* framePtr,
  2.                             uint16_t frameSize)
  3. {
  4.     uint16_t dataLoop = 0;
  5.     uint16_t receiveDataCounter = 0;
  6.     uint16_t expectedFrameSize = 0;
  7.     while(1)
  8.     {
  9.         volatile uint32_t timeout = 0;
  10.         while (!CHECK_IF_DATA_RECEIVE(UART_PORT)) {
  11.             timeout++;
  12.             if(timeout == REC_TIMEOUT) { break; }
  13.         }
  14.         if(timeout == REC_TIMEOUT) { break; }
  15.         else
  16.         {
  17.             *(framePtr + dataLoop) = (uint8_t)(UART_PORT->DR & 0x00FF);
  18.             if((dataLoop) == 1)
  19.             {
  20.                 expectedFrameSize = *(framePtr + dataLoop);
  21.             }
  22.             dataLoop++;
  23.             receiveDataCounter++;
  24.             if(receiveDataCounter == frameSize)
  25.             {
  26.                 dataLoop = 0;
  27.             }
  28.             else if(expectedFrameSize == receiveDataCounter)
  29.             {
  30.                 break;
  31.             }
  32.         }
  33.     }
  34.     return receiveDataCounter;
  35. }

W funkcji powyżej założyłem, że długość ramki danych jest przechowywana na drugiej pozycji.

Teraz odbieranie danych za pomocą przerwań. Na samym początku trzeba uruchomić przerwania, po ustawieniu parametrów NVIC przez CubeMx należy jeszcze włączyć przerwania od RX oraz od ewentualnych błędów transmisji.

  1. void UartEnableInterrupt(USART_TypeDef* UART_PORT)
  2. {
  3.     LL_USART_EnableIT_RXNE(UART_PORT);
  4.     LL_USART_EnableIT_ERROR(UART_PORT);
  5. }

Odbieranie danych wygląda następująco:

  1. static void USART_RX_Receive(USART_TypeDef* UART_PORT, uint8_t *rxBufPtr)
  2. {
  3.     *(rxBufPtr + ReceiveBufferPosition) = LL_USART_ReceiveData8(UART_PORT);
  4.     ReceiveBufferPosition++;
  5.     if(ReceiveBufferPosition == (REC_BUFFER_SIZE-1))
  6.     {
  7.         ReceiveBufferPosition=0;
  8.     }
  9. }

W funkcji powyżej dane są odbierane i dodawane do bufora.

Funkcja obsługi przerwania wygląda następująco:

  1. void USART1_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN USART1_IRQn 0 */
  4.   if(LL_USART_IsActiveFlag_RXNE(USART1) && LL_USART_IsEnabledIT_RXNE(USART1))
  5.   {
  6.       USART_RX_Receive(USART1, &receiveBuffer[0]);
  7.   }
  8.   else
  9.   {
  10.     /* Check if ERROR flag occure */
  11.     if(LL_USART_IsActiveFlag_ORE(USART1)) {
  12.         errorCounter++;
  13.     }
  14.     else if(LL_USART_IsActiveFlag_FE(USART1)) {
  15.         errorCounter++;
  16.     }
  17.     else if(LL_USART_IsActiveFlag_NE(USART1)) {
  18.         errorCounter++;
  19.     }
  20.   }
  21. }

Teraz przejdę do opisu obsługi DMA dla UARTU. Przykład przesyłał odebrane dane z powrotem do UART'u.

Na samym początku konfiguracja DMA dla linii TX oraz RX w programie CubeMx:


Dodatkowo każdy z kanałów ma uruchomione przerwania. Powinny one zostać automatycznie włączone w zakładce NVIC Settings. Przerwania od UART'u muszą zostać wyłączone:


Po wygenerowaniu projektu inicjalizacja UART1 będzie wyglądać następująco:

  1. static void MX_USART1_UART_Init(void)
  2. {
  3.  
  4.   /* USER CODE BEGIN USART1_Init 0 */
  5.  
  6.   /* USER CODE END USART1_Init 0 */
  7.  
  8.   LL_USART_InitTypeDef USART_InitStruct = {0};
  9.  
  10.   LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
  11.  
  12.   /* Peripheral clock enable */
  13.   LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_USART1);
  14.  
  15.   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOB);
  16.   /**USART1 GPIO Configuration  
  17.   PB6   ------> USART1_TX
  18.   PB7   ------> USART1_RX
  19.   */
  20.   GPIO_InitStruct.Pin = LL_GPIO_PIN_6|LL_GPIO_PIN_7;
  21.   GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  22.   GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
  23.   GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  24.   GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
  25.   GPIO_InitStruct.Alternate = LL_GPIO_AF_7;
  26.   LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  27.  
  28.   /* USART1 DMA Init */
  29.  
  30.   /* USART1_RX Init */
  31.   LL_DMA_SetChannelSelection(DMA2, LL_DMA_STREAM_2, LL_DMA_CHANNEL_4);
  32.  
  33.   LL_DMA_SetDataTransferDirection(DMA2, LL_DMA_STREAM_2, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
  34.  
  35.   LL_DMA_SetStreamPriorityLevel(DMA2, LL_DMA_STREAM_2, LL_DMA_PRIORITY_LOW);
  36.  
  37.   LL_DMA_SetMode(DMA2, LL_DMA_STREAM_2, LL_DMA_MODE_NORMAL);
  38.  
  39.   LL_DMA_SetPeriphIncMode(DMA2, LL_DMA_STREAM_2, LL_DMA_PERIPH_NOINCREMENT);
  40.  
  41.   LL_DMA_SetMemoryIncMode(DMA2, LL_DMA_STREAM_2, LL_DMA_MEMORY_INCREMENT);
  42.  
  43.   LL_DMA_SetPeriphSize(DMA2, LL_DMA_STREAM_2, LL_DMA_PDATAALIGN_BYTE);
  44.  
  45.   LL_DMA_SetMemorySize(DMA2, LL_DMA_STREAM_2, LL_DMA_MDATAALIGN_BYTE);
  46.  
  47.   LL_DMA_DisableFifoMode(DMA2, LL_DMA_STREAM_2);
  48.  
  49.   /* USART1_TX Init */
  50.   LL_DMA_SetChannelSelection(DMA2, LL_DMA_STREAM_7, LL_DMA_CHANNEL_4);
  51.  
  52.   LL_DMA_SetDataTransferDirection(DMA2, LL_DMA_STREAM_7, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
  53.  
  54.   LL_DMA_SetStreamPriorityLevel(DMA2, LL_DMA_STREAM_7, LL_DMA_PRIORITY_LOW);
  55.  
  56.   LL_DMA_SetMode(DMA2, LL_DMA_STREAM_7, LL_DMA_MODE_NORMAL);
  57.  
  58.   LL_DMA_SetPeriphIncMode(DMA2, LL_DMA_STREAM_7, LL_DMA_PERIPH_NOINCREMENT);
  59.  
  60.   LL_DMA_SetMemoryIncMode(DMA2, LL_DMA_STREAM_7, LL_DMA_MEMORY_INCREMENT);
  61.  
  62.   LL_DMA_SetPeriphSize(DMA2, LL_DMA_STREAM_7, LL_DMA_PDATAALIGN_BYTE);
  63.  
  64.   LL_DMA_SetMemorySize(DMA2, LL_DMA_STREAM_7, LL_DMA_MDATAALIGN_BYTE);
  65.  
  66.   LL_DMA_DisableFifoMode(DMA2, LL_DMA_STREAM_7);
  67.  
  68.   /* USER CODE BEGIN USART1_Init 1 */
  69.  
  70.   /* USER CODE END USART1_Init 1 */
  71.   USART_InitStruct.BaudRate = 115200;
  72.   USART_InitStruct.DataWidth = LL_USART_DATAWIDTH_8B;
  73.   USART_InitStruct.StopBits = LL_USART_STOPBITS_1;
  74.   USART_InitStruct.Parity = LL_USART_PARITY_NONE;
  75.   USART_InitStruct.TransferDirection = LL_USART_DIRECTION_TX_RX;
  76.   USART_InitStruct.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
  77.   USART_InitStruct.OverSampling = LL_USART_OVERSAMPLING_16;
  78.   LL_USART_Init(USART1, &USART_InitStruct);
  79.   LL_USART_ConfigAsyncMode(USART1);
  80.   LL_USART_Enable(USART1);
  81.   /* USER CODE BEGIN USART1_Init 2 */
  82.  
  83.   /* USER CODE END USART1_Init 2 */
  84. }

Teraz zdefiniuje strukturę zawierającą buffor nadawania i odbierania, flagi pomocnicze itp.

  1. typedef struct DMA_UART_SendReceiveData{
  2.     uint8_t sendReceiveBuffer[200];
  3.     uint8_t bufferPosition;
  4.     uint8_t receiveDMA_ControlFlow_Flag;
  5.     uint32_t dmaReceiveData;
  6.     uint8_t transmitComplete_Flag;
  7. }DMA_UART_TypeDef;

W celu uruchomienia DMA należy skonfigurować kilka parametrów.

Ważnym elementem jest konieczność wyłączenia DMA przed przystąpieniem do konfiguracji.

  1.   LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_2);
  2.   LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_7);
  3.   LL_DMA_ClearFlag_TC2(DMA2);
  4.   LL_DMA_ClearFlag_TE2(DMA2);
  5.   LL_DMA_ClearFlag_TC7(DMA2);
  6.   LL_DMA_ClearFlag_TE7(DMA2);
  7.   LL_USART_EnableDMAReq_RX(USART1);
  8.   LL_USART_EnableDMAReq_TX(USART1);
  9.   LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_2);
  10.   LL_DMA_EnableIT_TE(DMA2, LL_DMA_STREAM_2);
  11.   LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_7);
  12.   LL_DMA_EnableIT_TE(DMA2, LL_DMA_STREAM_7);
  13.   LL_DMA_ClearFlag_TC2(DMA2);
  14.   LL_DMA_ClearFlag_TE2(DMA2);
  15.   LL_DMA_ClearFlag_TC7(DMA2);
  16.   LL_DMA_ClearFlag_TE7(DMA2);
  17.   LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_2,
  18.                           LL_USART_DMA_GetRegAddr(USART1),
  19.                           (uint32_t)&DMAUart_Struct.dmaReceiveData,
  20.                           LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_2));
  21.   LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_7,
  22.           (uint32_t)&DMAUart_Struct.dmaReceiveData,
  23.           LL_USART_DMA_GetRegAddr(USART1),
  24.           LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_7));

Tutaj uruchamiane są przerwania od zakończenia transmisji danych lub zakończenia ich odbierania, oraz flaga informująca o błędach w transmisji. Następnie konfigurowany jest adres tak aby można było przesyłać dane z podanego miejsca w pamięci do UART'u bądź odwrotnie. Zależy to kierunku transmisji danych.

Jako miejsce zapisu podałem pojedynczą zmienną zamiast tablicy ponieważ dane będę odbierał pojedynczo. Można skonfigurować DMA dla większych wartości, jednak tutaj małą niedogodnością jest to, że przy odbieraniu danych DMA wygeneruje przerwanie dopiero po odbiorze zadanej ilości znaków. Co nie koniecznie musi być problematyczne, ponieważ dane będą zapisywane do wybranego bufora automatycznie po ich odebraniu. Co oznacza, że nie trzeba czekać na wygenerowanie przerwania aby można było przystąpić do obsługi krótszej ramki danych. 

Np. jeśli chce się skonfigurować dane nadawane bądź odbierane na dwóch buforach to wyglądało by to następująco:

  1. //GLobaalne zmienne:
  2. uint8_t buff1[200] = {0x00};
  3. uint8_t buff2[254] = {0x00};
  4.  
  5.   LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_2,
  6.                           LL_USART_DMA_GetRegAddr(USART1),
  7.                           (uint32_t)&buff1,
  8.                           LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_2));
  9.   LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_7,
  10.           (uint32_t)&buff2,
  11.           LL_USART_DMA_GetRegAddr(USART1),
  12.           LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_7));

Funkcje odpowiedzialne za uruchomienie transmisji danych bądź ich odbieranie:

  1. void Usart_DMA_Transmit(uint16_t sendDataLength)
  2. {
  3.   LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_7);
  4.   LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_7, sendDataLength);
  5.   LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_7);
  6. }
  7.  
  8. void UART_DMA_EnableReceive(uint16_t dataLength)
  9. {
  10.     LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_2);
  11.     LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_2, dataLength);
  12.     LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_2);
  13.     DMAUart_Struct.receiveDMA_ControlFlow_Flag=1;
  14. }

W powyższych funkcjach należy wyłączyć dany strumień, ustawić długość danych do przesłania po czym ponownie go uruchomić. Jako argument podawana jest tutaj długość danych po jakim DMA zakończy działanie. Przy ponownym uruchomieniu funkcji dane zapisane/przesyłane będą od pierwszego miejsca w pamięci.

Teraz opisuję przykład Echo który odbiera i odrazu odsyła odebrane dane. W związku z tym przejdę teraz do obsługi przerwań.

  1. void DMA2_Stream2_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN DMA2_Stream2_IRQn 0 */
  4.       if(LL_DMA_IsActiveFlag_TC2(DMA2))
  5.       {
  6.         LL_DMA_ClearFlag_TC2(DMA2);
  7.         DMA2_RecieveComplete();
  8.       }
  9.       else if(LL_DMA_IsActiveFlag_TE2(DMA2))
  10.       {
  11.           LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_2);
  12.           LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_7);
  13.       }
  14.   /* USER CODE END DMA2_Stream2_IRQn 0 */
  15.   /* USER CODE BEGIN DMA2_Stream2_IRQn 1 */
  16.   /* USER CODE END DMA2_Stream2_IRQn 1 */
  17. }
  18.  
  19. /**
  20.   * @brief This function handles DMA2 stream7 global interrupt.
  21.   */
  22. void DMA2_Stream7_IRQHandler(void)
  23. {
  24.   /* USER CODE BEGIN DMA2_Stream7_IRQn 0 */
  25.   if(LL_DMA_IsActiveFlag_TC7(DMA2))
  26.   {
  27.     LL_DMA_ClearFlag_TC7(DMA2);
  28.     DMA2_TransmitComplete();
  29.   }
  30.   else if(LL_DMA_IsActiveFlag_TE7(DMA2))
  31.   {
  32.     LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_2);
  33.     LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_7);
  34.   }
  35.   /* USER CODE END DMA2_Stream7_IRQn 0 */
  36.   /* USER CODE BEGIN DMA2_Stream7_IRQn 1 */
  37.   /* USER CODE END DMA2_Stream7_IRQn 1 */
  38. }

Jak widać powyżej czekam tutaj na wygenerowanie informacji o zakończeniu transmisji/nadawania danych.

Funkcja wywoływana w przerwaniu odpowiedzialna za przepisanie danych do buffora, oraz uruchomienie nadawania danych wygląda następująco:

  1. void DMA2_RecieveComplete(void)
  2. {
  3.     DMAUart_Struct.receiveDMA_ControlFlow_Flag = 2;
  4.  
  5.     DMAUart_Struct.sendReceiveBuffer[DMAUart_Struct.bufferPosition] = (uint8_t)DMAUart_Struct.dmaReceiveData;
  6.     DMAUart_Struct.bufferPosition++;
  7.  
  8.     if(DMAUart_Struct.bufferPosition == 200)
  9.     {
  10.         DMAUart_Struct.bufferPosition = 0;
  11.     }
  12.  
  13.     Usart_DMA_Receive(1);
  14. }

Po przekroczeniu rozmiaru buffora dane są ponownie zapisywane do niego od pozycji 0.

W przypadku konieczności przesłania większej ilości danych wystarczy zmienić adres z którego DMA pobiera dane. Bądź jak już wykorzystywany jest większy bufor to wprowadzić do niego nowe wartości.

Poniżej przykładowa zmiana miejsca w pamięci do pobierania danych dla nadawania:

  1. LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_7);
  2. LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_7,
  3.           (uint32_t)&testTxBuff,
  4.           LL_USART_DMA_GetRegAddr(USART1),
  5.           LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_7));
  6. USART_Transmit_WaitUntillFinished(sizeof(testTxBuff) - 1);
  7.  
  8. void USART_Transmit_WaitUntillFinished(uint16_t transmitDataLength)
  9. {
  10.   LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_7);
  11.   LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_7, transmitDataLength);
  12.   LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_7);
  13.   while (!DMAUart_Struct.transmitComplete_Flag) {}
  14.   DMAUart_Struct.transmitComplete_Flag=0;
  15. }

W funkcji przesyłającej dane czekam na zakończenie nadawania danych (czyli ustawienie flagi w przerwaniu). Spowodowane jest to tym, aby pozwolić na przesłanie danych by zbyt szybko nie zmienić ponownie miejsca w pamięci na początkową wartość. 

Cały projekt można pobrać z dysku Google pod tym linkiem.