четвер, 27 жовтня 2016 р.

Зовсім просто про акселерометр та магнітометр LSM303DLHC із HAL

Простий акселерометр. (c) Wiki
Плата STM32F3Discovery обладнана двома дуже цікавими сенсорами -- акселерометром і магнітометром, об'єднаними в одному корпусі -- LSM303DLHC та гіродатчиком (MEMS-гіроскопом) L3GD20.

Почнемо із акселерометра-магнітометра.

Документація на нього знаходиться тут: "Ultra compact high performance e-compass: 3D accelerometer and 3D magnetometer module", там же можна скачати офіційну документацію -- так-званий Datasheet (на жаль, посилання доволі швидко псуються, сайт часто реорганізовують, чи що, так що шукайте на сторінці). Щоб зрозуміти, як користуватися описаним в документації, варто почитати так-звані Appnote, наприклад: "AN3192 Application note: Using LSM303DLH for a tilt compensated electronic compass" (На жаль, на цей документ немає посилань із сторінки LSM303DLHC -- все ж, трішки інша модифікація, є хіба значно коротші, виду: "DT0058: Computing tilt measurement and tilt-compensated e-compass"). Бо із самої документації витягнути необхідне трохи важко -- це довідник, а не підручник...

Опишемо коротко його основні характеристики:

  • З точки зору програмного забезпечення акселерометр і магнітометр можна вважати незалежними пристроями.
  • Вони під'єднані до шини -- I2C, кожен із своєю адресою. 
  • Підтримують частоти шини 100 кГц і 400 кГц. 
  • Обидва пристрої -- трьох-осьові, здатні вимірювати всі три компоненти прискорення чи магнітного поля. (Як ми пам'ятаємо, і те і те є векторною величиною).
  • Діапазон прискорень, на вибір (із різною точністю):  ±2g/±4g/±8g/±16g.
  • Діапазон напруження магнітного поля: ±1.3 / ±1.9 / ±2.5 / ±4.0 / ±4.7 / ±5.6 / ±8.1 гаусів. 
  • Дві лінії переривань, які можна сконфігурувати за бажанням. Наприклад, генерувати переривання у відповідь на вільне падіння.
  • Два сигнали RDY -- data ready, про наявність чергового виміру. (Пам'ятаємо, що вимірювання не відбувається миттєво, потребує певного часу).
Орієнтацію осей відносно мікросхеми LSM303DLHC  можна побачити на наступному малюнку із документації:
Орієнтація осей приладів відносно корпусу і схема підключення. (c) STMicroelectronics.

На платі STM32F3Discovery пристрій підключено так:
Як видно, піни I2C під'єднані до PB6 і PB7, які можуть функціонувати як піни I2C-1 мікроконтролера, STM32F303, що на платі STM32F3Discovery.

Як працювати із акселерометром

Для початку, перерахуємо рівні абстракції, які беруть участь у взаємодії мікроконтролера та нашого пристрою:

  1. Фізичний зв'язок забезпечується шиною I2C.
  2. Протокол шини I2C описує, як саме відбуватиметься обмін інформацією між пристроями на шині (наприклад акселерометром і мікроконтролером).
  3. Керування акселерометром та читання даних від нього здійснюється за допомогою його регістрів (описаних в документації). При записі пристрій вважає, що перший байт даних від мікроконтролера буде номером регістра а наступний (наступні) -- даними для запису в регістр (регістри). Аналогічно, для читання мікроконтролер повідомляє, котрий регістр його цікавить, після чого очікує на його вміст. Як і при записі, можна читати декілька послідовних регістрів за раз.
  4. Дані отримуються у вигляді цілих чисел, які згідно спеціальної процедури можна перетворити у значення прискорення в тих чи інших одиницях.
Всі ці рівні абстракції слід реалізувати для успішної роботи. Розглянемо їх.

1  -- організація  обміну даних шиною I2C


Фізичні та логічні принципи роботи шини описані, наприклад, тут: "Робота з EEPROM пам'яттю 24CXX -- огляд" --  на прикладі взаємодії з одним із різновидів пам'яті.

Реалізувати протокол роботи цієї шини відносно просто "вручну", за допомогою GPIO. Приклад такої реалізації: "Робота з EEPROM пам'яттю 24CXX -- Software, Arduino". Звичайно, мова там про інший мікроконтролер, але невеликі зміни в тексті, виду заміни DigitalWrite() на HAL_GPIO_WritePin(), дозволять скористатися цим кодом і на STM32.

Однак, через поширеність даної шини, багато мікроконтролерів, включаючи STM32F3 (як і, здається, всі інші сімейства STM32), обладнані готовим периферійним пристроями/модулями для роботи із нею. Детальніше про них поговоримо іншого разу, зараз достатньо знати, що робота з цим пристроєм виглядає так:
  • Конфігурується режим роботи шини.
  • Надаються дані для передачі на підлеглий пристрій.
  • Якщо після цього очікується його відповідь, вона читається.
  • За потреби можна налаштувати переривання у відповідь на різні етапи обміну інформацією.
  • За потреби можна організувати обмін з використанням DMA.
Посилання стосовно роботи із периферією I2C на STM32 (звертайте увагу на конкретні моделі контролерів -- точне співпадіння буде далеко не завжди!):

2 -- взаємодія із  акселерометром

LSM303DLHC є підлеглим (Slave) пристроєм, тому мікроконтролеру доведеться бути головним (Master).

Фізична адреса акселерометра LSM303DLHC:
  • 00110010 (32h) для запису, 
  • 00110011 (33h) для читання (див. особливості роботи I2C -- молодший біт адреси визначає, відбувається читання з пристрою чи запис у нього).
Фізична адреса магнітометра LSM303DLHC:
  • 00111100 (3Ch) для запису,
  •  00111101 (3Dh) для читання.
Подальший розгляд буде на прикладі акселерометра -- для магнітометра все дуже схоже, хоча і є нюанси.

Запис в регістр відбувається наступним чином (список нижче, для спрощення, включає елементи більш низькорівневих абстракцій із п.1):
  • Дається сигнал START. 
  • Передається адреса акселерометра для запису.
  • За отримання підтвердження від нього, передається номер регістра, у який слід записувати. 
  • Старший біт номера регістра вказує, чи здійснювати автоінкремент. Якщо відбувається запис у один регістр -- не потрібно. Якщо є бажання за один сеанс записати у декілька регістрів, автоінкремент буде корисним: після кожного запису, номер регістра, у який слід записувати, автоматично збільшуватиметься, і кожен наступний байт даних потраплятиме в наступний регістр.
  • Після підтвердження про отримання номера регістра, посилається один (чи більше) байт даних для запису у нього.
  • Для завершення обміну посилається сигнал STOP.
Виглядає це якось так:
ST -- START, SP -- STOP, SAD -- адреса пристрою, SAK -- підтвердження (ACK), SUB -- номер регістра.
(c) STMicroelectronics, datasheet на LSM303DLHC
 Читання із регістра:
  • Початок такий же -- передається START, адреса для запису (див. опис І2С, ідея, що спочатку в пристрій записується номер регістра, що нас цікавить), в обох випадках очікується на підтвердження. Після чого починаються відмінності:
  • Посилається сигнал RESTART, після якого йде та ж адреса пристрою, але для читання.
  • Тоді підлеглий пристрій, після послідовних підтверджень, посилає дані контролеру, аж поки не отримає NACK i STOP.
  • За ввімкненого автоінкременту так можна прочитати декілька регістрів підряд.
SR -- RESTART, NMAK -- NACK в термінах I2C.
(c) STMicroelectronics, datasheet на LSM303DLHC

3 -- керування акселерометром

Можливостей у нього багато. Регістрів, відповідно -- теж. Ось короткий список (їх адреси йдуть із 0x20 до 0x3D в тому ж порядку, хоча, краще звіртеся із документацією):

  • CTRL_REG1_A, CTRL_REG2_A, CTRL_REG3_A, CTRL_REG4_A, CTRL_REG5_A, CTRL_REG6_A -- регістри керування.
  • REFERENCE_A -- значення для генерації переривань (Reference value for interrupt generation).
  • STATUS_REG_A -- стан акселерометра.
  • OUT_X_L_A, OUT_X_H_A -- молодший і старший байти прискорення вздовж осі X;
  • OUT_Y_L_A, OUT_Y_H_A -- для осі Y, 
  • OUT_Z_L_A, OUT_Z_H_A -- для осі Z. 
  • FIFO_CTRL_REG_A, FIFO_SRC_REG_A -- керування внутрішнім буфером вимірів.
  • INT1_CFG_A, INT1_SRC_A, INT1_THS_A, INT1_DURATION_A -- керування першою лінією переривань; 
  • INT2_CFG_A, INT2_SRC_A, INT2_THS_A, INT2_DURATION_A -- керування другою лінією переривань.
  • CLICK_CFG_A, CLICK_SRC_A, CLICK_THS_A -- керування режимом виявлення Double-click та Single-click.
  • TIME_LIMIT_A, TIME_LATENCY_A, TIME_WINDOW_A -- часові інтервали/таймаути для налаштування розпізнавання кліків.
Для магнітометра є аналогічна табличка, див. документацію.

Всього різноманіття режимів не розглядатимемо (тим більше, що документація надміру лаконічна -- певне, автори вважають, що читач зуби з'їв на таких акселерометрах). Найважливішими є регістри  CTRL_REG1_A, CTRL_REG4_A, всі шість регістрів даних (OUT_X_L_A і т.д.) та регістр статусу, STATUS_REG_A. Розглянемо їх вміст (всі таблиці взято із документації):



Регістри даних після кожного оновлення містять старші та молодші біти даних, тому табличок для них не наводжу.

Біти ODR з CTRL_REG1_A задають частоту вимірів:

Коментар до цієї таблички: пристрій має два режими, один "нормальний" -- із високою точністю, інший -- економний, коли точність страждає (на два значущих біти) заради зменшення енергоспоживання. Більшість варіантів дають однакову частоту вимірів в обох режимах, відмінності є лише у двох останніх -- для високочастотних вимірів.

Наприклад, якщо ODR[3:0] == 0b0100, виміри відбуватимуться 50 раз на секунду.

Вибір між цими режимами відбувається за допомогою двох біт одночасно -- LPen з CTRL_REG1_A і HR з CTRL_REG4_A:


Отож, енергозбереження вимикаємо: LPen == 0, всі канали вмикаємо: Zen == Yen == Xen == 1. Маємо: CTRL_REG1_A == 0b01000111.

Переходимо до CTRL_REG4_A: HR == 1, як вже визначилися. По решті біт:
  • BDU == 0 -- залишаємо, як є.
  • BLE ==  0 -- порядок слідування біт, вважаємо, що LSB нас влаштовує.
  • До чого тут біт SIM -- не зрозуміло, у нас же I2C а не SPI. Не чіпаємо, по замовчуванню рівний 0.
  • FS[1:0] -- найбільш цікаві, обирають інтервал вимірів, 00: ±2 g, 01: ±4 g, 10: ±8 g, 11: ±16 g. Залишаємо рівним нулю, інтервал ±2 g.
Залишаємо (встановлюємо) CTRL_REG4_A == 0.

Після цього можна починати читати дані із  OUT_X_L_A, OUT_X_H_A і т.д.

Щоб знати, що саме читається, є сенс поглядати на регістр статусу:
  • Біт ZYXDA вказує, що є нові дані,  а ZDA/YDA/XDA -- на котрих каналах вони є. 
  • Біт ZYXOR -- що відбулося переповнення -- попередній вимір було замінено новим перш ніж його прочитали. Аналогічні біти є для кожного із каналів: ZOR/YOR/XOR.
З програмним інтерфейсом, поки -- все.

4 -- інтерпретація даних

Користувач отримує для кожної осі пару байт -- вміст регістрів OUT_X_L_A, OUT_X_H_A для осі X та аналогічних для Y i Z, які варто зразу зібрати у єдине число:

(int16_t)( ((uint16_t)OUT_X_H_A << 8) + OUT_X_L_A )

З "сирими" байтами маніпулюємо як із беззнаковими числами (uint16_t), але кінцевий результат слід вважати знаковим (int16_t).

В результаті ми отримали ціле знакове 16-бітне число. Питання -- як із нього добути прискорення?

В документації нормального опису цього не знайшов -- можливо, недостатньо уважно читав, однак колупання в документації та прикладах, доповнене експериментами, дозволило встановити процедуру:
  • Молодших 4 біти в звичайному режимі та 6 біт в економному не означають нічого -- їх слід просто відкинути (зсунути праворуч на 4 чи 6 біт  або поділити на 16 чи 64).
Ціна "поділки" отриманого числа визначається діапазоном вимірювання, де mg -- мілі-g, тисячні долі прискорення вільного падіння (LSB -- найменш значущий біт):
  • 1 mg/LSB для ±2 g (FS[1:0] == 00)
  • 2 mg/LSB для ±4 g (FS[1:0] == 10)
  • 4 mg/LSB для ±8 g (FS[1:0] == 01)
  • 12 mg/LSB для ±16 g (FS[1:0] == 11) (В документації та прикладах пише саме 12 а не 16, хоча інтервал 16g).
Тобто, якщо після відкидання біт, результат помножити на точність -- отримаємо прискорення в мікро-g. Поділивши його на 1000 (перетворіть перед тим до double!) -- в прискореннях вільного падіння на Землі. Помноживши на g~9.82 Н/м^2 -- прискорення в системі СІ.

Відповідний код,  для обраного в попередньому розділі режиму, може виглядати так:

int16_t raw = ((int16_t)((uint16_t)OUT_X_H_A << 8) + OUT_X_L_A);
double accel = (double)(raw >> 4)/1000;

Так як контролери не дуже люблять працювати із числами з рухомою крапкою (floating-point), часто є сенс обмежитися цілочисельною формою представлення.


Middleware

Все описане вище треба якось реалізовувати. HAL надає лише інструменти для роботи із I2C, але й вони достатньо низькорівневі.

Повчально реалізувати взаємодію самостійно, покладаючись лише на HAL чи й CMSIS (хоча, останнє буде значно менш портабельним). Однак, часто реалізовувати все самому -- непрактично. На щастя, бібліотека Cube для STM32F3 (STM32Cube_FW_F3_V1.6.0) містить готові реалізації коду роботи із периферією різних демоплат, зокрема -- і тими що є на STM32F3Discovery.

Знаходиться код в STM32Cube_FW_F3_V1.6.0/Drivers/BSP -- він називається Board Support Package (BSP). Приклади використання -- див. STM32Cube_FW_F3_V1.6.0/Projects/STM32F3-Discovery.

Рівні абстракції коду для демоплат STM32. (Взято із документації на STM32Cube_FW_F3_V1.6.0)
BSP можна поділити на три частини:
  • Директорія Drivers/BSP/Common  містить загальні оголошення, наприклад для акселерометра (accelero.h), гіроскопа (gyro.h), і т.д. Важливою їх частиною є оголошення драйверів, із вказівниками на функції, котрі виконуватимуть відповідні операції. 
  • Директорії конкретних пристроїв, наприклад Drivers/BSP/lsm303dlhc чи Drivers/BSP/l3gd20, містять код, що реалізовує інтерфейс, заданий в "Common".
  • Директорії конкретних демоплат, наприклад: Drivers/BSP/STM32F3-Discovery, де є зовсім простий у використанні код, що дозволяє легко (хай і в обмеженому варіанті) скористатися можливостями відповідних плат.

Загальний код


Наприклад, в accelero.h міститься структура-"драйвер", ACCELERO_DrvTypeDef, оголошена так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef struct
{  
  void      (*Init)(uint16_t);
  void      (*DeInit)(void); 
  uint8_t   (*ReadID)(void);
  void      (*Reset)(void);
  void      (*LowPower)(void);
  void      (*ConfigIT)(void);
  void      (*EnableIT)(uint8_t);
  void      (*DisableIT)(uint8_t);
  uint8_t   (*ITStatus)(uint16_t);
  void      (*ClearIT)(void);
  void      (*FilterConfig)(uint8_t);
  void      (*FilterCmd)(uint8_t);
  void      (*GetXYZ)(int16_t *);
}ACCELERO_DrvTypeDef;

Назви функцій говорять самі за себе. Також там є структури із конфігурацією відповідних пристроїв:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* ACCELERO struct */
typedef struct
{
  uint8_t Power_Mode;                         /* Power-down/Normal Mode */
  uint8_t AccOutput_DataRate;                 /* OUT data rate */
  uint8_t Axes_Enable;                        /* Axes enable */
  uint8_t High_Resolution;                    /* High Resolution enabling/disabling */
  uint8_t BlockData_Update;                   /* Block Data Update */
  uint8_t Endianness;                         /* Endian Data selection */
  uint8_t AccFull_Scale;                      /* Full Scale selection */
  uint8_t Communication_Mode;
}ACCELERO_InitTypeDef;

/* ACCELERO High Pass Filter struct */
typedef struct
{
  uint8_t HighPassFilter_Mode_Selection;      /* Internal filter mode */
  uint8_t HighPassFilter_CutOff_Frequency;    /* High pass filter cut-off frequency */
  uint8_t HighPassFilter_AOI1;                /* HPF_enabling/disabling for AOI function on interrupt 1 */
  uint8_t HighPassFilter_AOI2;                /* HPF_enabling/disabling for AOI function on interrupt 2 */
  uint8_t HighPassFilter_Data_Sel;
  uint8_t HighPassFilter_Stat;
}ACCELERO_FilterConfigTypeDef;


Драйвери конкретних периферійних пристроїв


Конкретні драйвери знаходяться у парах файлів виду lsm303dlhc.h/lsm303dlhc.c.

lsm303dlhc.h  містить імена регістрів акселерометра (фрагмент списку):


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* Exported constant IO ------------------------------------------------------*/
#define ACC_I2C_ADDRESS                      0x32

/* Acceleration Registers */
#define LSM303DLHC_WHO_AM_I_ADDR             0x0F  /* device identification register */
#define LSM303DLHC_CTRL_REG1_A               0x20  /* Control register 1 acceleration */
#define LSM303DLHC_CTRL_REG2_A               0x21  /* Control register 2 acceleration */
#define LSM303DLHC_CTRL_REG3_A               0x22  /* Control register 3 acceleration */
#define LSM303DLHC_CTRL_REG4_A               0x23  /* Control register 4 acceleration */
#define LSM303DLHC_CTRL_REG5_A               0x24  /* Control register 5 acceleration */
#define LSM303DLHC_CTRL_REG6_A               0x25  /* Control register 6 acceleration */
#define LSM303DLHC_REFERENCE_A               0x26  /* Reference register acceleration */
#define LSM303DLHC_STATUS_REG_A              0x27  /* Status register acceleration */
#define LSM303DLHC_OUT_X_L_A                 0x28  /* Output Register X acceleration */
#define LSM303DLHC_OUT_X_H_A                 0x29  /* Output Register X acceleration */
#define LSM303DLHC_OUT_Y_L_A                 0x2A  /* Output Register Y acceleration */
#define LSM303DLHC_OUT_Y_H_A                 0x2B  /* Output Register Y acceleration */
#define LSM303DLHC_OUT_Z_L_A                 0x2C  /* Output Register Z acceleration */
#define LSM303DLHC_OUT_Z_H_A                 0x2D  /* Output Register Z acceleration */ 

* * * * * * * * * * * * * * * * 

Різноманітні константи, наприклад (прочитайте коментар до кінця і згадайте описане вище):


1
2
3
4
#define LSM303DLHC_ACC_SENSITIVITY_2G     ((uint8_t)1)  /*!< accelerometer sensitivity with 2 g full scale [mg/LSB] */
#define LSM303DLHC_ACC_SENSITIVITY_4G     ((uint8_t)2)  /*!< accelerometer sensitivity with 4 g full scale [mg/LSB] */
#define LSM303DLHC_ACC_SENSITIVITY_8G     ((uint8_t)4)  /*!< accelerometer sensitivity with 8 g full scale [mg/LSB] */
#define LSM303DLHC_ACC_SENSITIVITY_16G    ((uint8_t)12) /*!< accelerometer sensitivity with 12 g full scale [mg/LSB] */

Оголошення функцій роботи із пристроєм (вказівники на які будуть у структурі-драйвері):


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* ACC functions */
void    LSM303DLHC_AccInit(uint16_t InitStruct);
void    LSM303DLHC_AccDeInit(void);
uint8_t LSM303DLHC_AccReadID(void);
void    LSM303DLHC_AccRebootCmd(void);
void    LSM303DLHC_AccFilterConfig(uint8_t FilterStruct);
void    LSM303DLHC_AccFilterCmd(uint8_t HighPassFilterState);
void    LSM303DLHC_AccReadXYZ(int16_t* pData);
void    LSM303DLHC_AccFilterClickCmd(uint8_t HighPassFilterClickState);
void    LSM303DLHC_AccIT1Enable(uint8_t LSM303DLHC_IT);
void    LSM303DLHC_AccIT1Disable(uint8_t LSM303DLHC_IT);
void    LSM303DLHC_AccIT2Enable(uint8_t LSM303DLHC_IT);
void    LSM303DLHC_AccIT2Disable(uint8_t LSM303DLHC_IT);
void    LSM303DLHC_AccINT1InterruptEnable(uint8_t ITCombination, uint8_t ITAxes);
void    LSM303DLHC_AccINT1InterruptDisable(uint8_t ITCombination, uint8_t ITAxes);
void    LSM303DLHC_AccINT2InterruptEnable(uint8_t ITCombination, uint8_t ITAxes);
void    LSM303DLHC_AccINT2InterruptDisable(uint8_t ITCombination, uint8_t ITAxes);
void    LSM303DLHC_AccClickITEnable(uint8_t ITClick);
void    LSM303DLHC_AccClickITDisable(uint8_t ITClick);
void    LSM303DLHC_AccZClickITConfig(void);

/* COMPASS / ACCELERO IO functions */
void    COMPASSACCELERO_IO_Init(void);
void    COMPASSACCELERO_IO_ITConfig(void);
void    COMPASSACCELERO_IO_Write(uint16_t DeviceAddr, uint8_t RegisterAddr, uint8_t Value);
uint8_t COMPASSACCELERO_IO_Read(uint16_t DeviceAddr, uint8_t RegisterAddr);

Та, нарешті, оголошення структури, яка служить своєрідним драйвером, визначення якої є в lsm303dlhc.c:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
ACCELERO_DrvTypeDef Lsm303dlhcDrv =
{
  LSM303DLHC_AccInit,
  LSM303DLHC_AccDeInit,
  LSM303DLHC_AccReadID,
  LSM303DLHC_AccRebootCmd,
  0,
  LSM303DLHC_AccZClickITConfig,
  0,
  0,
  0,
  0,
  LSM303DLHC_AccFilterConfig,
  LSM303DLHC_AccFilterCmd,
  LSM303DLHC_AccReadXYZ
};

Нулі -- нульові вказівники, вказують, що відповідні операції не підтримуються. Якщо порівняти із оголошенням структури ACCELERO_DrvTypeDef, видно, що обробка переривання даною реалізацією драйвера не підтримується. Доводиться вручну.

Такий код, призначений бути посередником між низькорівневими функціями мікроконтролера та вищими рівнями, називають Middleware (в інших сферах це слово має схожі, хоч і відмінні, значення, зокрема і STM вживає його для більш високорівневих бібліотек -- FS, USB, тощо).

З фрагментів вище видно переваги його використання -- не треба самостійно возитися із деталями реалізації. І його недоліки -- немає гарантії підтримки всіх необхідних функцій, доступних пристрою -- от, як обробки переривань в прикладі вище. Правда, ніхто не заважає ввімкнути її самостійно -- дещо порушуючи абстракцію, що є цілком прийнятним в світі мікроконтролерів по чисто технічним причинам -- потворності апаратних інтерфейсів (вільно цитуючи Таненбаума) та обмеженості ресурсів.

Мінімалістичне використання можливостей демоплати

В згадуваному Drivers/BSP/STM32F3-Discovery міститься код налаштування периферії даної плати, функції маніпуляції її світлодіодами, кнопкою та "серйозною" периферією -- акселерометром-магнітометром і гіроскопом.


З її використанням читання та інтерпретація показів акселерометра виглядатиме так (вважаємо, що printf() виводить через UART чи Semihosing):


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  /* USER CODE BEGIN 2 */
  if(BSP_ACCELERO_Init() != HAL_OK)
  {
    /* Initialization Error */
 printf("Error initializing HAL.");
    while(1){}
  }
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  int16_t buffer[3] = {0};
  while (1)
  {
   BSP_ACCELERO_GetXYZ(buffer);
   printf("%i %i %i \n", buffer[0],buffer[1],buffer[2]);
   printf("%g %g %g \n", (double)(buffer[0]/16)/1000.0,
      (double)(buffer[1]/16)/1000.0,
    (double)(buffer[2]/16)/1000.0);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }

Де функції BSP_ACCELERO_Init()  і BSP_ACCELERO_GetXYZ() реалізовано в Drivers/BSP/STM32F3-Discovery/stm32f3_discovery_accelerometer.c.

BSP_ACCELERO_Init()  цікава тим, що демонструє процес ініціалізації:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
uint8_t BSP_ACCELERO_Init(void)
{  
  uint8_t ret = ACCELERO_ERROR;
  uint16_t ctrl = 0x0000;
  ACCELERO_InitTypeDef LSM303DLHC_InitStructure;
  ACCELERO_FilterConfigTypeDef LSM303DLHC_FilterStructure;
 
  if(Lsm303dlhcDrv.ReadID() == I_AM_LMS303DLHC)
  {
    /* Initialize the gyroscope driver structure */
    AccelerometerDrv = &Lsm303dlhcDrv;
  
  /* MEMS configuration ------------------------------------------------------*/
   /* Fill the accelerometer structure */
    LSM303DLHC_InitStructure.Power_Mode = LSM303DLHC_NORMAL_MODE;
    LSM303DLHC_InitStructure.AccOutput_DataRate = LSM303DLHC_ODR_50_HZ;
    LSM303DLHC_InitStructure.Axes_Enable= LSM303DLHC_AXES_ENABLE;
    LSM303DLHC_InitStructure.AccFull_Scale = LSM303DLHC_FULLSCALE_2G;
    LSM303DLHC_InitStructure.BlockData_Update = LSM303DLHC_BlockUpdate_Continous;
    LSM303DLHC_InitStructure.Endianness=LSM303DLHC_BLE_LSB;
    LSM303DLHC_InitStructure.High_Resolution=LSM303DLHC_HR_ENABLE;
    
    /* Configure MEMS: data rate, power mode, full scale and axes */
    ctrl |= (LSM303DLHC_InitStructure.Power_Mode | LSM303DLHC_InitStructure.AccOutput_DataRate | \
                       LSM303DLHC_InitStructure.Axes_Enable);
    
    ctrl |= ((LSM303DLHC_InitStructure.BlockData_Update | LSM303DLHC_InitStructure.Endianness | \
                      LSM303DLHC_InitStructure.AccFull_Scale | LSM303DLHC_InitStructure.High_Resolution) << 8);
    
  /* Configure the accelerometer main parameters */
    AccelerometerDrv->Init(ctrl);
  
  /* Fill the accelerometer LPF structure */
    LSM303DLHC_FilterStructure.HighPassFilter_Mode_Selection =LSM303DLHC_HPM_NORMAL_MODE;
    LSM303DLHC_FilterStructure.HighPassFilter_CutOff_Frequency = LSM303DLHC_HPFCF_16;
    LSM303DLHC_FilterStructure.HighPassFilter_AOI1 = LSM303DLHC_HPF_AOI1_DISABLE;
    LSM303DLHC_FilterStructure.HighPassFilter_AOI2 = LSM303DLHC_HPF_AOI2_DISABLE;
    
    /* Configure MEMS: mode, cutoff frquency, Filter status, Click, AOI1 and AOI2 */
    ctrl = (uint8_t) (LSM303DLHC_FilterStructure.HighPassFilter_Mode_Selection |\
                      LSM303DLHC_FilterStructure.HighPassFilter_CutOff_Frequency|\
                      LSM303DLHC_FilterStructure.HighPassFilter_AOI1|\
                      LSM303DLHC_FilterStructure.HighPassFilter_AOI2);

  /* Configure the accelerometer LPF main parameters */
    AccelerometerDrv->FilterConfig(ctrl);

    ret = ACCELERO_OK;
  }  
  else
  {
    ret = ACCELERO_ERROR;
  }

  return ret;
}

У рядках 15-21 заповнюється структура із описом бажаної конфігурації (доволі прозорим!) Потім, у рядках 24-28, готує 16-бітне слово конфігурації та передає його функції із драйвера. Яка викликається за вказівником у структурі Lsm303dlhcDrv. Він, у свою чергу, вказує на функцію із Drivers/BSP/Components/lsm303dlhc/lsm303dlhc.c  з іменем LSM303DLHC_AccInit():


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void LSM303DLHC_AccInit(uint16_t InitStruct)
{  
  uint8_t ctrl = 0x00;
  
  /*  Low level init */
  COMPASSACCELERO_IO_Init();
  
  /* Write value to ACC MEMS CTRL_REG1 register */
  ctrl = (uint8_t) InitStruct;
  COMPASSACCELERO_IO_Write(ACC_I2C_ADDRESS, LSM303DLHC_CTRL_REG1_A, ctrl);
  
  /* Write value to ACC MEMS CTRL_REG4 register */
  ctrl = (uint8_t) (InitStruct << 8);
  COMPASSACCELERO_IO_Write(ACC_I2C_ADDRESS, LSM303DLHC_CTRL_REG4_A, ctrl);
}

Назва функції COMPASSACCELERO_IO_Write() промовиста, але, все рівно подивимося на неї. Знаходиться вона в файлі Drivers/BSP/STM32F3-Discovery/stm32f3_discovery.c:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static void I2Cx_WriteData(uint16_t Addr, uint8_t Reg, uint8_t Value)
{
  HAL_StatusTypeDef status = HAL_OK;
  
  status = HAL_I2C_Mem_Write(&I2cHandle, Addr, (uint16_t)Reg, I2C_MEMADD_SIZE_8BIT, &Value, 1, I2cxTimeout);
  
  /* Check the communication status */
  if(status != HAL_OK)
  {
    /* Execute user timeout callback */
    I2Cx_Error();
  }
}

void COMPASSACCELERO_IO_Write(uint16_t DeviceAddr, uint8_t RegisterAddr, uint8_t Value)
{
  /* call I2Cx Read data bus function */
  I2Cx_WriteData(DeviceAddr, RegisterAddr, Value);
}

Все, дійшли до ручки HAL.

Ніякої містики у тому всьому коді немає, але самостійно писати -- довелося б повозитися.

Вартує також глянути реалізацію BSP_ACCELERO_GetXYZ() -- без неї не зрозуміло, що саме ми отримуємо на виході. Насправді, вона лише викликає функцію із драйвера за вказівником:


1
2
3
4
5
6
7
void BSP_ACCELERO_GetXYZ(int16_t *pDataXYZ)
{
  if(AccelerometerDrv->GetXYZ!= NULL)
  {   
    AccelerometerDrv->GetXYZ(pDataXYZ);
  }
}

А та вже виконує всю роботу:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void LSM303DLHC_AccReadXYZ(int16_t* pData)
{
  int16_t pnRawData[3];
  uint8_t ctrlx[2]={0,0};
  int8_t buffer[6];
  uint8_t i = 0;
  uint8_t sensitivity = LSM303DLHC_ACC_SENSITIVITY_2G;
  
  /* Read the acceleration control register content */
  ctrlx[0] = COMPASSACCELERO_IO_Read(ACC_I2C_ADDRESS, LSM303DLHC_CTRL_REG4_A);
  ctrlx[1] = COMPASSACCELERO_IO_Read(ACC_I2C_ADDRESS, LSM303DLHC_CTRL_REG5_A);
  
  /* Read output register X, Y & Z acceleration */
  buffer[0] = COMPASSACCELERO_IO_Read(ACC_I2C_ADDRESS, LSM303DLHC_OUT_X_L_A); 
  buffer[1] = COMPASSACCELERO_IO_Read(ACC_I2C_ADDRESS, LSM303DLHC_OUT_X_H_A);
  buffer[2] = COMPASSACCELERO_IO_Read(ACC_I2C_ADDRESS, LSM303DLHC_OUT_Y_L_A);
  buffer[3] = COMPASSACCELERO_IO_Read(ACC_I2C_ADDRESS, LSM303DLHC_OUT_Y_H_A);
  buffer[4] = COMPASSACCELERO_IO_Read(ACC_I2C_ADDRESS, LSM303DLHC_OUT_Z_L_A);
  buffer[5] = COMPASSACCELERO_IO_Read(ACC_I2C_ADDRESS, LSM303DLHC_OUT_Z_H_A);
  
  /* Check in the control register4 the data alignment*/
  if(!(ctrlx[0] & LSM303DLHC_BLE_MSB)) 
  {
    for(i=0; i<3; i++)
    {
      pnRawData[i]=((int16_t)((uint16_t)buffer[2*i+1] << 8) + buffer[2*i]);
    }
  }
  else /* Big Endian Mode */
  {
    for(i=0; i<3; i++)
    {
      pnRawData[i]=((int16_t)((uint16_t)buffer[2*i] << 8) + buffer[2*i+1]);
    }
  }
  
  /* Normal mode */
  /* Switch the sensitivity value set in the CRTL4 */
  switch(ctrlx[0] & LSM303DLHC_FULLSCALE_16G)
  {
  case LSM303DLHC_FULLSCALE_2G:
    sensitivity = LSM303DLHC_ACC_SENSITIVITY_2G;
    break;
  case LSM303DLHC_FULLSCALE_4G:
    sensitivity = LSM303DLHC_ACC_SENSITIVITY_4G;
    break;
  case LSM303DLHC_FULLSCALE_8G:
    sensitivity = LSM303DLHC_ACC_SENSITIVITY_8G;
    break;
  case LSM303DLHC_FULLSCALE_16G:
    sensitivity = LSM303DLHC_ACC_SENSITIVITY_16G;
    break;
  }
  
  /* Obtain the mg value for the three axis */
  for(i=0; i<3; i++)
  {
    pData[i]=(pnRawData[i] * sensitivity);
  }
}

В рядках 10-11 читає конфігурацію акселерометра, в 14-19 -- його покази, визначає поточний діапазон та чутливість (рядки 39-53) і множить сирі виміри на неї. Але зайві молодші біти не відкидає! Таким чином, якщо вихід функції поділити на 16, то це буде прискорення в мілі-g -- тисячних долях прискорення вільного падіння.


Підсумок -- підготовка проекту

Отож, як отримати проект, що містить підтримку роботи із периферією плати STM32F3Discovery, зокрема -- LSM303DLHC?

1. Створюємо проект в STM32CubeMX. Настав час відкрити таємницю. Цей редактор конфігурації містить підтримку багатьох демоплат. Порядок пошуку нашої плати показано на рисунку (клікніть по ньому, щоб роздивитися):

Клік по стрілочці, обведеній синім (на скріншоті -- вже клікнуто) відкриє опис плати та кнопки доступу до документації.
Клікабельно!
2. В результаті отримуємо таку конфігурацію, де вже вибрано правильні налаштування пінів, що йдуть до периферії цієї плати (клікабельно!):


Для роботи потрібно хіба включити I2C1 -- як виділено ліворуч.

3. Після цього -- імпортуємо код в SW4STM32, як описано в "Importing an STM32CubeMX generated project under System Workbench for STM32". (Зверніть увагу, що частина описаних там дій потрібна лише для певних версій інструментів! Не робіть їх бездумно!)

3. За бажанням -- додаємо підтримку Semihosting, як описано в "Зовсім просто про далекомір HC-SR04 із GPIO/HAL " (розділ "Додаток: SemiHosting i libc для лінивих", зверніть увагу на особливість виводу floating-point чисел).

4. Тепер додаємо Board Support Package (BSP) для нашої плати. Для спрощення -- щоб самостійно не шукати їх в бібліотеці Cube (хоча, шляхи до них є вище), потрібні файли можна скачати тут: STM32F3Discovery_BSP_Drivers.zip та розархівовуємо в директорію щойно створеного проекту. Після того варто в SW4STM32 переключитися на вкладку "Project explorer" і натиснути F5 ("Refresh" в контекстному меню).

5. Слід ще підказати середовищу, де шукати щойно додані файли заголовків:

Клікабельно!

Можна писати код. Наприклад, такий як наведено вище, на початку розділу "Мінімалістичне використання можливостей демоплати". Не забудьте включити всі необхідні файли заголовків!
Увага! Така конфігурація буде мати одну виражену потворність. Ініціалізація периферії відбуватиметься двічі -- раз кодом згенерованим HAL, інший -- кодом BSP. Конкретно тут воно не створює проблем, а видалення зайвої ініціалізації ускладнило б рецепт приготування нашого навчального проекту. Однак, в реальних проектах -- не залишайте так!

Завершальне слово

Ось і все. Не такі ті акселерометри страшні.

Можна сказати, що це не "зовсім просто", як обіцяв заголовок -- зовсім просто це "скачайте проект і все запрацює". Але тут ставив собі ціль максимально просто, наскільки це мені доступно, описано всі важливі елементи роботи із нашим доволі складним пристроєм. Значна частина "життя" обчислювальних систем виглядає схоже.

Роботу на перериваннях і використання вбудованого FIFI та високочастотних фільтрів поки що залишаємо на самостійний розгляд.

Дякую за увагу!

2 коментарі: