Cross-Compiling C++ I2C LCD Program for Raspberry Pi with CLion
A working project is available below
Overview
This guide shows how to cross-compile a C++ program on x86-64 Linux to control an I2C LCD display on Raspberry Pi 3 (aarch64) using CLion IDE and the standard Linux I2C interface.
Development System Setup (x86-64 Linux)
1. Install Cross-Compilation Tools
sudo apt update
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu cmake
2. Match Cross-Compiler Version to Target Pi
It’s important to match your cross-compiler version with the GCC version running on your Raspberry Pi to avoid compatibility issues.
Check Pi GCC Version
# On your Raspberry Pi, check the GCC version
ssh pi@your-pi-ip "gcc --version"
# Example output: gcc (Debian 12.2.0-14) 12.2.0
Downgrade Cross-Compiler if Needed
# If your host has a newer version, downgrade to match the Pi
# For Arch Linux users:
sudo downgrade aarch64-linux-gnu-gcc
# Select version 12.2.0 when prompted
# Also downgrade the C++ compiler:
sudo downgrade aarch64-linux-gnu-g++
# For Ubuntu/Debian users, you may need to install specific versions:
# sudo apt install gcc-12-aarch64-linux-gnu g++-12-aarch64-linux-gnu
# sudo update-alternatives --install /usr/bin/aarch64-linux-gnu-gcc aarch64-linux-gnu-gcc /usr/bin/aarch64-linux-gnu-gcc-12 100
# sudo update-alternatives --install /usr/bin/aarch64-linux-gnu-g++ aarch64-linux-gnu-g++ /usr/bin/aarch64-linux-gnu-g++-12 100
Verify Cross-Compiler Version
# Verify the versions match
aarch64-linux-gnu-gcc --version
aarch64-linux-gnu-g++ --version
# Should show: gcc (GCC) 12.2.0 or similar
CLion IDE Setup
3. Create New Project in CLion
- File → New Project
- Choose C++ Executable
- Set project name:
Rpi_Project1
- Choose location and create project
4. Create Toolchain File in CLion
- In CLion, create a new directory: Right-click project root → New → Directory → name it
cmake
- Right-click cmake folder → New → File → name it
aarch64-toolchain.cmake
- Add the following content:
cmake/aarch64-toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_C_FLAGS "-D_GNU_SOURCE -fno-builtin")
set(CMAKE_CXX_FLAGS "-D_GNU_SOURCE -fno-builtin")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
5. Update CMakeLists.txt
Replace the content of CMakeLists.txt
with:
cmake_minimum_required(VERSION 3.10)
project(Rpi_Project1)
set(CMAKE_CXX_STANDARD 17)
add_executable(Rpi_Project1 main.cpp)
target_compile_options(Rpi_Project1 PRIVATE -Wall -Wextra)
message(STATUS "Using Linux I2C interface - no special libraries needed")
6. Create Source Code
Replace the content of main.cpp
with:
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
int main() {
// Open I2C device
int fd = open("/dev/i2c-1", O_RDWR);
if (fd < 0 || ioctl(fd, I2C_SLAVE, 0x27) < 0) {
std::cerr << "I2C error" << std::endl;
return 1;
}
// Helper functions
auto write_byte = [&](uint8_t data) { write(fd, &data, 1); };
auto strobe = [&](uint8_t data) {
write_byte(data | 0x0C); // EN high + backlight
usleep(1);
write_byte(data | 0x08); // EN low + backlight
usleep(50);
};
auto write_4bits = [&](uint8_t value) {
uint8_t data = (value & 0x0F) << 4;
write_byte(data | 0x08); // backlight on
strobe(data);
};
auto send_cmd = [&](uint8_t value) {
uint8_t high = value & 0xF0;
uint8_t low = (value << 4) & 0xF0;
write_byte(high | 0x08); strobe(high);
write_byte(low | 0x08); strobe(low);
};
auto send_char = [&](char c) {
uint8_t high = c & 0xF0;
uint8_t low = (c << 4) & 0xF0;
write_byte(high | 0x09); strobe(high | 0x01); // RS=1 for data
write_byte(low | 0x09); strobe(low | 0x01);
};
// Initialize LCD
write_byte(0x08); usleep(50000); // backlight on, wait
write_4bits(0x03); usleep(5000); // 8-bit mode
write_4bits(0x03); usleep(5000);
write_4bits(0x03); usleep(5000);
write_4bits(0x02); usleep(5000); // switch to 4-bit
send_cmd(0x28); // 4-bit, 2 lines
send_cmd(0x0C); // display on
send_cmd(0x06); // auto increment
send_cmd(0x01); // clear
usleep(2000);
// Display "Hello World!"
for (char c : std::string("Cat!")) {
send_char(c);
}
std::cout << "Hello World displayed!" << std::endl;
sleep(3);
// Clean up: clear screen and turn off backlight
send_cmd(0x01); // clear display
usleep(2000); // wait for clear to complete
write_byte(0x00); // turn off backlight
close(fd);
return 0;
}
7. Configure CLion Toolchain
- Settings → Build, Execution, Deployment → Toolchains
- Click + and select System
- Configure:
- Name:
RPi3
- CMake:
Bundled
- Build tool:
empty
(detected: ninja) - C Compiler:
aarch64-linux-gnu-gcc
- C++ Compiler:
aarch64-linux-gnu-g++
- Debugger:
Bundled GDB
- Click OK
8. Configure CLion CMake Profile
- Settings → Build, Execution, Deployment → CMake
- Click +
- Configure:
- Name:
RPi
- Build type:
Debug
- Toolchain:
RPi3
- Generator:
Use default Ninja
- CMake options:
-DCMAKE_TOOLCHAIN_FILE=cmake/aarch64-toolchain.cmake
- Click OK
9. Configure Remote Debugging
- Click Run/Debug configurations → Edit configurations
- Click + → Remote GDB Server
- Click the cog wheel next to Credentials
- Enter your Raspberry Pi SSH credentials and save password
- Click OK
- Configure:
- Name:
Rpi-config
- GDB:
Bundled GDB
- Credentials: Select your SSH credentials
- Upload:
Always
- Verify Before launch section shows Build
Raspberry Pi Setup
1. Enable I2C Interface
sudo raspi-config
# Navigate to: Interface Options → I2C → Enable
sudo reboot
2. Install Required Tools
sudo apt update
sudo apt install i2c-tools gdbserver
3. Setup I2C Permissions for Non-Root Users
By default, only root can access I2C devices. Here’s how to setup proper permissions:
Setup I2C Group and Permissions
# 1. Remove any existing broken rules
sudo rm -f /etc/udev/rules.d/10-local_i2c_group.rules
# 2. Create the correct udev rule
sudo sh -c 'echo "SUBSYSTEM==\"i2c-dev\", GROUP=\"i2c\", MODE=\"0664\"" > /etc/udev/rules.d/99-i2c.rules'
# 3. Create i2c group if it doesn't exist
sudo groupadd i2c 2>/dev/null || true
# 4. Add your user to the i2c group
sudo usermod -aG i2c $USER
# 5. Apply the rules immediately
sudo udevadm control --reload-rules
sudo udevadm trigger
# 6. Manually fix current session (temporary)
sudo chown root:i2c /dev/i2c-1
sudo chmod 664 /dev/i2c-1
# 7. Reboot for all changes to take effect
sudo reboot
Verify I2C Access
# After reboot, check permissions
ls -l /dev/i2c*
# Should show: crw-rw-r-- 1 root i2c 89, 1 Aug 25 16:39 /dev/i2c-1
# Check you're in the i2c group
groups $USER
# Should show: pi adm dialout cdrom sudo audio video plugdev games users input netdev i2c
# Test I2C access (should work without sudo)
i2cdetect -y 1
4. Hardware Connection
Connect I2C LCD to Raspberry Pi:
- VCC → 5V (Pin 2)
- GND → Ground (Pin 6)
- SDA → GPIO 2 (Pin 3)
- SCL → GPIO 3 (Pin 5)
5. Test I2C Connection
# Check I2C devices exist
ls /dev/i2c* # Should show /dev/i2c-1
# Scan for devices (should work without sudo now)
i2cdetect -y 1 # Should show your LCD address (commonly 0x27)
Expected output when LCD is connected:
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- 27 -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
Running and Debugging
Build and Run from CLion
- Select RPi build configuration at the top
- Click Build (hammer icon) to cross-compile
- Select Rpi-config debug configuration
- Click Debug (bug icon) for remote debugging
The program will automatically build, upload to your Raspberry Pi, and start debugging remotely.
Features and Benefits
- No root required: Works with proper I2C group permissions
- No special libraries: Uses standard Linux I2C interface
- Portable: Works on any Linux system with I2C support
- Compatible: Doesn’t conflict with other I2C tools like
i2cdetect
- Cross-platform: Easy to adapt for other embedded Linux systems
- Simple: Clean, straightforward code without complex dependencies
- CLion Integration: Full IDE support with remote debugging
Notes
- Common LCD addresses: 0x27, 0x3F, 0x26
- Pin mapping: Based on PCF8574 I2C backpack
- GCC version matching: Critical for avoiding runtime compatibility issues
- CLion debugging: Automatically uploads and starts gdbserver
- Verify architecture:
file Rpi_Project1
should show “ARM aarch64” - I2C permissions: Must be set up once per Pi, persists across reboots
Troubleshooting
- Permission denied on I2C: Ensure user is in i2c group and has rebooted
- Device not found: Check I2C is enabled and LCD connected
- Wrong address: Use
i2cdetect -y 1
to find correct address - Garbage text: Verify pin mapping and connections
- CLion upload fails: Check SSH credentials and Pi connectivity
- Debug connection fails: Ensure gdbserver is installed on Pi
- udev rule not working: Check
/etc/udev/rules.d/99-i2c.rules
exists and reboot - Runtime errors: Ensure cross-compiler version matches Pi GCC version (12.2.0)
- Segmentation faults: Usually caused by GCC version mismatch between host and target
- I2C hanging: Try
sudo chmod 666 /dev/i2c-1
as temporary fix, then investigate udev rules
Filed under: Uncategorized - @ August 25, 2025 4:17 pm