Dual Joystick Position Tracking Solution
Track and store dual joystick movements in real time with persistent memory logging and precise direction detection
What you'll learn and build
Intro
The Dual Joystick Position Tracking solution integrates two Joystick 3 Click boards and Flash 12 Click to enable simultaneous real-time tracking and persistent storage of dual joystick inputs. Designed for gaming controllers, robotics, and multi-axis input systems, this solution captures raw ADC values from both joysticks, determines their directional states, and logs any movement changes to non-volatile Flash memory. With a clear and responsive input-tracking mechanism, UART feedback, and dual-channel capability, it offers a reliable platform for interactive systems requiring precise user input history.
mikroBUS 1
Joystick 3 Click
Joystick 3 Click is a compact add-on board that can fulfill your directional analog input needs. This board features 2765, a high-quality mini 2-axis analog output thumbstick from Adafruit Industries. This small joystick is a 'self-centering' analog-type with a black rocker cap similar to the PSP joysticks. It comprises two 10kΩ potentiometers, one for up/down and another for left/right direction. Knowing that this joystick represents an analog type, it connects with mikroBUS™ through the SPI interface through the MCP3204 12-bit A/D converter. This Click board™ is suitable for numerous applications as a human-machine interface device (HMI), robotics, and other control interfaces.
mikroBUS 2
Flash 12 Click
Flash 12 Click is a compact add-on board representing a highly reliable memory solution. This board features the AT25EU0041A, a 4Mbit serial flash memory from Renesas, known for its ultra-low power consumption. This Click board™ is specifically designed to address the needs of systems operating at the IoT network's edge, providing an optimal solution for program code storage and execution directly from NOR Flash memory. It stands out for its innovative erase architecture, offering short erase times and low power consumption across operations, including reading, programming, and erasing. This Click board™ is ideal for developers working on the next generation of IoT devices requiring low-power read capabilities, fast erase times, boot/code shadow memory, and event/data logging.
mikroBUS 4
Joystick 3 Click
Joystick 3 Click is a compact add-on board that can fulfill your directional analog input needs. This board features 2765, a high-quality mini 2-axis analog output thumbstick from Adafruit Industries. This small joystick is a 'self-centering' analog-type with a black rocker cap similar to the PSP joysticks. It comprises two 10kΩ potentiometers, one for up/down and another for left/right direction. Knowing that this joystick represents an analog type, it connects with mikroBUS™ through the SPI interface through the MCP3204 12-bit A/D converter. This Click board™ is suitable for numerous applications as a human-machine interface device (HMI), robotics, and other control interfaces.
Features overview
Development board
Clicker 4 for STM32F4 is a compact development board designed for quickly building custom gadgets. It features an STM32F407VGT6 MCU, four mikroBUS™ sockets for Click board™ connectivity, power management, and more—making it ideal for rapid application development. At its core, the STM32F407VGT6 MCU, powered by an Arm® Cortex®-M4 32-bit processor running up to 168 MHz, ensures ample processing power for demanding tasks. Alongside two 1x20 pin headers, its four mikroBUS™ sockets provide access to a vast and growing range of Click boards™. Clearly marked sections offer an intuitive, user-friendly interface for faster development. Clicker 4 not only accelerates prototyping but can also be integrated directly into projects without additional hardware modifications. Four 4.2mm (0.165”) mounting holes at each corner enable easy installation with screws.
Microcontroller Overview
MCU Card / MCU

Architecture
ARM Cortex-M4
MCU Memory (KB)
10
Silicon Vendor
STMicroelectronics
Pin count
100
RAM (Bytes)
100
Step by step
Project assembly
Track your results in real time
Application Output
1. Application Output - In Debug mode, the 'Application Output' window enables real-time data monitoring, offering direct insight into execution results. Ensure proper data display by configuring the environment correctly using the provided tutorial.

2. UART Terminal - Use the UART Terminal to monitor data transmission via a USB to UART converter, allowing direct communication between the Click board™ and your development system. Configure the baud rate and other serial settings according to your project's requirements to ensure proper functionality. For step-by-step setup instructions, refer to the provided tutorial.

3. Plot Output - The Plot feature offers a powerful way to visualize real-time sensor data, enabling trend analysis, debugging, and comparison of multiple data points. To set it up correctly, follow the provided tutorial, which includes a step-by-step example of using the Plot feature to display Click board™ readings. To use the Plot feature in your code, use the function: plot(*insert_graph_name*, variable_name);. This is a general format, and it is up to the user to replace 'insert_graph_name' with the actual graph name and 'variable_name' with the parameter to be displayed.

