Der KeplerBRAIN V4 Soccer Bodensensor besteht aus zwei Komponenten - der Sensorplatine und einer KeplerBRAIN V4 Stamp. Auf der Sensorplatine befinden sich die Sensoren, auf der KeplerBRAIN V4 Stamp werden die Sensordaten mit einem Mikrocontroller ermittelt und weiter verarbeitet.
Die Sensorplatine
Design und Layout by Janez Rotman 2019
Auf der Sensorplatine befinden sich auf der Unterseite 8 Reflexionssensoren. Gleichzeitig dient die Platine als Grundplatte für den mechanischen Aufbau eines Soccer Roboters. Auf der Oberseite kann über Steckleisten ein KeplerBRAIN V4 Stamp Board aufgesteckt werden. Zusätzlich sind dort zwei Taster und zwei LEDs platziert, welche spezielle Aufgaben in der Firmware übernehmen können.
Die von den Reflexionssensoren gemessenen Intensitäten von reflektiertem Licht werden als Spannungen an analogen Eingängen des Mikrocontrollers gemessen und in digitale Werte umgewandelt.
Die KeplerBRAIN V4 Stamp
Die KeplerBRAIN V4 Stamp ist ein Mikrocontrollerboard im Briefmarkenformat für die die Entwicklung eigener Sensoren innerhalb des KeplerBRAIN V4 Robotik-Systems. Darauf befindet sich der gleiche STM32F411 Mikrocontroller, wie auf dem Mainboard und allen anderen Sensoren. Von diesem Mikrocontroller sind IO-Pins, analoge Eingangspins und I2C-Pins herausgeführt, sodass damit umfangreiche Möglichkeiten für die Entwicklung bereitstehen.
Dieses Board wird mit dem KeplerBRAIN V4 Mainboard über eine der drei SPI Schnittstellen verbunden, über welche die Datenübertragung erfolgt und auch Versorgungspannungen von 3,3V und 5V zu Verfügung gestellt werden. Zusätzlich befindet sich auf dem Board ein sechspoliger Anschluss für den selben Programmieradapter mit dem auch der Mikrocontroller auf dem Mainboard programmiert werden kann.
Firmware für den Soccer Bodensensor
Der folgende Code ist quasi das Betriebssystem des Sensors. Dieser kann 1:1 so in der Arduino IDE kompilliert und auf das die KeplerBRAIN V4 Stamp übertragen werden. In der Folge ist es aber auch möglich die Firmware zu erweitern, um z. B. die Analyse und weitere Verarbeitung der Sensorwerte direkt am Sensor auszuführen.
Die Werte des von einem ADC (Analog Digital Converter) konvertierten Spannungen der Reflexionssensoren liegen im Bereich zwischen 0 und 1023 und werden im Array adcValues[] abgelegt. Diese werden dem Array spi_data[] zugewiesen und dabei durch 10 dividiert um Werte zu erhalten, die innerhalb des Wertebereichs eines Bytes (Werte zwischen 0 und 255) liegen.
Die Werte im Array spi_data[] werden über die SPI-Schnittstelle in einem durchgehenden Stream von 9 Werten zum Mikrocontoller eines KeplerBRAIN Mainboards geschickt. Der erste Wert eines solchen Datenpakets ist jeweils die Zahl 250, mit der der Beginn eines Datenpakets vom empfangenden Mikrocontroller identifiziert werden kann. Dabei ist darauf zu achten, dass keiner der folgenden 8 Zahlen den Wert 250 annehmen darf.
Erweiterung der Firmware
Es ist möglich, bereits auf dem Sensorboard eine Analyse der gemessenen Werte vorzunehmen oder Berechnungen mit den Werten durchzuführen und dann nur die Ergebnisse zum Mikrocontroller auf dem Mainboard zu übertragen. Dazu sind im folgenden Code in den Funktionen setup() und loop() zwei Bereiche mit dem Kommentar // User Code vorgesehen, in denen eigener Code platziert werden kann.
Auch ist es möglich über den Programmieradapter direkt Werte im Serial Monitor der Arduino IDE zum Testen des eigenen Codes anzeigen zu lassen. Siehe dazu Arduino > Arduino C C++ > Serial Monitor
// *******************************************************************************// *** KeplerBRAIN V4 Sensor STAMP SPI Client Soccer Bodensensor V 29.11.2024 ***// *******************************************************************************// PB1 F Front// PC2 FL Front Left// PC5 FR Front Right// PC0 L Left// PB0 R Right// PC3 BL Back Left// PA1 BR Back Right// PA0 B Back// PA15 Transistor LEDs Boden// PB7 Led left// PB8 Button left// PB9 Led right// PC13 Button right#include "stm32f4xx_hal.h"// ADC1 HandlerADC_HandleTypeDef hadc1;// DMA Handler für ADCDMA_HandleTypeDef hdma_adc1;// Array zum Speichern der ADC-Werteuint32_t adcValues[8];// SPI Datavolatile uint8_t spi_data[9] = {250,1,2,3,4,5,6,7,8};volatile uint8_t tx_index = 0;// Funktion zur Initialisierung des ADC mit DMAvoid ADC1_Init(void){// Takt für ADC1 aktivieren__HAL_RCC_ADC1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_DMA2_CLK_ENABLE(); // DMA2 für ADC1// ADC-Pins initialisierenGPIO_InitTypeDef GPIO_InitStruct = {0};// PC2, PC0, PC3, PB1, PC5, PB0, PA1, PA0 als Analog-Eingänge konfigurierenGPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_0 | GPIO_PIN_3 | GPIO_PIN_5; // PC-PinsGPIO_InitStruct.Mode = GPIO_MODE_ANALOG;GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_0; // PB-PinsHAL_GPIO_Init(GPIOB, &GPIO_InitStruct);GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_0; // PA-PinsHAL_GPIO_Init(GPIOA, &GPIO_InitStruct);// ADC1 konfigurierenhadc1.Instance = ADC1;hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // Prescaler für geringere Taktfrequenzhadc1.Init.Resolution = ADC_RESOLUTION_8B; // 12-Bit-Auflösunghadc1.Init.ScanConvMode = ENABLE; // Scan-Modus aktivieren (mehrere Kanäle)hadc1.Init.ContinuousConvMode = ENABLE; // Kontinuierliche Konvertierunghadc1.Init.DiscontinuousConvMode = DISABLE; // Kein diskontinuierlicher Modushadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; // Keine externe Auslösunghadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // Daten rechtsbündighadc1.Init.NbrOfConversion = 8; // Anzahl der Kanälehadc1.Init.DMAContinuousRequests = ENABLE; // DMA-Anforderungen kontinuierlichhadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV; // End-of-Conversion-Sequenzif (HAL_ADC_Init(&hadc1) != HAL_OK){// Fehler bei der Initialisierung}// Konfigurieren der ADC-KanäleADC_ChannelConfTypeDef sConfig = {0};sConfig.Channel = ADC_CHANNEL_9; // PB1 frontsConfig.Rank = 1;sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES;HAL_ADC_ConfigChannel(&hadc1, &sConfig);sConfig.Channel = ADC_CHANNEL_12; // PC2 front leftsConfig.Rank = 2;HAL_ADC_ConfigChannel(&hadc1, &sConfig);sConfig.Channel = ADC_CHANNEL_15; // PC5 front rightsConfig.Rank = 3;HAL_ADC_ConfigChannel(&hadc1, &sConfig);sConfig.Channel = ADC_CHANNEL_10; // PC0 leftsConfig.Rank = 4;HAL_ADC_ConfigChannel(&hadc1, &sConfig);sConfig.Channel = ADC_CHANNEL_8; // PB0 rightsConfig.Rank = 5;HAL_ADC_ConfigChannel(&hadc1, &sConfig);sConfig.Channel = ADC_CHANNEL_13; // PC3 back leftsConfig.Rank = 6;HAL_ADC_ConfigChannel(&hadc1, &sConfig);sConfig.Channel = ADC_CHANNEL_1; // PA1 back rightsConfig.Rank = 7;HAL_ADC_ConfigChannel(&hadc1, &sConfig);sConfig.Channel = ADC_CHANNEL_0; // PA0 backsConfig.Rank = 8;HAL_ADC_ConfigChannel(&hadc1, &sConfig);// DMA für ADC konfigurierenhdma_adc1.Instance = DMA2_Stream0; // STM32F411RE verwendet DMA2 Stream 0 für ADC1hdma_adc1.Init.Channel = DMA_CHANNEL_0;hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;hdma_adc1.Init.Mode = DMA_CIRCULAR; // Zirkulärer Modus für kontinuierliche Konvertierunghdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;if (HAL_DMA_Init(&hdma_adc1) != HAL_OK){// Fehler bei der DMA-Initialisierung}// Verknüpfen des DMA-Handles mit ADC__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);// Start des ADC mit DMAHAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcValues, 8);}// Initialisierung der Systemuhr für STM32F411REvoid SystemClock_Config(void){RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};RCC_OscInitTypeDef RCC_OscInitStruct = {0};// Konfigurieren des OszillatorsRCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;RCC_OscInitStruct.HSIState = RCC_HSI_ON;RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;RCC_OscInitStruct.PLL.PLLM = 16;RCC_OscInitStruct.PLL.PLLN = 336;RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4;RCC_OscInitStruct.PLL.PLLQ = 4;if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK){// Fehler bei der Oszillator-Konfigurationwhile (1);}// Initialisieren des TaktsystemsRCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK){// Fehler bei der Taktkonfigurationwhile (1);}}void setup(){SPI1_Init(); // Initialize SPI1 as Slave with interruptNVIC_EnableIRQ(SPI1_IRQn); // Enable the SPI1 interrupt in NVICHAL_Init(); // HAL InitialisierungSystemClock_Config(); // Systemuhr konfigurierenADC1_Init(); // ADC und DMA initialisierenpinMode(PA15,OUTPUT); // Transistor LEDs BodendigitalWrite(PA15,LOW); // LEDs Boden LOW:ein HIGH:auspinMode(PB7,OUTPUT); // LED linksdigitalWrite(PB7, LOW); // LED links auspinMode(PB9,OUTPUT); // LED rechtspinMode(PB8,INPUT); // Taster linkspinMode(PC13,INPUT); // Taster rechts// User Code//Serial.begin(115200);}void loop(){spi_data[1] = adcValues[0] / 10;spi_data[2] = adcValues[1] / 10;spi_data[3] = adcValues[2] / 10;spi_data[4] = adcValues[3] / 10;spi_data[5] = adcValues[4] / 10;spi_data[6] = adcValues[5] / 10;spi_data[7] = adcValues[6] / 10;spi_data[8] = adcValues[7] / 10;// User Code/*Serial.print(adcValues[0]);Serial.print(" ");Serial.print(adcValues[1]);Serial.print(" ");Serial.print(adcValues[2]);Serial.print(" ");Serial.print(adcValues[3]);Serial.print(" ");Serial.print(adcValues[4]);Serial.print(" ");Serial.print(adcValues[5]);Serial.print(" ");Serial.print(adcValues[6]);Serial.print(" ");Serial.println(adcValues[7]);*/}// Initialize SPI1 in slave mode with interruptvoid SPI1_Init(){// Enable clocks for GPIOA and SPI1RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;// Configure PA4 (NSS), PA5 (SCK), PA6 (MISO), PA7 (MOSI) as alternate functionGPIOA->MODER &= ~(GPIO_MODER_MODER4 | GPIO_MODER_MODER5 | GPIO_MODER_MODER6 | GPIO_MODER_MODER7);GPIOA->MODER |= (GPIO_MODER_MODER4_1 | GPIO_MODER_MODER5_1 | GPIO_MODER_MODER6_1 | GPIO_MODER_MODER7_1);GPIOA->AFR[0] |= (5 << (4 * 4)) | (5 << (5 * 4)) | (5 << (6 * 4)) | (5 << (7 * 4)); // Set AF5 for SPI1// Configure SPI1 as a slaveSPI1->CR1 = 0; // Reset CR1SPI1->CR1 &= ~SPI_CR1_MSTR; // Set as slaveSPI1->CR1 &= ~SPI_CR1_SSM; // Disable software slave managementSPI1->CR1 |= SPI_CR1_SPE; // Enable SPISPI1->CR2 |= SPI_CR2_RXNEIE; // Enable RXNE interruptSPI1->CR2 |= SPI_CR2_TXEIE; // Enable TXE interrupt}extern "C" void SPI1_IRQHandler(void){// Check if RXNE (Receive Not Empty) flag is set and handle RX firstif (SPI1->SR & SPI_SR_RXNE){uint8_t rx_value = SPI1->DR;//Serial.println(rx_value);}// Check if TXE (Transmit Empty) flag is set, but only if not busyif (SPI1->SR & SPI_SR_TXE){// Send data from tx_buffer if in "send mode"if (tx_index < 9){// Write data to SPISPI1->DR = spi_data[tx_index];tx_index++;}else{// Reset buffer indextx_index = 0;}}}
Programmbeispiel - Einlesen von Werten des Soccer Bodensensors
Der Bodensensor wird an die SPI1 Schnittstelle eines KeplerBRAIN V4 Mainboards angeschlossen. Am Display sollen laufend die aktuellen, gemessenen Werte der 8 Reflexionssensoren angezeigt werden.
Dazu muss eine SPI Datenübertragung initiiert werden. Dies geschieht dadurch, dass die CS-Leitung (Chip Select) der SPI-Verbindung auf LOW gesetzt wird. Mit der Funktion spi.transfer(0xFF) wird ein Byte aus dem Datenstream ausgelesen. Kommt in diesem die Zahl 250 vor, so ist der erste Wert des Datenpakets aus 9 Werten identifiziert und die nächsten 8 Werte werden in den Variablen value_1, ..., value_8 abgelegt. Am Ende der SPI Kommunikation muss die CS-Leitung wieder auf HIGH gesetzt werden.
#include "KeplerBRAIN_V4.h"uint8_t value_1;uint8_t value_2;uint8_t value_3;uint8_t value_4;uint8_t value_5;uint8_t value_6;uint8_t value_7;uint8_t value_8;void setup(){KEPLERBRAIN_INIT();WRITE_LCD_TEXT(1,1," ");WRITE_LCD_TEXT(1,2," ");}void loop(){// read 8 Bytes from Soccer BodensensordigitalWrite(SPI1, LOW);if(spi.transfer(0XFF) == 250){value_1 = spi.transfer(0XFF);value_2 = spi.transfer(0XFF);value_3 = spi.transfer(0XFF);value_4 = spi.transfer(0XFF);value_5 = spi.transfer(0XFF);value_6 = spi.transfer(0XFF);value_7 = spi.transfer(0XFF);value_8 = spi.transfer(0XFF);}digitalWrite(SPI1, HIGH);// read 8 Bytes from Soccer BodensensorWRITE_LCD_TEXT(1, 1, String(value_1)+" "+String(value_2)+" "+String(value_3)+" "+String(value_4));WRITE_LCD_TEXT(1, 2, String(value_5)+" "+String(value_6)+" "+String(value_7)+" "+String(value_8));}
Erklärungen zu diesem Programmbeispiel
Zeile 3 - 10: uint8_t value_1;
Zunächst werden 8 Variablen vom Typ uint8_t definiert, in welchen die vom Sensorboard abgefragten Zahlen abgelegt werden.
Zeile 21 - 38: // read 8 Bytes from Soccer Bodensensor BEGIN - END
In diesem Code-Abschnitt sind grundsätzlich keine Änderungen vorzunehmen. Laufend werden über die SPI-Schnittstelle Zahlen von der OpenMV Cam abgefragt. Entspricht in diesem Daten-Stream eine Zahl dem Wert 250, so wird diese als erste Zahl der Neunergruppe identifiziert und alle weiteren werden in den Variablen value1, value2, ...abgelegt.
Zeile 41,42: WRITE_LCD_TEXT(1, 1, String(value_1)+" "+String(value_2)+" "+String(value_3)+" "+String(value_4));
Mit diesen Code-Zeilen werden die Zahlen der Variablen value1, value2, ... am Display angezeigt.


