KursyPoradnikiInspirujące DIYForum

Kurs STM32 F1 HAL – #12 – I2C w praktyce, akcelerometr

Kurs STM32 F1 HAL – #12 – I2C w praktyce, akcelerometr

Na zakończenie kursu STM32 omówimy bardziej rozbudowany przykład. Na warsztat weźmiemy moduł z układem LSM303D, który zawiera m.in. akcelerometr oraz magnetometr.

Zobaczymy, jak można odczytywać z niego dane za pomocą I2C oraz jak zaprezentować wyniki na wyświetlaczu graficznym.

Podstawowe informacje o LSM303D

Na początek musimy poznać nasz czujnik. Jest on nieco bardziej skomplikowany, niż omawiane wcześniej. Na szczęście dostępna jest do niego dobra dokumentacja. W naszym przykładzie nie omówimy wszystkich możliwości układu LSM303D. Skupimy się jednak na realizacji możliwie prostego i uniwersalnego mechanizmu odczytu danych z tego czujnika.

Gotowe zestawy do kursów Forbota

 Komplet elementów  Gwarancja pomocy  Wysyłka w 24h

Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!

Zamów w Botland.com.pl »

 Najważniejsze materiały to:

  • datasheet - w nim znajdziemy pełny opis możliwości układu, protokół komunikacji itd.
  • schemat modułu - jak zwykle obraz jest wart więcej niż tysiąc słów. Warto zwrócić uwagę na sposób podłączenia układu oraz obecne rezystory podciągające

Przy okazji ważna uwaga. Omawiamy czujnik LSM303D, jednak można się zetknąć z układami o podobnej nazwie, ale innym działaniu. Są to starsze modele LSM303DLM i LSM303DLHC - pomimo bardzo podobnej nazwy, ich sposób działania jest inny. Układy te nie są już produkowane. Istnieje nikła szansa, że się na nie natkniemy, ale sporo dostępnych przykładów i bibliotek przeznaczonych jest dla starszych modeli, które mogę nie działać z nowym LSM303D.

Moduł LSM303D wykorzystywany w tej części. Źródło: POLOLU - producent.

Moduł LSM303D wykorzystywany w tej części. Źródło: POLOLU - producent.

LSM303D - komunikacja z modułem

W poprzedniej części kursu omówiliśmy sposób komunikacji poprzez I2C. Teraz możemy wykorzystać zdobytą wiedzę oraz napisany już program do rozpoczęcia pracy z nowym układem. Czytając datasheet zobaczymy, że komunikacja jest bardzo podobna do poznanej poprzednio.

Poprzednio używaliśmy układu 24AA01, który posiadał adres 0xa0. Nowy układ może mieć jeden z dwóch adresów 0x3c lub 0x3a, w zależności od stanu wyprowadzenia SA0. Oglądając schemat modułu zobaczymy, że na płytce znajduje się rezystor podciągający, co oznacza, że linia SA0 ma domyślnie stan wysoki, a adres układu to 0x3a. Na schemacie zobaczymy również, że linie SDA i SCL posiadają już rezystory podciągające. Możemy więc zrezygnować z podłączania własnych rezystorów podciągających.

Teraz możemy podłączyć moduł zgodnie z rysunkiem:

i2c-lsm303_bb

STM32 - schemat podłączenia modułu z LSM303D.

Gdy mamy podłączony układ, czas zająć się programem. Wykorzystamy kod napisany poprzednio, dostosowując go do nowego układu.

Po pierwsze musimy zmienić adres układu. Poprzednio było to 0xa0, teraz zdefiniujemy stałą, w której zapiszemy adres układu. Ułatwi to ewentualną modyfikację adresu w przyszłości:

#define LSM303D_ADDR			0x3a

Poprzednio wywoływaliśmy HAL_I2C_Mem_Read oraz HAL_I2C_Mem_Write, ale znaczna liczba parametrów oraz konieczność rzutowania typu na uint8_t nie jest zbyt wygodna. Dlatego w nowym programie napiszemy własne funkcje, które nieco ułatwią nam pracę. Na początek napiszemy funkcję odczytującą wartość z rejestru układu LSM303D. Schemat komunikacji z układem jest taki sam jak w przypadku pamięci eeprom, więc chociaż to nie pamięć również wywołamy HAL_I2C_Mem_Read.

