czwartek, 23 sierpnia 2018

[1] Embedded C - Google Unit test - Biblioteka Nrf24l01+

W tym poście chciałbym opisać sposób wykonania testów jednostkowych do niektórych funkcji z biblioteki do układu Nrf24l01 przygotowanej pod STM32F4.

[Źródło: http://academy.trendonix.com]


Program:


Do testów wykorzystam Google Test oraz program Visual Studio.

W tym przypadku do wyboru mamy trzy rozwiązania, aby projekt się kompilował bez zbytniego kombinowania, należy umieści albo definicję, która uruchomi opcję testów jednostkowych, bądź kopiowanie poszczególnych funkcji wraz z modyfikacją pod przechodzenie testów.

Pierwszy sposób przez wprowadzanie definicji zwiększa objętość kodu co może powodować zaciemnienie całego obrazu. Dodatkowo gdy chce się wykorzystywać np. UART do debugowania w czasie rzeczywistym to ilość dodatkowych linii w kodzie zwiększa się dosyć znacząco.

Drugi sposób natomiast wymaga dosyć dużego samozaparcia aby testowane funkcje odpowiadały tym znajdującym się w projekcie głównym. Co oznacza, że jak wprowadzamy zmianę w kodzie znajdującym się w projekcie głównym to musimy pamiętać o dodatkowym wprowadzeniu zmiany w projekcie testowy.

Trzecią opcją jest wykorzystywanie nowych definicji dla rejestrów itp. Dzięki temu w dodatkowym projekcie testowym podmieniamy potrzebne definicje na własne, co pozwala nam łatwo sprawdzić czy odpowiednie bity zostały tam ustawione. Co też oznacza, że jest dosyć sporo elementów które będzie trzeba odznaczyć na czas testowania.

Ostatnia wspomniana opcja działa bardzo dobrze z testami CppUTest, które dodaje się jako osobny projekt w Eclipsie. Następnie wystarczy dodać ścieżki do pliku z bibliotekami do projektu głównego, i można przejść do testowania. Jest to natomiast temat na kolejny post.

W poniższych przykładach pominę testowanie funkcji inicjalizujących SPI oraz GPIO, ponieważ przeprowadzanie testów tych funkcji jest bezsensowne. Spowodowane jest to tym iż w takiej funkcji możemy sprawdzić jedynie czy dane z parametrami zostały wstawione do struktury. Natomiast nie jesteśmy pewni czy dana część układu będzie działała zgodnie z naszymi oczekiwaniami.

Poniżej przejdę przez niektóre funkcje razem z przykładowymi testami.

Zacznę od czegoś prostego czyli funkcji sprawdzającej przekazane wartości Payload:

  1. static uint8_t checkThenSetPayloadSize(uint8_t payload_size) { /* GOOGLE TEST WRITE */
  2.     if(payload_size > 32)
  3.     {
  4.         payload_size = 32;
  5.     }
  6.     return payload_size;
  7. }

Funkcja posiada dwie ścieżki, jedna gdy wprowadzona zostanie wartość niższa niż 32, wtedy zwrócona zostanie wprowadzona wartość. Druga ścieżka zwraca wartość 32, gdy zostanie wprowadzona wyższa liczba. W związku z tym, że są dwie ścieżki wykonania więc potrzebne są dwa testy:

  1. TEST(checkThenSetPayloadSize,
  2.     PayloadSizeLowerThanMax_ReturnPassArgument)
  3. {
  4.     uint8_t passedPayloadSize = MAX_PAYLOAD_SIZE - 1;
  5.     uint8_t payloadSizeReturn = checkThenSetPayloadSize(passedPayloadSize);
  6.     EXPECT_EQ(passedPayloadSize, payloadSizeReturn);
  7. }
  8. TEST(checkThenSetPayloadSize,
  9.     PayloadSizeBiggerThanMax_ReturnMaxPayloadSize){
  10.     uint8_t passedPayloadSize = MAX_PAYLOAD_SIZE + 1;
  11.     uint8_t payloadSizeReturn = checkThenSetPayloadSize(passedPayloadSize);
  12.     EXPECT_EQ(payloadSizeReturn, MAX_PAYLOAD_SIZE);
  13. }

Dzięki temu, że sprawdzają one jak działa funkcja to można wykonać szybki refaktoring funkcji głównej:

  1. static uint8_t checkThenSetPayloadSize(uint8_t payload_size)
  2. {
  3.     return (payload_size > MAX_PAYLOAD_SIZE) ? MAX_PAYLOAD_SIZE : payload_size;
  4. }

I tak funkcja została zmodyfikowana, a dzięki temu, że są napisane do niej testy to nie trzeba wgrywać programu na płytkę. Pozwala to szybsze wprowadzanie zmian, ponieważ mamy pewność, że nie wprowadziliśmy żadnego głupiego błędu.

Teraz przejdę do testowanie funkcji przesyłających dane przez SPI:

  1. static uint8_t spiSendData(SPI_TypeDef* SPIx, uint8_t data)
  2. {
  3.     if (!((SPIx)->CR1 & SPI_CR1_SPE))
  4.     {
  5.         return 0;
  6.     }
  7.     while ((SPIx->SR & SPI_FLAG_TXE) == 0 || (SPIx->SR & SPI_FLAG_BSY));
  8.     SPIx->DR = data;
  9.     while ((SPIx->SR & SPI_FLAG_RXNE) == 0 || (SPIx->SR & SPI_FLAG_BSY));
  10.     return SPIx->DR;
  11. }

Dane do struktury należy umieścić w teście, inaczej całość utknie w pętli while i test się zawiesi:

  1. TEST(spiSendData,
  2.     spiDeviceDisable) {
  3.     SPI_TypeDef spiTest;
  4.     spiTest.CR1 = 0x00;
  5.     uint8_t returnValue = spiSendData(&spiTest, 0xA5);
  6.     EXPECT_EQ(returnValue, 0);
  7. }
  8. TEST(spiSendData,
  9.     returnWritedData) {
  10.     SPI_TypeDef spiTest;
  11.     spiTest.CR1 = SPI_CR1_SPE;
  12.     spiTest.SR = SPI_FLAG_TXE | SPI_SR_RXNE;
  13.     uint8_t returnValue = spiSendData(&spiTest, 0xA5);
  14.     EXPECT_EQ(0xA5, returnValue);
  15. }

Tutaj są dwa testy jeden sprawdza czy funkcja w przypadku wyłączonego interfejsu SPI zwraca 0, druga natomiast sprawdza czy dane zostały wprowadzone do wysłania. W takim przypadku zwracany jest wysyłany bajt.

Teraz czas na funkcje przesyłającą większą ilość danych przez SPI:

  1. static uint8_t spiWriteMultiData(SPI_TypeDef* SPIx, uint8_t* dataOut, uint8_t* dataIn, uint32_t count)
  2. {
  3.     uint32_t i;
  4.     if (spi_CheckIfEnabled(SPIx))
  5.     {
  6.         return 0x00;
  7.     }
  8.     spi_WaitPrevTransEnd(SPIx);
  9.     for (= 0; i < count; i++) {
  10.         SPIx->DR = dataOut[i];
  11.         spi_WaitPrevTransEnd(SPIx);
  12.         dataIn[i] = SPIx->DR;
  13.     }
  14.     return 0x01;
  15. }

Tutaj podobnie jak wcześniej możliwe są dwie ścieżki, gdy SPI będzie wyłączone i gdy dane zostaną przesłane:

  1. TEST(spiWriteMultiData,
  2.     spiIsDisabled_ReturnZero) {
  3.     SPI_TypeDef spiTest;
  4.     spiTest.CR1 = 0x00;
  5.     uint8_t dataIn[32] = { 0x00 };
  6.     uint8_t dataOut[32] = { 0x00 };
  7.     uint8_t returnValue = spiWriteMultiData(&spiTest, dataOut, dataIn, 32);
  8.     EXPECT_EQ(returnValue, 0x00);
  9. }
  10. TEST(spiWriteMultiData,
  11.     spiIsDisabled_ReturnOneAndWriteDataIntoTables) {
  12.     SPI_TypeDef spiTest;
  13.     spiTest.CR1 = SPI_CR1_SPE;
  14.     spiTest.SR = SPI_FLAG_TXE | SPI_SR_RXNE;
  15.     uint8_t dataIn[32];
  16.     uint8_t dataOut[32];
  17.     memset(dataOut, 0xA5, 32);
  18.     memset(dataIn, 0x00, 32);
  19.     uint8_t returnValue = spiWriteMultiData(&spiTest, dataOut, dataIn, 32);
  20.     EXPECT_EQ(returnValue, 0x01);
  21.     EXPECT_TRUE(0 == std::memcmp(dataIn, dataOut, sizeof(dataIn)));
  22. }

Pierwszy test sprawdza czy SPI zostało włączone. Druga natomiast sprawdza dwie rzeczy. Pierwsza z nich dotyczy czy zostało osiągnięty koniec, czyli czy została zwrócona wartość zero. Następnym sprawdzanym elementem jest to czy obie tablice mają te same wartości.

Funkcja odczytująca rejestr układu nrf24L01:

  1. static uint8_t nrf24l01_ReadRegister(uint8_t regAddr)
  2. {
  3.     uint8_t readedData = 0;
  4.     uint8_t dummyData = 0xFF;
  5.     uint8_t registerAddr = setReadRegisterMask(regAddr);
  6.     NRF24L01_CS_ON();
  7.     #ifdef USE_REGISTER_COMMANDS
  8.     spiSendData(NRF24L01_SPI, registerAddr);
  9.     readedData = spiSendData(NRF24L01_SPI, dummyData);
  10.     #else
  11.     uint8_t status = HAL_SPI_TransmitReceive(&hspi3, &registerAddr, &readedData, 1, 1000);
  12.     if (status != HAL_OK) {
  13.         return 0xFF;
  14.     }
  15.     if (readedData != NRF24L01_REG_STATUS){
  16.         HAL_SPI_TransmitReceive(&hspi3, &dummyData, &readedData, 1, 1000);
  17.     }
  18.     #endif
  19.     NRF24L01_CS_OFF();
  20.     return readedData;
  21. }

Funkcja odczytu HAL_SPI_TransmitReceive w wersji mocno uproszczonej, tylko na potrzeby testu:

  1. static HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi,
  2.     uint8_t *pTxData, uint8_t *pRxData,
  3.     uint16_t Size, uint32_t Timeout)
  4. {
  5.     if (*(pTxData + 0) == 0x01)
  6.     {
  7.         *(pRxData + 0) = 0xA5;
  8.     }
  9.     else if((*(pTxData + 0) == 0x07))
  10.     {
  11.         *(pRxData + 0) = 0xFF;
  12.     }
  13.     return HAL_OK;
  14. }

W testach możliwe są trzy scenariusze, mianowicie gdy uda się odczytać wartość, gdy odczytywany będzie tylko rejestr statusowy lub gdy funkcja przesyłająca dane wywoła błąd.

  1. TEST(nrf24l01_ReadRegister,
  2.     expectedBehaviorReadAllData) {
  3.     uint8_t readAddrTestValue = HAL_SPI_TransmitReceive_READ_OK;
  4.     uint8_t expectedValue = 0xA5;
  5.     uint8_t returnValue = nrf24l01_ReadRegister(readAddrTestValue);
  6.     EXPECT_EQ(returnValue, expectedValue);
  7. }
  8. TEST(nrf24l01_ReadRegister,
  9.     readOnlyStatusRegister) {
  10.     uint8_t readAddrTestValue = HAL_SPI_TransmitReceive_READ_STATUS;
  11.     uint8_t expectedValue = 0xFF;
  12.     uint8_t returnValue = nrf24l01_ReadRegister(readAddrTestValue);
  13.     EXPECT_EQ(returnValue, expectedValue);
  14. }
  15. TEST(nrf24l01_ReadRegister,
  16.     readErrorFromHalFunction) {
  17.     uint8_t readAddrTestValue = HAL_SPI_TransmitReceive_READ_ERROR;
  18.     uint8_t expectedValue = 0xFF;
  19.     uint8_t returnValue = nrf24l01_ReadRegister(readAddrTestValue);
  20.     EXPECT_EQ(returnValue, expectedValue);
  21. }

