Skocz do zawartości

Optymalizacja kodu ADC + DMA + odczyt pięciu kości EEPROM przez I2C


DeadGeneratio

Pomocna odpowiedź

Dzień dobry. Mam kilka pytań, może ktoś ma dobre poradniki, pomysły bądź książki dotyczące optymalizacji kodu. To co aktualnie napisałem to lepianka z brązowej mazi trzymająca się na słomie, aczkolwiek działa. Zacznę od samego ADC. Docelowo wykorzystam szesnaście kanałów a więc wszystkie jakie są dostępne w ADC - płytka to stm32 F407 DISC 1. Celem jest budowa korektora graficznego opartego na cyfrowych filtrach. Siedem potencjometrów będzie kontrolowało poszczególne częstotliwości dla lewego kanału dźwięku, kolejne siedem tak samo z prawym, piętnasty kanał będzie odpowiadał za balas lewo - prawo, i ostatni, szesnasty za kontrolę głośności. Aktualnie kod pobiera dane w trybie DMA, jednak niepokoi mnie to, że pomimo braku ruchu gałką potencjometru i tak wykonywane są kolejne przerwania w funkcji HAL_ADC_ConvCpltCallback. Jest to oczywiste - następuje wykonanie tej funkcji za każdym razem, gdy zostanie zapełniony wcześniej wskazany bufor. Tutaj pierwsze pytanie - czy jest jakaś opcja aby załączyć takie przerwanie dopiero w momencie wykrycia zmiany wartości na jakimkolwiek kanale, czy też raczej iść w stronę dodania dodatkowego przycisku na korektorze w stylu "aktualizuj wartość filtrów". Niemniej, i tak trzeba wyłączyć taką funkcję do czasu wykrycia zmiany przycisku bądź dźwigni. Preferowałbym zostanie przy DMA z racji na brak udziału procesora w zapisywaniu tych wartości do pamięci - będę potrzebował spory zapas mocy na późniejsze cyfrowe przetwarzanie dźwięku.

 

Drugie pytanie - jak przekazać tablicę do funkcji od razu w całości zamiast dzielenia na poszczególne kawałki. Zamiast w przerwaniu przykładowo w pętli for wywoływać funkcję kilkunastokrotnie, od razu przekazać macierz, i uzyskać wynik w postaci całej macierzy za jednym zamachem. Przykład tego kodu, z którego oczywiście nie jestem dumny, ale pozwolił sprawdzić podstawowe działanie na dwóch potencjometrach poniżej:

/* USER CODE BEGIN 4 */
uint8_t checkPotentiometer(uint8_t temp){
	int j = 0;
	if(temp >=0 && temp < 11){
		j = 0;
	}else if(temp >=11 && temp < 22){
		j = 20;
	}else if(temp >=22 && temp < 33){
		j = 40;
	}else if(temp >=33 && temp < 44){
		j = 60;
	}else if(temp >=44 && temp < 55){
		j = 80;
	}else if(temp >=55 && temp < 64){
		j = 100;
	}
	return j;
}


void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    Filter30Hz = checkPotentiometer(l_potentiometer[0]);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz, 0xFF, &checkSum[0], sizeof(checkSum[0]), HAL_MAX_DELAY);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz+4, 0xFF, &checkSum[1], sizeof(checkSum[1]), HAL_MAX_DELAY);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz+8, 0xFF, &checkSum[2], sizeof(checkSum[2]), HAL_MAX_DELAY);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz+12, 0xFF, &checkSum[3], sizeof(checkSum[3]), HAL_MAX_DELAY);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz+16, 0xFF, &checkSum[4], sizeof(checkSum[4]), HAL_MAX_DELAY);
    Filter60Hz = checkPotentiometer(l_potentiometer[1]);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz, 0xFF, &checkSum[5], sizeof(checkSum[5]), HAL_MAX_DELAY);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz+124, 0xFF, &checkSum[6], sizeof(checkSum[6]), HAL_MAX_DELAY);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz+128, 0xFF, &checkSum[7], sizeof(checkSum[7]), HAL_MAX_DELAY);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz+132, 0xFF, &checkSum[8], sizeof(checkSum[8]), HAL_MAX_DELAY);
    HAL_I2C_Mem_Read(&hi2c1, FirstEEPROM, Filter30Hz+136, 0xFF, &checkSum[9], sizeof(checkSum[9]), HAL_MAX_DELAY);
	}
	liczbaPrzerwan++;
}

/* USER CODE END 4 */

