Wykrywanie gestów 1D – analogowy czujnik SHARP i STM32Cube.AI

Po krótkim wstępie teoretycznym na temat sztucznej inteligencji, STM32Cube.AI i pakietu Keras, czas na to co inżynier lubi najbardziej, czyli praktyka. Na początek zajmiemy się analogowym czujnikiem odległości SHARP i prostym wykrywaniem gestów 1D.

Czujnik SHARP GP2Y0A21YK0F był już obsługiwany przeze mnie w jednym z poprzednich artykułów pt. „Analogowy czujnik odległości SHARP„. Jeżeli nie wiesz, w jaki sposób działa jest ten czujnik, warto zajrzeć do tego artykułu przed przeczytaniem dzisiejszego wpisu. Analogicznie przykład zaimplementujemy dla mikrokontrolera STM32L476RG na zestawie Nucleo-L476RG w środowisku STM32CubeIDE.

Jestem świadomy, że czujnik SHARP nie jest najlepszym wyborem, jeżeli chodzi o gamę czujników odległości dostępną na rynku. Są znacznie lepsze sensory, np. czujniki ToF (Time of Flight). Chciałbym jednak pokazać, że nawet na gorszej jakości komponentach jesteśmy w stanie dzięki AI uzyskać zadowalające efekty.

W artykułach dotyczących sztucznej inteligencji materiał będzie podzielony zazwyczaj na trzy części:

  • pobieranie i przygotowanie danych treningowych
  • proces tworzenia i uczenia modelu sztucznej sieci neuronowej
  • import modelu i uruchomienie przykładu dla STM32.

Taki schemat jest dość naturalną koleją rzeczy przy pracach nad AI. Czasami pierwszy etap możemy pominąć, jeżeli skorzystamy z gotowej bazy danych.

Przygotowanie danych treningowych do procesu uczenia

Aby wykorzystać sieć neuronową, musimy najpierw ją przygotować i wytrenować. Do procesu uczenia wymagana jest pula danych treningowych – im większa i bogatsza w różne scenariusze, tym łatwiej jest osiągnąć dobre wyniki uczenia i działania sieci. Dane treningowe składają się z tensora wejściowego (wektor, macierz) oraz etykiet, które określają oczekiwany wynik działania sieci. Dzięki obu tym elementom w procesie uczenia jesteśmy w stanie dobrać wagi tak, aby sieć działała zgodnie z naszymi oczekiwaniami. Po zaimportowaniu sieci do docelowego projektu będziemy podawali już tylko tensor wejściowy, a sieć na podstawie wyuczonego algorytmu poda nam odpowiedź. Wiedząc już, jak będziemy pracować z AI, możemy przejść do przygotowania danych treningowych.

Do obsługi czujnika będziemy potrzebowali wejścia analogowego. W tej roli wykorzystamy kanał 5 przetwornika ADC1 (ADC1_IN5) podłączone do pinu PA0. Wybieramy „Analog->ADC1”, a następnie przy wejściu IN5 wybieramy tryb „IN5 Single-ended”. W naszym przypadku będziemy wykonywali pomiar tylko na jednym kanale. Aby zautomatyzować odczyt danych o odległości, wykorzystamy zewnętrzne źródło wyzwalania pomiarów w postaci timera, który co 40 ms generował sygnał dla ADC. Taką (mniej więcej) maksymalną częstotliwość próbkowania (25 Hz) oferuje nasz czujnik.

W celu poprawnego skonfigurowania przetwornika wybieramy zatem brak dodatkowego Prescalera, rozdzielczość 12-bitów oraz wyrównanie danych do prawej. Ponieważ mamy tylko jeden pomiar, nie będziemy potrzebowali trybu skanowania. Pomiar ADC będziemy chcieli mieć wywoływany co 40 ms przez timera, dlatego tryb ciągły też nie będzie nam potrzebny. Włączamy natomiast DMA Continuous Request Request i End Of Conversion Mode jako zakończenie pojedynczej konwersji.

