Niedawno przedstawiłem opis programowanej EEPROM poprzez układ Arduino. Tym razem przyszła kolej na STM32. Może to być w niektórych projektach wyjątkowo przydatny element do zastosowania, ponieważ STM32F103 jak i również STM32F4 nie posiada wbudowanej pamięci EEPROM. Do komunikacji z pamiecią zostanie wykorzystany interfejs I2C.
I2C
Inter-Integrated Circuit został opracowany przez firmę Philips. Jest wykorzystywany do transmisji szeregowej, synchronicznej w trybie naprzemiennego odbierania i nadawania danych (half-duplex).
Dostępne są trzy wersje tego interfejsu różniące się między sobą szybkością transmisji:
- Standardowy (Standard) - 100kb/s
- Szybki (Fast Mode) - 400kb/s
- Wysoka prędkość (High Speed Mode) - 3,4 Mb/s
Do komunikacji wykorzystywane są dwa przewody SDA i SCL. Ta druga zapewnia przesłanie sygnału zegarowego, pierwsza natomiast pozwala na transmisję danych. Dane przesyłane są w obu kierunkach poprzez tą linię.
Obie linie są podłączone do wejść bądź wyjść z otwartym kolektorem bądź z otwartym drenem. Układy mogą na magistrali ustawiać tylko stan niski. Wysoki natomiast otrzymuje się poprzez podciągniecie linii danych i zegarowej do zasilania poprzez rezystory (np. 4,7 kOhm).
Można ustawić cztery tryby pracy tych układów:
- master transmit - wysyła dane do podrzędnego poprzez nadrzędny układ
- master receive - nadrzędny odbiera dane od podrzędnego
- slave trasmit - podrzędny wysyła
- slave receive - podrzędny odbiera
Pamięć EEPROM
Wykorzystywana kość pamięci 24AA01 została wyprodukowana przez firmę Microchip. Posiada ona 128B pojemności. Częstotliwość pracy wynosi 100kHz lub 400kHz. Ilość cykli zapisu i odczytu przekracza 1 mln. Sprzedawane jest ona w obudowach ośmionóżkowych PDIP, SOIC, TSSOP, DFN, TDFN, MSOP oraz pięcionóżkowych SOT-23, SC-70.
Rys. 1. Typy obudów
Opis wyprowadzeń oraz sposób podłączenia:
- 1 - A0 Adresowanie - NC
- 2 - A1 Adresowanie - NC
- 3 - A2 Adresowanie - NC
- 4 - VSS - GND - GND
- 5 - SDA - linia danych - I2C1 lub I2C2
- 6 - SCL - linia zegarowa - I2C1 lub I2C2
- 7 - WP - Write Protect - ochrona przed zapisem, trzeba podłączyć do masy aby mieć możliwość zapisu danych. - GND
- 8 - VCC - zasilanie od 1,7V do 5,5V - 3,3V
Linie SDA i SCL wymagają rezystora podciągającego dla 100 kHz jest to 10 kOhm, dla 400 kHz jest to wartość 2 kOhm. Ja wykorzystałem rezystory 4,7 kOhm podciągnięte pod zasilanie 3.3V.
Programowanie
Działanie oraz komunikacja wygląda w następujący sposób:
Wystawiony jest sygnał rozpoczęcia transmisji tzw. start. Inicjalizowany jest on poprzez ustawienie stanu niskiego na linii SDA w trakcie występowania stanu wysokiego na SCL. Gdy ten sygnał zostanie wykryty układy podrzędne przełączają się w stan odbierania danych. W pierwszej kolejności wysyłany jest adres układu docelowego, z którym ma nastąpić komunikacja. Następnie 8 bitów danych. Układ odbierający wystawia bit potwierdzenia odbioru danych (ACK). Po nim zostaje wysłany znak stop. Po całej operacji przesłania zostaje zwolniona linia SDA gdy na linii SCL panuje stan wysoki. Takie zmiany stanów pomiędzy liniami SDA i SCL pojawia się tylko w momęcie wysyłania sygnałów start i stop.
Po zakończeniu jednej transmisji można odrazu rozpocząć kolejną. Przy takich operacja istnieje możliwość pominięcia sygnału zakończenia transmisji stop.
Wykorzystywany mikrokontroler posiada dwa układy interfejsy I2C, które znajdują się na następujących wyprowadzeniach:
- I2C 1 - SDA PB7 ; SCL PB6
- I2C 2 - SDA ; SCL
Mogą one obsługiwać transmisję zarówno 7 jak i 10 bitową. Transmisję można rozróżnić na standardową bądź szybką. Dodatkowo możliwe jest wykrywanie błędów transmisji, obliczania sumy kontrolnej CRC, być źródłem przerwań bądź żądań danych dla DMA.
Biblioteka API udostępnia strukturę, w której istnieje możliwość wypełnienia takich pól jak:
- I2C_Mode - określenie tryby pracy układu, czy I2C czy SMBus, to drugie jest standardem pochodzacym od I2C
- I2C_DutyCycle - pozwala na określenie stosunku trwania sygnału wysokiego do niskiego na linii SCK. Jest to ważny element dla taktowania powyżej 100kHz.
- I2C_OwnAddress1 - określa adres układu po stronie mikrokontrolera, jeśli jest on układem podrzędnym
- I2C_AcknowledgedAddress - wybranie trybu adresowania
- I2C_ClockSpeed - pozwala na określenie występującego sygnału zegarowego na linii SCK
I2C taktowane jest z szyny APB1. Piny od wyprowadzeń w konfiguracji otwarty dren.
Poniżej przedstawiam prosty program, którego zadaniem jest zapis a następnie odczyt danych. Całość została opisana z odpowiednim komentarzem.
- #include <stdio.h>
- #include "stm32f10x.h"
- //Wysłanie danych przez układ USART na komputer
- void SendCharc(char c);
- //Potrzebne dla przesyłania danych z wykorzystaniem funckji printf
- int __io_putchar(int c);
- //Zapis do pamięci
- void EEPROM_WRITE(uint32_t addr, const void* data, int size);
- //Odczyt z pamięci
- void EEPROM_READ(uint32_t addr, void* data, int size);
- //Inicjalizacja GPIO
- void GPIOInit(void);
- //Inicjalizacja USARTU
- void USARTInit(void);
- //Inicjalizacja I2C
- void I2CInit(void);
- int main(void)
- {
- uint32_t uruchomienia = 0;
- uint32_t pomocnicza = 3;
- char znak = 'a';
- GPIOInit();
- USARTInit();
- I2CInit();
- EEPROM_READ(0x20, &uruchomienia, sizeof(uruchomienia));
- EEPROM_READ(0x40, &pomocnicza, sizeof(pomocnicza));
- uruchomienia++;
- EEPROM_WRITE(0x20, &uruchomienia, sizeof(uruchomienia));
- pomocnicza = pomocnicza + 3;
- printf("\n\rLiczba uruchomień zmiennej %ld \n\r", uruchomienia);
- printf("Zmienna pomocnicza %ld \n\r", pomocnicza);
- EEPROM_WRITE(0x40, &pomocnicza, sizeof(pomocnicza));
- for(;;);
- }
- //Inicjalizacja GPIO
- void GPIOInit(void)
- {
- GPIO_InitTypeDef GPIOInit;
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
- //Definicja pinów dla USARTU
- GPIO_StructInit(&GPIOInit);
- GPIOInit.GPIO_Pin = GPIO_Pin_2;
- //GPIOInit.GPIO_Speed = GPIO_Speed_50MHz;
- GPIOInit.GPIO_Mode = GPIO_Mode_AF_PP;
- GPIO_Init(GPIOA, &GPIOInit);
- GPIOInit.GPIO_Pin = GPIO_Pin_3;
- //GPIOInit.GPIO_Speed = GPIO_Speed_50MHz;
- GPIOInit.GPIO_Mode = GPIO_Mode_IN_FLOATING;
- GPIO_Init(GPIOA, &GPIOInit);
- //Definicja pinów dla EEPROM GPIOB6 - SCL, GPIOB7 - SDA
- GPIOInit.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
- GPIOInit.GPIO_Mode = GPIO_Mode_AF_OD;
- GPIOInit.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init(GPIOB, &GPIOInit);
- }
- //Inicjalizacja USARTU
- void USARTInit(void)
- {
- USART_InitTypeDef USARTInit;
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
- //Ustawienie prędkości transmisji 9600bps
- USARTInit.USART_BaudRate = 115200;
- //Ustawienie dlugosci slowa
- USARTInit.USART_WordLength = 8;
- //Ustawienie bitu stopu
- USARTInit.USART_StopBits = USART_StopBits_1;
- //Brak kontroli parzystosci
- USARTInit.USART_Parity = USART_Parity_No;
- //Kontrola przepływu danych
- USARTInit.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
- //Tryb pracy TX i RX
- USARTInit.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
- USART_Init(USART2, &USARTInit);
- USART_Cmd(USART2, ENABLE);
- }
- //Inicjalizacja I2C
- void I2CInit(void)
- {
- I2C_InitTypeDef I2CInit;
- I2C_StructInit(&I2CInit);
- //Tryb pracy układu I2C
- I2CInit.I2C_Mode = I2C_Mode_I2C;
- //Stosunek trwania stanu wysokiego do niskiego linii SCK
- I2CInit.I2C_DutyCycle = I2C_DutyCycle_2;
- //Adres I2C po stronie mikrokontrolera jeśli pracuje jako slave
- I2CInit.I2C_OwnAddress1 = 0x00;
- //Wlaczenie potwierdzenia transmisji
- I2CInit.I2C_Ack = I2C_Ack_Enable;
- //Częstotliwośc SCK
- I2CInit.I2C_ClockSpeed = 100000;
- I2C_Init(I2C1, &I2CInit);
- I2C_Cmd(I2C1, ENABLE);
- }
- //Ustawienie adresu pamięci
- void EEPROM_ADDRES(uint32_t addr)
- {
- //Rozpoczęcie komunikacji
- I2C_GenerateSTART(I2C1, ENABLE);
- //Czekanie aż polecenie zostanie wykonane
- while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
- //W tym kroku wysłany jest identyfikator układu
- I2C_Send7bitAddress(I2C1, 0xa0, I2C_Direction_Transmitter);
- //Odczekanie na odpowiedź
- while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
- //Wysłanie adresu komórki pamięci do jakiej będą zapisywane dane.
- I2C_SendData(I2C1, addr);
- while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
- }
- //Zapis do pamięci
- void EEPROM_WRITE(uint32_t addr, const void* data, int size)
- {
- int i;
- const uint8_t* buffer = (uint8_t*)data;
- //Wybranie adresu
- EEPROM_ADDRES(addr);
- //Przejście przez cały buffor z danymi
- for (i = 0; i < size; i++)
- {
- I2C_SendData(I2C1, buffer[i]);
- while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
- }
- //Wygenerowanie sygnału Stop
- I2C_GenerateSTOP(I2C1, ENABLE);
- }
- //Odczyt z pamięci
- void EEPROM_READ(uint32_t addr, void* data, int size)
- {
- //Deklaracja zmiennych
- int i;
- uint8_t* buffer = (uint8_t*)data;
- //Wybór adresu urządzenia
- EEPROM_ADDRES(addr);
- //Wysłanie sygnału start, następnie czekanie na odpowiedź
- I2C_GenerateSTART(I2C1, ENABLE);
- while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
- //Odbiór tylko jednego bajtu, wylacz potwierdzenie
- I2C_AcknowledgeConfig(I2C1, ENABLE);
- //Wysłanie adresu układu, ustawienie i2c jako master
- I2C_Send7bitAddress(I2C1, 0xa0, I2C_Direction_Receiver);
- //Odczekanie na EV6
- while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
- for (i = 0; i < size - 1; i++)
- {
- while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
- buffer[i] = I2C_ReceiveData(I2C1);
- }
- //Odbiór pojedyńczego bajtu
- I2C_AcknowledgeConfig(I2C1, DISABLE);
- //Wygenerowanie sygnału stop
- I2C_GenerateSTOP(I2C1, ENABLE);
- //Odczekanie na sygnał
- while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
- //Odczytaj dane z rejestru
- buffer[i] = I2C_ReceiveData(I2C1);
- }
- //Wysłanie danych przez układ USART na komputer
- void SendCharc(char c)
- {
- while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
- USART_SendData(USART2, c);
- }
- //Potrzebne dla przesyłania danych z wykorzystaniem funckji printf
- int __io_putchar(int c)
- {
- SendCharc(c);
- return c;
- }