sobota, 5 czerwca 2021

STM32 - H723ZG - I2C EEPROM

W tym poście chciałbym opisać konfigurację oraz obsługę I2C na przykładzie pamięci EEPROM. Do odczytu wykorzystałem kość 24AA01.

[Źródło: www.botland.com.pl]

I2C:


Komunikacja przez I2C odbywa się za pomocą dwóch linii SDA oraz SCL. Pierwsza to dwukierunkowa linia danych, druga to linia sygnału zegarowego. Urządzeniem nadrzędnym w tym przypadku będzie STM32H7. Każdy z podłączonych układów musi zawierać unikalny adres. 

Dokładniejszy opis interfejsu można znaleźć np. tutaj.

Projektowanie:


W przypadku projektowania płytki zawierającej interfejs I2C, do komunikacji z pamięcią EEPROM lub innym układem, należy pamiętać o kilku ważnych elementach:

Rezystory podciągające linie SDA oraz SCL do VCC np. przez rezystory o wartości 4,7k.
Filtracja napięcia zasilania wchodzącego do układu np. kondensator 0.1uF.

Rezystory szeregowe - czasami stosuję się je na liniach. Pomagają one w redukcji zakłóceń oraz przesłuchów pomiędzy liniami. Ich wartość musi być odpowiednio dobrana ponieważ w przypadku za dużych wartości możemy znacząco zmienić zbocze przesyłanego sygnału. Standardowo stosuje się rezystory z przedziału od 0 do około 50ohm. W zależności od szybkości zegara taktującego magistralę I2C. Najczęściej spotykałem się z wartościami 33Ohm. 

Program:


Do tego projektu nie wykorzystuje kodu wygenerowanego przez CubeMx. Przeniosłem wszystkie funkcje do osobnego pliku z lekką modyfikacją parametrów.

  1. extern I2C_HandleTypeDef hi2c1;
  2.  
  3. uint8_t I2C1_Enable(void)
  4. {
  5.     I2C1_gpio();
  6.     return I2C1_Interf_Enable();
  7. }
  8.  
  9. static void I2C1_gpio(void)
  10. {
  11.     GPIO_InitTypeDef GPIO_InitStruct = {0};
  12.  
  13.     GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
  14.     GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
  15.     GPIO_InitStruct.Pull = GPIO_NOPULL;
  16.     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  17.     GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
  18.     HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  19. }
  20.  
  21. static uint8_t I2C1_Interf_Enable(void)
  22. {
  23.     __HAL_RCC_I2C1_CLK_ENABLE();
  24.  
  25.     hi2c1.Instance = I2C1;
  26.     hi2c1.Init.Timing = 0x10C0ECFF;
  27.     hi2c1.Init.OwnAddress1 = 0xff;
  28.     hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  29.     hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  30.     hi2c1.Init.OwnAddress2 = 0xff;
  31.     hi2c1.Init.OwnAddress2Masks = I2C_OA2_NOMASK;
  32.     hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  33.     hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  34.  
  35.     if (HAL_I2C_Init(&hi2c1) != HAL_OK) { return 0x01u; }
  36.     if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK) { return 0x02u; }
  37.     if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK) { return 0x03u; }
  38.  
  39.     HAL_I2C_Init(&hi2c1);
  40.    
  41.     return 0x00u;
  42. }

Komunikacja odbywa się przez interfejs I2C1 z linii GPIOB_8 (SCL) oraz GPIOB_9 (SDA). Funkcja uruchamiająca interfejs zwróci wartość inną niż 0, gdy wystąpi jakiś błąd podczas uruchamiania I2C.

W przypadku gdy nie znamy adresu urządzeń podłączonych po I2C można skorzystać z poniższej funkcji. Która przeszuka na linii występowanie urządzeń na linii.

  1. __attribute__((__unused__)) static uint8_t I2C1_SearchConnectedDevices(void)
  2. {
  3.     uint8_t numberOfConnectedDevices = 0;
  4.     __attribute__((__unused__)) uint8_t array[128] = {0x00};    //Array contains data when device find. Use in debug mode
  5.     HAL_StatusTypeDef result;
  6.  
  7.     for (uint8_t i=1; i<128; i++)
  8.     {
  9.       result = HAL_I2C_IsDeviceReady(&hi2c1, (uint16_t)(i<<1), 2, 2);
  10.       if (result == HAL_OK)
  11.       {
  12.           numberOfConnectedDevices++;
  13.           array[i] = i;
  14.       }
  15.     }
  16.  
  17.     return numberOfConnectedDevices;
  18. }

Funkcja wprowadzi adres urządzenia do tablicy. Po zakończeniu działania zwróci ilość urządzeń jakie zostały podłączone do magistrali I2C1. Została ona zdefiniowana jako statyczna z dodatkowym atrybutem unused. Dzięki temu nie będzie zwracane ostrzeżenie przez kompilator, gdy funkcja nie będzie rozpoznawalna.

Poniżej odczytanie pojedynczego bajtu danych z pamięci:

  1. EEPROM_StatusTypedef EEPROM_ReadByte(uint8_t *valueToRead, uint8_t address)
  2. {
  3.     uint8_t readedVal = 0;
  4.     EEPROM_StatusTypedef opstatus = (EEPROM_StatusTypedef)HAL_I2C_Mem_Read(&hi2c1, 0xA0, address, 1, (uint8_t*)&readedVal, 1, HAL_MAX_DELAY);
  5.  
  6.     *(valueToRead + 0) = readedVal;
  7.  
  8.     return opstatus;
  9. }

Zapis bajtu danych do pamięci:

  1. EEPROM_StatusTypedef EEPROM_WriteByte(uint8_t *valueToWrite, uint8_t address)
  2. {
  3.     uint8_t writeData = *(valueToWrite + 0);
  4.     return (EEPROM_StatusTypedef)(HAL_I2C_Mem_Write(&hi2c1, 0xA0, address, 1, (uint8_t*)&writeData, 1, HAL_MAX_DELAY));
  5. }

Wszystkie funkcje zapisujące i odczytujące większe ilości danych bazują na dwóch powyższych funkcjach. Ich implementacja zależy od potrzeb użytkownika.

Przy zapisie i odczycie danych z układu należy pamiętać o czasie potrzebnym na wykonanie poprawnego zapisu pamięci przez kość EEPROM.


Zgodnie z dokumentacją wynosi ona 5 ms.

Aby zapewnić poprawne działanie można skorzystać z jednego z następujących sposobów.

Pierwszy z nich wprowadza zwykłe opóźnienie po operacji zapisu:

  1. HAL_I2C_Mem_Write(...)
  2. HAL_Delay(4)

Nie musi ono wynosić dokładnie 5 ms ponieważ będą wykonywane różne operacje po wykonaniu zapisu.

Kolejnym sposobem jest wykonywanie zapisu i odczytu w pętli while, aż do uzyskania poprawnego wyniku operacji.

  1. while(HAL_I2C_Mem_Read(&hi2c1, ...) != HAL_OK) { HAL_Delay(1); }

Ten sposób może być zawodny ponieważ w przypadku uszkodzenia kości, lub wprowadzeniu niepoprawnych parametrów do funkcji, program może utknąć w pętli nieskończonej.

Następny sposób pozwala wykonuje zapis w pętli while natomiast czeka zadaną ilość prób przed zakończeniem działania funkcji.

  1. EEPROM_StatusTypedef EEPROM_WriteBuffData(uint8_t *valueToWrite, uint8_t dataSize, uint8_t address) {
  2.     uint8_t counter = 0;
  3.     for(uint8_t i=0; i<16; i++)
  4.     {
  5.         while(HAL_I2C_Mem_Write(&hi2c1, 0xA0, address + i, 1, (uint8_t*)&valueToWrite[i], 1, HAL_MAX_DELAY) != HAL_OK) {
  6.             HAL_Delay(1);
  7.             counter++;
  8.             if(counter == 10) { return EEPROM_ERROR; }
  9.         };
  10.         counter = 0;
  11.     }
  12.  
  13.     return EEPROM_OK;
  14. }