W ustawieniach trybu regularnej konwersji, wybieramy zewnętrzne zdarzenie jako Timer 3 Trigger Out Event (czyli przepełnienie od timera 3) oraz zbocze narastające. W ustawieniach pomiaru wybieramy kanał 5 (wejście na pinie PA0) oraz najdłuższy czas konwersji, czyli 640,5 cykli, co zapewni nam dobrą dokładność. Pełna konfiguracja widoczna jest poniżej.

Żeby wykorzystać przesyłanie danych za pomocą DMA, w zakładce DMA Settings wybieramy ADC1 i DMA1 Channel 1. W ustawieniach może pozostać tryb Circular, bez inkrementacji adresów (mamy tylko jeden pomiar) oraz długość danych Half Word.

Na koniec włączamy jeszcze przerwania od DMA, dzięki czemu będziemy wiedzieli kiedy zakończył się pomiar, dane są gotowe w naszej zmiennej i można już obliczyć odległość.

Teraz powinniśmy skonfigurować jeszcze Timer 3 w taki sposób, aby wywoływał nam pomiar na ADC co 40 ms. Wybieramy zatem Timers->TIM3. W górnej części zaznaczamy Clock Source jako Internal Clock.

Aby skonfigurować licznik, musimy odpowiednio ustawić wartości: Prescaler i Counter Period. Potrzebujemy informacji o częstotliwości taktowania Timera 3. Mikrokontroler taktowany będzie z częstotliwością 80 MHz. Aby uzyskać częstotliwość 25 Hz ustawiamy wartości jak na grafice poniżej. Zaznaczamy również opcję Trigger Event Selection TRGO jako Update Event.

Będziemy korzystali z przycisku umieszczonego na płytce w trybie przerwania EXTI i interfejsu USART2 widocznego jako port COM w konfiguracji domyślnej tworzonej przy projekcie z Nucleo-L476RG, czyli w trybie asynchronicznym z prędkością transmisji 115200 bps.

Teraz możemy wygenerować projekt i przejść do napisania pierwszej części programu. Na początku musimy uruchomić odczyt ADC i obsłużyć przerwanie od DMA. Aby wystartować pomiar, uruchamiamy przetwornik ADC1 oraz TIMER 3 generujący sygnał do konwersji.

void ADC_start_data_measurement(void)
{
	  HAL_ADC_Start_DMA(&hadc1, &adc_measurement, 1);
	  HAL_TIM_Base_Start(&htim3);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
	if(adc_measurement == 0)
		return;

	distance_cm = CONVERT_ADC_TO_DISTANCE(adc_measurement);

	if(distance_cm > DISTANCE_MAX_VALUE)
	{
		distance_cm = DISTANCE_MAX_VALUE;
	}

	adc_data_is_ready = true;
}

Obsługujemy również przerwanie od przycisku. Posłuży nam on jako informacją, że wykonaliśmy gest, którego chcemy nauczyć sieć.

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == B1_Pin)
	{
		button_is_pushed = true;
	}
}

Następnie tworzymy funkcję do zbierania danych i wysyłania ich przez magistralę USART. Dane będziemy gromadzili w tablicy adc_data[] i dodawali kolejne elementy po odczytaniu nowych danych. Założyłem, że bufor będzie miał rozmiar 64 próbek, co przy próbkowaniu z częstotliwością 25 Hz da nam ok. 2,5 s odczytów. Po zapisaniu 64 danych do bufora będziemy jest przesyłali w formie ciągu znaków Ascii do komputera i zapisywali do pliku. Na końcu każdego bufora 64 danych będziemy dodawali linię z etykietą 0, gdy gest nie był wykonany lub 1, gdy go wykonaliśmy.

