Kursy • Poradniki • Inspirujące DIY • Forum
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 nie są już produkowane i istnieje raczej mała szansa, że się na nie natkniemy, ale dostępnych sporo przykładów i bibliotek przeznaczonych dla starszych modeli, które mogę nie działać z nowym LSM303D.
Starsze modele LSM303DLM i LSM303DLHC działają inaczej niż omawiany LSM303D. Należy dokładnie sprawdzać oznaczenie posiadanego układu.
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.
Różnica, to inny adres układu oraz wysyłanie numeru rejestru
zamiast adresu w pamięci EEPROM.
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.
Moduł akcelerometru posiada wbudowane rezystory podciągające, więc nie ma konieczności podłączania rezystorów do linii SDA i SCL. Dodatkowo domyślnie wybierany jest adres 0x3a oraz komunikacja po I2C.
Teraz możemy podłączyć moduł zgodnie z rysunkiem:
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
Procedury, które napisaliśmy poprzednio nazywały się eeprom_write i eeprom_read. W nowym programie możemy je wykorzystać, ale dla czytelności kodu zmienimy im nazwy. Nazwiemy je lsm_write i lsm_read oraz zmienimy nazwę parametru - teraz zamiast adresu w pamięci przekazujemy numer rejestru w układzie LSM303D.
Kod z tymi zmianami wygląda następująco:
void lsm_set_reg(uint8_t reg)
{
I2C_GenerateSTART(I2C1, ENABLE);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_Send7bitAddress(I2C1, LSM303D_ADDR, I2C_Direction_Transmitter);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
I2C_SendData(I2C1, 0x80 | reg);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
}
void lsm_write(uint8_t reg, const void* data, int size)
{
int i;
const uint8_t* buffer = (uint8_t*)data;
lsm_set_reg(reg);
for (i = 0; i < size; i++) {
I2C_SendData(I2C1, buffer[i]);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
}
I2C_GenerateSTOP(I2C1, ENABLE);
}
void lsm_read(uint8_t reg, void* data, int size)
{
int i;
uint8_t* buffer = (uint8_t*)data;
lsm_set_reg(reg);
I2C_GenerateSTART(I2C1, ENABLE);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_AcknowledgeConfig(I2C1, ENABLE);
I2C_Send7bitAddress(I2C1, LSM303D_ADDR, I2C_Direction_Receiver);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
for (i = 0; i < size - 1; i++) {
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
buffer[i] = I2C_ReceiveData(I2C1);
}
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
buffer[i] = I2C_ReceiveData(I2C1);
}
Pełna lista oraz opis dostępnych rejestrów znajduje się w dokumentacji LSM303D. Poniżej fragment dokumentacji:
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 jego zawartość, sprawdzimy czy jest poprawna i wyślemy przez UART odpowiedni komunikat. Program, który to realizuje wygląda następująco:
uint8_t who_am_i = 0;
lsm_read(0x0f, &who_am_i, sizeof(who_am_i));
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 <stdio.h>
#include "stm32f10x.h"
#define LSM303D_ADDR 0x3a
void send_char(char c)
{
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
USART_SendData(USART2, c);
}
int __io_putchar(int c)
{
if (c=='n')
send_char('r');
send_char(c);
return c;
}
void lsm_set_reg(uint8_t reg)
{
I2C_GenerateSTART(I2C1, ENABLE);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_Send7bitAddress(I2C1, LSM303D_ADDR, I2C_Direction_Transmitter);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
I2C_SendData(I2C1, 0x80 | reg);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
}
void lsm_write(uint8_t reg, const void* data, int size)
{
int i;
const uint8_t* buffer = (uint8_t*)data;
lsm_set_reg(reg);
for (i = 0; i < size; i++) {
I2C_SendData(I2C1, buffer[i]);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
}
I2C_GenerateSTOP(I2C1, ENABLE);
}
void lsm_read(uint8_t reg, void* data, int size)
{
int i;
uint8_t* buffer = (uint8_t*)data;
lsm_set_reg(reg);
I2C_GenerateSTART(I2C1, ENABLE);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_AcknowledgeConfig(I2C1, ENABLE);
I2C_Send7bitAddress(I2C1, LSM303D_ADDR, I2C_Direction_Receiver);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
for (i = 0; i < size - 1; i++) {
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
buffer[i] = I2C_ReceiveData(I2C1);
}
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
buffer[i] = I2C_ReceiveData(I2C1);
}
int main(void)
{
GPIO_InitTypeDef gpio;
I2C_InitTypeDef i2c;
USART_InitTypeDef uart;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
GPIO_StructInit(&gpio);
gpio.GPIO_Pin = GPIO_Pin_2;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &gpio);
gpio.GPIO_Pin = GPIO_Pin_3;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio);
USART_StructInit(&uart);
uart.USART_BaudRate = 115200;
USART_Init(USART2, &uart);
USART_Cmd(USART2, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; // SCL, SDA
gpio.GPIO_Mode = GPIO_Mode_AF_OD;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
I2C_StructInit(&i2c);
i2c.I2C_Mode = I2C_Mode_I2C;
i2c.I2C_ClockSpeed = 100000;
I2C_Init(I2C1, &i2c);
I2C_Cmd(I2C1, ENABLE);
printf("Wyszukiwanie akcelerometru...n");
uint8_t who_am_i = 0;
lsm_read(0x0f, &who_am_i, sizeof(who_am_i));
if (who_am_i == 0x49) {
printf("Znaleziono akcelerometr LSM303Dn");
} else {
printf("Niepoprawna odpowiedź układu(0x%02X)n", who_am_i);
}
for(;;);
}
W efekcie powinniśmy zobaczyć następujący komunikat w okienku terminala portu szeregowego:
Natomiast układ na płytce stykowej wyglądał tak:
Odczyt temperatury
Zanim przejdziemy dalej, napiszemy kilka funkcji pomocniczych oraz uporządkujemy nieco program. Rejestry sterujące LSM303D są 8-bitowe, więc do zapisu i odczytu możemy napisać funkcje, które nieco ułatwią korzystanie z nich (unikniemy używania sizeof() za każdym razem):
void lsm_write_reg(uint8_t reg, uint8_t value)
{
lsm_write(reg, &value, sizeof(value));
}
uint8_t lsm_read_reg(uint8_t reg)
{
uint8_t value = 0;
lsm_read(reg, &value, sizeof(value));
return value;
}
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;
lsm_read(reg, &value, sizeof(value));
return value;
}
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 "magiczych liczb".
Plik nagłówkowy lsm303d.h wygląda następująco:
#ifndef LSM303D_H_
#define LSM303D_H_
#include <stdint.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 void lsm_write(uint8_t reg, const void* data, int size);
extern void lsm_read(uint8_t reg, void* data, int size);
extern void lsm_write_reg(uint8_t reg, uint8_t value);
extern uint8_t lsm_read_reg(uint8_t reg);
extern int16_t lsm_read_value(uint8_t reg);
#endif /* LSM303D_H_ */
Natomiast plik z kodem lsm303d.c:
#include "stm32f10x.h"
#include "lsm303d.h"
#define LSM303D_ADDR 0x3a
void lsm_set_reg(uint8_t reg)
{
I2C_GenerateSTART(I2C1, ENABLE);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_Send7bitAddress(I2C1, LSM303D_ADDR, I2C_Direction_Transmitter);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
I2C_SendData(I2C1, 0x80 | reg);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
}
void lsm_write(uint8_t reg, const void* data, int size)
{
int i;
const uint8_t* buffer = (uint8_t*)data;
lsm_set_reg(reg);
for (i = 0; i < size; i++) {
I2C_SendData(I2C1, buffer[i]);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
}
I2C_GenerateSTOP(I2C1, ENABLE);
}
void lsm_read(uint8_t reg, void* data, int size)
{
int i;
uint8_t* buffer = (uint8_t*)data;
lsm_set_reg(reg);
I2C_GenerateSTART(I2C1, ENABLE);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_AcknowledgeConfig(I2C1, ENABLE);
I2C_Send7bitAddress(I2C1, LSM303D_ADDR, I2C_Direction_Receiver);
while (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
for (i = 0; i < size - 1; i++) {
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
buffer[i] = I2C_ReceiveData(I2C1);
}
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
buffer[i] = I2C_ReceiveData(I2C1);
}
void lsm_write_reg(uint8_t reg, uint8_t value)
{
lsm_write(reg, &value, sizeof(value));
}
uint8_t lsm_read_reg(uint8_t reg)
{
uint8_t value = 0;
lsm_read(reg, &value, sizeof(value));
return value;
}
int16_t lsm_read_value(uint8_t reg)
{
int16_t value = 0;
lsm_read(reg, &value, sizeof(value));
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.
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. Jak pamiętamy, w części kursu o LCD utworzyliśmy moduł delay.h, zawierający funkcję delay_ms. Możemy z niego skorzystać po prostu kopiując pliki delay.h i delay.c do naszego projektu. Odczyt temperatury jest już bardzo 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)
delay_ms(100);
while (1) {
int16_t temp = lsm_read_value(LSM303D_TEMP_OUT);
printf("Temp = %dn", temp);
delay_ms(200);
}
Pełny kod programu widzimy poniżej. Należy oczywiście pamiętać o dołączeniu plików delay.h, delay.c oraz lsm303d.h i lsm303d.c:
#include <stdio.h>
#include "stm32f10x.h"
#include "lsm303d.h"
#include "delay.h"
void send_char(char c)
{
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
USART_SendData(USART2, c);
}
int __io_putchar(int c)
{
if (c=='n')
send_char('r');
send_char(c);
return c;
}
int main(void)
{
GPIO_InitTypeDef gpio;
I2C_InitTypeDef i2c;
USART_InitTypeDef uart;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
GPIO_StructInit(&gpio);
gpio.GPIO_Pin = GPIO_Pin_2;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &gpio);
gpio.GPIO_Pin = GPIO_Pin_3;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio);
USART_StructInit(&uart);
uart.USART_BaudRate = 115200;
USART_Init(USART2, &uart);
USART_Cmd(USART2, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; // SCL, SDA
gpio.GPIO_Mode = GPIO_Mode_AF_OD;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
I2C_StructInit(&i2c);
i2c.I2C_Mode = I2C_Mode_I2C;
i2c.I2C_ClockSpeed = 100000;
I2C_Init(I2C1, &i2c);
I2C_Cmd(I2C1, ENABLE);
SysTick_Config(SystemCoreClock / 1000);
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)
delay_ms(100);
while (1) {
int16_t temp = lsm_read_value(LSM303D_TEMP_OUT);
printf("Temp = %dn", temp);
delay_ms(200);
}
}
Rezultat działania widzimy w oknie terminala - powinna być to aktualna temperatura:
Akcelerometr
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):
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 <stdio.h>
#include "stm32f10x.h"
#include "lsm303d.h"
#include "delay.h"
void send_char(char c)
{
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
USART_SendData(USART2, c);
}
int __io_putchar(int c)
{
if (c=='n')
send_char('r');
send_char(c);
return c;
}
int main(void)
{
GPIO_InitTypeDef gpio;
I2C_InitTypeDef i2c;
USART_InitTypeDef uart;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
GPIO_StructInit(&gpio);
gpio.GPIO_Pin = GPIO_Pin_2;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &gpio);
gpio.GPIO_Pin = GPIO_Pin_3;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio);
USART_StructInit(&uart);
uart.USART_BaudRate = 115200;
USART_Init(USART2, &uart);
USART_Cmd(USART2, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; // SCL, SDA
gpio.GPIO_Mode = GPIO_Mode_AF_OD;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
I2C_StructInit(&i2c);
i2c.I2C_Mode = I2C_Mode_I2C;
i2c.I2C_ClockSpeed = 100000;
I2C_Init(I2C1, &i2c);
I2C_Cmd(I2C1, ENABLE);
SysTick_Config(SystemCoreClock / 1000);
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)
lsm_write_reg(LSM303D_CTRL1, 0x40|0x07); // AODR2 (25Hz) | AXEN | AYEN | AZEN
delay_ms(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);
delay_ms(200);
}
}
Efekt działania powinien wyglądać mniej więcej jak na obrazku:
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:
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.
Akcelerometr odczytuje przyspieszenie ziemskie, które wynosi właśnie 1 g.
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.
Odpowiednia kalibracja i filtracja, to całkiem skomplikowane zadanie.
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:
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.
Po prostu wybierzemy 2 osie i tylko nimi się będziemy zajmować.
Ja wybrałem osie X i Z.
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:
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:
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):
- delay,
- 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();
delay_ms(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.
Pełny kod programu:
#include <math.h>
#include <stdio.h>
#include "stm32f10x.h"
#include "lsm303d.h"
#include "delay.h"
#include "lcd.h"
#include "bitmap.h"
void send_char(char c)
{
while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
USART_SendData(USART2, c);
}
int __io_putchar(int c)
{
if (c=='n')
send_char('r');
send_char(c);
return c;
}
int main(void)
{
GPIO_InitTypeDef gpio;
I2C_InitTypeDef i2c;
USART_InitTypeDef uart;
SPI_InitTypeDef spi;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
GPIO_StructInit(&gpio);
gpio.GPIO_Pin = GPIO_Pin_2;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &gpio);
gpio.GPIO_Pin = GPIO_Pin_3;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio);
USART_StructInit(&uart);
uart.USART_BaudRate = 115200;
USART_Init(USART2, &uart);
USART_Cmd(USART2, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; // SCL, SDA
gpio.GPIO_Mode = GPIO_Mode_AF_OD;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
I2C_StructInit(&i2c);
i2c.I2C_Mode = I2C_Mode_I2C;
i2c.I2C_ClockSpeed = 100000;
I2C_Init(I2C1, &i2c);
I2C_Cmd(I2C1, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_7; // SCK, MOSI
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);
gpio.GPIO_Pin = GPIO_Pin_6; // MISO
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio);
gpio.GPIO_Pin = LCD_DC|LCD_CE|LCD_RST;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &gpio);
GPIO_SetBits(GPIOC, LCD_CE|LCD_RST);
SPI_StructInit(&spi);
spi.SPI_Mode = SPI_Mode_Master;
spi.SPI_NSS = SPI_NSS_Soft;
spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
SPI_Init(SPI1, &spi);
SPI_Cmd(SPI1, ENABLE);
SysTick_Config(SystemCoreClock / 1000);
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_CTRL5, 0x80|0x10); // TEMP_EN | M_ODR2 (50Hz)
lsm_write_reg(LSM303D_CTRL1, 0x40|0x07); // AODR2 (25Hz) | AXEN | AYEN | AZEN
delay_ms(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();
delay_ms(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 (kompasem cyfrowym). Zadanie to pozostawiam dla chętnych, jako zadanie domowe.
Nawigacja kursu
W kolejnym odcinku zajmiemy się małym podsumowaniem całego kursu. Przyjdzie również pora na QUIZ sprawdzający Waszą wiedzę! Jeśli nie chcesz przeoczyć kolejnego odcinka, to skorzystaj z poniższego formularza i zapisz się na powiadomienia o nowych publikacjach!
Autor kursu: Piotr (Elvis) Bugalski
Redakcja: Damian (Treker) Szymański
Załączniki
Powiązane wpisy
kurs, kursSTM32, programowanie, stm32
Trwa ładowanie komentarzy...