Podobnie wyglądają testy dla funkcji wpisującej dane:

  1. static HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi,
  2.     uint8_t *pData, uint16_t Size, uint32_t Timeout)
  3. {
  4.     if (*(pData + 0) == HAL_SPI_Transmit_READ_OK){
  5.         return HAL_OK;
  6.     }
  7.     else if (*(pData + 0) == HAL_SPI_Transmit_READ_ERROR){
  8.         return HAL_ERROR;
  9.     }
  10.     return HAL_OK;
  11. }
  12. static uint8_t nrf24l01_WriteReg(uint8_t addr, uint8_t value)
  13. {
  14.     HAL_StatusTypeDef operationStatus = HAL_OK;
  15.     addr = setWriteRegisterMask(addr);
  16.     NRF24L01_CS_ON();
  17.     #ifdef USE_REGISTER_COMMANDS
  18.     spiSendData(NRF24L01_SPI, addr);
  19.     spiSendData(NRF24L01_SPI, value);
  20.     #else
  21.     operationStatus = HAL_SPI_Transmit(&hspi3, &addr, 1, 1000);
  22.     if (operationStatus == HAL_OK)
  23.     {
  24.         operationStatus = HAL_SPI_Transmit(&hspi3, &value, 1, 1000);
  25.     }
  26.     #endif
  27.     NRF24L01_CS_OFF();
  28.     return operationStatus;
  29. }

