W tym poście chciałbym opisać obsługę akcelerometru LIS3DSH zamontowanej na płytce STM32 Discovery w różnych trybach.
[Źródło: http://www.st.com/en/evaluation-tools/stm32f4discovery.html]
Układ był już opisywany na moim blogu, natomiast tym razem chciałem bardzie rozwinąć temat obsługi z wykorzystaniem przerwań oraz DMA.
Opis układu:
LIS3DSH jest to 3-osiowy akcelerometr, który umożliwia pomiar przyśpieszenia (w zakresie +/- 2g, 4g, 8g, 16g). W czujnik wbudowany została maszyna stanu oraz funkcje auto testu.
Inicjalizacja:
Przed inicjalizacją czujnika można odczytać rejestry INFO1, INFO2 oraz WHO_AM_I:
Zgodnie z dokumentacją rejestr INFO1 powinien mieć wartość 33(0x21), INFO2 0, WHO_AM_I 63(0x3F).
- uint8_t LIS3DSH_ReadId(LIS3DSH_SensorId_TypeDef *id_ptr){
- uint8_t id_reg_val[3] = {0x00};
- HAL_StatusTypeDef opStatus = LIS3DSH_ReadSPI(LIS3DSH_INFO1, &id_reg_val[0], 3);
- if(opStatus == 0) {
- id_ptr->info1 = id_reg_val[0];
- id_ptr->info2 = id_reg_val[1];
- id_ptr->whoami = id_reg_val[2];
- if(id_ptr->info1 == 33 &&
- id_ptr->info2 == 0 &&
- id_ptr->whoami == 63)
- {
- return 0;
- }
- return 0xFF;
- }
- return (uint8_t)opStatus;
- }
Inicjalizacja czujnika wykonuje się w kilku funkcjach. Na samym początku należy wprowadzić podstawowe parametry konfiguracyjne do struktury:
- typedef struct {
- LIS3DSH_DataRate_Enum_TypeDef dataRateSetting;
- LIS3DSH_FullScale_Enum_TypeDef fullScaleSetting;
- LIS3DSH_AntiAliasing_Enum_TypeDef antiAliasingSetting;
- LIS3DSH_Axis_Enum_TypeDef axisSetting;
- LIS3DSH_Interrupt_Enum_TypeDef interruptSetting;
- float sensiLevelSetting;
- }LIS3DSH_ParaTypeDef;
- void LIS3DSH_Initial_DataStructure(LIS3DSH_ParaTypeDef *lis3dsh_str_ptr, const LIS3DSH_DataRate_Enum_TypeDef dataRateSetting,
- const LIS3DSH_FullScale_Enum_TypeDef fullScaleSetting, const LIS3DSH_AntiAliasing_Enum_TypeDef antiAliasingSetting,
- const LIS3DSH_Axis_Enum_TypeDef axisSetting, const LIS3DSH_Interrupt_Enum_TypeDef interruptSetting)
- {
- lis3dsh_str_ptr->dataRateSetting = dataRateSetting;
- lis3dsh_str_ptr->fullScaleSetting = fullScaleSetting;
- lis3dsh_str_ptr->antiAliasingSetting = antiAliasingSetting;
- lis3dsh_str_ptr->axisSetting = axisSetting;
- lis3dsh_str_ptr->interruptSetting = interruptSetting;
- }
- void LIS3DSH_Initial_Default_DataStructure(LIS3DSH_ParaTypeDef *lis3dsh_str_ptr)
- {
- lis3dsh_str_ptr->dataRateSetting = DATARATE_25_EN;
- lis3dsh_str_ptr->fullScaleSetting = FULLSCALE_4_EN;
- lis3dsh_str_ptr->antiAliasingSetting = FILTER_50_EN;
- lis3dsh_str_ptr->axisSetting = XYZ_AXIS_ENABLE;
- lis3dsh_str_ptr->interruptSetting = INTERRUPT_EN;
- }
Kolejnym krokiem jest wpisanie danych z struktury do czujnika:
- uint8_t LIS3DSH_Initialization(LIS3DSH_ParaTypeDef *lis3dsh_str_ptr)
- {
- uint8_t tempData = 0;
- uint8_t opStatus = 0xFF;
- lis3dsh_InitCalibrationData();
- tempData |= (lis3dsh_str_ptr->axisSetting & 0x07);
- tempData |= (lis3dsh_str_ptr->dataRateSetting & 0xF0);
- opStatus = LIS3DSH_WriteSPI(LIS3DSH_CTRL_REG4, &tempData, 1);
- if(opStatus != 0) { return opStatus; }
- tempData = 0;
- tempData |= (lis3dsh_str_ptr->antiAliasingSetting & 0xC0);
- tempData |= (lis3dsh_str_ptr->fullScaleSetting & 0x38);
- opStatus = LIS3DSH_WriteSPI(LIS3DSH_CTRL_REG5, &tempData, 1);
- if(opStatus != 0) { return opStatus; }
- opStatus = lis3dsh_setInterruptInt1(lis3dsh_str_ptr->interruptSetting);
- if(opStatus != 0x00) { return opStatus; }
- lis3dsh_str_ptr->sensiLevelSetting = lis3dsh_SetSensiLevel_BasedFullscale(lis3dsh_str_ptr->fullScaleSetting);
- return 0;
- }
Polling:
W przypadku pobierania danych w ciągiem należy czekać na gotowość czujnika do przesłania kolejnych informacji:
- uint8_t LIS3DSH_Poll_Data(void)
- {
- uint32_t startTick = HAL_GetTick();
- if(lis3dsh_readStatusReg() == 1) {
- return true;
- }
- return false;
- }
- static uint8_t lis3dsh_readStatusReg(void)
- {
- uint8_t statReg = 0;
- LIS3DSH_ReadSPI(LIS3DSH_STATUS_ADDR, &statReg, 1);
- if(statReg & 0x07) {
- return 1;
- }
- return 0;
- }
Wartość 0x07 w rejestrze status (0x27) oznacza nowe dane dla osi X, Y, Z.
Można też wykorzystać odliczanie czasu w celu zmniejszenia ilości danych pobieranych z czujnika:
- typedef struct {
- uint8_t start_Count_Flag;
- uint32_t start_Tick_Value;
- }LIS3DSH_Timeout_Typedef;
- uint8_t LIS3DSH_PollData_Timeout(LIS3DSH_Timeout_Typedef *timeoutStruct_Ptr, uint32_t timeout)
- {
- if(lis3dsh_timeout((LIS3DSH_Timeout_Typedef *)&timeoutStruct_Ptr, timeout) == 1)
- {
- if(lis3dsh_readStatusReg() == 1) {
- timeoutStruct_Ptr->start_Count_Flag = 0;
- return true;
- }
- }
- return false;
- }
- static uint8_t lis3dsh_timeout(LIS3DSH_Timeout_Typedef *timeoutStruct_Ptr, uint32_t timeout_ms)
- {
- if(timeoutStruct_Ptr->start_Count_Flag == 0) {
- timeoutStruct_Ptr->start_Tick_Value = HAL_GetTick();
- timeoutStruct_Ptr->start_Count_Flag = 1;
- }
- if((HAL_GetTick() - timeoutStruct_Ptr->start_Tick_Value) < timeout_ms)
- {
- return 1;
- }
- return 0;
- }
W tym przypadku przekroczeniu zadanego czasu sprawdzana jest możliwość pobrania danych z czujnika. Dopiero po zatwierdzeniu z obu funkcji przechodzę do odczytu danych z czujnika.
- while (1)
- {
- if(LIS3DSH_PollData_Timeout(1000) == 1)
- {
- LIS3DSH_ScaledData_t = LIS3DSH_GetReadScaledData();
- HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
- }
- }
Przerwania:
W celu uruchomienia przerwania należy ustawić odpowiednią wartość w rejestrze konfiguracyjnym CTRL_REG3 (0x23):
Należy ustawić flagę DR_EN oraz INT1_EN.
- static uint8_t lis3dsh_setInterruptInt1(LIS3DSH_Interrupt_Enum_TypeDef intEnStat)
- {
- uint8_t opStatus = 0;
- if(intEnStat == INTERRUPT_EN)
- {
- uint8_t tempData = 0x88;
- opStatus = LIS3DSH_WriteSPI(LIS3DSH_CTRL_REG3_ADDR, &tempData, 1);
- if(opStatus != 0) { return opStatus; }
- }
- return opStatus;
- }
Przerwanie jest przypisane do pinu PE0 dla INT1 układu LIS3DSH. PE1 obsługuje pin INT2.
Przerwanie zostaje ustawione gdy są gotowe dane do przesłania. W obsłudze przerwania tak aby było ono najszybciej wykonane najłatwiej ustawić flagę.
- volatile uint8_t lis3dsh_dataReady_irq_flag = 0;
- void EXTI0_IRQHandler(void)
- {
- HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
- }
- void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
- {
- if(GPIO_Pin == GPIO_PIN_0)
- {
- lis3dsh_dataReady_irq_flag = 1;
- }
- }
- //w main ...
- while(1) {
- if(lis3dsh_dataReady_irq_flag == 1)
- {
- lis3dsh_dataReady_irq_flag = 0;
- LIS3DSH_ScaledData_t = LIS3DSH_GetReadScaledData(&LIS3DSH_Config, &LIS3DSH_BiasScaleData_t);
- }
- }
Jeśli przerwanie zostało wywołane i flaga została ustawiona na 1, to w pętli while nastąpi odczyt danych z czujnika.
DMA:
W przykładzie opisującym DMA wykorzystam przerwanie INT1 inicjalizujące odczyt danych.
Na samym początku należy zmienić kolejność wywoływania bibliotek inicjalizujących DMA oraz SPI:
- //Z
- MX_SPI1_Init();
- MX_DMA_Init();
- //NA
- MX_DMA_Init();
- MX_SPI1_Init();
Wygenerowana inicjalizacja SPI z DMA wygląda następująco:
- void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
- {
- GPIO_InitTypeDef GPIO_InitStruct = {0};
- if(hspi->Instance==SPI1)
- {
- __HAL_RCC_SPI1_CLK_ENABLE();
- __HAL_RCC_GPIOA_CLK_ENABLE();
- /**SPI1 GPIO Configuration
- PA5 ------> SPI1_SCK
- PA6 ------> SPI1_MISO
- PA7 ------> SPI1_MOSI
- */
- GPIO_InitStruct.Pin = SPI1_SCK_Pin|SPI1_MISO_Pin|SPI1_MOSI_Pin;
- GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
- GPIO_InitStruct.Pull = GPIO_NOPULL;
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
- GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
- HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
- /* SPI1 DMA Init */
- /* SPI1_RX Init */
- hdma_spi1_rx.Instance = DMA2_Stream0;
- hdma_spi1_rx.Init.Channel = DMA_CHANNEL_3;
- hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
- hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
- hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
- hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
- hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
- hdma_spi1_rx.Init.Mode = DMA_NORMAL;
- hdma_spi1_rx.Init.Priority = DMA_PRIORITY_HIGH;
- hdma_spi1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
- if (HAL_DMA_Init(&hdma_spi1_rx) != HAL_OK)
- {
- Error_Handler();
- }
- __HAL_LINKDMA(hspi,hdmarx,hdma_spi1_rx);
- /* SPI1_TX Init */
- hdma_spi1_tx.Instance = DMA2_Stream3;
- hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3;
- hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
- hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
- hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
- hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
- hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
- hdma_spi1_tx.Init.Mode = DMA_NORMAL;
- hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;
- hdma_spi1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
- if (HAL_DMA_Init(&hdma_spi1_tx) != HAL_OK)
- {
- Error_Handler();
- }
- __HAL_LINKDMA(hspi,hdmatx,hdma_spi1_tx);
- }
- }
Do tego należy uruchomić przerwania od SPI.
- static void MX_SPI1_Init(void)
- {
- hspi1.Instance = SPI1;
- hspi1.Init.Mode = SPI_MODE_MASTER;
- hspi1.Init.Direction = SPI_DIRECTION_2LINES;
- hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
- hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
- hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
- hspi1.Init.NSS = SPI_NSS_SOFT;
- hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
- hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
- hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
- hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
- hspi1.Init.CRCPolynomial = 10;
- if (HAL_SPI_Init(&hspi1) != HAL_OK)
- {
- Error_Handler();
- }
- HAL_NVIC_SetPriority(SPI1_IRQn, 0, 0);
- HAL_NVIC_EnableIRQ(SPI1_IRQn);
- }
Jak wspomniałem wcześniej do obsługi wykorzystywane są przerwanie od INT1 informujące o możliwości odczytu danych. W tym przerwaniu rozpoczynam odczyt danych z czujnika (funkcja HAL_SPI_TransmitReceive_DMA). Bufory z danymi zapisanymi i odczytanymi są buforami globalnymi. Zakończenie odbierania danych wywołuje przerwania od SPI1 gdzie obrabiam otrzymane wyniki i ustawiam flagę odczytu w celu obrobienia danych w pętli głównej programu.
- void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
- {
- if(GPIO_Pin == GPIO_PIN_0 && LIS3DSH_t.lis3dsh_dataReady_irq_flag == 0)
- {
- LIS3DSH_DMA_StartRead();
- LIS3DSH_t.lis3dsh_dataReady_irq_flag = 1;
- HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14);
- }
- }
- void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
- if(hspi->Instance == SPI1 && LIS3DSH_t.lis3dsh_dataReady_irq_flag == 1)
- {
- LIS3DSH_t.lis3dsh_dataReady_irq_flag = 0;
- LIS3DSH_DMA_ReadComplete(&LIS3DSH_t.LIS3DSH_RawData_t);
- LIS3DSH_t.convertData_From_Global = 1;
- HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
- }
- }
- while (1)
- {
- /* USER CODE END WHILE */
- /* USER CODE BEGIN 3 */
- if(LIS3DSH_t.convertData_From_Global == 1)
- {
- LIS3DSH_t.convertData_From_Global = 0;
- LIS3DSH_t.LIS3DSH_ScaledData_t = LIS3DSH_DMA_ConvertData(&LIS3DSH_t.LIS3DSH_RawData_t,
- &LIS3DSH_t.LIS3DSH_Config_t,
- &LIS3DSH_t.LIS3DSH_BiasScaleData_t);
- }
- }
Poniżej wszystkie funkcje obsługujące DMA w LIS3DSH:
- uint8_t LIS3DSH_DMA_StartRead(void)
- {
- LIS3DSH_DMA_Buffers_t.glob_spiBuf[0] = LIS3DSH_OUT_X_L | 0x80;
- _LIS3DHS_CS_ENBALE;
- HAL_StatusTypeDef opStatus = HAL_SPI_TransmitReceive_DMA(&hspi1, (uint8_t *)&LIS3DSH_DMA_Buffers_t.glob_spiBuf[0], (uint8_t *)&LIS3DSH_DMA_Buffers_t.glob_buffer[0], 8);
- if(opStatus == HAL_OK)
- {
- return 1;
- } else{
- _LIS3DHS_CS_DISABLE;
- return 0;
- }
- }
- void LIS3DSH_DMA_ReadComplete(volatile LIS3DSH_RawData_TypeDef *rawData_ptr)
- {
- _LIS3DHS_CS_DISABLE;
- LIS3DSH_t.LIS3DSH_RawData_t.x = ((LIS3DSH_DMA_Buffers_t.glob_buffer[2] << 8) + LIS3DSH_DMA_Buffers_t.glob_buffer[1]);
- LIS3DSH_t.LIS3DSH_RawData_t.y = ((LIS3DSH_DMA_Buffers_t.glob_buffer[4] << 8) + LIS3DSH_DMA_Buffers_t.glob_buffer[3]);
- LIS3DSH_t.LIS3DSH_RawData_t.z = ((LIS3DSH_DMA_Buffers_t.glob_buffer[6] << 8) + LIS3DSH_DMA_Buffers_t.glob_buffer[5]);
- }
- LIS3DSH_ScaleData_TypeDef LIS3DSH_DMA_ConvertData(volatile LIS3DSH_RawData_TypeDef *rawData_ptr, LIS3DSH_ParaTypeDef *lis3dsh_str_ptr, LIS3DSH_BiasScaleData_TypeDef *lis3dsh_bias_scale_ptr)
- {
- LIS3DSH_ScaleData_TypeDef tempScaledData;
- tempScaledData.x = (rawData_ptr->x * lis3dsh_str_ptr->sensiLevelSetting * lis3dsh_bias_scale_ptr->X_ScaleData) + 0.0f - lis3dsh_bias_scale_ptr->X_BiasData;
- tempScaledData.y = (rawData_ptr->y * lis3dsh_str_ptr->sensiLevelSetting * lis3dsh_bias_scale_ptr->Y_ScaleData) + 0.0f - lis3dsh_bias_scale_ptr->Y_BiasData;
- tempScaledData.z = (rawData_ptr->z * lis3dsh_str_ptr->sensiLevelSetting * lis3dsh_bias_scale_ptr->Z_ScaleData) + 0.0f - lis3dsh_bias_scale_ptr->Z_BiasData;
- return tempScaledData;
- }
Testy:
W tej części chciałbym opisać na jakie elementy warto zwrócić uwagę podczas wykonywania programu oraz w jaki sposób przetestować funkcjonalność.
Korzystanie z liczb typu float:
Układ stm32 jest wyposażony w jednostkę FPU (Float Pointing Unit), która znacząco skraca czas potrzebny na wykonanie operacji na liczbach zmiennoprzecinkowych.
Jak można zaobserwować w tabelce powyżej większość operacji na liczbach zmiennoprzecinkowych odbywa się w pojedynczych instrukcjach.
Aby ją uruchomić należy ustawić odpowiednią flagę kompilatora (-mfpu=fpv4-sp-d16). W CubeIDE ustawiana jest ona w konfiguracji projektu.
- -mfpu=fpv4-sp-d16
Inicjalizacja czujnika:
Przed uruchomieniem urządzenia warto sprawdzić czy komunikujemy się z poprawnym modułem. Tutaj najprostszym sposobem jest skorzystanie z danych z rejestru WHOAMI i odczytanie ID układu (Funkcja i rejestr opisany w części inicjalizacji)
Komunikacja:
Cały proces komunikacji jest obsługiwany przez biblioteki HAL. W związku z tym najlepszym sposobem sprawdzania działania jest obsługiwanie wartości zwracanych podczas zapisu/odczytu danych. Jeśli otrzymane wyniki są różne od HAL_OK, wtedy proces komunikacji nie przeszedł poprawnie i trzeba wykonać odpowiednie działania np. ponowne wysłanie komendy zapisu/odczytu danych, ponowna inicjalizacja czujnika, reset procesora, wywołanie informacji o błędnym działaniu czujnika.
Testy jednostkowe:
Jedynym elementem jaki można przetestować w testach jednostkowych jest funkcja obrabiające otrzymane wyniki pomiaru. I tutaj jest to dosyć naciągane, ponieważ funkcje obliczające wynik nie będą ulegały zmianie i ich implementacja przez cały czas życia projektu pozostanie niezmieniona i raczej nie będzie poprawiana. Pozostałe elementy zależą od elementów sprzętowych jak pobieranie danych z SPI, zakłócenia na tych liniach itp. itd.
Podobnie wygląda sytuacja np. z taką funkcją:
- static float lis3dsh_SetSensiLevel_BasedFullscale(const LIS3DSH_FullScale_Enum_TypeDef fullScaleSetting)
- {
- if(fullScaleSetting == FULLSCALE_2_EN) { return LIS3DSH_SENSI_0_06G; }
- else if(fullScaleSetting == FULLSCALE_4_EN) { return LIS3DSH_SENSI_0_12G; }
- else if(fullScaleSetting == FULLSCALE_6_EN) { return LIS3DSH_SENSI_0_18G; }
- else if(fullScaleSetting == FULLSCALE_8_EN) { return LIS3DSH_SENSI_0_24G; }
- else if(fullScaleSetting == FULLSCALE_16_EN) { return LIS3DSH_SENSI_0_73G; }
- else { return LIS3DSH_SENSI_0_12G; }
- }
gdzie na podstawie jednego parametru zwracamy drugi. W przypadku takich funkcji napisanie testów jednostkowych będzie całkowicie bezsensowne.
Jeśli powstała by funkcja wykonująca różne obliczenia, czy różne akcje w zależności od odczytanych wyników czy kilku parametrów wejściowych to napisanie testów sprawdzających jej działanie względem różnych warunków wejściowych byłoby pomocne.