void ai_sharp_train_data_collect(void)
{
	uint8_t tx_buffer[TX_BUFFER_SIZE];
	static uint32_t adc_data[DATA_BUFFER_SIZE];
	uint32_t i;
	static uint32_t cnt = 0;
	int32_t size;

	if(true == adc_data_is_ready)
	{
		adc_data_is_ready = false;

		for(i=0; i<(DATA_BUFFER_SIZE-1); i++)
		{
			adc_data[i] = adc_data[i+1];
		}

		adc_data[DATA_BUFFER_SIZE-1] = distance_cm;

		cnt++;

		if(1 == button_is_pushed)
		{
			cnt = DATA_BUFFER_SIZE;
		}

		if(cnt >= DATA_BUFFER_SIZE)
		{
			cnt = 0;

			for(i=0; i<DATA_BUFFER_SIZE; i++ )
			{
			  size = sprintf((char *)tx_buffer, "%d,", (int)adc_data[i]);
			  HAL_UART_Transmit(&huart2, tx_buffer, size, 1);
			}

			if(true == button_is_pushed)
			{
				size = sprintf((char *)tx_buffer, "\n1\n");
				button_is_pushed = false;
			}
			else
			{
				size = sprintf((char *)tx_buffer, "\n0\n");
			}

			HAL_UART_Transmit(&huart2, tx_buffer, size, 1);
		}
	}
}

Aby uruchomić akwizycję danych, w funkcji main() dodajemy dwie linie – start odczytu przed pętlą while(1) oraz w pętli funkcję do zbierania danych.

/* USER CODE BEGIN 2 */
  ADC_start_data_measurement();

  /* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
  while (1)
  {
	  ai_sharp_train_data_collect();
    /* USER CODE END WHILE */
  }

Musimy jeszcze poprawnie podłączyć czujnik wg schematu poniżej.

Po uruchomieniu programu powinniśmy zapisać przesyłane przez USART2 dane do pliku. Do tego celu wykorzystamy terminal RealTerm. Potrzebna będzie podstawowa konfiguracja. W zakładce Display wybieramy Display As Ansi.

Następnie w zakładce Port wybieramy Baud 115200 oraz właściwy port COM. Następnie klikamy Open.

Aby zapisywać dane do pliku, w zakładce Capture wybieramy plik, do którego chcemy zapisać i klikamy Start Overwrite lub Start Append, w zależności czy chcemy nadpisać dane w pliku, czy nie. Od tego momentu wszystkie dane będziemy mieli logowane do pliku.

Przykładowe dwa rekordy danych zamieszczam poniżej.

27,30,30,31,30,30,31,30,31,31,31,31,31,31,31,31,31,31,31,31,27,31,30,27,30,30,30,30,30,30,29,29,29,29,29,29,29,29,28,28,26,28,28,26,28,28,28,28,28,28,28,28,27,27,26,25,25,24,24,24,22,24,24,24
0

29,29,29,29,29,29,29,29,28,28,26,28,28,26,28,28,28,28,28,28,28,28,27,27,26,25,25,24,24,24,22,24,24,24,24,24,25,25,23,21,16,12,8,8,10,14,19,24,29,32,29,30,26,20,16,10,10,12,15,18,22,25,25,28
1

W przypadku, gdy wykonamy gest, musimy wcisnąć przycisk. W trakcie generowanie danych treningowych pamiętajmy o tym, aby zebrać ich jak najwięcej, ale też aby reprezentowały różne ruchy, które mogą się wydarzyć w trakcie działania docelowej aplikacji. Dzięki temu sieć będzie w stanie lepiej wykrywać docelowe gesty i prawidłowo reagować na inne. Na poniższym filmie przedstawiam, jaki gest będzie wykrywany przez mój model.

Tworzenie modelu Keras i uczenie sieci neuronowej

Mamy już przygotowane dane treningowe, zatem pora przejść do stworzenia i uczenia modelu sieci. Jak wspominałem w poprzednim artykule pt. „Sztuczna inteligencja – wstęp do STM32Cube.AI i Keras„, tę część zadania będziemy realizowali za pomocą pakietu Keras i języka Python. Jeżeli nie zainstalowałeś jeszcze potrzebnych bibliotek, teraz jest na to najwyższa pora.