Testy dla tej funkcji wyglądają następująco:

  1. TEST(nrf24l01_WriteReg,
  2.     expectedBehaviorWriteOk_ReturnHalOk) {
  3.     HAL_StatusTypeDef expectedOperationStatus = HAL_OK;
  4.     uint8_t someTestData = 0x01;
  5.     uint8_t returnValue = nrf24l01_WriteReg(HAL_SPI_Transmit_READ_OK, someTestData);
  6.     EXPECT_EQ(expectedOperationStatus, returnValue);
  7. }
  8. TEST(nrf24l01_WriteReg,
  9.     transmitAddressError_ReturnHalError) {
  10.     HAL_StatusTypeDef expectedOperationStatus = HAL_ERROR;
  11.     uint8_t someTestData = 0x01;
  12.     uint8_t returnValue = nrf24l01_WriteReg(HAL_SPI_Transmit_READ_ERROR, someTestData);
  13.     EXPECT_EQ(expectedOperationStatus, returnValue);
  14. }
  15. TEST(nrf24l01_WriteReg,
  16.     transmitErrorWhile_ReturnHalError) {
  17.     HAL_StatusTypeDef expectedOperationStatus = HAL_ERROR;
  18.     uint8_t someTestData = HAL_SPI_Transmit_READ_ERROR;
  19.     uint8_t returnValue = nrf24l01_WriteReg(HAL_SPI_Transmit_READ_ERROR, someTestData);
  20.     EXPECT_EQ(expectedOperationStatus, returnValue);
  21. }

Testowanie funkcji odpowiadającej za ustawienie bitu w rejestrze:

  1. static uint8_t nrf24l01_WriteBit(uint8_t regAdr,
  2.                                 const uint8_t bitToWrite,
  3.                                 uint8_t setResetBit)
  4. {
  5.     uint8_t valueToWrite = 0;
  6.     valueToWrite = nrf24l01_ReadRegister(regAdr);
  7.     if (setResetBit)
  8.     {
  9.         valueToWrite |= 1 << bitToWrite;
  10.     }
  11.     else
  12.     {
  13.         valueToWrite &= ~(1 << bitToWrite);
  14.     }
  15.     nrf24l01_WriteReg(regAdr, valueToWrite);
  16.     return valueToWrite;
  17. }

W tym przypadku należy sprawdzić czy dany bit został ustawiony bądź skasowany:

  1. TEST(nrf24l01_WriteBit,
  2.     valueToWrite_SetBit) {
  3.     uint8_t regAddress = HAL_SPI_TransmitReceive_TEST_WRITE_BIT;
  4.     uint8_t expectedValue = 0xA6;
  5.     uint8_t returnValue = nrf24l01_WriteBit(regAddress, 1, 1);
  6.     EXPECT_EQ(expectedValue, returnValue);
  7. }
  8. TEST(nrf24l01_WriteBit,
  9.     valueToWrite_ResetBit) {
  10.     uint8_t regAddress = HAL_SPI_TransmitReceive_TEST_WRITE_BIT;
  11.     uint8_t expectedValue = 0xA4;
  12.     uint8_t returnValue = nrf24l01_WriteBit(regAddress, 1, 0);
  13.     EXPECT_EQ(expectedValue, returnValue);
  14. }

Podobnie wyglądają testy dla zapisu i odczytu danych z urządzenia. Podstawiam pod symulacyjne testy zapisu i odczytu pożądane wartości i sprawdzam czy uzyskane odpowiedzi spełniają warunki testowe.

Teraz funkcja sprawdzająca czy są dane w buforze RX modułu radiowego:

  1. static uint8_t nrf24l01_RxFifoEmpty(void)
  2. {
  3.     uint8_t regValue = nrf24l01_ReadRegister(NRF24L01_REG_FIFO_STATUS);
  4.     if (regValue == 0xFF)
  5.     {
  6.         return 0xFF;
  7.     }
  8.     uint8_t returnVal = Nrf24l01_Check_Bit(regValue, NRF24L01_RX_EMPTY);;
  9.     return returnVal;
  10. }

Na samym początku dodaje warunek do funkcji HAL_SPI_TransmitReceive:

  1. else if (*(pTxData + 0) == NRF24L01_REG_FIFO_STATUS)
  2. {
  3.     *(pRxData + 0) = glob_testValueForReadRegisteFifoStatus;
  4. }

Do niego wprowadzam zmienną globalną, która pozwoli mi przetestować wszystkie możliwe scenariusze dla tej funkcji:

  1. TEST(nrf24l01_RxFifoEmpty,
  2.     RXFifoIsEmptyBitNeedToBeClear_ReturnZero)
  3. {
  4.     uint8_t expectedValue = 0;
  5.     glob_testValueForReadRegisteFifoStatus = 0x00;
  6.     uint8_t retValue = nrf24l01_RxFifoEmpty();
  7.     EXPECT_EQ(expectedValue, retValue);
  8. }
  9. TEST(nrf24l01_RxFifoEmpty,
  10.     RXFifoHasSomeDataBitNeedToBeSet_ReturnOne)
  11. {
  12.     uint8_t expectedValue = 1;
  13.     glob_testValueForReadRegisteFifoStatus = 0x01;
  14.     uint8_t retValue = nrf24l01_RxFifoEmpty();
  15.     EXPECT_EQ(expectedValue, retValue);
  16. }
  17. TEST(nrf24l01_RxFifoEmpty,
  18.     ReadError_Return0xFF)
  19. {
  20.     uint8_t expectedValue = 0xFF;
  21.     glob_testValueForReadRegisteFifoStatus = 0xFF;
  22.     uint8_t retValue = nrf24l01_RxFifoEmpty();
  23.     EXPECT_EQ(expectedValue, retValue);
  24. }

Teraz funkcja odczytująca status operacji. W niej należy sprawdzić czy zwraca odczytany status, lub czy zwraca błąd odczytu:

  1. static uint8_t nrf24l01_GetStatus(void) {
  2.     uint8_t status = 0x00;
  3.     HAL_StatusTypeDef operationStatus = HAL_OK;
  4.     NRF24L01_CS_ON();
  5.     #ifdef USE_REGISTER_COMMANDS
  6.     status = spiSendData(NRF24L01_SPI, NRF24L01_NOP_MASK);
  7.     #else
  8.     uint8_t dataToSend[1] = { NRF24L01_NOP_MASK };
  9.     operationStatus = HAL_SPI_TransmitReceive(&hspi3, dataToSend, &status, 1, 1000);
  10.     if (operationStatus != HAL_OK)
  11.     {
  12.         status = 0xFF;
  13.     }
  14.     #endif
  15.     /* Pull up chip select */
  16.     NRF24L01_CS_OFF();
  17.     return status;
  18. }

Tutaj podobnie jak poprzednio sprawdzam status operacji:

  1. TEST(nrf24l01_GetStatus,
  2.     ReadOK_ReurnSameDataAsInGlobalBuffer) {
  3.     glob_testCheckTransmissionStatusAndFifo = 0x05;
  4.     uint8_t operationStatus = nrf24l01_GetStatus();
  5.     EXPECT_EQ(glob_testCheckTransmissionStatusAndFifo, operationStatus);
  6. }
  7. TEST(nrf24l01_GetStatus,
  8.     ReadError_ReturnFF) {
  9.     glob_testCheckTransmissionStatusAndFifo = 0x18;
  10.     uint8_t operationStatus = nrf24l01_GetStatus();
  11.     EXPECT_EQ(0xFF, operationStatus);
  12. }

Jak widać większość testów dla tej biblioteki opiera się na sprawdzaniu czy dane zostały odpowiednio przesłane bądź czy zostały odpowiednio odebrane i przetworzone.

Pliki do przygotowanych testów można pobrać z dysku Google pod tym linkiem.