Kurs STM32 LL cz. 20. Komunikacja I2C w trybie DMA
Komunikacja z pamięcią EEPROM w przypadku dużych ilości danych może być obciążająca dla jednostki CPU. Warto wtedy skorzystać z kontrolera DMA i bezpośredniego transferu danych.
Lista lekcji „Kurs STM32 Low Layer”
- Kurs STM32 LL cz. 1. Biblioteki Low Layer, Nucleo-G071RB, STM32CubeIDE
- Kurs STM32 LL cz. 2. Przygotowanie projektu
- Kurs STM32 LL cz. 3. Wewnętrzne i zewnętrzne źródła zegara
- Kurs STM32 LL cz. 4. Pętla PLL i taktowanie układów peryferyjnych
- Kurs STM32 LL cz. 5. Budowa GPIO i sterowanie wyjściem
- Kurs STM32 LL cz. 6. Wyjście GPIO i przerwania EXTI
- Kurs STM32 LL cz. 7. Interfejs USART, transmisja danych w trybie polling
- Kurs STM32 LL cz. 8. Komunikacja USART w trybie przerwań
- Kurs STM32 LL cz. 9. Kontroler DMA, komunikacja USART w trybie DMA
- Kurs STM32 LL cz. 10. Rodzaje i budowa Timerów, Timer w funkcji licznika
- Kurs STM32 LL cz. 11. Timer w trybie Input Capture
- Kurs STM32 LL cz. 12. Timer w trybie Output Compare i PWM
- Kurs STM32 LL cz. 13. Wstęp do konwertera ADC
- Kurs STM32 LL cz. 14. Konwersja ADC Single Channel i Multi Channel w trybie Polling
- Kurs STM32 LL cz. 15. Konwersja ADC Single Channel i Multi Channel w trybie przerwań
- Kurs STM32 LL cz. 16. Konwersja ADC Single Channel i Multi Channel w trybie DMA
- Kurs STM32 LL cz. 17. Wstęp do magistrali I2C
- Kurs STM32 LL cz. 18. Komunikacja I2C w trybie polling
- Kurs STM32 LL cz. 19. Komunikacja I2C w trybie przerwań
- Kurs STM32 LL cz. 20. Komunikacja I2C w trybie DMA
- Kurs STM32 LL cz. 21. Wprowadzenie do interfejsu SPI
- Kurs STM32 LL cz. 22. Komunikacja SPI w trybie polling
- Kurs STM32 LL cz. 23. Komunikacja SPI w trybie przerwań
- Kurs STM32 LL cz. 24. Komunikacja SPI w trybie DMA
Kontroler DMA
Direct Memory Access (DMA), czyli mechanizm bezpośredniego dostępu do pamięci to technika przesyłania danych z pominięciem jednostki CPU.
DMA pozwala na przesyłanie danych:
- z pamięci do pamięci
- z pamięci do układu peryferyjnego
- z układu peryferyjnego do pamięci
- z układu peryferyjnego do układu peryferyjnego
W mikrokontrolerach STM32 możemy mieć do dyspozycji jeden lub dwa kontrolery DMA. Każdy kontroler ma kilka kanałów, a każdy kanał może obsługiwać różne rodzaje transferów. W STM32G071 dostępny jest jeden kontroler DMA1 z 7 kanałami.
To, jak zbudowany jest przepływ danych w STM32, przedstawia poniższa grafika.
Kontroler DMA podłączony jest do szyny AHB (Advanced High-Performance Bus), która komunikuje go z pamięcią SRAM i Flash oraz za pośrednictwem szyny APB (Advanced Peripheral Bus) z układami peryferyjnymi. Układem nadzorującym współpracę między szynami jest Bus Matrix.
W przypadku, gdy dwa kanały DMA chcą wykonywać transfer jednocześnie, Arbiter rozdziela czas dostępu do Bus Matrix dla każdego kanału.
Gdy dostępne są dwa oddzielne kontrolery DMA, mogą one korzystać z Bux Matrix jednocześnie, dopóki nie korzystają z tych samych układów peryferyjnych.
Szyna AHB implementuje algorytm karuzelowy (round robin), według którego przydziela dla każdego z układów przedział czasowy na wykonanie operacji, nie uwzględniając żadnych priorytetów. Kontroler DMA może więc spowolnić pracę CPU, jeżeli oba elementy potrzebują dostępu do tego samego obszaru pamięci lub układu peryferyjnego (zdarza się to bardzo rzadko). Nie może jednak zablokować CPU, ponieważ arbiter rozdziela po równo cykle pracy pomiędzy poszczególne elementy mikrokontrolera.
W starszych mikrokontrolerach (seria F i L) kontroler DMA podzielony jest na kanały. Do każdego z kanałów dołączonych jest kilka układów peryferyjnych i tylko jeden z nich może być jednocześnie obsługiwany jest dany kanał DMA.
W nowszych układach (seria G) do każdego z kanałów można podpiąć dowolny układ peryferyjny z dostępnych. Za zarządzanie odpowiada DMAMUX. DMAMUX pozwala także na użycie generatora DMA do układów peryferyjnych, które nie mają funkcji DMA.
DMA generuje trzy rodzaje przerwań:
- po wykonaniu połowy transferu
- po wykonaniu całego transferu
- w przypadku błędu
DMA może przesyłać dane po 8, 16 lub 32 bity (odpowiednio Byte, Half Word i Word). Szerokość danych zależna jest od rodzaju wysyłanych przez nas danych (po stronie pamięci) oraz od rozmiaru rejestru. Dodatkowo przed rozpoczęciem transferu DMA ustalany jest rozmiar, czyli ilość danych do przesłania.
Każdy transfer DMA może mieć przypisany jeden z 4 priorytetów, według których kontroler wykonuje transfery w przypadku, gdy jednocześnie wystąpi potrzeba obsługi dwóch lub więcej kanałów.
- very high
- high
- medium
- low
Poza tym kontroler DMA ma możliwość automatycznej inkrementacji adresów po stronie układu peryferyjnego i pamięci oraz możliwość cyklicznego transferu (po zakończeniu jednego rozpoczyna się kolejny od początku skonfigurowanego adresu – na zasadzie bufora kołowego). Pozwala to dobrać sposób przesyłania danych według potrzeb.
Rejestry DMA
Rejestr DMA_ISR – rejestr z flagami przerwań dla każdego z 7 kanałów
Rejestr DMA_IFCR – czyszczenie flag przerwań
DMA_CCRx – rejestr konfiguracyjny dla kanału x
Bity MEM2MEM – włączenie transferu pamięć-pamięć
Bit PL[1:0] – wybór priorytetu
Bity MSIZE[1:0] – rozmiar danych po stronie pamięci
Bity PSIZE[1:0] – rozmiar danych po stronie układu peryferyjnego
Bit MINC – włączenie zwiększania adresu po stronie pamięci
Bit PINC – włączenie zwiększania adresu po stronie układu peryferyjnego
Bit CIRC – tryb kołowy
Bit DIR – kierunek transferu
Bit TEIE – włączenie przerwania od błędu
Bit HTIE – włączenie przerwania od połowy transferu
Bit TCIE – włączenie przerwania od pełnego transferu
Bit EN – włączenie kanału
Rejestr DMA_CNDTRx – rozmiar transferu dla kanału x
DMA_CPARx – adres układu peryferyjnego dla kanału x
DMA_CMARx – adres układu pamięci dla kanału x
Multiplekser DMA
W układach z serii G do każdego z kanałów można podpiąć dowolny układ peryferyjny z puli dostępnych. Za zarządzanie przepływem danych odpowiada multiplekser, czyli DMAMUX.
DMAMUX ma za zadanie przekierować na wybrany kanał sygnał z układu peryferyjnego, którego chcemy użyć. Dzięki temu mamy dużą elastyczność pod względem używania DMA. Nie pojawi się problem często występujący w poprzednich seriach STM32 (np. F1), gdzie nie mogliśmy użyć dwóch peryferiów w trybie DMA, bo były podłączone do tego samego kanału.
W STM32G071 mamy do dyspozycji 77 wejść mogących korzystać z kontrolera DMA. Pełną listę przedstawia tabela.
Rodzaj wejścia dla kanału x konfigurujemy przy pomocy rejestru DMAMUX_CxCR i bitów DMAREQ_ID[6:0].
DMA w magistrali I2C
DMA (Direct Memory Access) może być włączone do transmisji poprzez ustawienie bitu TXDMAEN w rejestrze I2C_CR1. Dane są ładowane z obszaru SRAM skonfigurowanego za pomocą peryferii DMA do rejestru I2C_TXDR, gdy ustawiony jest bit TXIS.
Analogicznie jest w przypadku odbierania. DMA można włączyć do odbioru przez ustawienie bitu RXDMAEN w rejestrze I2C_CR1. Dane są ładowane z rejestru I2C_RXDR do obszaru pamięci SRAM skonfigurowanego za pomocą peryferii DMA, gdy ustawiony jest bit RXNE.
Tak jak w przypadku przerwań, inicjalizacja, adres slave, kierunek, ilość bajtów i bit START są programowane ręcznie (adres slave nie może być przesyłany za pomocą DMA). Tylko dane są przesyłane za pomocą DMA.
Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.
[PROGRAM] Zapis danych do pamięci EEPROM w trybie DMA
Korzystając z DMA możemy jeszcze bardziej zautomatyzować przesyłanie danych do pamięci EEPROM. Dzisiaj pokaże Ci, jak to skonfigurować.
Inicjalizacja I2C będzie wyglądała bardzo podobnie jak dotychczas. Konfigurujemy piny SDA i SCL w trybie funkcji alternatywnej jako wyjścia Open-Drain.
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
LL_GPIO_SetPinSpeed(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinOutputType(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_OUTPUT_OPENDRAIN);
LL_GPIO_SetPinPull(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetAFPin_8_15(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_AF_6);
LL_GPIO_SetPinMode(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetPinOutputType(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_OUTPUT_OPENDRAIN);
LL_GPIO_SetPinPull(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_SPEED_FREQ_VERY_HIGH);
LL_GPIO_SetAFPin_8_15(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_AF_6);
LL_GPIO_SetPinMode(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_MODE_ALTERNATE);
Następnie włączamy taktowanie I2C.
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);
Analogicznie jak w poprzedniej części, konfigurujemy filtry, rejestr TIMINGR, oraz tryb pracy I2C przy wyłączonym peryferium. Następnie włączamy I2C.
LL_I2C_Disable(i2c);
LL_I2C_ConfigFilters(i2c, LL_I2C_ANALOGFILTER_ENABLE, 0x00);
LL_I2C_SetTiming(i2c, 0x10707DBC);
LL_I2C_EnableClockStretching(i2c);
LL_I2C_SetMode(i2c, LL_I2C_MODE_I2C);
LL_I2C_Enable(i2c);
Teraz przechodzimy do konfiguracji DMA. Włączamy taktowanie dla kontrolera.
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
Następnie konfigurujemy parametry transferu czyli peryferium I2C1_TX. Wybieramy kierunek transmisji z pamięci do układu peryferyjnego oraz priorytet. Ustawiamy również tryb jako normalny (nie cykliczny) oraz konfigurujemy automatyczną inkrementację adresów dla pamięci. Korzystamy z wartości o szerokości bajta, dlatego wybieramy wyrównanie DATAALIGN_BYTE zarówno po stronie pamięci, jak i rejestru I2C.
LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_I2C1_TX);
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_HIGH);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_NORMAL);
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_BYTE);
Teraz włączamy przerwania od zakończenia transmisji DMA.
NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);
Wysyłanie rozpoczynamy od “ręcznego” ustawienia adresu i wygenerowania START.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, (uint32_t)(size+1), LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_WRITE);
Następnie wysyłamy jeden bajt z adresem komórki pamięci EEPROM.
while(LL_I2C_IsActiveFlag_TXIS(i2c) == 0)
;
LL_I2C_TransmitData8(i2c, (uint8_t)reg_addr);
Teraz konfigurujemy resztę wysyłania przy pomocy DMA. Ustawimy adres danych do wysłania oraz adres rejestru I2C1_TX. Wybieramy również kierunek transmisji.
LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, (uint32_t)tx_buffer.data_ptr, LL_I2C_DMA_GetRegAddr(I2C1, LL_I2C_DMA_REG_DATA_TRANSMIT), LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1));
Następnie podajemy ilość danych do wysłania przez DMA.
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, tx_buffer.count);
Teraz włączamy DMA w rejestrze I2C oraz włączamy kanał 1 w kontrolerze DMA.
LL_I2C_EnableDMAReq_TX(I2C1);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);
Po zakończeniu transferu otrzymamy przerwanie od Transfer Complete DMA1, w którym możemy wyczyścić flagę STOP.
void DMA_Channel1_IRQHandler(void)
{
if (LL_DMA_IsActiveFlag_TC1(DMA1))
{
LL_DMA_ClearFlag_TC1(DMA1);
LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1);
LL_I2C_ClearFlag_STOP(i2c);
}
}
Aby zapisać dane do pamięci EEPROM, wystarczy wywołać funkcję.
i2c_reg_write_dma(EEPROM_ADDR, 0x00, data_w, sizeof(data_w));
[PROGRAM] Odczyt danych z pamięci EEPROM w trybie DMA
Inicjalizacja I2C w przypadku odbioru będzie wyglądała bardzo podobnie jak dla wysyłania danych. Konfigurujemy piny SDA i SCL w trybie funkcji alternatywnej jako wyjścia Open-Drain.
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);
LL_GPIO_SetPinSpeed(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinOutputType(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_OUTPUT_OPENDRAIN);
LL_GPIO_SetPinPull(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetAFPin_8_15(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_AF_6);
LL_GPIO_SetPinMode(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetPinOutputType(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_OUTPUT_OPENDRAIN);
LL_GPIO_SetPinPull(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_SPEED_FREQ_VERY_HIGH);
LL_GPIO_SetAFPin_8_15(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_AF_6);
LL_GPIO_SetPinMode(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, LL_GPIO_MODE_ALTERNATE);
Następnie włączamy taktowanie I2C.
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);
Analogicznie jak w poprzedniej części, konfigurujemy filtry, rejestr TIMINGR, oraz tryb pracy I2C przy wyłączonym peryferium. Następnie włączamy I2C.
LL_I2C_Disable(i2c);
LL_I2C_ConfigFilters(i2c, LL_I2C_ANALOGFILTER_ENABLE, 0x00);
LL_I2C_SetTiming(i2c, 0x10707DBC);
LL_I2C_EnableClockStretching(i2c);
LL_I2C_SetMode(i2c, LL_I2C_MODE_I2C);
LL_I2C_Enable(i2c);
Teraz przechodzimy do konfiguracji DMA. Włączamy taktowanie dla kontrolera.
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
Następnie konfigurujemy parametry transferu czyli peryferium I2C1_RX. Wybieramy kierunek transmisji z układu peryferyjnego do pamięci oraz priorytet. Ustawiamy również tryb jako normalny (nie cykliczny) oraz konfigurujemy automatyczną inkrementację adresów dla pamięci. Korzystamy z wartości o szerokości bajta, dlatego wybieramy wyrównanie DATAALIGN_BYTE zarówno po stronie pamięci, jak i rejestru I2C.
LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_I2C1_RX);
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_HIGH);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_NORMAL);
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_BYTE);
Teraz włączamy przerwania od zakończenia transferu DMA.
NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);
Wysyłanie rozpoczynamy od “ręcznego” ustawienia adresu i wygenerowania START.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, 1, LL_I2C_MODE_SOFTEND, LL_I2C_GENERATE_START_WRITE);
Następnie wysyłamy jeden bajt z adresem komórki pamięci EEPROM i czekamy na koniec transmisji.
while(LL_I2C_IsActiveFlag_TXIS(i2c) == 0)
;
LL_I2C_TransmitData8(i2c, (uint8_t)reg_addr);
while(LL_I2C_IsActiveFlag_TC(i2c) == 0)
;
Teraz konfigurujemy odbieranie danych.
LL_I2C_HandleTransfer(i2c, slave_addr, LL_I2C_ADDRSLAVE_7BIT, (uint32_t)size, LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_READ);
Dane przeniesiemy z rejestru I2C do pamięci przy pomocy DMA. Ustawimy adres danych odebranych oraz adres rejestru I2C1_RX. Wybieramy również kierunek transmisji.
LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, LL_I2C_DMA_GetRegAddr(I2C1, LL_I2C_DMA_REG_DATA_RECEIVE), (uint32_t)rx_buffer.data_ptr, LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1));
Następnie podajemy ilość danych do odebrania przez DMA.
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, rx_buffer.count);
Teraz włączamy DMA w rejestrze I2C oraz włączamy kanał 1 w kontrolerze DMA.
LL_I2C_EnableDMAReq_RX(I2C1);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);
Po zakończeniu transferu otrzymamy przerwanie od Transfer Complete DMA1, w którym możemy wyczyścić flagę STOP.
void DMA_Channel1_IRQHandler(void)
{
if (LL_DMA_IsActiveFlag_TC1(DMA1))
{
LL_DMA_ClearFlag_TC1(DMA1);
LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1);
LL_I2C_ClearFlag_STOP(i2c);
}
}
Teraz wystarczy wywołać funkcję odczytu danych z pamięci.
i2c_reg_read_dma(EEPROM_ADDR, 0x00, data_r, sizeof(data_r));
Chciałbyś otrzymywać na bieżąco informacje o nowych artykułach z kursu? Zapisz się do newslettera!
TO NIE TYLKO MAIL Z INFORMACJĄ O NOWEJ LEKCJI, ALE TAKŻE DODATKOWE MATERIAŁY. NIE PRZEGAP NOWEJ TREŚCI I DODATKOWYCH BONUSÓW. PRZEJDŹ DO STRONY KURSU I PODAJ SWÓJ ADRES E-MAIL. NIE ZAPOMNIJ POTWIERDZIĆ CHĘCI DOŁĄCZENIA W PIERWSZEJ WIADOMOŚCI!