Software Support
Library Description
Dual Joystick Position Tracking solution is developed using the NECTO Studio, ensuring compatibility with mikroSDK's open-source libraries and tools. Designed for plug-and-play implementation and testing, this solution is fully compatible with all development, starter, and mikromedia boards featuring a mikroBUS™ socket.
Example Description
The Dual Joystick Position Tracking solution integrates Joystick 3 Click and Flash 12 Click to track and store two joystick positions simultaneously. The Joystick 3 Click captures raw ADC values for two joystick inputs and detects movement directions for each. The Flash 12 Click stores these positions in non-volatile memory for persistent logging. This system can be used for interactive applications requiring real-time tracking and position history storage, such as gaming controllers, robotic systems, and user input tracking with dual inputs.
Key functions:
joystick3_init
- Initializes the Joystick 3 Click for reading joystick movements and ADC values for both joysticks.joystick3_read_raw_adc
- Reads the raw ADC values corresponding to the X and Y axes of both joysticks.joystick3_get_position
- Converts the raw ADC values into specific joystick position states (e.g., UP, DOWN, LEFT, RIGHT, NEUTRAL) for each joystick.flash12_init
- Initializes the Flash 12 Click for storing joystick positions in non-volatile memory.flash12_memory_write
- Writes the position data of both joysticks to Flash memory at a specified memory address.flash12_memory_read
- Reads the stored joystick positions from Flash memory for verification.
Application Init
The initialization sequence configures the system for dual joystick position tracking:
1. The UART logger is initialized for debugging.
2. The Joystick 3 Click is set up for reading joystick positions through its SPI interface.
3. The Flash 12 Click is initialized for persistent storage of joystick positions in Flash memory.
If any initialization step fails, the system logs an error and halts to prevent undefined behavior.
Application Task
The main loop performs the following operations:
1. Reads joystick ADC values from both joysticks to detect movement.
2. Maps the ADC values from both joysticks to their respective positions (e.g., UP, DOWN, LEFT, RIGHT).
3. Compares the current positions of both joysticks with their previous positions to detect any changes.
4. Logs the detected positions of both joysticks to Flash memory if a change is detected.
5. Reads and verifies the stored data from Flash memory for accurate logging and verification.
6. Waits 2 seconds before checking for the next position change.
Open Source
Code example
The complete application code and a ready-to-use project are available through the NECTO Studio Package Manager for direct installation in the NECTO Studio. The application code can also be found on the MIKROE GitHub account.
/*
* Solution Name: Dual Joystick Real-Time Position Tracking Solution
*
* Description:
* This embedded application continuously tracks joystick positions
* using two Joystick 3 Click boards and logs the detected positions
* to Flash memory via the Flash 12 Click.
*
* The system utilizes the following Click boards:
* - Joystick 3 Click (x2): Captures joystick positions in raw ADC values and detects
* movement directions for both joysticks.
* - Flash 12 Click: Stores detected joystick positions in non-volatile memory.
*
* The `application_init` function initializes both Joystick 3 Click boards and the Flash 12 Click,
* ensuring proper configuration and communication, and sets up the UART logger for debugging.
*
* The `application_task` function monitors position changes for both joysticks and logs the detected
* positions to Flash memory when a new position is detected. It also verifies stored data
* by reading from memory.
*
* Hardware Setup:
* - MIKROBUS_1: Joystick 3 Click #1 (Joystick position tracking)
* - MIKROBUS_2: Flash 12 Click (Persistent storage for joystick positions)
* - MIKROBUS_4: Joystick 3 Click #2 (Second joystick position tracking)
*
* Key Features:
* - Real-time detection of joystick position changes for both joysticks.
* - Logging of joystick positions to Flash memory.
* - Verification of stored data through memory read-back.
* - UART-based logging for displaying position updates.
*
* Development Environment:
* - [NECTO Studio](https://www.mikroe.com/necto)
* - [mikroSDK v2.0](https://www.mikroe.com/mikrosdk) framework
* - MIKROE [Click boards](https://www.mikroe.com/click-boards) Add-ons
*
* Author: Branko Jaksic
* Date: March, 2025
*/
// ------------------------------------------------------------------- INCLUDES
#include "board.h"
#include "log.h"
#include "flash12.h"
#include "joystick3.h"
// ------------------------------------------------------------- PRIVATE MACROS
// Starting memory address
#define STARTING_ADDRESS 0x012345
// ------------------------------------------------------------------ VARIABLES
static log_t logger;
static flash12_t flash12;
static joystick3_t joystick3_1; // First Joystick (mikroBUS_1)
static joystick3_t joystick3_2; // Second Joystick (mikroBUS_4)
typedef uint8_t joystick3_position_t;
// ------------------------------------------------------ APPLICATION FUNCTIONS
void application_init ( void )
{
log_cfg_t log_cfg; /**< Logger config object. */
joystick3_cfg_t joystick3_cfg_1; /**< Click config object. */
joystick3_cfg_t joystick3_cfg_2; /**< Click config object. */
flash12_cfg_t flash12_cfg; /**< Click config object. */
/**
* Logger initialization.
* Default baud rate: 115200
* Default log level: LOG_LEVEL_DEBUG
* @note If USB_UART_RX and USB_UART_TX
* are defined as HAL_PIN_NC, you will
* need to define them manually for log to work.
* See @b LOG_MAP_USB_UART macro definition for detailed explanation.
*/
LOG_MAP_USB_UART( log_cfg );
log_init( &logger, &log_cfg );
log_info( &logger, " Application Init " );
// Joystick 3 (1st) Click initialization.
joystick3_cfg_setup( &joystick3_cfg_1 );
JOYSTICK3_MAP_MIKROBUS( joystick3_cfg_1, MIKROBUS_1 );
if ( SPI_MASTER_ERROR == joystick3_init( &joystick3_1, &joystick3_cfg_1 ) )
{
log_error( &logger, " Communication init." );
for ( ; ; );
}
// Joystick 3 (2nd) Click initialization.
joystick3_cfg_setup( &joystick3_cfg_2 );
JOYSTICK3_MAP_MIKROBUS( joystick3_cfg_2, MIKROBUS_4 );
if ( SPI_MASTER_ERROR == joystick3_init( &joystick3_2, &joystick3_cfg_2 ) )
{
log_error( &logger, " Communication init." );
for ( ; ; );
}
// Flash 12 Click initialization.
flash12_cfg_setup( &flash12_cfg );
FLASH12_MAP_MIKROBUS( flash12_cfg, MIKROBUS_2 );
if ( SPI_MASTER_ERROR == flash12_init( &flash12, &flash12_cfg ) )
{
log_error( &logger, " Communication init." );
for ( ; ; );
}
if ( FLASH12_ERROR == flash12_default_cfg ( &flash12 ) )
{
log_error( &logger, " Default configuration." );
for ( ; ; );
}
log_info( &logger, " Application Task " );
}
void read_logged_positions ( void )
{
uint32_t mem_address_1 = STARTING_ADDRESS;
uint32_t mem_address_2 = STARTING_ADDRESS + 0x100;
uint8_t data_buf[32] = { 0 };
log_printf( &logger, "\r\n--- Reading Stored Joystick Positions ---\r\n" );
// Read Joystick 1 logged positions
log_printf( &logger, "Joystick 1 Positions:\r\n" );
while ( 1 )
{
memset( data_buf, 0, sizeof( data_buf ) );
if ( FLASH12_OK != flash12_memory_read( &flash12, mem_address_1, data_buf, sizeof( data_buf ) ) )
{
log_printf( &logger, "Error reading memory at 0x%.6lX\r\n", mem_address_1 );
break;
}
// Stop if we reach an empty space (default erased flash value is 0xFF)
if ( data_buf[0] == 0xFF || data_buf[0] == '\0' )
{
log_printf( &logger, "End of Joystick 1 logged data.\r\n" );
break;
}
log_printf( &logger, "Memory 0x%.6lX: %s\r\n", mem_address_1, data_buf );
mem_address_1 += strlen( (char*)data_buf ) + 1; // Move to the next stored entry
}
// Read Joystick 2 logged positions
log_printf( &logger, "Joystick 2 Positions:\r\n" );
while ( 1 )
{
memset( data_buf, 0, sizeof( data_buf ) );
if ( FLASH12_OK != flash12_memory_read( &flash12, mem_address_2, data_buf, sizeof( data_buf ) ) )
{
log_printf( &logger, "Error reading memory at 0x%.6lX\r\n", mem_address_2 );
break;
}
// Stop if we reach an empty space (default erased flash value is 0xFF)
if ( data_buf[0] == 0xFF || data_buf[0] == '\0' )
{
log_printf( &logger, "End of Joystick 2 logged data.\r\n" );
break;
}
log_printf( &logger, "Memory 0x%.6lX: %s\r\n", mem_address_2, data_buf );
mem_address_2 += strlen( (char*)data_buf ) + 1; // Move to the next stored entry
}
log_printf( &logger, "-----------------------------------------\r\n" );
}
const char* joystick_position_to_string( joystick3_position_t position )
{
switch ( position )
{
case JOYSTICK3_POSITION_NEUTRAL: return "NEUTRAL";
case JOYSTICK3_POSITION_UP: return "UP";
case JOYSTICK3_POSITION_DOWN: return "DOWN";
case JOYSTICK3_POSITION_LEFT: return "LEFT";
case JOYSTICK3_POSITION_RIGHT: return "RIGHT";
case JOYSTICK3_POSITION_UPPER_LEFT: return "UPPER-LEFT";
case JOYSTICK3_POSITION_UPPER_RIGHT: return "UPPER-RIGHT";
case JOYSTICK3_POSITION_LOWER_LEFT: return "LOWER-LEFT";
case JOYSTICK3_POSITION_LOWER_RIGHT: return "LOWER-RIGHT";
default: return "UNKNOWN";
}
}
void application_task ( void )
{
static uint32_t mem_address_1 = STARTING_ADDRESS; // Rolling memory address
static uint32_t mem_address_2 = STARTING_ADDRESS + 0x100; // Offset for second joystick
static joystick3_position_t last_position_1 = JOYSTICK3_POSITION_NEUTRAL;
static joystick3_position_t last_position_2 = JOYSTICK3_POSITION_NEUTRAL;
uint16_t raw_x1, raw_y1, raw_x2, raw_y2;
uint8_t data_buf[32] = { 0 }; // Buffer for logging position
// Read Joystick 1 raw ADC values
if ( JOYSTICK3_OK == joystick3_read_raw_adc ( &joystick3_1, &raw_x1, &raw_y1 ) )
{
joystick3_position_t current_position_1 = joystick3_get_position( raw_x1, raw_y1 );
// Log position if it changes
if ( current_position_1 != last_position_1 )
{
const char* position_str_1 = joystick_position_to_string( current_position_1 );
log_printf( &logger, "Joystick 1 position: %s\r\n", position_str_1 );
// Write position to Flash memory
memset( data_buf, 0, sizeof( data_buf ) );
strncpy( (char*)data_buf, position_str_1, sizeof( data_buf ) - 1 );
if ( FLASH12_OK == flash12_memory_write( &flash12,
mem_address_1,
data_buf,
strlen( position_str_1 ) + 1 ) )
{
log_printf( &logger, "Stored JoyStick 1 in Flash at 0x%.6lX: %s\r\n",
mem_address_1,
data_buf );
mem_address_1 += strlen( position_str_1 ) + 1; // Move address forward
}
last_position_1 = current_position_1; // Update last position
}
read_logged_positions(); // Call to read and verify stored positions
}
// Read Joystick 2 raw ADC values
if ( JOYSTICK3_OK == joystick3_read_raw_adc ( &joystick3_2, &raw_x1, &raw_y2 ) )
{
joystick3_position_t current_position_2 = joystick3_get_position( raw_x2, raw_y2 );
// Log position if it changes
if ( current_position_2 != last_position_2 )
{
const char* position_str_2 = joystick_position_to_string( current_position_2 );
log_printf( &logger, "Joystick 2 position: %s\r\n", position_str_2 );
// Write position to Flash memory
memset( data_buf, 0, sizeof( data_buf ) );
strncpy( (char*)data_buf, position_str_2, sizeof( data_buf ) - 1 );
if ( FLASH12_OK == flash12_memory_write( &flash12,
mem_address_2,
data_buf,
strlen( position_str_2 ) + 1 ) )
{
log_printf( &logger, "Stored JoyStick 2 in Flash at 0x%.6lX: %s\r\n",
mem_address_2,
data_buf );
mem_address_2 += strlen( position_str_2 ) + 1; // Move address forward
}
last_position_2 = current_position_2; // Update last position
}
read_logged_positions(); // Call to read and verify stored positions
}
Delay_ms( 100 );
}
int main ( void )
{
/* Do not remove this line or clock might not be set correctly. */
#ifdef PREINIT_SUPPORTED
preinit();
#endif
application_init( );
for ( ; ; )
{
application_task( );
}
return 0;
}
// ------------------------------------------------------------------------ END
Additional Support
Resources
Category:Human-Machine Interface (HMI)