Kość pamięci zawiera wbudowany adres MAC. Można go odczytać w następujący sposób:

  1. EEPROM_StatusTypedef EEPROM_ReadMac(uint8_t *macBuffer)
  2. {
  3.     uint8_t readedData[6] = {0x00};
  4.     EEPROM_StatusTypedef opstatus = (EEPROM_StatusTypedef)HAL_I2C_Mem_Read(&hi2c1, 0xA0, ADR_MAC_ADRESS, 1, (uint8_t*)&readedData, 6, HAL_MAX_DELAY);
  5.  
  6.     for(uint8_t i = 0; i<6; i++) { *(macBuffer + i) = readedData[i]; }
  7.  
  8.     return opstatus;
  9. }

W przypadku korzystania z testów jednostkowych może zajść konieczność wprowadzenia zmodyfikowanych funkcji w celu symulacji dostępu do układu EEPROM. Z tego powodu przygotowałem zmodyfikowany zestaw funkcji, który zastępuje instrukcje pobierające informacje z kości pamięci. Ich załączenie wykonuje przez zmianę definicji preprocesora.

  1. static const uint8_t EEPROM_DEFAULT_MEM_DATA[128] = {
  2.         0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  3.         0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  4.         0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0xEE, 0x0E, 0xFF,
  5.         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  6.         0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x02, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  7.         0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0x00, 0x0F, 0x0F, 0x93, 0x02, 0x06, 0x03, 0x03, 0x00, 0x00, 0x00,
  8.         0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  9.         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  10. };
  11.  
  12. static void I2C1_gpio(void);
  13. static uint8_t I2C1_Interf_Enable(void);
  14.  
  15. uint8_t I2C1_Enable(void)
  16. {
  17.     I2C1_gpio();
  18.     return I2C1_Interf_Enable();
  19. }
  20.  
  21. EEPROM_StatusTypedef EEPROM_ReadByte(uint8_t *valueToRead, uint8_t address)
  22. {
  23.     EEPROM_StatusTypedef opstatus = EEPROM_OK;
  24.     *(valueToRead + 0) = EEPROM_DEFAULT_MEM_DATA[address];
  25.     return opstatus;
  26. }
  27.  
  28. EEPROM_StatusTypedef EEPROM_WriteByte(uint8_t *valueToWrite, uint8_t address) { return (EEPROM_OK); }
  29.  
  30. EEPROM_StatusTypedef EEPROM_ReadMac(uint8_t *macBuffer)
  31. {
  32.     EEPROM_StatusTypedef opstatus = EEPROM_OK;
  33.     for(uint8_t i = 0; i<6; i++) { *(macBuffer + i) = 0xA5; }
  34.     return opstatus;
  35. }
  36.  
  37. uint8_t EEPROM_ReadSettingsData(uint8_t addr) { return EEPROM_DEFAULT_MEM_DATA[addr]; }
  38.  
  39. uint8_t EEPROM_WriteSetting(uint8_t valToWrite, uint8_t addr) { return 1; }
  40.  
  41. uint8_t EEPROM_ResetMemmoryToDefaultSettingsSaveSerialNumber(void) { return 0; }
  42.  
  43. uint8_t EEPROM_ReadWholeDataFromMemmory(uint8_t *arrayPtr)
  44. {
  45.     for(uint8_t i = 0; i<128; i++) { *(arrayPtr + i) = EEPROM_DEFAULT_MEM_DATA[i]; }
  46.     return 0x00;
  47. }
  48.  
  49. static void I2C1_Interf_Enable(void) { }
  50.  
  51. static uint8_t I2C1_gpio(void) { return 0x00u; }