W tym poście chciałbym opisać sposób komunikacji z pamięcią FRAM MB85RC256V.
MB85RC256V jest układem pamięci nieulotnej FRAM o pojemności 32KB (32768 bajtów). Do komunikacji wykorzystywany jest interfejs I2C.
Pamięć charakteryzuje się:
- Bardzo szybkim czasem zapisu,
- Możliwość zapisu danych bezpośrednio, bez konieczności kasowania,
- Dużą wytrzymałością na cykle zapisu,
- Niskim poborem prądu,
- Małym ryzykiem utraty danych, z powodu szybkiego zapisu,
Schemat blokowy:
Powyższy diagram przedstawia strukturę wewnętrzną pamięci FRAM typu MB85RC256V. Składa się ona z następujących bloków funkcjonalnych:
- Control Circuit (Jednostka sterująca) – odpowiada za kontrolę operacji odczytu i zapisu oraz komunikację z zewnętrznym światem. Odbiera dane z interfejsu I²C (linie SCL i SDA) oraz steruje pozostałymi blokami w celu wykonania odpowiednich operacji.
- Serial/Parallel Converter (Konwerter szeregowo-równoległy) – przekształca dane odbierane i wysyłane przez linię SDA (interfejs I²C) z formatu szeregowego na równoległy i odwrotnie, umożliwiając ich dalsze przetwarzanie wewnątrz układu.
- Address Counter (Licznik adresu) – zarządza aktualnym adresem odczytu lub zapisu danych. Umożliwia automatyczne inkrementowanie adresu, co jest przydatne przy sekwencyjnym dostępie do danych.
- FRAM Array (Macierz pamięci FRAM) – główny blok pamięci zorganizowany w formie macierzy 32 768 × 8 bitów (32 kB). Komórki pamięci są ułożone w wiersze i kolumny.
- Row Decoder (Dekoder wiersza) – na podstawie adresu wybiera odpowiedni wiersz w macierzy pamięci.
- Column Decoder / Sense Amp / Write Amp (Dekoder kolumn / wzmacniacz odczytu / wzmacniacz zapisu) – dekoder kolumny wybiera odpowiednią kolumnę w macierzy. Wzmacniacz odczytu (Sense Amplifier) wzmacnia bardzo słabe sygnały elektryczne odczytane z komórek pamięci do poziomów logicznych „0” lub „1”. Wzmacniacz zapisu (Write Amplifier) generuje odpowiednio silne sygnały, aby skutecznie zapisać dane do komórek FRAM.
Bloki Sense Amp i Write Amp pełnią kluczowe funkcje wzmacniania sygnałów odczytu i zapisu. Komórki pamięci FRAM przechowują dane jako bardzo małe sygnały elektryczne, dlatego:
-
Przy odczycie: sygnał jest zbyt słaby, by mógł być bezpośrednio użyty – dlatego wymaga wzmocnienia przez sense amplifier.
-
Przy zapisie: konieczne jest dostarczenie odpowiednio silnego sygnału, który zmieni stan polaryzacji materiału ferroelektrycznego, co umożliwia trwałe zapisanie informacji.
Podobne rozwiązania stosuje się także w innych technologiach pamięci, takich jak SRAM i DRAM, jednak w FRAM nie występuje konieczność cyklicznego odświeżania danych ani ograniczenie liczby cykli zapisu.
Piny:
- A0 do A2 są to piny adresowe. Pozwalają na podłączenie do 8 urządzeń na jednej magistrali. Gdy niepodłączone są podciągane do "L" (zero).
- VSS to masa układu.
- SDA linia danych I2C.
- SCL linia zegara I2C.
- WP to zabezpieczenie przed zapisem. Gdy ustawione wysoko to zapis jest zablokowany. Dla stanu L zapis jest dozwolony. Wewnętrznie jest podciągnięty do L, czyli zapis dozwolony.
- VDD zasilanie układu.
Dużą zaletą pamięci tego typu jest brak konieczności oczekiwania po wykonaniu operacji zapisu. Zwiększa to prędkość pracy i efektywność zapisu danych.
Programownie:
Poniżej przygotuję dwa programy, jeden dla płytki ESP32 z Adruino. Drugi dla STM32H7.
Dla ESP32 z Arduino nie ma specjalnej filozofii, instaluje odpowiednia bibliotekę podłączam SDA, SCL i zasilanie po czym przechodzę do odczytu:
- #include <Wire.h>
- #include <Adafruit_FRAM_I2C.h>
- Adafruit_FRAM_I2C fram = Adafruit_FRAM_I2C();
- void setup() {
- Serial.begin(115200);
- if (fram.begin()) {
- Serial.println("FRAM znaleziony!");
- } else {
- Serial.println("Nie znaleziono FRAM!");
- while (1);
- }
- fram.write(0, 42); // Zapisz wartość 42 pod adresem 0
- fram.write(1, 55); // Zapisz wartość 42 pod adresem 0
- fram.write(2, 19); // Zapisz wartość 42 pod adresem 0
- uint8_t val = fram.read(0); // Odczytaj wartość spod adresu 0
- uint8_t val1 = fram.read(1); // Odczytaj wartość spod adresu 0
- uint8_t val2 = fram.read(2); // Odczytaj wartość spod adresu 0
- Serial.print("Odczytana wartość: ");
- Serial.print(val);
- Serial.print(" ");
- Serial.print(val1);
- Serial.print(" ");
- Serial.println(val2);
- }
- void loop() {
- }
Biblioteka powyżej tworzy połączenie z pamięcią po I2C. Sprawdza czy adres urządzenia się zgadza,
domyślnie w bibliotece są opisane dwa:
- #define MB85RC_DEFAULT_ADDRESS \
- (0x50) ///<* 1010 + A2 + A1 + A0 = 0x50 default */
- #define MB85RC_SECONDARY_ADDRESS \
- (0x7C) ///< secondary ID for manufacture id info
W przypadku podłączenia wszystkich pinów A0 do A2 do GND adres kości to 0x50. Gdy wszyskie piny podłączone do VCC adres wynosi 0x57. Gdy A0 do VCC to adress 0x51 itp itd.
Biblioteka nie robi tutaj specjalnie dużo. Wobec tego można przygotowac całe oprogramowanie bez jej użycia:
- #include <Wire.h>
- #define FRAM_I2C_ADDRESS 0x50 // Domyślny adres MB85RC256V
- void writeFRAM(uint16_t addr, uint8_t data) {
- Wire.beginTransmission(FRAM_I2C_ADDRESS);
- Wire.write((addr >> 8) & 0xFF);
- Wire.write(addr & 0xFF);
- Wire.write(data);
- Wire.endTransmission();
- }
- uint8_t readFRAM(uint16_t addr) {
- Wire.beginTransmission(FRAM_I2C_ADDRESS);
- Wire.write((addr >> 8) & 0xFF);
- Wire.write(addr & 0xFF);
- Wire.endTransmission(false);
- Wire.requestFrom(FRAM_I2C_ADDRESS, 1);
- if (Wire.available()) {
- return Wire.read();
- }
- return 0;
- }
- void setup() {
- Serial.begin(115200);
- Wire.begin(21,22);
- writeFRAM(0, 42);
- writeFRAM(1, 55);
- writeFRAM(2, 19);
- uint8_t val0 = readFRAM(0);
- uint8_t val1 = readFRAM(1);
- uint8_t val2 = readFRAM(2);
- Serial.print("Odczytane wartości: ");
- Serial.print(val0);
- Serial.print(" ");
- Serial.print(val1);
- Serial.print(" ");
- Serial.println(val2);
- }
- void loop() {
- }
Powyższy kod wykonuje to samo co ten przedstawiony wcześniej, tylko nie korzysta z biblioteki Adafruit.
Teraz przejdę do kodu dla STM32. Tutaj nie ma nic specjalnie skomplikowanego, całość zapisu danych dotyczy komunikacji po I2C.
Inicjalizacja I2C w trybie standardowym:
- void MX_I2C1_Init(void)
- {
- hi2c1.Instance = I2C1;
- hi2c1.Init.Timing = 0x10C0ECFF;
- hi2c1.Init.OwnAddress1 = 0;
- hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
- hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
- hi2c1.Init.OwnAddress2 = 0;
- 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)
- {
- Error_Handler();
- }
- /** Configure Analogue filter */
- if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK)
- {
- Error_Handler();
- }
- /** Configure Digital filter */
- if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK)
- {
- Error_Handler();
- }
- }
Spróbowałem też uruchomić układ w trybie Fast Mode Plus. Całość działa bez problemu. Dla niego główną różnicą jest timing, który jest ustawiany na wartość:
- hi2c1.Init.Timing = 0x00401242;
Główną różnicą jest tutaj prędkość transmisji danych. Dla trybu standardowego wynosi ona 100kHz. Dla Fast+ jest to już 1MHz.
Timing odpowiada za konfigurację rejestru I2C_TIMINGR_register. Który rozkłada się w następujący sposób:
Bity | Nazwa | Nazwa |
---|---|---|
31-28 | Presc | Prescaler |
27-24 | ------ | ------- |
23-20 | SCLDEL | Opoznienie zbocza narastajacego SCL do SDA |
19-16 | SDADEL | Opooznienie SDA do SCL zbocza narastajacego |
15-8 | SCLH | Czas SCL w stanie wysokim |
7-0 | SCLL | Czas SCL w stanie niskim |
Funckje zapisu danych:
- uint8_t FRAM_WriteByte(const uint16_t devAddr, uint16_t memAddress, uint8_t valueToWrite) {
- return HAL_I2C_Mem_Write(
- &hi2c1,
- MB85RC_ADDRESS_DEFAULT,
- memAddress,
- I2C_MEMADD_SIZE_16BIT,
- &valueToWrite,
- 1,
- 100
- );
- }
- uint8_t FRAM_WriteBlock(uint16_t memAddress, uint8_t *data, uint16_t length) {
- return HAL_I2C_Mem_Write(
- &hi2c1,
- MB85RC_ADDRESS_DEFAULT,
- memAddress,
- I2C_MEMADD_SIZE_16BIT,
- data,
- length,
- 100
- );
- }
W funkcjach należy pamiętać o 16 bitowym adresowaniu (I2C_MEMADD_SIZE_16BIT).
Podobnie wyglądają funkcje odczytu:
- uint8_t FRAM_ReadByte(const uint16_t devAddr, uint16_t memAddress, uint8_t *valueToRead) {
- return HAL_I2C_Mem_Read(
- &hi2c1,
- MB85RC_ADDRESS_A000,
- memAddress,
- I2C_MEMADD_SIZE_16BIT,
- valueToRead,
- 1,
- 100
- );
- }
- uint8_t FRAM_ReadBlock(uint16_t memAddress, uint8_t *buffer, uint16_t length) {
- return HAL_I2C_Mem_Read(
- &hi2c1,
- MB85RC_ADDRESS_A000,
- memAddress,
- I2C_MEMADD_SIZE_16BIT,
- buffer,
- length,
- 100
- );
- }