Getting started in using real-time interaction for embedded systems doesn’t have to be complicated. In this project, we demonstrate how to control a 16x2 I2C LCD using the RT-Spark STM32F407 board, with all display updates triggered directly from a USART terminal. By pairing STM32CubeIDE with Terminalbpp, we create a simple yet powerful workflow where text and commands can be sent instantly to the LCD, which is ideal for testing, debugging, and building interactive interfaces.
Whether you're exploring I2C communication, experimenting with STM32 peripherals, or looking for a practical way to visualise data through text, this project offers a clear and hands-on example of integrating real-time display control into your embedded applications.
Step 1: Wiring and ConnectionBefore initiating the software configuration, it is essential to establish a reliable hardware connection between the STM32F407 microcontroller and the 16x2 I2C LCD. The I2C interface simplifies this process, requiring only four connections to enable communication.
Connections:
VCC→VCC(5V) on STM32F407GND→GNDSDA→PB7(I2C Data)SCL→PB6(I2C Clock)
Ensure that all connections are secure to prevent communication errors, flickering, or unexpected LCD behavior. Once the wiring is complete and verified, power the board and terminal to prepare for the subsequent coding phase.
Step 2: Install the NecessitiesYou cannot start this project without the right applications or code. Before starting, ensure you have a header file for the I2C LCD, so that the code is compatible with the materials you have. You can use the given repository located in the "Code" section.
Step 3: Set up your "Pinout & Configuration" in your IOC File CorrectlyTo ensure smooth communication between the RT-Spark STM32F407, the 16x2 I2C LCD, and the USART terminal, proper configuration of your .ioc file is essential. This step defines your pin assignments, peripheral settings, and code-generation structure before any firmware is written.
Here are the configurations used in this project:
I2C1for the LCD- Set the mode to
I2C - Verify that
PB6(SCL) andPB7(SDA) match your hardware wiring. - Navigate to Connectivity, and select
USART1 - Mode: Asynchronous
- Verify no yellow warning pins appear in the pinout view.
With these settings in place, your project is now correctly configured and ready for adding LCD functions, terminal parsing, and real-time display logic.
Before we start coding, we need to ensure that our clock runs smoothly. Therefore, in that case, ensuring that the correct clock configuration is a must to be able to run the code with fewer errors.
Here are the clock configurations that need to be adjusted:
Input Frequency (KHz): 32.768LSI RC:32HSI RC: 16PLLM: /8PLLN: x168PLLP: /2PLLQ: /4PLLCLKenabledSYSCLK: 168AHB: /1HCLK: 168APB1: 42APB2: 84FCLK: 168
Now that the clock is configured to its right settings, we have our ioc file configured, and it's time to connect the terminal to our IDE, and let's start coding!
After configuring the IOC file and wiring your LCD, the next step is to link Terminalbpp with STM32CubeIDE so you can send commands directly to the STM32 in real time. Start by opening the serial terminal inside STM32CubeIDE by navigating to Window → Show View → Other, then selecting the Terminal view. A terminal panel will appear at the bottom of the workspace, ready for configuration.
Choose your board’s COM port and set the serial parameters to match your USART1 setup, typically 9600 baud rate, and 8N1 (8 data bits, no parity, and one stop bit). After confirming these values, click Connect, and the terminal will begin monitoring the MCU’s UART output. Once connected, anything you type into Terminalbpp will be transmitted to your STM32, allowing your firmware to process the text and update the I2C LCD instantly.
Once all of these are finished, let's proceed to coding!
Step 6: CodingSTM32CubeIDE is watching, your terminal is ready, and the STM32F407 is silently judging your semicolons. One wrong line could summon mysterious bugs, endless debugging loops, or that dreaded “it worked yesterday” syndrome. Brace yourself, because your strings will dance, your pointers will judge, and the LCD might just ghost you if you forget a semicolon.
For our project, here is the code used:
/* USER CODE BEGIN Header */
/**
****************************************************************************
* @file : main.c
* @brief : Main program body
****************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
****************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include <string.h>
#include "i2c_lcd.h"
#include "stm32f4xx.h"
/* Private variables ---------------------------------------------------------*/
I2C_HandleTypeDef hi2c1;
UART_HandleTypeDef huart1;
I2C_LCD_HandleTypeDef lcd;
char rx_char;
/* USER CODE BEGIN PV */
char line0[17] = {0};
char line1[17] = {0};
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
static void MX_I2C1_Init(void);
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_I2C1_Init();
// LCD setup
lcd.hi2c = &hi2c1;
lcd.address = 0x4E;
lcd_init(&lcd);
lcd_clear(&lcd);
lcd_gotoxy(&lcd, 0, 0);
uint8_t x = 0, y = 0; // Cursor position
while (1)
{
HAL_UART_Receive(&huart1, (uint8_t *)&rx_char, 1, HAL_MAX_DELAY);
// Integrating Backspace to clear characters
if (rx_char == '\b' || rx_char == 127) // 127 = DEL key on many terminals
{
if (x > 0)
{
x--; // go left
}
else if (y > 0)
{
y--; // go to previous line
x = 15;
}
lcd_gotoxy(&lcd, x, y);
lcd_putchar(&lcd, ' '); // erase
lcd_gotoxy(&lcd, x, y); // reposition
continue;
}
// Enter clears LCD
if (rx_char == '\r' || rx_char == '\n')
{
lcd_clear(&lcd);
x = 0;
y = 0;
lcd_gotoxy(&lcd, x, y);
continue;
}
// Characters typed in LCD
lcd_putchar(&lcd, rx_char);
x++;
if (y == 0)
{
line0[x] = rx_char;
}
else
{
line1[x] = rx_char;
}
// If LCD screen is full, lines will "scroll" up
if (x > 16)
{
strcpy(line0, line1);
// Clear line1 buffer
memset(line1, ' ', 16);
// Refresh LCD
lcd_gotoxy(&lcd, 0, 0);
lcd_puts(&lcd, line0);
lcd_gotoxy(&lcd, 0, 1);
lcd_puts(&lcd, line1);
// Reset to start of line1
y = 1;
x = 0;
lcd_gotoxy(&lcd, x, y);
}
}
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief I2C1 Initialization Function
* @param None
* @retval None
*/
static void MX_I2C1_Init(void)
{
/* USER CODE BEGIN I2C1_Init 0 */
/* USER CODE END I2C1_Init 0 */
/* USER CODE BEGIN I2C1_Init 1 */
/* USER CODE END I2C1_Init 1 */
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN I2C1_Init 2 */
/* USER CODE END I2C1_Init 2 */
}
/**
* @brief USART1 Initialization Function
* @param None
* @retval None
*/
static void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 9600;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
/* USER CODE END USART1_Init 2 */
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin : Up_Button_Pin */
GPIO_InitStruct.Pin = Up_Button_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(Up_Button_GPIO_Port, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}
/* USER CODE BEGIN 4 */
void UART1_SendString(char *str)
{
HAL_UART_Transmit(&huart1, (uint8_t *)str, strlen(str), HAL_MAX_DELAY);
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */Step 7: TestingWith the code uploaded and the wiring complete, it’s time to see if the STM32F407 and the 16x2 I2C LCD are actually cooperating—or silently judging your work. Testing confirms that your program performs as expected and that all interactions, inputs, and outputs function correctly.
Testing Procedure:
- Power On: Turn on the board and activate the terminal.
- Upload Code: Ensure the program loads successfully via STM32CubeIDE, and that the terminal is connected to the development board (STM32F407).
- Observe LCD Output: Check that messages display correctly on the 16x2 I2C LCD. Watch for proper formatting, updates, and scrolling behavior.
- Interaction Check: Test all inputs, such as special characters and
BREAK(found in Terminalbpp), to verify the LCD responds as intended. - Error Handling: If anything misbehaves, take notes—these “bugs” are just your code’s way of asking for attention.
Proper testing guarantees system reliability and helps catch issues early, so your project runs smoothly without surprises when deployed.
Congratulations! You have successfully connected, programmed, and tested your STM32F407 microcontroller with a 16x2 I2C LCD using Terminalbpp and STM32CubeIDE. Through this project, you’ve learned how to establish reliable hardware connections, write effective code, and verify system functionality step by step.
This project not only demonstrates the power of combining microcontroller programming with simple display hardware but also provides a solid foundation for more advanced applications, such as interactive interfaces (i.e., calculator), sensor monitoring, and real-time data visualization. With the skills gained from this project, the possibilities are limited only by your imagination (and your patience with debugging).
Happy coding, and may your LCD never ghost you!










Comments