Skip to content

Commit b6bb69e

Browse files
authored
Add a demo for using LVGL, DRAM, LLVM Toolchain, ELF on an STM32F746G discovery board (#104)
Demonstrating a "full" graphical firmware running on an STM32 microcontroller board, concretely the STM32F746G "Discovery" board. Multiple interesting technologies demonstrated: - **ELF file format**, linking with lld, with a custom simple linker script (and thus it builds identically on both macOS and Linux hosts) - **LLVM Embedded Toolchain for ARM** - **LVGL** graphical/input/animation library - The **DRAM, LCD, touch panel, GPIO pins and interrupts** on the STM32F746G - **No other SDKs or library dependencies** -- all the startup code, including MCU, board and peripheral initialization is done in Swift source code Additionally, this sample code: - Has **LSP integration** set up via the `.sourcekit-lsp/config.json` file, confirmed to work in multiple code editors (VS Code, Sublime Text, Zed) - Uses **SwiftPM's toolset.json** to define compiler and linker flags - Has a host OS (macOS, Linux) **"simulator" using SDL** that can use the same "business logic" code to render the same LVGL UI.
1 parent 3cd0f63 commit b6bb69e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+15963
-17
lines changed

.github/workflows/build-stm.yml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
example: [stm32-blink]
18+
example: [stm32-blink, stm32-lvgl]
1919

2020
steps:
2121
- name: Checkout repo
2222
uses: actions/checkout@v4
2323

24+
- name: Fixup for running locally in act
25+
if: ${{ env.ACT }}
26+
run: echo /opt/acttoolcache/node/18.20.8/x64/bin >> $GITHUB_PATH
27+
2428
- name: Set up Python
2529
uses: actions/setup-python@v5
2630
with:
@@ -32,8 +36,21 @@ jobs:
3236
- name: Install Swift
3337
uses: ./.github/actions/install-swift
3438

39+
- name: Set environment variables
40+
run: |
41+
echo "STM_BOARD=STM32F746G_DISCOVERY" >> $GITHUB_ENV
42+
3543
- name: Build ${{ matrix.example }}
3644
working-directory: ${{ matrix.example }}
3745
run: |
38-
export STM_BOARD=STM32F746G_DISCOVERY
39-
./build-elf.sh
46+
if [[ -f ./fetch-dependencies.sh ]]; then
47+
./fetch-dependencies.sh
48+
fi
49+
50+
if [[ -f ./build-elf.sh ]]; then
51+
./build-elf.sh
52+
fi
53+
54+
if [[ -f Makefile ]]; then
55+
make
56+
fi

.swiftformatignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
./harmony/*
2+
./stm32-lvgl/*
23
./stm32-lcd-logo/Sources/STM32F7X6/*
4+
./stm32-lvgl/Sources/Registers/*
35
./stm32-neopixel/Sources/STM32F7X6/*
46
./stm32-uart-echo/Sources/STM32F7X6/*

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Each example in this repository contains build and deployment instructions, howe
3333
| [rpi-picow-blink-sdk](./rpi-picow-blink-sdk) | Raspberry Pi Pico W | Pico SDK | Blink an LED to signal 'SOS' in Morse code repeatedly with Swift & the Pico SDK. | <img width="300" src="https://github.com/apple/swift-embedded-examples/assets/26223064/a4949a2e-1887-4325-8f5f-a681963c93d7"> |
3434
| [stm32-blink](./stm32-blink) | STM32F746G-DISCO | None | Blink an LED repeatedly. | <img width="300" src="https://github.com/apple/swift-embedded-examples/assets/1186214/739e98fd-a438-4a64-a7aa-9dddee25034b"> |
3535
| [stm32-lcd-logo](./stm32-lcd-logo) | STM32F746G-DISCO | None | Animate the Swift Logo on the built-in LCD. | <img width="300" src="https://github.com/apple/swift-embedded-examples/assets/1186214/9e117d81-e808-493e-a20c-7284ea630f37"> |
36+
| [stm32-lvgl](./stm32-lvgl) | STM32F746G-DISCO || Baremetal setup of LCD, touch panel, DRAM, using the LLVM Embedded toolchain for ARM. Renders graphics, animations, and reacts to user input via LVGL. Includes a macOS/Linux SDL based host simulation app. | <img width="300" src="https://github.com/user-attachments/assets/3b4fefd3-1656-4768-9c64-6cbcb3ff9665"> |
3637
| [stm32-neopixel](./stm32-neopixel) | STM32F746G-DISCO | None | Control NeoPixel LEDs using SPI. | <img width="300" src="https://github.com/apple/swift-embedded-examples/assets/1186214/9c5d8f74-f8aa-4632-831e-212a3e35e75a"> |
3738
| [stm32-uart-echo](./stm32-uart-echo) | STM32F746G-DISCO | None | Echo user input using UART. | <img width="300" src="https://github.com/apple/swift-embedded-examples/assets/1186214/97d3c465-9a07-4b86-9654-0c2aaaa43b3d">|
3839

Tools/elf2hex.py

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,23 @@
1818
# file format suitable for flashing onto some embedded devices.
1919
#
2020
# Usage:
21-
# $ elf2hex.py <input> <output> [--symbol-map <output>]
21+
# $ elf2hex.py <input> <output> [--symbol-map <output>] [--relocate-data-segment]
2222
#
2323
# Example:
2424
# $ elf2hex.py ./blink ./blink.hex --symbol-map ./blink.symbols
2525
#
26+
# The --relocate-data-segment option expects to be able to locate symbols with names
27+
# - __data_start
28+
# - __flash_data_start
29+
# - __flash_data_len
30+
# and then it physically relocates a segment located at __data_start to
31+
# __flash_data_start, without changing virtual/physical addresses of any ELF
32+
# headers. This means that the .hex file is not validly mapped until a boot-time
33+
# reverse relocation step.
34+
#
35+
# See the linker script used in a particular demo folder for a detailed
36+
# explanation of the linking, packing, and runtime relocation scheme.
37+
#
2638

2739
import argparse
2840
import json
@@ -36,6 +48,7 @@ def main():
3648
parser.add_argument('input')
3749
parser.add_argument('output')
3850
parser.add_argument('--symbol-map')
51+
parser.add_argument('--relocate-data-segment', action='store_true')
3952
args = parser.parse_args()
4053

4154
inf = open(args.input, "rb")
@@ -70,32 +83,54 @@ def emit(vmaddr, data):
7083
vmaddr += chunklen
7184

7285
elffile = elftools.elf.elffile.ELFFile(inf)
73-
for segment in elffile.iter_segments():
74-
if segment.header.p_type != "PT_LOAD":
75-
continue
76-
vmaddr = segment.header.p_paddr
77-
data = segment.data()
78-
emit(segment.header.p_paddr, data)
79-
80-
chunklen = 0
81-
vmaddr = 0
82-
recordtype = "01" # EOF
83-
emitrecord(f"{chunklen:02X}{vmaddr:04X}{recordtype}")
8486

8587
symbol_map = {}
8688
symtab_section = elffile.get_section_by_name(".symtab")
8789
for s in symtab_section.iter_symbols():
8890
if s.entry.st_info.type not in ["STT_FUNC", "STT_NOTYPE"]:
8991
continue
90-
if s.entry.st_shndx == "SHN_ABS":
91-
continue
9292
if s.name == "":
9393
continue
9494
symbol_map[s.name] = s.entry.st_value
9595

9696
if args.symbol_map is not None:
9797
pathlib.Path(args.symbol_map).write_text(json.dumps(symbol_map))
9898

99+
relocations = {}
100+
if args.relocate_data_segment:
101+
__flash_data_start = symbol_map["__flash_data_start"]
102+
__data_start = symbol_map["__data_start"]
103+
__flash_data_len = symbol_map["__flash_data_len"]
104+
print("Relocation info:")
105+
print(f" __flash_data_start = 0x{__flash_data_start:08x}")
106+
print(f" __data_start = 0x{__data_start:08x}")
107+
print(f" __flash_data_len = 0x{__flash_data_len:08x}")
108+
relocations = {__data_start: __flash_data_start}
109+
110+
for segment in elffile.iter_segments():
111+
if segment.header.p_type != "PT_LOAD":
112+
continue
113+
vmaddr = segment.header.p_paddr
114+
data = segment.data()
115+
flags = ""
116+
flags += "r" if segment.header.p_flags & 0x4 else "-"
117+
flags += "w" if segment.header.p_flags & 0x2 else "-"
118+
flags += "x" if segment.header.p_flags & 0x1 else "-"
119+
print(f"PT_LOAD {flags} at 0x{segment.header.p_paddr:08x} - "
120+
f"0x{segment.header.p_paddr + len(data):08x}, "
121+
f"size {len(data)} "
122+
f"(0x{len(data):04x})")
123+
placement_addr = segment.header.p_paddr
124+
if segment.header.p_paddr in relocations:
125+
placement_addr = relocations[segment.header.p_paddr]
126+
print(f" ... relocating to 0x{placement_addr:08x}")
127+
emit(placement_addr, data)
128+
129+
chunklen = 0
130+
vmaddr = 0
131+
recordtype = "01" # EOF
132+
emitrecord(f"{chunklen:02X}{vmaddr:04X}{recordtype}")
133+
99134
inf.close()
100135
outf.close()
101136

stm32-lvgl/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lvgl
2+
llvm-toolchain

stm32-lvgl/.sourcekit-lsp/config.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"swiftPM": {
3+
"configuration": "release",
4+
"triple": "armv7em-none-none-eabi",
5+
"toolsets": ["toolset.json"],
6+
"swiftCompilerFlags": [
7+
"-enable-experimental-feature", "Embedded",
8+
"-enable-experimental-feature", "Extern"
9+
]
10+
}
11+
}

stm32-lvgl/Makefile

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
##===----------------------------------------------------------------------===##
2+
##
3+
## This source file is part of the Swift open source project
4+
##
5+
## Copyright (c) 2025 Apple Inc. and the Swift project authors.
6+
## Licensed under Apache License v2.0 with Runtime Library Exception
7+
##
8+
## See https://swift.org/LICENSE.txt for license information
9+
##
10+
##===----------------------------------------------------------------------===##
11+
12+
# Paths
13+
REPOROOT := $(shell git rev-parse --show-toplevel)
14+
TOOLSROOT := $(REPOROOT)/Tools
15+
TOOLSET := $(PWD)/toolset.json
16+
ELF2HEX := $(TOOLSROOT)/elf2hex.py
17+
SWIFT_BUILD := swift build
18+
NM := nm
19+
LLVM_TOOLCHAIN := $(PWD)/llvm-toolchain
20+
21+
# Flags
22+
ARCH := armv7em
23+
TARGET := $(ARCH)-none-none-eabi
24+
SWIFT_BUILD_ARGS := \
25+
--configuration release \
26+
--triple $(TARGET) \
27+
--toolset $(TOOLSET) \
28+
--product Application
29+
BUILDROOT := $(shell $(SWIFT_BUILD) $(SWIFT_BUILD_ARGS) --show-bin-path)
30+
31+
.PHONY: build
32+
build:
33+
@echo "checking dependencies..."
34+
35+
if [[ ! -d $(PWD)/lvgl ]]; then echo "\n *** LVGL checkout not found, please run ./fetch-dependencies.sh\n" ; exit 1 ; fi
36+
if [[ ! -d $(PWD)/llvm-toolchain ]]; then echo "\n *** LLVM toolchain checkout not found, please run ./fetch-dependencies.sh\n" ; exit 1 ; fi
37+
38+
mkdir -p .build
39+
40+
@echo "configuring LVGL..."
41+
cmake -B .build/lvgl -G Ninja ./lvgl \
42+
-DCMAKE_EXPORT_COMPILE_COMMANDS=On \
43+
-DTOOLCHAIN_PATH=$(LLVM_TOOLCHAIN) \
44+
-DCMAKE_TOOLCHAIN_FILE=../clang-arm-toolchain.cmake \
45+
-DLV_CONF_PATH=../Sources/CLVGL/include/lv_conf.h
46+
47+
@echo "building LVGL..."
48+
cmake --build .build/lvgl
49+
50+
@echo "building..."
51+
$(SWIFT_BUILD) \
52+
$(SWIFT_BUILD_ARGS) \
53+
--verbose
54+
55+
@echo "disassembling..."
56+
$(LLVM_TOOLCHAIN)/bin/llvm-objdump --all-headers --disassemble --mcpu=cortex-m7 \
57+
$(BUILDROOT)/Application \
58+
| c++filt | swift demangle > $(BUILDROOT)/Application.disassembly
59+
60+
@echo "extracting binary..."
61+
$(ELF2HEX) \
62+
$(BUILDROOT)/Application $(BUILDROOT)/Application.hex --relocate
63+
ls -al $(BUILDROOT)/Application.hex
64+
@echo "\n *** All done, build succeeded!\n"
65+
66+
flash:
67+
@echo "flashing..."
68+
st-flash --reset --format ihex write $(BUILDROOT)/Application.hex
69+
70+
simulator:
71+
mkdir -p .build
72+
73+
@echo "configuring LVGL..."
74+
cmake -B .build/lvgl-host -G Ninja ./lvgl \
75+
-DCMAKE_EXPORT_COMPILE_COMMANDS=On \
76+
-DLV_CONF_PATH=../Sources/CLVGL/include/lv_conf.h
77+
78+
@echo "building LVGL..."
79+
cmake --build .build/lvgl-host
80+
81+
@echo "building..."
82+
$(SWIFT_BUILD) \
83+
--configuration release \
84+
--product HostSDLApp \
85+
--verbose
86+
87+
@echo "running..."
88+
$(PWD)/.build/release/HostSDLApp
89+
90+
.PHONY: clean
91+
clean:
92+
@echo "cleaning..."
93+
@swift package clean
94+
@rm -rf .build

stm32-lvgl/Package.resolved

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"originHash" : "27891c7454528c694f8ba4111e7214ff70b5364bdb54bce808c6e0d4b188bd81",
3+
"pins" : [
4+
{
5+
"identity" : "swift-argument-parser",
6+
"kind" : "remoteSourceControl",
7+
"location" : "https://github.com/apple/swift-argument-parser.git",
8+
"state" : {
9+
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
10+
"version" : "1.5.0"
11+
}
12+
},
13+
{
14+
"identity" : "swift-mmio",
15+
"kind" : "remoteSourceControl",
16+
"location" : "https://github.com/apple/swift-mmio",
17+
"state" : {
18+
"branch" : "main",
19+
"revision" : "5232c5129a8c70beafc3d6acfbae2716c1b6822a"
20+
}
21+
},
22+
{
23+
"identity" : "swift-syntax",
24+
"kind" : "remoteSourceControl",
25+
"location" : "https://github.com/swiftlang/swift-syntax.git",
26+
"state" : {
27+
"revision" : "0687f71944021d616d34d922343dcef086855920",
28+
"version" : "600.0.1"
29+
}
30+
},
31+
{
32+
"identity" : "swiftsdl2",
33+
"kind" : "remoteSourceControl",
34+
"location" : "https://github.com/ctreffs/SwiftSDL2.git",
35+
"state" : {
36+
"revision" : "30a2886bd68e43fc19ba29b63ffe230ac0e4db7a",
37+
"version" : "1.4.1"
38+
}
39+
}
40+
],
41+
"version" : 3
42+
}

stm32-lvgl/Package.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// swift-tools-version: 5.10
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "stm32-lvgl",
7+
platforms: [
8+
.macOS(.v11)
9+
],
10+
products: [
11+
.executable(name: "Application", targets: ["Application"])
12+
],
13+
dependencies: [
14+
.package(url: "https://github.com/apple/swift-mmio", branch: "main"),
15+
.package(url: "https://github.com/ctreffs/SwiftSDL2.git", from: "1.4.0"),
16+
],
17+
targets: [
18+
//
19+
// FIRMWARE TARGETS
20+
//
21+
22+
.executableTarget(
23+
name: "Application",
24+
dependencies: [
25+
"Registers",
26+
"Support",
27+
"CLVGL",
28+
]),
29+
30+
// SVD2Swift \
31+
// --input Tools/SVDs/stm32f7x6.patched.svd \
32+
// --output stm32-lvgl/Sources/STM32F7x6 \
33+
// --peripherals FLASH LTDC RCC PWR FMC SCB DBGMCU USART1 STK NVIC SYSCFG \
34+
// GPIOA GPIOB GPIOC GPIOD GPIOE GPIOF GPIOG GPIOH GPIOI GPIOJ GPIOK \
35+
// I2C1 I2C2 I2C3 I2C4 \
36+
// --access-level public
37+
.target(
38+
name: "Registers",
39+
dependencies: [
40+
.product(name: "MMIO", package: "swift-mmio")
41+
]),
42+
43+
.target(name: "Support"),
44+
45+
.target(name: "CLVGL"),
46+
47+
//
48+
// HOST TARGETS
49+
//
50+
51+
.executableTarget(
52+
name: "HostSDLApp",
53+
dependencies: [
54+
.product(name: "SDL", package: "SwiftSDL2"),
55+
"CLVGL",
56+
],
57+
swiftSettings: [.enableExperimentalFeature("Extern")],
58+
linkerSettings: [.unsafeFlags(["-L.build/lvgl-host/lib", "-llvgl", "-llvgl_demos"])]),
59+
])

0 commit comments

Comments
 (0)