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.
- extern I2C_HandleTypeDef hi2c1;
- uint8_t I2C1_Enable(void)
- {
- I2C1_gpio();
- return I2C1_Interf_Enable();
- }
- static void I2C1_gpio(void)
- {
- GPIO_InitTypeDef GPIO_InitStruct = {0};
- GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
- GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
- GPIO_InitStruct.Pull = GPIO_NOPULL;
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
- GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
- HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
- }
- static uint8_t I2C1_Interf_Enable(void)
- {
- __HAL_RCC_I2C1_CLK_ENABLE();
- hi2c1.Instance = I2C1;
- hi2c1.Init.Timing = 0x10C0ECFF;
- hi2c1.Init.OwnAddress1 = 0xff;
- hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
- hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
- hi2c1.Init.OwnAddress2 = 0xff;
- hi2c1.Init.OwnAddress2Masks = I2C_OA2_NOMASK;
- hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
- hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
- if (HAL_I2C_Init(&hi2c1) != HAL_OK) { return 0x01u; }
- if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK) { return 0x02u; }
- if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK) { return 0x03u; }
- HAL_I2C_Init(&hi2c1);
- return 0x00u;
- }
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.
- __attribute__((__unused__)) static uint8_t I2C1_SearchConnectedDevices(void)
- {
- uint8_t numberOfConnectedDevices = 0;
- __attribute__((__unused__)) uint8_t array[128] = {0x00}; //Array contains data when device find. Use in debug mode
- HAL_StatusTypeDef result;
- for (uint8_t i=1; i<128; i++)
- {
- result = HAL_I2C_IsDeviceReady(&hi2c1, (uint16_t)(i<<1), 2, 2);
- if (result == HAL_OK)
- {
- numberOfConnectedDevices++;
- array[i] = i;
- }
- }
- return numberOfConnectedDevices;
- }
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:
- EEPROM_StatusTypedef EEPROM_ReadByte(uint8_t *valueToRead, uint8_t address)
- {
- uint8_t readedVal = 0;
- EEPROM_StatusTypedef opstatus = (EEPROM_StatusTypedef)HAL_I2C_Mem_Read(&hi2c1, 0xA0, address, 1, (uint8_t*)&readedVal, 1, HAL_MAX_DELAY);
- *(valueToRead + 0) = readedVal;
- return opstatus;
- }
Zapis bajtu danych do pamięci:
- EEPROM_StatusTypedef EEPROM_WriteByte(uint8_t *valueToWrite, uint8_t address)
- {
- uint8_t writeData = *(valueToWrite + 0);
- return (EEPROM_StatusTypedef)(HAL_I2C_Mem_Write(&hi2c1, 0xA0, address, 1, (uint8_t*)&writeData, 1, HAL_MAX_DELAY));
- }
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.
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:
- HAL_I2C_Mem_Write(...)
- 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.
- 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.
- EEPROM_StatusTypedef EEPROM_WriteBuffData(uint8_t *valueToWrite, uint8_t dataSize, uint8_t address) {
- uint8_t counter = 0;
- for(uint8_t i=0; i<16; i++)
- {
- while(HAL_I2C_Mem_Write(&hi2c1, 0xA0, address + i, 1, (uint8_t*)&valueToWrite[i], 1, HAL_MAX_DELAY) != HAL_OK) {
- HAL_Delay(1);
- counter++;
- if(counter == 10) { return EEPROM_ERROR; }
- };
- counter = 0;
- }
- return EEPROM_OK;
- }
Kość pamięci zawiera wbudowany adres MAC. Można go odczytać w następujący sposób:
- EEPROM_StatusTypedef EEPROM_ReadMac(uint8_t *macBuffer)
- {
- uint8_t readedData[6] = {0x00};
- EEPROM_StatusTypedef opstatus = (EEPROM_StatusTypedef)HAL_I2C_Mem_Read(&hi2c1, 0xA0, ADR_MAC_ADRESS, 1, (uint8_t*)&readedData, 6, HAL_MAX_DELAY);
- for(uint8_t i = 0; i<6; i++) { *(macBuffer + i) = readedData[i]; }
- return opstatus;
- }
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.
- static const uint8_t EEPROM_DEFAULT_MEM_DATA[128] = {
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
- 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0xEE, 0x0E, 0xFF,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x02, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
- 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0x00, 0x0F, 0x0F, 0x93, 0x02, 0x06, 0x03, 0x03, 0x00, 0x00, 0x00,
- 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
- };
- static void I2C1_gpio(void);
- static uint8_t I2C1_Interf_Enable(void);
- uint8_t I2C1_Enable(void)
- {
- I2C1_gpio();
- return I2C1_Interf_Enable();
- }
- EEPROM_StatusTypedef EEPROM_ReadByte(uint8_t *valueToRead, uint8_t address)
- {
- EEPROM_StatusTypedef opstatus = EEPROM_OK;
- *(valueToRead + 0) = EEPROM_DEFAULT_MEM_DATA[address];
- return opstatus;
- }
- EEPROM_StatusTypedef EEPROM_WriteByte(uint8_t *valueToWrite, uint8_t address) { return (EEPROM_OK); }
- EEPROM_StatusTypedef EEPROM_ReadMac(uint8_t *macBuffer)
- {
- EEPROM_StatusTypedef opstatus = EEPROM_OK;
- for(uint8_t i = 0; i<6; i++) { *(macBuffer + i) = 0xA5; }
- return opstatus;
- }
- uint8_t EEPROM_ReadSettingsData(uint8_t addr) { return EEPROM_DEFAULT_MEM_DATA[addr]; }
- uint8_t EEPROM_WriteSetting(uint8_t valToWrite, uint8_t addr) { return 1; }
- uint8_t EEPROM_ResetMemmoryToDefaultSettingsSaveSerialNumber(void) { return 0; }
- uint8_t EEPROM_ReadWholeDataFromMemmory(uint8_t *arrayPtr)
- {
- for(uint8_t i = 0; i<128; i++) { *(arrayPtr + i) = EEPROM_DEFAULT_MEM_DATA[i]; }
- return 0x00;
- }
- static void I2C1_Interf_Enable(void) { }
- static uint8_t I2C1_gpio(void) { return 0x00u; }