Liczba przerwań pozwoliła mi na sprawdzenie ile razy jest wykonywany ten kod - wychodzi na to, że w ciągu sekundy około 1000 razy. Niepotrzebne marnowanie zasobów mikrokontrolera.

Do głowy przychodzi mi pomysł wyzwalania ADC poprzez Timer ustawiony przykładowo na sekundowe odliczanie, aczkolwiek to i tak marnowanie energii na robienie tego samego, gdy urządzenie będzie stało w miejscu przez godziny bez zmiany parametrów.

Link do komentarza
Share on other sites

(edytowany)

Mały update: poprawiłem znacząco obsługę ADC + DMA - teraz działa na podstawie wyzwalania z wewnętrznego timera. Póki co, działa konwersja wartości uzyskanych z kanałów na określoną przez mnie liczbę, sporym ułatwieniem jest to, że kod napisałem w taki sposób, aby można było wykonać za jednym zamachem n-przekształceń, zamiast ciągle odwoływać się w funkcji do poszczególnych wartości z przerwania. Aktualnie kod wykonuje się co sekundę, w międzyczasie DMA jest nieaktywne, nie są wykorzystywane żadne zasoby CPU. Jeśli coś można tu poprawić, proszę o komentarz. Kod i screeny poniżej:

/* USER CODE BEGIN PD */
#define n_channels 2 // definiowanie liczby wykorzystywanych kanałów
/* USER CODE END PD */
  
/* USER CODE BEGIN PV */

uint8_t adc_buffer[n_channels];
uint8_t value[n_channels];
uint32_t interrupt_val = 0;

/* USER CODE END PV */

/* USER CODE BEGIN 2 */

  HAL_ADC_Start_DMA(&hadc3, adc_buffer, n_channels);
  HAL_TIM_Base_Start_IT(&htim2);

/* USER CODE END 2 */

/* USER CODE BEGIN 4 */

void convertADCdata(){
	for(int i=0; i<=sizeof(adc_buffer)-1;i++){
		uint8_t statement = 1;
		uint8_t step = 1;
		do{
			if(adc_buffer[i] > 6 * step){ // 6 <- mnożnik umożliwiający zmianę zakresu przetworzonej wartości, tutaj 0-11
				step++;
			}else{
				value[i] = step;
				statement = 0;
			}
		}while(statement != 0);
	}
}
/*
void updateFilters(){

}
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc){
	convertADCdata();
	//updateFilters();
	interrupt_val++;
}

/* USER CODE END 4 */

 

Screen.png

Edytowano przez DeadGeneratio
Link do komentarza
Share on other sites

@DeadGeneratio a może zmniejsz częstotliwość taktowania ADC, ustaw tak żeby liczba cykli potrzebna do spróbkowania przetwornikiem wyszła np 1kHz. daj do DMA 16 elementową tablicę, circular mode. Jak zapamięta wszystko to dostaniesz przerwanie - jedno co 16ms.

Jeżeli chcesz filtrację to możliwe że masz jakieś sprzętowe opcje. Może daj bufor 16*liczba_próbek_do_filtracji np 8 -> 16*8 i daj to całe na DMA. Wtedy jeszcze rzadziej dostaniesz przerwanie a jak już przyjdzie to możesz sobie uśrednić te pomiary w przerwaniu i głównym kodzie mieć już tablicę tylko 16 przefiltrowanych wartości.

Dnia 23.05.2023 o 17:13, DeadGeneratio napisał:

Drugie pytanie - jak przekazać tablicę do funkcji od razu w całości zamiast dzielenia na poszczególne kawałk

Tablica jest wskaźnikiem, przekaż wskaźnik do funkcji i rozmiar. Rozmiar albo jako stała/define dla całego kodu, albo jako kolejny argument. Obejrzyj jak wygląda funkcja np. memcpy

  • Lubię! 2
Link do komentarza
Share on other sites

Wydaje mi się, że nie zerknąłeś na mój komentarz poniżej. Czy teraz nie jest to jeszcze lepiej wykonywane aniżeli zmniejszenie taktowania ADC? Dzięki temu, że jest uruchamiany co jakiś czas, nie są marnowane zasoby, a nie zmieniając taktowania pomiary są wykonywane szybciej. Nie jestem pewien czy dobrze myślę, ale gdybym ustawił faktycznie przerwanie co 16 ms to kod mógł by się zaczął krzaczyć, będę musiał zaprzęgnąć I2S z zegarem 96kHz + master clock nie pamiętam ile, ale kilka razy więcej. Będzie to działało na przerwaniach więc nie jestem pewien czy to dobry pomysł czekać tak długo na ADC, w momencie, gdy dane do I2S muszą być przekazywane w sposób ciągły.

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