Kod wygląda następująco:

uint8_t lsm_read_reg(uint8_t reg)
{
	uint8_t value = 0;

	HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY);

	return value;
}

Pełna lista oraz opis dostępnych rejestrów znajduje się w dokumentacji LSM303D. Poniżej jej fragment:

LSM_01

Fragment dokumentacji układu LSM303D.

Zaczniemy od sprawdzenia, czy nasz układ w ogóle działa. Jak widzimy, dostępny jest rejestr WHO_AM_I (adres 0x0f). Jest to rejestr tylko do odczytu, który zawsze powinien zwracać wartość 01001001b, czyli 0x49.

Odczytamy teraz jego zawartość, sprawdzimy czy jest poprawna i wyślemy przez UART odpowiedni komunikat. Program, który to realizuje wygląda następująco:

printf("Wyszukiwanie akcelerometru...n");
	uint8_t who_am_i = lsm_read_reg(0xf);

	 if (who_am_i == 0x49) {
		 printf("Znaleziono akcelerometr LSM303Dn");
	 } else {
		 printf("Niepoprawna odpowiedź układu (0x%02X)n", who_am_i);
	 }

Komunikację przez UART oraz przekierowanie printf już znamy, więc program nie powinien być trudny do przeanalizowania:

#include "stm32f1xx.h"

#define LSM303D_ADDR			0x3a

I2C_HandleTypeDef i2c;
UART_HandleTypeDef uart;

void send_char(char c)
{
	HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000);
}

int __io_putchar(int ch)
{
	if (ch == 'n')
		send_char('r');
	send_char(ch);
	return ch;
}

uint8_t lsm_read_reg(uint8_t reg)
{
	uint8_t value = 0;

	HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY);

	return value;
}

int main(void)
{
	SystemCoreClock = 8000000; // taktowanie 8Mhz
	HAL_Init();

	__HAL_RCC_GPIOA_CLK_ENABLE();
	__HAL_RCC_GPIOB_CLK_ENABLE();
	__HAL_RCC_GPIOC_CLK_ENABLE();
	__HAL_RCC_I2C1_CLK_ENABLE();
	__HAL_RCC_USART2_CLK_ENABLE();

	GPIO_InitTypeDef gpio;
	gpio.Mode = GPIO_MODE_AF_OD;
	gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;		// SCL, SDA
	gpio.Pull = GPIO_PULLUP;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOB, &gpio);

	gpio.Mode = GPIO_MODE_AF_PP;
	gpio.Pin = GPIO_PIN_2;
	gpio.Pull = GPIO_NOPULL;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Mode = GPIO_MODE_AF_INPUT;
	gpio.Pin = GPIO_PIN_3;
	HAL_GPIO_Init(GPIOA, &gpio);

	i2c.Instance             = I2C1;
	i2c.Init.ClockSpeed      = 100000;
	i2c.Init.DutyCycle       = I2C_DUTYCYCLE_2;
	i2c.Init.OwnAddress1     = 0xff;
	i2c.Init.AddressingMode  = I2C_ADDRESSINGMODE_7BIT;
	i2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
	i2c.Init.OwnAddress2     = 0xff;
	i2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
	i2c.Init.NoStretchMode   = I2C_NOSTRETCH_DISABLE;
	HAL_I2C_Init(&i2c);

	uart.Instance = USART2;
	uart.Init.BaudRate = 115200;
	uart.Init.WordLength = UART_WORDLENGTH_8B;
	uart.Init.Parity = UART_PARITY_NONE;
	uart.Init.StopBits = UART_STOPBITS_1;
	uart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	uart.Init.OverSampling = UART_OVERSAMPLING_16;
	uart.Init.Mode = UART_MODE_TX_RX;
	HAL_UART_Init(&uart);

	printf("Wyszukiwanie akcelerometru...n");
	uint8_t who_am_i = lsm_read_reg(0xf);

	 if (who_am_i == 0x49) {
		 printf("Znaleziono akcelerometr LSM303Dn");
	 } else {
		 printf("Niepoprawna odpowiedź układu (0x%02X)n", who_am_i);
	 }

	 while (1) {
	 }
}

W efekcie powinniśmy zobaczyć następujący komunikat w okienku terminala portu szeregowego:

LSM_02

Efekt programu sprawdzającego działanie modułu z LSM303D.