Kod programu do obróbki danych i tworzenia modelu będę pisał w notatnikach Jupyter (Jupyter Notebook) domyślnie dodanych przy instalacji Anaconda. Jest to dość wygodne narzędzie polecane do przetwarzania danych i obliczeń, ponieważ możemy tworzyć fragmenty kodu i uruchamiać je niezależnie, co znacznie skraca czas potrzebny na ich wykonanie – przykładowo dane możemy zaimportować raz na początku, a nie za każdym razem przy trenowaniu modelu.

Pierwszym etapem jest przygotowanie danych treningowych. Musimy je pobrać z pliku oraz odpowiednio przetworzyć. Na czym to przygotowanie danych ma polegać? Przede wszystkich potrzebujemy dwóch zbiorów danych: treningowego i testowego. Na zbiorze treningowych przeprowadzamy proces uczenia sieci, a na zbiorze testowym sprawdzamy, jak sieć radzi sobie z nowymi danymi. Sprawdzanie skuteczności działania sieci na zbiorze treningowym mijałoby się z celem – sieć widziała już te dane i jest na nie przygotowana. Nam zależy na tym, żeby sieć prawidłowo działała także na innych danych wejściowych.

Dzielimy zatem nasze dane na cztery tablice: dwie z danymi wejściowymi z czujnika (treningowe i testowe) oraz dwie z etykietami wyjściowymi. Ile powinno być danych testowych w porównaniu do treningowych? Istnieją różne podejścia. Spotkałem się z przykładami, gdzie było ich po równo (jeśli mamy duże zbiory danych), a innym razem danych testowych było kilka razy mniej niż treningowych (zazwyczaj na małych zbiorach danych). Ponieważ my pracujemy na niewielkim zbiorze danych (ja zebrałem 258 próbek, duży zbiór to np. kilkadziesiąt tysięcy), do zbioru testowego oddzielimy 10% próbek. Aby lepiej reprezentowały one wszystkie zdarzenia, nie będziemy brali pierwszy lub ostatnich 26 rekordów, ale wybierzemy do zbioru testowego co dziesiątą próbkę. Podziału wykonałem w sposób „ręczny”, aby dobrze widać było logikę postępowania.

import numpy as np

f = open('capture_AI_1.txt','r')

train_values = []
train_results = []

test_values = []
test_results = []

line_count = 0
record_count = 0

for line in f:
    line_count += 1
    fields = line.strip().split(',')
    if (record_count % 10) > 0:
        if line_count % 2 == 1:
            train_values.append(fields)            
        elif line_count % 2 == 0:   
            train_results.append(int(fields[0]))
            record_count += 1;
    elif (record_count % 10) == 0:
        if line_count % 2 == 1:
            test_values.append(fields)            
        elif line_count % 2 == 0:   
            test_results.append(int(fields[0]))
            record_count += 1;
        
f.close()

Rozmiar poszczególnych zbiorów możemy sprawdzić za pomocą funkcji len, np. len(train_values) lub len(test_values).

Ze względu na to, że sieci znacznie lepiej radzą sobie z przetwarzaniem małych liczb, dobrze byłoby przeprowadzić normalizację danych. Ma ona bardzo duże znaczenie, gdy jako dane wejściowe podajemy wartości z różnych zakresów (np. odległość od 0 do 100 oraz temperaturę od 20 do 50). Normalizacja pozwala ujednolicić dane. W tym celu musimy przekształcić nasze dane do typu float oraz podzielić je przez maksymalną wartość dla naszego pomiaru (80 cm), dzięki czemu otrzymamy dane z zakresu od 0 do 1.

train_data = []
train_labels = []
test_data = []
test_labels = []

train_data = np.array(train_values).astype('float32')
train_labels = np.array(train_results).astype('float32')

test_data = np.array(test_values).astype('float32') 
test_labels = np.array(test_results).astype('float32')


max_dis = 80
train_data /= max_dis
test_data /= max_dis

Teraz przechodzimy do utworzenia modelu i procesu uczenia sieci. Jest to dość rozbudowany proces, choć ilość linii kodu na to nie wskazuje. Sposób doboru liczby, typu warstw oraz parametrów uczenia jest dość intuicyjny i bazuje na doświadczeniu użytkownika z sieciami, uczeniem maszynowym i pakietem Keras. Parametry dobieramy na zasadzie metody „prób i błędów”, a im więcej sieci stworzysz i nauczysz, tym łatwiej będzie Ci ocenić już na początku, jakie parametry mogą być skuteczne w danym przypadku. Poniżej przedstawię moją propozycję budowy sieci i parametrów uczenia – jeżeli chcesz zgłębić ten temat, polecam książkę „Deep Learning. Praca z językiem Python i biblioteką Keras” Francois Chollet-a.

Do budowy modelu sieci użyjemy klasy Sequential. Ze względu na typ danych (jednowymiarowy wektor), zastosujemy warstwy typu Dense. W przykładzie użyłem dwóch warstw o rozmiarze 16 jednostek, z funkcją aktywacji relu. W pierwszej warstwie podajemy rozmiar tensora wejściowego (w naszym przypadku 64), w następnych warstwach rozmiar ten będzie już automatycznie dobierany na podstawie warstwy poprzedniej. Ostatnia warstwa (w naszym przypadku trzecia) powinna mieć rozmiar wyjściowy odpowiadający ilości rodzajów etykiet, czyli u nas 1 (mamy klasyfikator binarny).

from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(64,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

Teraz za pomocą funkcji compile konfigurujemy parametry uczenia sieci (optymalizator, funkcję straty oraz monitorowanie dokładności). Wybieramy funkcję straty entropi krzyżowej binarnej (mamy na wyjściu 0 lub 1) i optymalizator rmsprop.

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])

Teraz możemy wywołać proces uczenia. Wykonamy 50 epok (cykli uczenia) z wsadami danych po 8 próbek.

model.fit(train_data, train_labels, epochs=50, batch_size=8)

Na koniec sprawdzimy działanie sieci na testowym zbiorze danych.

results = model.evaluate(test_data, test_labels)

Utworzona w ten sposób sieć pozwala nam uzyskać dokładność na poziomie 0,92 %, co jest dość przyzwoitym wynikiem. Jeżeli stworzylibyśmy większy zbiór danych oraz dopracowali dobór parametrów uczenia, z pewnością bylibyśmy w stanie uzyskać jeszcze lepszy wynik.

Na koniec zapisujemy nasz model na dysku.

model.save('data_AI_1.h5')

Teraz czas na kluczowy moment, czyli import modelu do programu dla STM32.

Importowanie modelu i przykład dla STM32

Przed nami ostatni etap, czyli importowanie modelu i implementacja w kodzie programu. Wracamy zatem do naszego projektu w STM32CubeIDE i otwieramy konfigurator.

Przechodzimy do okna „Software Packs -> Select Components” (Alt+O) i zaznaczamy okno przy X-CUBE-AI -> Core. Ponieważ to nasza pierwsza aplikacja, wykorzystamy domyślny szablon poprzez wybór „Application -> ApplicationTemp”.

Zamykamy okno i w konfiguratorze z listy po lewej stronie wybieramy „Software Packs -> X-CUBE-AI”. Następnie klikamy „Add network”. W oknie dodajemy nazwę (ja zostawiłem domyślną network), typ modelu jako „Keras” i rodzaj jako „Saved model”. Podajemy ścieżkę do zapisanego modelu. W ostatniej części okna możemy wybrać parametry kompresji i rodzaj walidacji. Dzisiaj nie będziemy się tym zajmować. Wybieramy zatem przycisk „Analyze”.