@DeadGeneratio może trochę zbyt pobieżnie obejrzałem. Przeczytałem jeszcze raz.

Wracając do pierwszego wpisu, ja bym się nie przejmował że ADC odczytuje niezmieniające się napiecia na potencjometrach - skąd ADC ma wiedzieć że coś się zmieniło jak nie sprawdził. 15 kanałów to 15 konwersji jedną jednostką przetwornika. Ale ok, możesz je przesłuchiwać z zadaną częstotliwością wyzwalając DMA timerem, to będzie super. Daj wtedy max częstotliwość na ADC. Tylko uwaga! Dobierz liczbę cykli próbkowania do impedancji na każdym z kanałów, załóż najgorszy wariant. Im większa impedacja tym więcej cykli zegara na spróbkowanie. Poczytaj jak działa przetwornik SAR w stmkach.

 

Patrzę na twój kod z drugiego wpisu, widzę że masz jakiś timer i adc. Masz funkcję callback od zakończenia konwersji ADC.

Nie jestem ekspertem, ale przyszło mi trochę pracować w STMach. Załóżmy że chcesz pomiary co 10ms i pomiar 15 wartości jest nieporównywalnie krótszy w stosunku do tych 10ms. Jakbym miał to zrobić to bym zrobił tak:

  • bufor 15 wartości,
  • timer basic odpalony z przerwaniem "start_base_timer_it"
  • w funkcji obsługi przepełnienia timera dajesz wyzwolenie DMA z argumentem na bufor 15 wartosci
  • dma single perip->memory, pewnie 16bit danych.
  • w adc ustaw te 15 kanałów "rank 1,2,3..."
  • przerwanie od timera wyższy priorytet od tego od dma
  • obsłuż callback od dma, nie od ADC. W tej funkcji przekonwertuj dane - zajmie to stosunkowo malo czasu.

Żeby nie miec ifa możesz dać strukturę z unią:

typedef struct measurements_t {
  union {
    uint16_t buffer[15];
    struct {
      uint16_t left[7];
      uint16_t right[7];
      uint16_t balance;
    };
  };
} measurements_t;

union to współdzielenie pamięci. Pamiętaj tylko o problemie indianowości.

A i zapoznaj się z 19 stroną https://www.st.com/resource/en/datasheet/stm32f407ig.pdf tu masz jaki kanał DMA łączy się z ADC i dalej z pamięcią. W niektórych STMkach niekażda pamięć łączy się z peryferiami.

Edytowano przez Gieneq
  • Lubię! 2
Link do komentarza
Share on other sites

Dnia 24.05.2023 o 22:22, DeadGeneratio napisał:

Mały update: poprawiłem znacząco obsługę ADC + DMA - ...

A w ogóle jest Ci do czegoś potrzebna informacja o zakończeniu konwersji? Bo na mój gust można w ogóle wyłączyć przerwania z ADC/DMA, niech sobie działa w tle. I po prostu wyliczaj filtry na bazie aktualnych wskazań potencjometrów - ciągle.

Druga sprawa, ten straszny "elseIf" w checkPotentiometer, da się uprościć do:

uint8_t checkPotentiometer(uint8_t temp){
	return ((temp * 5) / 55) * 20;
}

 

  • Lubię! 2
Link do komentarza
Share on other sites

35 minut temu, kaworu napisał:

Druga sprawa, ten straszny "elseIf" w checkPotentiometer, da się uprościć do

Z poprawką na wartość z poza zakresu, kiedy input > 63

  • Lubię! 2
Link do komentarza
Share on other sites

(edytowany)
Dnia 26.05.2023 o 08:49, Gieneq napisał:
typedef struct measurements_t {
  union {
    uint16_t buffer[15];
    struct {
      uint16_t left[7];
      uint16_t right[7];
      uint16_t balance;
    };
  };
} measurements_t;

Zaraz poszukam dobrych źródeł na temat struktur i przeczytam datasheet który podesłałeś, ale czy przypadkiem nie wchodzę już w programowanie obiektowe, które jest można powiedzieć perełką języka C++? Ponoć w zwykłym C nie istnieje pojęcie obiektów, struktur i klas.

 

Poprawka, przeczytałem, że istnieją struktury w zwykłym C, ale klasy już nie. Stąd moje wcześniejsze pytanie. Natomiast odnośnie tego strasznego if else, poprawiłem już ten kod - w drugim komentarzu checkPotentiometer() został zamieniony na convertADCdata()

Edytowano przez DeadGeneratio
Link do komentarza
Share on other sites

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.