Natomiast układ na płytce stykowej wyglądał tak:

STM32 - pierwsze próby z modułem LSM303D.

STM32 - pierwsze próby z modułem LSM303D.

Odczyt temperatury

Zanim przejdziemy dalej, napiszemy kilka funkcji pomocniczych oraz uporządkujemy nieco program. Napisaliśmy już funkcję do odczytu rejestru, więc teraz napiszemy jej odpowiednik do zapisu:

void lsm_write_reg(uint8_t reg, uint8_t value)
{
	HAL_I2C_Mem_Write(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY);
}

Dodatkowo czytając dokumentację, zauważymy, że odczytywane wartości np. temperatury, przyspieszenia, czy strumienia magnetycznego są wartościami 16-bitowymi ze znakiem (zapisanymi w kodzie U2).

Możemy również napisać prostą funkcję, która będzie odczytywała taką wartość:

int16_t lsm_read_value(uint8_t reg)
{
	int16_t value = 0;

	HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg | 0x80, 1, (uint8_t*)&value, sizeof(value), HAL_MAX_DELAY);

	return value;
}

Ta sztuczka z reg | 0x80 oznacza włączenie auto-inkrementacji. Dzięki temu gdy odczytujemy dwa bajty, zamiast dwa razy to samo otrzymujemy wartości z kolejnych rejestrów. Wszystko jest opisane w dokumentacji LSM303D (ale łatwo o tym zapomnieć).

Teraz czas trochę posprzątać - podobnie jak w części dotyczącej wyświetlacza, przeniesiemy funkcje sterujące układem LSM303D do osobnych plików. Dodamy również stałe, definiujące rejestry układu. Będziemy mogli wykorzystywać je w programie zamiast "magicznych liczb".

Plik nagłówkowy lsm303d.h wygląda następująco:

#ifndef LSM303D_H_
#define LSM303D_H_

#include "stm32f1xx.h"

#define LSM303D_TEMP_OUT			0x05
#define LSM303D_STATUS_M			0x07
#define LSM303D_OUT_X_M				0x08
#define LSM303D_OUT_Y_M				0x0a
#define LSM303D_OUT_Z_M				0x0c
#define LSM303D_WHO_AM_I			0x0f
#define LSM303D_CTRL0				0x1f
#define LSM303D_CTRL1				0x20
#define LSM303D_CTRL2				0x21
#define LSM303D_CTRL3				0x22
#define LSM303D_CTRL4				0x23
#define LSM303D_CTRL5				0x24
#define LSM303D_CTRL6				0x25
#define LSM303D_CTRL7				0x26
#define LSM303D_STATUS				0x27
#define LSM303D_OUT_X_A				0x28
#define LSM303D_OUT_Y_A				0x2a
#define LSM303D_OUT_Z_A				0x2c

extern I2C_HandleTypeDef i2c;

void lsm_write_reg(uint8_t reg, uint8_t value);
uint8_t lsm_read_reg(uint8_t reg);
int16_t lsm_read_value(uint8_t reg);

#endif /* LSM303D_H_ */

Natomiast plik z kodem lsm303d.c:

#include "lsm303d.h"

#define LSM303D_ADDR			0x3a

I2C_HandleTypeDef i2c;

void lsm_write_reg(uint8_t reg, uint8_t value)
{
	HAL_I2C_Mem_Write(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY);
}

uint8_t lsm_read_reg(uint8_t reg)
{
	uint8_t value = 0;

	HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg, 1, &value, sizeof(value), HAL_MAX_DELAY);

	return value;
}

int16_t lsm_read_value(uint8_t reg)
{
	int16_t value = 0;

	HAL_I2C_Mem_Read(&i2c, LSM303D_ADDR, reg | 0x80, 1, (uint8_t*)&value, sizeof(value), HAL_MAX_DELAY);

	return value;
}

Teraz możemy wrócić do programu głównego. Musimy odpowiednio skonfigurować LSM303D, czyli włączyć pomiar temperatury. W dokumentacji znajdujemy odpowiedni rejestr, czyli CTRL5.

LSM_03

Fragment dokumentacji LSM303D dotyczący pomiaru temperatury.

Jak widzimy, domyślnie pomiar temperatury jest wyłączony (pole TEMP_EN ma wartość 0). Zapiszemy więc do CTRL5 odpowiednią wartość

lsm_write_reg(LSM303D_CTRL5, 0x80|0x10);	// TEMP_EN | M_ODR2 (50Hz)

Przy okazji ustawiliśmy częstotliwość odczytów z magnetometru na 50Hz - nie wykorzystamy tego w tym momencie, ale skoro jest w tym samym rejestrze, to dlaczego nie ustawić jej "na zapas".

Po włączeniu pomiaru, musimy dać układowi trochę czasu, dodajemy więc małe opóźnienie. Odczyt temperatury jest już prosty - wystarczy wczytywać wartość z rejestrów TEMP_OUT_L i TEMP_OUT_H.

Napisaliśmy funkcję lsm_read_value, więc oba rejestry odczytamy jednocześnie. Funkcja wykona za nas również konwersję na 16-bitową wartość ze znakiem. Kod wygląda następująco:

lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz)

	 HAL_Delay(100);

	 while (1) {
		 int16_t temp = lsm_read_value(LSM303D_TEMP_OUT);
		 printf("Temp = %dn", temp);
		 HAL_Delay(200);
	 }

Pełny kod programu widzimy poniżej:

#include "stm32f1xx.h"
#include "lsm303d.h"

UART_HandleTypeDef uart;

void send_char(char c)
{
	HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000);
}

int __io_putchar(int ch)
{
	if (ch == 'n')
		send_char('r');
	send_char(ch);
	return ch;
}

int main(void)
{
	SystemCoreClock = 8000000; // taktowanie 8Mhz
	HAL_Init();

	__HAL_RCC_GPIOA_CLK_ENABLE();
	__HAL_RCC_GPIOB_CLK_ENABLE();
	__HAL_RCC_GPIOC_CLK_ENABLE();
	__HAL_RCC_I2C1_CLK_ENABLE();
	__HAL_RCC_USART2_CLK_ENABLE();

	GPIO_InitTypeDef gpio;
	gpio.Mode = GPIO_MODE_AF_OD;
	gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;		// SCL, SDA
	gpio.Pull = GPIO_PULLUP;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOB, &gpio);

	gpio.Mode = GPIO_MODE_AF_PP;
	gpio.Pin = GPIO_PIN_2;
	gpio.Pull = GPIO_NOPULL;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Mode = GPIO_MODE_AF_INPUT;
	gpio.Pin = GPIO_PIN_3;
	HAL_GPIO_Init(GPIOA, &gpio);

	i2c.Instance             = I2C1;
	i2c.Init.ClockSpeed      = 100000;
	i2c.Init.DutyCycle       = I2C_DUTYCYCLE_2;
	i2c.Init.OwnAddress1     = 0xff;
	i2c.Init.AddressingMode  = I2C_ADDRESSINGMODE_7BIT;
	i2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
	i2c.Init.OwnAddress2     = 0xff;
	i2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
	i2c.Init.NoStretchMode   = I2C_NOSTRETCH_DISABLE;
	HAL_I2C_Init(&i2c);

	uart.Instance = USART2;
	uart.Init.BaudRate = 115200;
	uart.Init.WordLength = UART_WORDLENGTH_8B;
	uart.Init.Parity = UART_PARITY_NONE;
	uart.Init.StopBits = UART_STOPBITS_1;
	uart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	uart.Init.OverSampling = UART_OVERSAMPLING_16;
	uart.Init.Mode = UART_MODE_TX_RX;
	HAL_UART_Init(&uart);

	printf("Wyszukiwanie akcelerometru...n");
	uint8_t who_am_i = lsm_read_reg(LSM303D_WHO_AM_I);

	 if (who_am_i == 0x49) {
		 printf("Znaleziono akcelerometr LSM303Dn");
	 } else {
		 printf("Niepoprawna odpowiedź układu (0x%02X)n", who_am_i);
	 }

	 lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz)

	 HAL_Delay(100);

	 while (1) {
		 int16_t temp = lsm_read_value(LSM303D_TEMP_OUT);
		 printf("Temp = %dn", temp);
		 HAL_Delay(200);
	 }
}

Rezultat działania widoczny w oknie terminala - powinna być to aktualna temperatura:

LSM_04

LSM303D - odczyt aktualnej temperatury.

Akcelerometr w praktyce na STM32

Poznaliśmy już trochę układ LSM303D, czas przejść do głównego tematu tej części kursu, czyli odczytu danych z akcelerometru. W tym celu musimy nieco zmienić konfigurację układu (włączyć akcelerometr) oraz odczytać dane.

Opis konfiguracji jak zwykle znajdziemy w nocie katalogowej. Tym razem interesuje nas rejestr CTRL1. Trzy najwyższe bity określają prędkość odczytywania danych (ustawimy 25Hz jako przykład). Natomiast trzy najniższe bity odpowiadają za uruchomienie odczytów (z osi Z, Y i X):

LSM_05

Fragment dokumentacji LSM303D dotyczący akcelerometru.

Konfigurację wykonamy instrukcją:

lsm_write_reg(LSM303D_CTRL1, 0x40|0x07); // AODR2 (25Hz) | AXEN | AYEN | AZEN

Teraz możemy, podobnie jak w przypadku temperatury pobierać i wyświetlać dane:

while (1) {
	int16_t a_x = lsm_read_value(LSM303D_OUT_X_A);
	int16_t a_y = lsm_read_value(LSM303D_OUT_Y_A);
	int16_t a_z = lsm_read_value(LSM303D_OUT_Z_A);
	printf("X = %d   Y = %d   Z = %dn", a_x, a_y, a_z);
}

W programie znajdziemy nieco więcej kodu, w tym zakomentowane instrukcje printf - omówimy je za chwilę. Cały program wygląda następująco:

#include <math.h>
#include "stm32f1xx.h"
#include "lsm303d.h"

I2C_HandleTypeDef i2c;
UART_HandleTypeDef uart;

void send_char(char c)
{
	HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000);
}

int __io_putchar(int ch)
{
	if (ch == 'n')
		send_char('r');
	send_char(ch);
	return ch;
}

int main(void)
{
	SystemCoreClock = 8000000; // taktowanie 8Mhz
	HAL_Init();

	__HAL_RCC_GPIOA_CLK_ENABLE();
	__HAL_RCC_GPIOB_CLK_ENABLE();
	__HAL_RCC_GPIOC_CLK_ENABLE();
	__HAL_RCC_I2C1_CLK_ENABLE();
	__HAL_RCC_USART2_CLK_ENABLE();

	GPIO_InitTypeDef gpio;
	gpio.Mode = GPIO_MODE_AF_OD;
	gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;		// SCL, SDA
	gpio.Pull = GPIO_PULLUP;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOB, &gpio);

	gpio.Mode = GPIO_MODE_AF_PP;
	gpio.Pin = GPIO_PIN_2;
	gpio.Pull = GPIO_NOPULL;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Mode = GPIO_MODE_AF_INPUT;
	gpio.Pin = GPIO_PIN_3;
	HAL_GPIO_Init(GPIOA, &gpio);

	i2c.Instance             = I2C1;
	i2c.Init.ClockSpeed      = 100000;
	i2c.Init.DutyCycle       = I2C_DUTYCYCLE_2;
	i2c.Init.OwnAddress1     = 0xff;
	i2c.Init.AddressingMode  = I2C_ADDRESSINGMODE_7BIT;
	i2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
	i2c.Init.OwnAddress2     = 0xff;
	i2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
	i2c.Init.NoStretchMode   = I2C_NOSTRETCH_DISABLE;
	HAL_I2C_Init(&i2c);

	uart.Instance = USART2;
	uart.Init.BaudRate = 115200;
	uart.Init.WordLength = UART_WORDLENGTH_8B;
	uart.Init.Parity = UART_PARITY_NONE;
	uart.Init.StopBits = UART_STOPBITS_1;
	uart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	uart.Init.OverSampling = UART_OVERSAMPLING_16;
	uart.Init.Mode = UART_MODE_TX_RX;
	HAL_UART_Init(&uart);

	printf("Wyszukiwanie akcelerometru...n");
	uint8_t who_am_i = lsm_read_reg(LSM303D_WHO_AM_I);

	 if (who_am_i == 0x49) {
		 printf("Znaleziono akcelerometr LSM303Dn");
	 } else {
		 printf("Niepoprawna odpowiedź układu (0x%02X)n", who_am_i);
	 }

	 lsm_write_reg(LSM303D_CTRL1, 0x40|0x07); // AODR2 (25Hz) | AXEN | AYEN | AZEN
	 lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz)

	 HAL_Delay(100);

	 while (1) {
		 int16_t a_x = lsm_read_value(LSM303D_OUT_X_A);
		 int16_t a_y = lsm_read_value(LSM303D_OUT_Y_A);
		 int16_t a_z = lsm_read_value(LSM303D_OUT_Z_A);
		 printf("X = %d   Y = %d   Z = %dn", a_x, a_y, a_z);

		 float x = a_x * 2.0f / 32678.0f;
		 float y = a_y * 2.0f / 32678.0f;
		 float z = a_z * 2.0f / 32678.0f;
		 //printf("X = %.2f   Y = %.2f   Z = %.2fn", x, y, z);

		 float alpha = atan2f(z, x);
		 //printf("alpha = %.2fn", alpha * 180.0f / M_PI + 90.0f);

		 HAL_Delay(200);
	 }
}

Efekt działania powinien wyglądać mniej więcej jak poniżej:

LSM_06

Efekt działania programu LSM303D - odczyt z akcelerometru.

Wyniki mogą być nieco zaskakujące, jeśli nie zajmowaliśmy się wcześniej akcelerometrami. Spróbujmy więc zrozumieć co oznaczają. Odczyty są kodowane 16-bitowo ze znakiem. Więc ich zakres wynosi od -32768 do 32767. Czytając dokumentację, odkryjemy, że akcelerometr LSM303D może działać w kilku zakresach pomiarowych: 2 g, 4 g, 8 g i 12 g. Domyślna jest konfiguracja 2 g, do niej odnoszą się wyniki.

Ponieważ przyspieszenie 2 g, to pełny zakres, odpowiada mu odczyt 32767. Minimalna wartość wynosi -2 g, i daje wynik -32768. Możemy więc przeliczyć otrzymane wyniki na znane z fizyki wartości przyspieszenia:

float x = a_x * 2.0f / 32678.0f;
float y = a_y * 2.0f / 32678.0f;
float z = a_z * 2.0f / 32678.0f;
printf("X = %.2f   Y = %.2f   Z = %.2fn", x, y, z);

W kodzie programu wystarczy odkomentować drugi printf (najlepiej zakomentowując pierwszy - inaczej wyniki będą nieco nieczytelne). Otrzymamy teraz następujący rezultat:

LSM_07

Przeliczone wartości odczytane z LSM303D.

Mając wyniki w takiej postaci, łatwiej jest zrozumieć ich znaczenie. Jak widzimy, wzdłuż osi X i Y mamy niewielkie odczyty, a na oś Z działa przyspieszenie prawie 1 g. Wynik może być pewnym zaskoczeniem, przecież nasza płytka leży spokojnie na biurku, nie przyspiesza w żadnym kierunku.

Mamy wiec poprawny wynik - na oś Z działa przyspieszenie ziemskie, a na X i Y - też trochę. Gdyby czujnik idealnie wypoziomować, powinniśmy otrzymać przyspieszenie zerowe na X i Y oraz 1 g dla osi Z. Niestety, nawet wtedy otrzymany wynik mógłby się zmienić np. po zmianie temperatury. Dlatego czujnik powinien być kalibrowany.

Co więcej chcąc obliczać np. przesunięcie robota, musimy pamiętać, że 1 g pochodzące od przyspieszenia ziemskiego i tak pojawi się w otrzymywanych wynikach.

STM32 i LSM303D - Projekt prostej poziomicy

Warto sprawdzić jak będą zmieniały się odczyty przechylając płytkę z czujnikiem. Na module z LSM303D znajdziemy nadruk z oznaczeniem kierunków osi X, Y oraz Z:

lsm303d-3-osiowy-cyfrowy-akcelerometr-magnetometr-i2cspi-modul-pololu

Wykorzystywany moduł, źródło zdjęcia: strona producenta Pololu.

Spróbujemy wykorzystać nasz czujnik do określania nachylenia czujnika - zbudujemy więc prostą, cyfrową poziomicę. Na początek uprościmy sobie trochę zadanie i zamiast w 3D, będziemy rozwiązywać problem w dwóch wymiarach.

Spróbujmy napisać program, który wyświetli kąt pod jakim nasz czujnik jest ułożony względem płaszczyzny ziemi. Z pomocą przychodzi nam trygonometria. Funkcja arcus tangens pozwala na przeliczenie uzyskanych wyników:

float alpha = atan2f(z, x);
printf("alpha = %.2fn", alpha * 180.0f / M_PI + 90.0f);

Ponieważ wyniki są w radianach, przeliczamy je na stopnie. Dodajemy też 90, aby uzyskać kąt między płytką, a płaszczyzną ziemi. Efekt wygląda jak na poniższym screenie:

LSM_08

LSM303D - odczyt kąta płytki względem płaszczyzny ziemi.

Do prezentowania wyników wykorzystamy wyświetlacz LCD, który omówiliśmy wcześniej. Mamy więc przygotowane odpowiednie procedury - wystarczy skopiować pliki omówione wcześniej.

Układ podłączamy zgodnie z rysunkiem:

i2c-lsm-lcd_bb

Kurs STM32 - schemat podłączenia modułu LSM303D i wyświetlacza graficznego.

Jednocześnie będziemy wykorzystywać komunikację przez SPI, I2C oraz UART. W tym celu kopiujemy następujące moduły z poprzednich przykładów (pliki .h oraz .c):

  • lsm303d,
  • lcd,
  • bitmap,
  • font.

Mając odpowiednie biblioteki, możemy przystąpić do pisania programu. W programie wyświetlimy poziomą linię - która będzie zachowywała kierunek nawet podczas obracania płytki z czujnikiem i wyświetlaczem.

Do poprzedniego programu dodajemy kod inicjalizujący wyświetlacz - był omówiony wcześniej. Teraz omówimy tylko pętlę główną programu:

while (1) {
	int x = LCD_WIDTH * lsm_read_value(LSM303D_OUT_Y_A) / 32768;
	int y = LCD_HEIGHT * lsm_read_value(LSM303D_OUT_X_A) / 32768;

	lcd_clear();
	lcd_draw_line(LCD_WIDTH / 2 + x, LCD_HEIGHT / 2 - y, LCD_WIDTH / 2 - x, LCD_HEIGHT / 2 + y);
	lcd_copy();

	HAL_Delay(100);
}

Odczytujemy przyspieszenie działające wzdłuż osi X oraz Y (układ dużo lepiej działa w pionie, dlatego zamiast osi Z tym razem używana jest oś Y).

Mając wektor przyspieszenie ziemskiego, łatwo możemy obliczyć współrzędne odcinka do niego prostopadłego. W tym celu zamieniamy miejscami współrzędne x i y oraz przeliczamy zakres odczytów akcelerometru. Pełne wyprowadzenie matematyczne nie jest trudne - wystarczą podstawy geometrii, które pominiemy już w kursie.

Kod programu:

#include "stm32f1xx.h"
#include "lsm303d.h"
#include "lcd.h"
#include "bitmap.h"

UART_HandleTypeDef uart;

void send_char(char c)
{
	HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000);
}

int __io_putchar(int ch)
{
	if (ch == 'n')
		send_char('r');
	send_char(ch);
	return ch;
}

int main(void)
{
	SystemCoreClock = 8000000; // taktowanie 8Mhz
	HAL_Init();

	__HAL_RCC_GPIOA_CLK_ENABLE();
	__HAL_RCC_GPIOB_CLK_ENABLE();
	__HAL_RCC_GPIOC_CLK_ENABLE();
	__HAL_RCC_I2C1_CLK_ENABLE();
	__HAL_RCC_USART2_CLK_ENABLE();
	 __HAL_RCC_SPI1_CLK_ENABLE();

	GPIO_InitTypeDef gpio;
	gpio.Mode = GPIO_MODE_AF_OD;
	gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;		// SCL, SDA
	gpio.Pull = GPIO_PULLUP;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOB, &gpio);

	gpio.Mode = GPIO_MODE_AF_PP;
	gpio.Pin = GPIO_PIN_2;
	gpio.Pull = GPIO_NOPULL;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Mode = GPIO_MODE_AF_INPUT;
	gpio.Pin = GPIO_PIN_3;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Mode = GPIO_MODE_AF_PP;
	gpio.Pin = GPIO_PIN_5 | GPIO_PIN_7;		// SCK, MOSI
	gpio.Pull = GPIO_NOPULL;
	gpio.Speed = GPIO_SPEED_FREQ_HIGH;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Mode = GPIO_MODE_AF_INPUT;
	gpio.Pin = GPIO_PIN_3;						// MISO
	gpio.Pull = GPIO_PULLDOWN;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Mode = GPIO_MODE_OUTPUT_PP;
	gpio.Pin = LCD_DC|LCD_CE|LCD_RST;
	gpio.Pull = GPIO_NOPULL;
	HAL_GPIO_Init(GPIOC, &gpio);
	HAL_GPIO_WritePin(GPIOC, LCD_CE|LCD_RST, GPIO_PIN_SET);

	spi.Instance = SPI1;
	spi.Init.Mode = SPI_MODE_MASTER;
	spi.Init.NSS = SPI_NSS_SOFT;
	spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;	// 1MHz
	spi.Init.Direction = SPI_DIRECTION_2LINES;
	spi.Init.CLKPhase = SPI_PHASE_1EDGE;
	spi.Init.CLKPolarity = SPI_POLARITY_LOW;
	spi.Init.DataSize = SPI_DATASIZE_8BIT;
	spi.Init.FirstBit = SPI_FIRSTBIT_MSB;
	spi.Init.TIMode = SPI_TIMODE_DISABLE;
	spi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
	spi.Init.CRCPolynomial = 7;
	HAL_SPI_Init(&spi);
	__HAL_SPI_ENABLE(&spi);

	i2c.Instance             = I2C1;
	i2c.Init.ClockSpeed      = 100000;
	i2c.Init.DutyCycle       = I2C_DUTYCYCLE_2;
	i2c.Init.OwnAddress1     = 0xff;
	i2c.Init.AddressingMode  = I2C_ADDRESSINGMODE_7BIT;
	i2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
	i2c.Init.OwnAddress2     = 0xff;
	i2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
	i2c.Init.NoStretchMode   = I2C_NOSTRETCH_DISABLE;
	HAL_I2C_Init(&i2c);

	uart.Instance = USART2;
	uart.Init.BaudRate = 115200;
	uart.Init.WordLength = UART_WORDLENGTH_8B;
	uart.Init.Parity = UART_PARITY_NONE;
	uart.Init.StopBits = UART_STOPBITS_1;
	uart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	uart.Init.OverSampling = UART_OVERSAMPLING_16;
	uart.Init.Mode = UART_MODE_TX_RX;
	HAL_UART_Init(&uart);

	lcd_setup();
	lcd_draw_bitmap(forbot_logo);
	lcd_copy();

	printf("Wyszukiwanie akcelerometru...n");
	uint8_t who_am_i = lsm_read_reg(LSM303D_WHO_AM_I);

	 if (who_am_i == 0x49) {
		 printf("Znaleziono akcelerometr LSM303Dn");
	 } else {
		 printf("Niepoprawna odpowiedź układu (0x%02X)n", who_am_i);
	 }

	 lsm_write_reg(LSM303D_CTRL1, 0x40|0x07); // AODR2 (25Hz) | AXEN | AYEN | AZEN
	 lsm_write_reg(LSM303D_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz)

	 HAL_Delay(1000);

	 while (1) {
	 	int x = LCD_WIDTH * lsm_read_value(LSM303D_OUT_Y_A) / 32768;
	 	int y = LCD_HEIGHT * lsm_read_value(LSM303D_OUT_X_A) / 32768;

	 	lcd_clear();
	 	lcd_draw_line(LCD_WIDTH / 2 + x, LCD_HEIGHT / 2 - y, LCD_WIDTH / 2 - x, LCD_HEIGHT / 2 + y);
	 	lcd_copy();

	 	HAL_Delay(100);
	 }
}

Działanie programu w praktyce widoczne jest na poniższym filmie:

Podsumowanie

W tej części kursu poznaliśmy podstawy działania czujnika LSM303D. Sprawdziliśmy sposób komunikacji z układem. Wykorzystaliśmy wbudowany termometr oraz akcelerometr. Nie zajęliśmy się natomiast magnetometrem. Zadanie to pozostaje dla chętnych, jako zadanie domowe.

Nawigacja kursu

W kolejnym odcinku zajmiemy się małym podsumowaniem całego kursu. Poruszymy również temat kontynuacji serii związanej z programowaniem STM32.

Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk
Redakcja: Damian Szymański

Załączniki

akcelerometr, kurs, kursSTM32F1HAL, programowanie, stm32, żyroskop

Trwa ładowanie komentarzy...