Jeżeli wszystko przebiegło pomyślnie, po chwili powinniśmy otrzymać raport. Znajdziemy w nim m.in. informację o modelu (ilość, typ warstw, dane wejściowe, wyjściowe) oraz o ilości pamięci Flash i RAM, jakiej model potrzebuje. W moim przypadku sieć zajmie ok. 5,3 kB Flash oraz 388 B RAM-u.

Model mamy zatem zaimportowany poprawnie. Dodamy jeszcze kilka zmian w projekcie – chciałbym za pomocą gestów włączać sterowanie jasnością diody (jasność będzie wybierana na podstawie odległości ręki od czujnika). Dodamy zatem dwie diody na pinach PA8 i PA9. Wyjście PA9 oznaczam jako DETECT_LED i zwykłe wyjście cyfrowe (GPIO_Output), PA8 jako CONTROL_LED i wyjście PWM. TIMER 1 do którego podłączony jest kanał 1 dostępny na wyjściu PA9 konfiguruję zgodnie z poniższą grafiką.

Konfiguracja pinów mikrokontrolera będzie wyglądała następująco.

Generujemy projekt i przechodzimy do napisania kodu. W drzewie projektu po lewej stronie pojawi nam się folder X-CUBE-AI, gdzie mamy pliki z naszą siecią (network, network_data, network_config) oraz z dodaną aplikacją domyślną (app_x-cube-ai). Pliki network zawierają dane dotyczące sieci (m.in. ilość danych wejściowych, wyjściowych), pliki network_data to tablica z wagami wygenerowanymi w procesie uczenia, a plik network_config informuje m.in. o wersji używanego pakiety X-CUBE-AI.

W pliku main() pojawiła się nam funkcja MX_X_CUBE_AI_Init(), która inicjalizuje sieć. Dzisiaj nie będziemy się zagłębiali w jej działanie. W pętli while natomiast dodana została funkcja MX_X_CUBE_AI_Process(), którą musimy zmodyfikować.

W domyślnym wzorcu została napisana bardzo nieczytelnie, dlatego możemy usunąć jej ciało. Dodamy natomiast tablice na dane wejściowe naszej sieci o rozmiarze 64 i typie float oraz zmienną float z wyjściem sieci.

static float nn_input[NETWORK_INPUT_DATA_SIZE] = { 0 };
float nn_output = 0.0f;

Następnie będziemy przypisywali do tablicy danych wejściowych kolejne próbki z pomiaru odległości analogicznie jak w przypadku zbierania danych treningowych. Ponieważ w procesie uczenia używaliśmy danych po normalizacji, tutaj również musimy podzielić zmierzony dystans przez maksymalną odległość, czyli 80.

for(i=0; i<(NETWORK_INPUT_DATA_SIZE-1); i++ )
{
	nn_input[i] = nn_input[i+1];
}

nn_input[NETWORK_INPUT_DATA_SIZE-1] = ((float)distance_cm/DISTANCE_MAX_VALUE);

Gdy zbierzemy 64 próbki (wcześniej mamy niepełne dane, byłoby bez sensu je analizować), wywołujemy funkcję ai_run, która spowoduje przetworzenie danych wejściowych przez sieć i umieszczenie wyników w zmiennej danych wyjściowych.

ai_run(nn_input, nn_output);
cnt = NETWORK_INPUT_DATA_SIZE;

Następnie sprawdzamy, czy nasza sieć wykryła gest (w drugim elemencie tablicy umieszczone jest prawdopodobieństwo wykrycia gestu). Założyłem, że satysfakcjonuje mnie wynik, jeżeli prawdopodobieństwo wyniesie co najmniej 70 %. Ustawiam flagę, że gest został wykryty oraz zaświecam diodę DETECT_LED.

if (nn_output > 0.7)
{
	cnt = 0;

	if(false == gesture_is_detected)
	{
		HAL_GPIO_WritePin(DETECT_LED_GPIO_Port, DETECT_LED_Pin, GPIO_PIN_SET);
		gesture_is_detected = true;
	}
}

Cała funkcję będzie wyglądała jak na listingu poniżej.

