Урок 79
HAL. LTDC. EmWin. Подключаем акселерометр LSM6DS0
Продолжим работу с библиотекой EmWin.
И сегодня мы попробуем добавить в наш проект график. И не просто график, а живой график, который нам будет в реальном времени показыавать состояние определённых показателей. А в качестве этих определённых показателей мы возьмём показания осей акселерометра LSM6DS0, который мы уже разобрали вдоль и поперёк и который установлен на плате расширения X-NUCLEO-IKS01A1, подключаемую через стандартный разъём Arduino, также присутствующий на нашей отладочной плате STM32F746G-DISCO снизу.
Только вот, есть небольшая проблема — наша плата расширения не воткнётся в разъём платы отладочной, так как будет мешать раъём LAN. Поэтому пришлось сочинить вот такие вот переходники, которые я впоследствии спаял из штырьевых линеек и гнездовых линеек. Пусть это не очень красиво, зато проблема была решена
Вот таким вот образом наши переходнички выглядят в разъёме отладочной платы
А вот так вместе с платой расширения
Также мы прекрасно помним, что данный датчик может подключаться как по шине I2C, так и по шине SPI. Ну, так как мы его уже подключали по первой шине, то и давайте не будем нарушать этих традиций.
Для этого посмотрим схему платы расширения и узнаем, на какие ножки разъёма у нас попадают контакты I2C
Ну вот и хорошо. Уже что-то есть. Также по схеме можно убедиться что больше ничего такого интересного ни на какие ножки у нас не попадает, что позволяет нам быть уверенными, что у нас ничего не зашунтируется и не повлияет на работу нашей общей схемы. Теперь узнаем что приходит на те же ножки у отладочной платы STM32F746G-DISCO
Ну вот практически вся нужная нам информация у нас есть.
Проект мы создадим на основе проекта урока 78 EMWIN_WM01 и назовём его EMWIN_Accel_LSM6DS0.
Запустим проект в Cube MX и включим шину I2C1
Ножки также при добавлении сошлись с образцовыми
В Configuration в I2C1 настроим скорость
и включим прерывания
Сгенерируем проект, подключим его в System Workbench, настроим его так же. как и в предыдущих уроках по EmWin и попробуем его собрать и прошить.
Если всё нормально работает, скопируем наши файлы для акселерометра из проекта урока 42 lsm6ds0.h и lsm6ds0.c в наш проект в соответствующие папки.
Сделаем проекту refresh и подключим нашу библиотеку в файле main.h
#include "DIALOG.h"
#include "lsm6ds0.h"
#include "stdint.h"
В файле main.c в главной функции main() вызовем инициализацию датчика
GUI_Clear();
Accel_Ini();
HAL_TIM_Base_Start_IT(&htim6);
Перейдём в файл lsm6ds0.h и подправим там подключенный файл, так как у нас серия седьмая контроллера, а заодно и библиотеку подключим
#include "stm32f7xx_hal.h"
#include "GUI.h"
Уберем дефайны светодиода
#define LD2_Pin GPIO_PIN_5
#define LD2_GPIO_Port GPIOA
#define LD2_ON HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) //GREEN
#define LD2_OFF HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET)
В файле удалим подключение шины USART
extern UART_HandleTypeDef huart2;
Функцию Error переименуем , так как у нас такая есть в другом модуле, а в её теле будем окрашивать экран дисплея в красный цвет и уходить в бесконечный цикл
void ErrorAcc(void)
{
GUI_SelectLayer(1);
GUI_SetBkColor(GUI_RED);
GUI_Clear();
while(1)
;
}
Удалим некоторый код из функции Accel_ReadAcc
buf2[7]=(uint8_t)zval;
HAL_UART_Transmit_DMA(&huart2,buf2,8);
if(xval>1500)
{
LD2_ON;
}
else
{
LD2_OFF;
}
HAL_Delay(20);
Удалим объявления переменных значений осей из функции и сделаем их глобальными
char str1[30]={0};
int16_t xval, yval, zval;
Немного изменим код в функции Accel_Ini
void Accel_Ini(void)
{
uint16_t ctrl = 0x0000;
HAL_Delay(1000);
if(Accel_ReadID()!=0x68) ErrorAcc();
AccInit(ctrl);
}
Ну и во всех местах, где встретится функция Error, переименуем её в ErrorAcc.
В GuiBuilder сочиним следующий диалог, добавив в него график, кнопку и три текстовых поля
Вы можете не мучиться и открыть файл диалога из проекта.
Затем сохраним изменения в файл, скопируем файл в наш проект и откроем его (WindowDLG.c) в System Workbench и подключим три заголовочных файла
// USER START (Optionally insert additional includes)
#include "stm32f7xx_hal.h"
#include "math.h"
#include "lsm6ds0.h"
// USER END
Соберём проект и прошьём контроллер, чтобы убедиться что наш диалог у нас уже на экране
Подключим значения наших осей
// USER START (Optionally insert additional static data)
extern int16_t xval, yval, zval;
// USER END
Добавим таймер для того, чтобы наш график и изменения текстовых полей оживили. Пока добавим для него глобальную переменную
extern int16_t xval, yval, zval;
WM_HTIMER hTimer1;
Инициализируем его в кейсе инициализации диалога WM_INIT_DIALOG
// USER START (Optionally insert additional code for further widget initialization)
hTimer1 = WM_CreateTimer(pMsg->hWin, 0, 100, 0);
// USER END
Для тех, кто не знаком с оконными приложения расскажу, что мы данной функцией создали таймер. Во входящих указателях у нас его хендл, идентификатор, период (у нас 100 милисекунд) и режим (у нас нулевой, так как режимы для таймера пока не придуманы и данный аргумент просто зарезервирован).
Ну и, как водится, создадим для таймера обработчик с помощью отдельного кейса и в этом обработчике соответственно будем вызывать опрос осей датчика
case WM_TIMER:
Accel_ReadAcc();
break;
Добавим глобальный текстовый массив
WM_HTIMER hTimer1;
char str01[10]={0};
В этом же обработчике покажем значения осей в наших текстовых полях, а также вызовем функцию GuiExec для обновления данных на экране
Accel_ReadAcc();
hItem = WM_GetDialogItem(pMsg->hWin, ID_TEXT_0);
sprintf(str01,"X: %05d", xval);
TEXT_SetText(hItem, str01);
hItem = WM_GetDialogItem(pMsg->hWin, ID_TEXT_1);
sprintf(str01,"Y: %05d", yval);
TEXT_SetText(hItem, str01);
hItem = WM_GetDialogItem(pMsg->hWin, ID_TEXT_2);
sprintf(str01,"Z: %05d", zval);
TEXT_SetText(hItem, str01);
GUI_Exec();
И в конце обработчика мы должны заново стартовать таймер, иначе работать не будет, здесь не Windows API. Такой порядок. Добавим сначала глобальную переменную
char str01[10]={0};
static int data_v;
Ну и вернёмся в процедуру обработки и запустим таймер заново
GUI_Exec();
data_v = pMsg->Data.v;
WM_RestartTimer(data_v, 10);
break;
Данная функция заставляет таймер запуститься через 10 милисекунд.
Соберём код и прошьём контроллер, теперь всё должно работать
Теперь нам хочется, конечно, чтобы график наш тоже работал. Это намного сложнее, но мы этого не боимся.
Для начала в инициализации диалога получим хендл на график и настроим некоторые его свойства (до создания таймера)
// USER START (Optionally insert additional code for further widget initialization)
hItem = WM_GetDialogItem(pMsg->hWin, ID_GRAPH_0);
GRAPH_SetColor(hItem,GUI_LIGHTBLUE,GRAPH_CI_BK);
GRAPH_SetColor(hItem,GUI_GREEN,GRAPH_CI_FRAME);
GRAPH_SetColor(hItem,GUI_LIGHTMAGENTA,GRAPH_CI_GRID);
GRAPH_SetLineStyleH(hItem,GUI_LS_DASH);
GRAPH_SetLineStyleV(hItem,GUI_LS_DASH);
GRAPH_SetGridVis(hItem,1);
GRAPH_SetGridDistY(hItem,25);
hTimer1 = WM_CreateTimer(pMsg->hWin, 0, 100, 0);
Теперь давайте разберёмся, что мы тут намутили.
Сначала, естественно, мы получили хендл нашего графика, установили фоновый цвет, цвет бордюра, цвет сетки, назначили на вертикальные и горизонтальные линии сетки пунктир, чтобы не кидалась в глаза и не отвлекала от основных линий графика, сделали сетку видимой, а также назначили рассотяние между горизонтальными линиями в 25 пикселей (по умолчанию, если не назначать, то будет 50).
Условимся, что по вертикали наш график будет показывать значение осей, не важно в каких единицах, нам не нужно пока чтобы было именно в G. Нам нужно попробовать как работает график и будет ли что-то подвисать и мерцать. А по горизонтали соответственно будет время.
Далее создадим три глобальные переменные для хендлов наших осей
static int data_v;
GRAPH_DATA_Handle hData1,hData2,hData3;
// USER END
Создадим теперь хендлы для данных, подключим их к графику и произведём некоторые настройки для них
GRAPH_SetGridDistY(hItem,25);
hData1 = GRAPH_DATA_YT_Create(GUI_YELLOW, 400, 0, 1);
hData2 = GRAPH_DATA_YT_Create(GUI_RED, 400, 0, 1);
hData3 = GRAPH_DATA_YT_Create(GUI_GREEN, 400, 0, 1);
GRAPH_AttachData(hItem, hData1);
GRAPH_AttachData(hItem, hData2);
GRAPH_AttachData(hItem, hData3);
GRAPH_DATA_XY_SetOffY(hData1,100);
GRAPH_DATA_XY_SetOffY(hData2,100);
GRAPH_DATA_XY_SetOffY(hData3,100);
Проделываем все действия для всех осей. Сначала создаём хендлы для данных, назначая для их графиков различные цвета, делаем максимальную длину 400 пикселей, так как графики будут двигаться справа налево и они не должны доходить до вертикальной оси. Затем мы подключаем оси к графику, и делаем вертикальное смещение, чтобы ноль у нас был по центру, а не снизу, так как данные наши могут принимать и отрицаетльные значения. Попробуем это увидеть, собрав проект и прошив контроллер
Вот так вот всё красиво. А на яву ещё красивей. Здесь у нас не очень заметны пунктирные желтые линии сетки. В видеоуроке это намного заметнее, так что смотрите.
Теперь поработаем со шкалой графика. Это отдельный пункт и без него ничего не получится. Добавим глобальную переменную для хендла шкалы
GRAPH_DATA_Handle hData1,hData2,hData3;
GRAPH_SCALE_Handle hScale;
Вернёмся в инициализацию, получим данный хендл и установим для шкалы определённые свойства
GRAPH_DATA_XY_SetOffY(hData3,100);
hScale = GRAPH_SCALE_Create(25, GUI_TA_RIGHT, GRAPH_SCALE_CF_VERTICAL, 25);
GRAPH_AttachScale(hItem, hScale);
GRAPH_SCALE_SetTextColor(hScale,GUI_BLACK);
То есть здесь мы получили хендл на шкалу, подключили её к графику и уставновили черный цвет. Вот результат, если мы прошьём контроллер, собрав соответственно проект
У нас появилась вертикальная шкала, но как мы помним, данные у нас смещены на 100 пикселей (а также и пунктов) вверх. Давайте и шкалу тоже сместим, благо для этого в библиотеке emWin также припасена отдельная функция
GRAPH_SCALE_SetTextColor(hScale,GUI_BLACK);
GRAPH_SCALE_SetOff(hScale,100);
hTimer1 = WM_CreateTimer(pMsg->hWin, 0, 100, 0);
Соберём код и прошьём контроллер. Теперь другое дело!
Ну вот. Теперь дело подошло к тому, что нам пора уже увидеть наш долгожданный график в реальности, то есть увидеть в реальном времени изменения показаний осей акселерометра.
Для этого зайдём в обработчик событий таймера (case WM_TIMER:) и добавим там следующий код
Accel_ReadAcc();
GRAPH_DATA_YT_AddValue(hData1, xval/170);
GRAPH_DATA_YT_AddValue(hData2, yval/170);
GRAPH_DATA_YT_AddValue(hData3, zval/170);
Код достаточно прост. Данная функция добавляет точку в самую правую часть графика, по высоте совпадающую со значением, переданным во втором входящем аргументе, а весь остальной график при этом сдвигает влево. всё это происходит раз в 10 милисекунд. так как мы выставили именно это значение в перезапуск таймера. На 170 мы делим потому, что показания у нас колеблятся от -16384 до +16383, а такие цифры в наш диапазон графика не влезут. Соберём код и прошьём контроллер. А вот и результат
Всё замечательно работает. А зачем же кнопка? А затем, что порой нам требуется повнимательней изучить какие-то процессы и мы не успеваем оценить всё это в реальном времени, так как показания на графике постоянно движутся. Поэтому иногда требуется этот процесс остановить. Для того и кнопка, которая также затем должна уметь данный процесс возобновить.
Нам нужен для начала будет статус, ну или состояние графика, чтобы программа знала, в данный момент график у нас движется или остановлен. Добавим такую переменную в глобальных
GRAPH_SCALE_Handle hScale;
static U8 graph_stat=0;
Теперь в конце кейса событий от таймера мы будем таймер уже стартовать только по условию, что график у нас не остановлен
if(graph_stat==0) WM_RestartTimer(data_v, 10);
Ну и, соответственно, напишем реакцию на отжатие нашей кнопки
case WM_NOTIFICATION_RELEASED:
// USER START (Optionally insert code for reacting on notification message)
hItem = WM_GetDialogItem(pMsg->hWin, ID_BUTTON_0);
if(graph_stat==0)
{
graph_stat=1;
BUTTON_SetText(hItem, "Start");
}
else
{
graph_stat=0;
BUTTON_SetText(hItem, "Stop");
WM_RestartTimer(data_v, 10);
}
// USER END
Теперь кратко по реакции. Если у нас график движется (статус 0), то мы устанавливаем статус 1 (график остановлен), а также меняем надпись на кнопке в «Start«, а если у нас статус 1, то также меняем статус, меняем надпись на кнопке, возвращая ей первоначальное значение, а также стартуем таймер, иначе мы никогда не попадём в обработчик таймера. Вот так всё оказалось просто. А вот и результат
Таким образом, мы теперь умеем не только просто работать с элементами управления окон, но и умеем это делать грамотно, доказав это написанием проекта работы сразу с тремя линиями графика, показывающими изменение показателей от трёх источников именно в реальном времени. То есть мы создали такой проект, который можно уже применить в конкретной ситуации.
Всем спасибо за внимание и удачи в постижении такой нелёгкой науки под названием «программирование«!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату можно приобрести здесь STM32F746G-DISCOVERY
Оценочную плату можно приобрести здесь STM32 X-NUCLEO-IKS01A1
Смотреть ВИДЕОУРОК (нажмите на картинку)
Спасибо большое! Все понятно…. Пока сам не начал переписывать под свои задачи и свое железо.
Уткнулся с таймеры и не могу сдвинуться с места.
static void _cbDialog(WM_MESSAGE * pMsg) {
WM_HWIN hItem;
int NCode;
int Id;
// USER START (Optionally insert additional variables)
// USER END
switch (pMsg->MsgId) {
case WM_INIT_DIALOG:
//
// Initialization of 'Graph'
//
hItem = WM_GetDialogItem(pMsg->hWin, ID_GRAPH_0);
GRAPH_SetBorder(hItem, 3, 3, 3, 3);
// USER START (Optionally insert additional code for further widget initialization)
hTimer = WM_CreateTimer(pMsg->hWin, 0, 100, 0);
// USER END
break;
case WM_TIMER:
GUI_Exec();
WM_RestartTimer(pMsg->hWin, 100);
break;
case WM_NOTIFY_PARENT:
Id = WM_GetId(pMsg->hWinSrc);
NCode = pMsg->Data.v;
switch(Id) {
case ID_BUTTON_0: // Notifications sent by '/2'
switch(NCode) {
Почему может не работать таймер? Кнопки работают
Потерял полдня, но разобрался….
Может кому полезно будет:
stm32f4xx_it.c
/* Private user code ———————————————————*/
/* USER CODE BEGIN 0 */
#include «GUI.h»
volatile GUI_TIMER_TIME OS_TimeMS;
/* USER CODE END 0 */
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
OS_TimeMS++;
/* USER CODE END SysTick_IRQn 1 */
}