Skip to content

Commit 15db232

Browse files
glenn-andrewsnashif
authored andcommitted
samples: SMF: LVGL: SMF-based Calculator
This sample crates a touchscreen desk calculator based on the sample state machine in _Practical UML Statecharts in C/C++_ by Miro Samek. Sample should build and run on any touchscreen-enabled board with sufficient resources. Tested on `disco_l475_iot1` board with `adafruit_2_8_tft_touch_v2` touchscreen. Signed-off-by: Glenn Andrews <[email protected]>
1 parent 56d241b commit 15db232

File tree

10 files changed

+1843
-0
lines changed

10 files changed

+1843
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
set(CMAKE_CXX_FLAGS "-fstack-usage")
4+
cmake_minimum_required(VERSION 3.20.0)
5+
6+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
7+
project(smf_calculator_demo)
8+
9+
target_sources(app PRIVATE src/main.c)
10+
target_sources(app PRIVATE src/smf_console_cmds.c)
11+
target_sources(app PRIVATE src/smf_calculator_thread.c)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
.. zephyr:code-sample:: smf_calculator
2+
:name: SMF Calculator
3+
:relevant-api: smf
4+
5+
Create a simple desk calculator using the State Machine Framework.
6+
7+
Overview
8+
********
9+
10+
This sample creates a basic desk calculator driven by a state machine written
11+
with the :ref:`State Machine Framework <smf>`.
12+
13+
The 'business logic' of the calculator is based on the statechart given in
14+
Fig 2.18 of *Practical UML Statecharts in C/C++* 2nd Edition by Miro Samek.
15+
This uses a three-layer hierarchical statechart to handle situations such as
16+
ignoring leading zeroes, and disallowing multiple decimal points.
17+
18+
The statechart has been slightly modified to display different output on the
19+
screen in the ``op_entered`` state depending on if a previous result is
20+
available or not.
21+
22+
.. figure:: img/smf_calculator.svg
23+
:align: center
24+
:alt: SMF Calculator Statechart
25+
:figclass: align-center
26+
27+
Statechart for the SMF Calculator business logic.
28+
29+
The graphical interface uses an LVGL button matrix for input and text label for
30+
output, based on the sample in samples/drivers/display. The state machine updates
31+
the output text label after every call to :c:func:`smf_run_state`.
32+
33+
:kconfig:option:`CONFIG_LV_Z_VDB_SIZE` has been reduced to 14% to allow it to run
34+
on RAM-constrained boards like the :ref:`disco_l475_iot1_board`.
35+
36+
Requirements
37+
************
38+
39+
The GUI should work with any touchscreen display supported by Zephyr. The shield
40+
must be passed to ``west build`` using the ``--shield`` option, e.g.
41+
``--shield adafruit_2_8_tft_touch_v2``
42+
43+
List of Arduino-based touchscreen shields:
44+
45+
- :ref:`adafruit_2_8_tft_touch_v2`
46+
- :ref:`buydisplay_2_8_tft_touch_arduino`
47+
- :ref:`buydisplay_3_5_tft_touch_arduino`
48+
49+
The demo should also work on STM32 Discovery Kits with built-in touchscreens e.g.
50+
51+
- :ref:`stm32f412g_disco_board`
52+
- :ref:`st25dv_mb1283_disco_board`
53+
- :ref:`stm32f7508_dk_board`
54+
- :ref:`stm32f769i_disco_board`
55+
56+
etc. These will not need a shield defined as the touchscreen is built-in.
57+
58+
59+
Building and Running
60+
********************
61+
62+
Below is an example on how to build for a :ref:`disco_l475_iot1_board` board with
63+
a :ref:`adafruit_2_8_tft_touch_v2`.
64+
65+
.. zephyr-app-commands::
66+
:zephyr-app: samples/subsys/smf/smf_calculator
67+
:board: disco_l475_iot1
68+
:goals: build
69+
:shield: adafruit_2_8_tft_touch_v2
70+
:compact:
71+
72+
For testing purpose without the need of any hardware, the :ref:`native_sim <native_sim>`
73+
board is also supported and can be built as follows;
74+
75+
.. zephyr-app-commands::
76+
:zephyr-app: samples/subsys/smf/smf_calculator
77+
:board: native_sim
78+
:goals: build
79+
:compact:
80+
81+
CLI control
82+
===========
83+
84+
As well as control through the GUI, the calculator can be controlled through the shell,
85+
demonstrating a state machine can receive inputs from multiple sources.
86+
The ``key <key>`` command sends a keypress to the state machine. Valid keys are
87+
``0`` through ``9`` for numbers, ``.``, ``+``, ``-``, ``*``, ``/`` and ``=`` to
88+
perform the expected function, ``C`` for Cancel, and ``E`` for Cancel Entry.
89+
90+
GUI update speed on the :ref:`disco_l475_iot1_board` with :ref:`adafruit_2_8_tft_touch_v2`
91+
touchscreen is of the order of 0.8s due to button matrices invalidating the entire
92+
matrix area when pressed, rather than just the button that was selected. This could
93+
be sped up by using 18 individual buttons rather than a single matrix, but is sufficient
94+
for this demo.
95+
96+
References
97+
**********
98+
99+
*Practical UML Statecharts in C/C++* 2nd Edition by Miro Samek
100+
https://www.state-machine.com/psicc2

samples/subsys/smf/smf_calculator/img/smf_calculator.svg

Lines changed: 655 additions & 0 deletions
Loading
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
CONFIG_DEBUG=y
2+
CONFIG_LOG=y
3+
CONFIG_SHELL=y
4+
5+
# Needed for boards that enable RTT backends for logging
6+
# e.g. nrf52840dk/nrf52840 and any others that enable it
7+
CONFIG_LOG_BACKEND_RTT=n
8+
9+
# Enable the state machine framework
10+
CONFIG_SMF=y
11+
CONFIG_SMF_ANCESTOR_SUPPORT=y
12+
CONFIG_SMF_INITIAL_TRANSITION=y
13+
14+
# Enable floating point support
15+
CONFIG_REQUIRES_FLOAT_PRINTF=y
16+
17+
# Enable thread awareness for debugging tools supporting it
18+
CONFIG_DEBUG_THREAD_INFO=y
19+
20+
# enable to use thread names
21+
CONFIG_THREAD_NAME=y
22+
23+
# Display Options
24+
CONFIG_DISPLAY=y
25+
CONFIG_DISPLAY_LOG_LEVEL_ERR=y
26+
27+
# LVGL Options
28+
CONFIG_LVGL=y
29+
CONFIG_LV_Z_MEM_POOL_SIZE=16384
30+
CONFIG_LV_Z_SHELL=y
31+
# Percentage of screen size for a video buffer
32+
# LVGL defaults sets this too high for resource-constrained boards.
33+
# 14% ~= 32KB RAM on adafruit_2_8_tft_touch_v2
34+
CONFIG_LV_Z_VDB_SIZE=14
35+
36+
# extra large in case you want to enable LV_USE_REFR_DEBUG
37+
CONFIG_MAIN_STACK_SIZE=4096
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
sample:
2+
description: SMF Calculator GUI Application
3+
name: smf_calculator
4+
tests:
5+
sample.smf.smf_calculator:
6+
filter: dt_chosen_enabled("zephyr,display")
7+
# Sample takes ~300k on disco_l475_iot1 board, add 50k just in case.
8+
# Ram usage is 70k on disco_l475_iot1 board with adafruit_2_8_tft_touch_v2.
9+
# Can be reduced by changing CONFIG_LV_Z_VDB_SIZE,
10+
# CONFIG_LV_Z_MEM_POOL_SIZE and CONFIG_MAIN_STACK_SIZE.
11+
min_flash: 350
12+
min_ram: 75
13+
harness: none
14+
tags:
15+
- samples
16+
- display
17+
- gui
18+
- lvgl
19+
- smf
20+
modules:
21+
- lvgl
22+
integration_platforms:
23+
- native_sim/native/64
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright (c) 2024 Glenn Andrews
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include "main.h"
8+
#include <zephyr/kernel.h>
9+
#include "smf_calculator_thread.h"
10+
#include <zephyr/drivers/display.h>
11+
#include <lvgl.h>
12+
#include <stdio.h>
13+
#include <string.h>
14+
#include <lvgl_input_device.h>
15+
#include <zephyr/logging/log.h>
16+
17+
#define LOG_LEVEL CONFIG_LOG_DEFAULT_LEVEL
18+
LOG_MODULE_REGISTER(main_app);
19+
20+
K_MSGQ_DEFINE(output_msgq, CALCULATOR_STRING_LENGTH, 2, 1);
21+
22+
#define CALCULATOR_BUTTON_LABEL_LENGTH 4
23+
struct calculator_button {
24+
char label[CALCULATOR_BUTTON_LABEL_LENGTH];
25+
struct calculator_event event;
26+
};
27+
28+
/**
29+
* Important: this enum MUST be in the order of the buttons on the screen
30+
* otherwise the mapping from button index to function will break
31+
*/
32+
enum calculator_ui_buttons {
33+
BUTTON_CANCEL_ENTRY = 0,
34+
BUTTON_CANCEL,
35+
BUTTON_7,
36+
BUTTON_8,
37+
BUTTON_9,
38+
BUTTON_DIVIDE,
39+
BUTTON_4,
40+
BUTTON_5,
41+
BUTTON_6,
42+
BUTTON_MULTIPLY,
43+
BUTTON_1,
44+
BUTTON_2,
45+
BUTTON_3,
46+
BUTTON_SUBTRACT,
47+
BUTTON_0,
48+
BUTTON_DECIMAL_POINT,
49+
BUTTON_EQUALS,
50+
BUTTON_ADD,
51+
};
52+
53+
struct calculator_button buttons[] = {
54+
[BUTTON_CANCEL_ENTRY] = {.label = "CE",
55+
.event = {.event_id = CANCEL_ENTRY, .operand = 'E'}},
56+
[BUTTON_CANCEL] = {.label = "C", .event = {.event_id = CANCEL_BUTTON, .operand = 'C'}},
57+
[BUTTON_ADD] = {.label = "+", .event = {.event_id = OPERATOR, .operand = '+'}},
58+
[BUTTON_SUBTRACT] = {.label = "-", .event = {.event_id = OPERATOR, .operand = '-'}},
59+
[BUTTON_MULTIPLY] = {.label = "*", .event = {.event_id = OPERATOR, .operand = '*'}},
60+
[BUTTON_DIVIDE] = {.label = "/", .event = {.event_id = OPERATOR, .operand = '/'}},
61+
[BUTTON_DECIMAL_POINT] = {.label = ".",
62+
.event = {.event_id = DECIMAL_POINT, .operand = '.'}},
63+
[BUTTON_EQUALS] = {.label = "=", .event = {.event_id = EQUALS, .operand = '='}},
64+
[BUTTON_0] = {.label = "0", .event = {.event_id = DIGIT_0, .operand = '0'}},
65+
[BUTTON_1] = {.label = "1", .event = {.event_id = DIGIT_1_9, .operand = '1'}},
66+
[BUTTON_2] = {.label = "2", .event = {.event_id = DIGIT_1_9, .operand = '2'}},
67+
[BUTTON_3] = {.label = "3", .event = {.event_id = DIGIT_1_9, .operand = '3'}},
68+
[BUTTON_4] = {.label = "4", .event = {.event_id = DIGIT_1_9, .operand = '4'}},
69+
[BUTTON_5] = {.label = "5", .event = {.event_id = DIGIT_1_9, .operand = '5'}},
70+
[BUTTON_6] = {.label = "6", .event = {.event_id = DIGIT_1_9, .operand = '6'}},
71+
[BUTTON_7] = {.label = "7", .event = {.event_id = DIGIT_1_9, .operand = '7'}},
72+
[BUTTON_8] = {.label = "8", .event = {.event_id = DIGIT_1_9, .operand = '8'}},
73+
[BUTTON_9] = {.label = "9", .event = {.event_id = DIGIT_1_9, .operand = '9'}},
74+
};
75+
76+
/* Where the result is printed */
77+
static lv_obj_t *result_label;
78+
79+
/**
80+
* LVGL v8.4 is not thread safe, so use a msgq to pass updates back
81+
* to the thread that calls lv_task_handler()
82+
*/
83+
void update_display(const char *output)
84+
{
85+
while (k_msgq_put(&output_msgq, output, K_NO_WAIT) != 0) {
86+
k_msgq_purge(&output_msgq);
87+
}
88+
}
89+
90+
static void lv_btn_matrix_click_callback(lv_event_t *e)
91+
{
92+
lv_event_code_t code = lv_event_get_code(e);
93+
lv_obj_t *obj = lv_event_get_target(e);
94+
95+
if (code == LV_EVENT_PRESSED) {
96+
uint32_t id;
97+
int rc;
98+
99+
id = lv_btnmatrix_get_selected_btn(obj);
100+
if (id >= ARRAY_SIZE(buttons)) {
101+
LOG_ERR("Invalid button: %d", id);
102+
return;
103+
}
104+
105+
rc = post_calculator_event(&buttons[id].event, K_FOREVER);
106+
if (rc != 0) {
107+
LOG_ERR("could not post to msgq: %d", rc);
108+
}
109+
}
110+
}
111+
112+
static int setup_display(void)
113+
{
114+
const struct device *display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
115+
116+
if (!device_is_ready(display_dev)) {
117+
LOG_ERR("Device not ready, aborting setup");
118+
return -ENODEV;
119+
}
120+
121+
static const char *const btnm_map[] = {buttons[BUTTON_CANCEL_ENTRY].label,
122+
buttons[BUTTON_CANCEL].label,
123+
"\n",
124+
buttons[BUTTON_7].label,
125+
buttons[BUTTON_8].label,
126+
buttons[BUTTON_9].label,
127+
buttons[BUTTON_DIVIDE].label,
128+
"\n",
129+
buttons[BUTTON_4].label,
130+
buttons[BUTTON_5].label,
131+
buttons[BUTTON_6].label,
132+
buttons[BUTTON_MULTIPLY].label,
133+
"\n",
134+
buttons[BUTTON_1].label,
135+
buttons[BUTTON_2].label,
136+
buttons[BUTTON_3].label,
137+
buttons[BUTTON_SUBTRACT].label,
138+
"\n",
139+
buttons[BUTTON_0].label,
140+
buttons[BUTTON_DECIMAL_POINT].label,
141+
buttons[BUTTON_EQUALS].label,
142+
buttons[BUTTON_ADD].label,
143+
"\n",
144+
""};
145+
146+
lv_obj_t *btn_matrix = lv_btnmatrix_create(lv_scr_act());
147+
148+
lv_btnmatrix_set_map(btn_matrix, (const char **)btnm_map);
149+
lv_obj_align(btn_matrix, LV_ALIGN_BOTTOM_MID, 0, 0);
150+
lv_obj_set_size(btn_matrix, lv_pct(CALC_BTN_WIDTH_PCT), lv_pct(CALC_BTN_HEIGHT_PCT));
151+
lv_obj_add_event_cb(btn_matrix, lv_btn_matrix_click_callback, LV_EVENT_ALL, NULL);
152+
153+
result_label = lv_label_create(lv_scr_act());
154+
lv_obj_set_width(result_label, lv_pct(CALC_RESULT_WIDTH_PCT));
155+
lv_obj_set_style_text_align(result_label, LV_TEXT_ALIGN_RIGHT, 0);
156+
lv_obj_align(result_label, LV_ALIGN_TOP_MID, 0, lv_pct(CALC_RESULT_OFFSET_PCT));
157+
158+
static lv_style_t style_shadow;
159+
160+
lv_style_init(&style_shadow);
161+
lv_style_set_shadow_width(&style_shadow, 5);
162+
lv_style_set_shadow_spread(&style_shadow, 2);
163+
lv_style_set_shadow_color(&style_shadow, lv_palette_main(LV_PALETTE_GREY));
164+
lv_obj_add_style(result_label, &style_shadow, 0);
165+
update_display("0");
166+
167+
lv_task_handler();
168+
display_blanking_off(display_dev);
169+
170+
return 0;
171+
}
172+
173+
int main(void)
174+
{
175+
printk("SMF Desk Calculator Demo\n");
176+
177+
int rc = setup_display();
178+
179+
if (rc != 0) {
180+
return rc;
181+
}
182+
183+
while (1) {
184+
char output[CALCULATOR_STRING_LENGTH];
185+
186+
if (k_msgq_get(&output_msgq, output, K_MSEC(50)) == 0) {
187+
lv_label_set_text(result_label, output);
188+
}
189+
190+
lv_task_handler();
191+
}
192+
return 0;
193+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright (c) 2024 Glenn Andrews
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/* Constants for control sizes as a percentage of screen size. */
8+
#define CALC_BTN_WIDTH_PCT 90
9+
#define CALC_BTN_HEIGHT_PCT 80
10+
#define CALC_RESULT_WIDTH_PCT 70
11+
#define CALC_RESULT_OFFSET_PCT 10
12+
13+
void update_display(const char *output);

0 commit comments

Comments
 (0)