void MX_X_CUBE_AI_Process(void)
{
    /* USER CODE BEGIN 6 */
	uint32_t i = 0;
	static uint32_t cnt = 0;

	static float nn_input[NETWORK_INPUT_DATA_SIZE] = { 0 };
	float nn_output;

	if(true == adc_data_is_ready)
	{
		adc_data_is_ready = false;

		cnt++;

		for(i=0; i<(NETWORK_INPUT_DATA_SIZE-1); i++ )
		{
			nn_input[i] = nn_input[i+1];
		}

		nn_input[NETWORK_INPUT_DATA_SIZE-1] = ((float)distance_cm/DISTANCE_MAX_VALUE);

		if(cnt > (NETWORK_INPUT_DATA_SIZE-1))
		{
			ai_run(nn_input, &nn_output);
			cnt = NETWORK_INPUT_DATA_SIZE;

			if (nn_output > 0.7)
			{
				cnt = 0;

				if(false == gesture_is_detected)
				{
					HAL_GPIO_WritePin(DETECT_LED_GPIO_Port, DETECT_LED_Pin, GPIO_PIN_SET);
					gesture_is_detected = true;
				}
			}
		}
	}
    /* USER CODE END 6 */
}

Teraz dodamy jeszcze funkcję sterującą jasnością diody. Jasność będzie regulowana co 100 ms i sterowana za pomocą zmierzonej odległości. Przedział pomiaru od 10 do 60 cm przeliczymy na jasność od 0 do 100 %. Zakończenie sterowania jasnością będzie wykrywane, jeżeli czujnik wykryje odległość powyżej 80 cm (zabierzemy rękę).

void LED_brightness_control(void)
{
	static uint32_t time_cnt = 0;

	if(time_cnt == 0)
		time_cnt = HAL_GetTick();

	if((HAL_GetTick() - time_cnt) > 100)
	{
		time_cnt = HAL_GetTick();

		if(distance_cm >= DISTANCE_MAX_VALUE)
		{
			gesture_is_detected = false;
			HAL_GPIO_WritePin(DETECT_LED_GPIO_Port, DETECT_LED_Pin, GPIO_PIN_RESET);
		}

		if(true == gesture_is_detected)
		{
			brightness = _CONVERT_DISTANCE_TO_BRIGHTNESS(distance_cm);

			if(brightness > BRIGHTNESS_MAX)
			{
				brightness = BRIGHTNESS_MAX;
			}
			else if(brightness < BRIGHTNESS_MIN)
			{
				brightness = BRIGHTNESS_MIN;
			}

			__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (uint32_t)brightness);
		}
	}
}

W funkcji main() musimy uruchomić tryb PWM dla TIM1 na kanale 1 oraz wywołać w pętli while(1) funkcję LED_brightness_control().

/* USER CODE BEGIN 2 */
  ADC_start_data_measurement();
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  LED_brightness_control();
    /* USER CODE END WHILE */

  MX_X_CUBE_AI_Process();
    /* USER CODE BEGIN 3 */
  }

Po podłączeniu LED (i rezystorów) do pinów PA8 i PA9, możemy uruchomić program.

Sposób działania przedstawiony został na poniższym filmie.

Podsumowanie

Udało nam się zbudować pierwszy projekt dla STM32 w oparciu o sztuczne sieci neuronowe. I chociaż działanie czujnika SHARP pozostawia wiele do życzenia, pokazałem, że nawet z najprostszymi elementami możemy w ciekawy sposób używać pakietu STM32Cube.AI. Model oczywiście można dopracować – sztuczna inteligencja to obszar, w którym nie ma jednego dobrego rozwiązania. Zachęcam do badania i prowadzenia własnych eksperymentów, którymi możecie się podzielić w komentarzach. Pełny projekt znajdziecie poniżej w repozytorium. Umieściłem w nim także pliki dotyczące pakietu Keras.

Do pobrania

Repozytorium GitHub

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *