3.2K Views
September 19, 25
スライド概要
iOSDC Japan 2025 day0の登壇資料です。
https://fortee.jp/iosdc-japan-2025/proposal/ddd3be00-7378-47f6-845c-b546b33f84e4
SwiftとLEGOとBluetooth LEが好きなプログラマ
Embedded Swiftで解き明かす コンピューターの仕組み 大庭 慎一郎 @ooba / @bricklife
20分で解き明かせるわけなかった
正味のタイトル Embedded SwiftとMMIOで コンピュータをゼロから動かす
話している人 大庭 慎一郎(おおば しんいちろう) ID: @ooba / @bricklife SwiftとLEGOが好きなプログラマ メルカリ1人目のiOSアプリエンジニア
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
Button("Request") {
Task {
do {
let url = URL(string: "https://api.github.com/search/repositories?q=swift")!
let (data, _) = try await URLSession.shared.data(from: url)
print(data)
} catch {
print(error)
}
}
}
}
.padding()
}
}
問い 画面描画やネットワーク通信は コンピュータでどのように 実行されるのか?
コンピュータを構成しているもの CPU GPU メモリ HDMI ストレージ Bluetooth USB Ethernet Wi-Fi
CPU Central Processing Unit = 中央処理装置
機械語 マシン語とも // アドレス0x45678900の値に1を足して戻す d2912000 f2a8ace0 f9400001 91000421 f9000001
アセンブリ言語 機械語を人間にも読みやすくした言語 // アドレス0x45678900の値に1を足して戻す d2912000 → mov x0, #0x8900 f2a8ace0 → movk x0, #0x4567, lsl #16 f9400001 → ldr x1, [x0] 91000421 → add x1, x1, #1 f9000001 ! str x1, [x0]
機械語に画面制御やネットワーク 制御の命令がある?
ISA Instruction Aet Architecture = 命令セットアーキテクチャ
RISC 【りすく】 Reduced Instruction Set Computer
Arm ISAで実行できる主な命令 1. データ処理系 (Data Processing Instructions) 算術演算、論理演算、ビット操作、フォーマット変換などの命令 2. ロード/ストア系 (Load and Store Instructions) メモリとレジスタ間のデータ転送を行う命令 3. 分岐/制御系 (Branch and Control Instructions, Program Flow) プログラムの流れを制御する命令 4. システム/例外系 (System Instructions, Exception Generating) OSや特権レベルとのやり取り、例外処理などを扱う命令 5. コプロセッサ/拡張系 (Coprocessor Instructions, Optional Extensions) コプロセッサ(浮動小数点やSIMDユニット)向けの命令や、特定の拡張(Crypto, SVEなど)など
コンピュータ内のつながり CPU GPU HDMI USB メモリ Ethernet ストレージ Wi-Fi Bluetooth
コンピュータ内のつながり CPU GPU HDMI USB メモリ Ethernet ストレージ Wi-Fi Bluetooth
コンピュータ内のつながり CPU PCIe ストレージ GPU HDMI メモリ USB Ethernet 無線コントローラ Wi-Fi Bluetooth
メモリマップドI/O Memory-Mapped I/O、MMIO
任意のアドレスの 読み書きができれば コンピュータを動かせる!
// Swiftでアドレス0x45678900を読み書きする let p = UnsafePointer<UInt64>(bitPattern: 0x4567_8900)! print(p.pointee) let b = UnsafeMutablePointer<UInt64>(bitPattern: 0x4567_8900)! b.pointee += 1 b[10] = 100
普通のアプリ開発では直接アクセスしない アプリ アプリ アプリ アプリ OS メモリ ストレージ ネットワーク
ベアメタル環境のための Embedded Swift
Embedded Swift in iOSDC Japan 2024
ゲームボーイアドバンス (GBA) ARM7TDMI(Arm 32 bit CPU) 2001年3月21日発売 General Internal Memory 00000000-00003FFF BIOS - System ROM 02000000-0203FFFF WRAM - On-board Work RAM 03000000-03007FFF WRAM - On-chip Work RAM 04000000-040003FE I/O Registers Internal Display Memory 05000000-050003FF BG/OBJ Palette RAM 06000000-06017FFF VRAM - Video RAM 07000000-070003FF OAM - OBJ Attributes (16 KBytes) (256 KBytes) (32 KBytes) (1 Kbyte) (96 KBytes) (1 Kbyte) External Memory (Game Pak) 08000000-09FFFFFF Game Pak ROM/FlashROM (max 32MB) 0A000000-0BFFFFFF Game Pak ROM/FlashROM (max 32MB) 0C000000-0DFFFFFF Game Pak ROM/FlashROM (max 32MB) 0E000000-0E00FFFF Game Pak SRAM (max 64 KBytes) https://problemkaputt.de/gbatek-gba-memory-map.htm より抜粋
// GBAの画面を描画するコード
@main
struct GameMain {
static func main() {
// 画面をモード3にして背景2を表示
let displayControll = UnsafeMutablePointer<UInt16>(bitPattern: 0x0400_0000)!
displayControll.pointee = 0x0403
// 背景2のフレームバッファを取得
let framebuffer = UnsafeMutablePointer<UInt16>(bitPattern: 0x0600_0000)!
for y in 0..<160 {
for x in 0..<240 {
let color = UInt16(x ^ y)
framebuffer[y * 240 + x] = color
}
}
}
}
while true {}
// GBAのボタン入力に応じて四角を描画するコード
@main
struct GameMain {
static func main() {
let displayControll = UnsafeMutablePointer<UInt16>(bitPattern: 0x0400_0000)!
displayControll.pointee = 0x0403
let framebuffer = UnsafeMutablePointer<UInt16>(bitPattern: 0x0600_0000)!
// ボタン入力のポインタを取得
let keyinput = UnsafePointer<UInt16>(bitPattern: 0x0400_0130)!
}
}
while true {
for y in 70..<90 {
for x in 110..<130 {
framebuffer[y * 240 + x] = keyinput.pointee
}
}
}
https://akkera102.sakura.ne.jp/gbadev/?tutorial.6 より抜粋
// 実はあやしい、GBAのボタン入力に応じて四角を描画するコード
@main
struct GameMain {
static func main() {
let displayControll = UnsafeMutablePointer<UInt16>(bitPattern: 0x0400_0000)!
displayControll.pointee = 0x0403
let framebuffer = UnsafeMutablePointer<UInt16>(bitPattern: 0x0600_0000)!
// ボタン入力のポインタを取得
let keyinput = UnsafePointer<UInt16>(bitPattern: 0x0400_0130)!
}
}
while true {
for y in 70..<90 {
for x in 110..<130 {
framebuffer[y * 240 + x] = keyinput.pointee
}
}
}
コンパイラの最適化が MMIOに悪影響
// 実はあやしい、GBAのボタン入力に応じて四角を描画するコード
@main
struct GameMain {
static func main() {
let displayControll = UnsafeMutablePointer<UInt16>(bitPattern: 0x0400_0000)!
displayControll.pointee = 0x0403
let framebuffer = UnsafeMutablePointer<UInt16>(bitPattern: 0x0600_0000)!
// ボタン入力のポインタを取得
let keyinput = UnsafePointer<UInt16>(bitPattern: 0x0400_0130)!
}
}
while true {
for y in 70..<90 {
for x in 110..<130 {
framebuffer[y * 240 + x] = keyinput.pointee
}
}
}
Volatile access in Embedded Swift
// 確実に動くGBAのボタン入力に応じて四角を描画するコード
import _Volatile
// experimental-featureでVolatileを有効にする必要あり
@main
struct GameMain {
static func main() {
let displayControll = UnsafeMutablePointer<UInt16>(bitPattern: 0x0400_0000)!
displayControll.pointee = 0x0403
let framebuffer = UnsafeMutablePointer<UInt16>(bitPattern: 0x0600_0000)!
// VolatileMappedRegisterで書き換える
let keyinput = VolatileMappedRegister<UInt16>(unsafeBitPattern: 0x04000130)
while true {
let color: UInt16 = keyinput.load()
for y in 0..<20 {
for x in 0..<20 {
framebuffer[(y + 70) * 240 + x + 110] = color
}
}
}
よりPCに近いベアメタル環境で MMIOしてみる
Raspberry Pi 4 Model B (ラズパイ4B) Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.8GHz https://www.raspberrypi.com/products/raspberry-pi-4-model-b/ より抜粋
ラズパイ4Bの構成 https://www.sci-pi.org.uk/arch/soc.html より抜粋
Embedded Swiftのサンプルプロジェクト集 https://github.com/swiftlang/swift-embedded-examples/
Embedded Swiftでカーネルイメージを作る % cd rpi-4b-blink % swiftly install % make % cp .build/release/Application.bin /Volumes/bootfs/kernel8.img % diskutil unmount bootfs
// Sources/Application/Application.swift
// 続き
import MMIO
import Support
@Register(bitWidth: 32)
struct GPSET1 {
@ReadWrite(bits: 10..<11, as: Bool.self)
var set: SET
}
func setLedOutput() {
gpio.gpfsel4.modify {
// setFunction Select 42 (fsel42) to 001
$0.fsel42b1 = true
$0.fsel42b2 = false
$0.fsel42b3 = false
}
}
@Register(bitWidth: 32)
struct GPCLR1 {
@ReadWrite(bits: 10..<11, as: Bool.self)
var clear: CLEAR
}
func ledOn() {
gpio.gpset1.modify {
$0.set = true
}
}
@Register(bitWidth: 32)
struct GPFSEL4 {
@ReadWrite(bits: 6..<7, as: Bool.self)
var fsel42b1: FSEL42b1
@ReadWrite(bits: 7..<8, as: Bool.self)
var fsel42b2: FSEL42b2
@ReadWrite(bits: 8..<9, as: Bool.self)
var fsel42b3: FSEL42b3
}
func ledOff() {
gpio.gpclr1.modify {
$0.clear = true
}
}
@RegisterBlock
struct GPIO {
@RegisterBlock(offset: 0x200020)
var gpset1: Register<GPSET1>
@RegisterBlock(offset: 0x20002c)
var gpclr1: Register<GPCLR1>
@RegisterBlock(offset: 0x200010)
var gpfsel4: Register<GPFSEL4>
}
@main
struct Application {
static func main() {
setLedOutput()
while true {
ledOn()
delay()
ledOff()
delay()
}
}
let gpio = GPIO(unsafeAddress: 0xFE00_0000)
func delay() {
for _ in 1..<1_000_000 { nop() }
}
swift-mmio https://github.com/apple/swift-mmio
@Register(bitWidth: 32)
struct GPSET1 {
@ReadWrite(bits: 10..<11, as: Bool.self)
var set: SET
}
@Register(bitWidth: 32)
struct GPCLR1 {
@ReadWrite(bits: 10..<11, as: Bool.self)
var clear: CLEAR
}
@Register(bitWidth: 32)
struct GPFSEL4 {
@ReadWrite(bits: 6..<7, as: Bool.self)
var fsel42b1: FSEL42b1
@ReadWrite(bits: 7..<8, as: Bool.self)
var fsel42b2: FSEL42b2
@ReadWrite(bits: 8..<9, as: Bool.self)
var fsel42b3: FSEL42b3
}
@RegisterBlock
struct GPIO {
@RegisterBlock(offset: 0x200020)
var gpset1: Register<GPSET1>
@RegisterBlock(offset: 0x20002c)
var gpclr1: Register<GPCLR1>
@RegisterBlock(offset: 0x200010)
var gpfsel4: Register<GPFSEL4>
}
let gpio = GPIO(unsafeAddress: 0xFE00_0000)
func setLedOutput() {
gpio.gpfsel4.modify {
// setFunction Select 42 (fsel42) to 001
$0.fsel42b1 = true
$0.fsel42b2 = false
$0.fsel42b3 = false
}
}
func ledOn() {
gpio.gpset1.modify {
$0.set = true
}
}
func ledOff() {
gpio.gpclr1.modify {
$0.clear = true
}
}
func delay() {
for _ in 1..<1_000_000 { nop() }
}
@main
struct Application {
static func main() {
setLedOutput()
while true {
ledOn()
delay()
ledOff()
@Register(bitWidth: 32)
struct GPSET1 {
@ReadWrite(bits: 10..<11, as: Bool.self)
var set: SET
}
@Register(bitWidth: 32)
struct GPCLR1 {
@ReadWrite(bits: 10..<11, as: Bool.self)
var clear: CLEAR
}
@Register(bitWidth: 32)
struct GPFSEL4 {
@ReadWrite(bits: 6..<7, as: Bool.self)
var fsel42b1: FSEL42b1
@ReadWrite(bits: 7..<8, as: Bool.self)
var fsel42b2: FSEL42b2
@ReadWrite(bits: 8..<9, as: Bool.self)
var fsel42b3: FSEL42b3
}
@RegisterBlock
struct GPIO {
@RegisterBlock(offset: 0x200020)
var gpset1: Register<GPSET1>
@RegisterBlock(offset: 0x20002c)
var gpclr1: Register<GPCLR1>
@RegisterBlock(offset: 0x200010)
var gpfsel4: Register<GPFSEL4>
}
let gpio = GPIO(unsafeAddress: 0xFE00_0000)
func setLedOutput() {
gpio.gpfsel4.modify {
// setFunction Select 42 (fsel42) to 001
$0.fsel42b1 = true
$0.fsel42b2 = false
$0.fsel42b3 = false
}
}
func ledOn() {
gpio.gpset1.modify {
$0.set = true
}
}
func ledOff() {
gpio.gpclr1.modify {
$0.clear = true
}
}
func delay() {
for _ in 1..<1_000_000 { nop() }
}
@main
struct Application {
static func main() {
setLedOutput()
while true {
ledOn()
delay()
ledOff()
@Register(bitWidth: 32)
struct GPSET1 {
@ReadWrite(bits: 10..<11, as: Bool.self)
var set: SET
}
@Register(bitWidth: 32)
struct GPCLR1 {
@ReadWrite(bits: 10..<11, as: Bool.self)
var clear: CLEAR
}
@Register(bitWidth: 32)
struct GPFSEL4 {
@ReadWrite(bits: 6..<7, as: Bool.self)
var fsel42b1: FSEL42b1
@ReadWrite(bits: 7..<8, as: Bool.self)
var fsel42b2: FSEL42b2
@ReadWrite(bits: 8..<9, as: Bool.self)
var fsel42b3: FSEL42b3
}
@RegisterBlock
struct GPIO {
@RegisterBlock(offset: 0x200020)
var gpset1: Register<GPSET1>
@RegisterBlock(offset: 0x20002c)
var gpclr1: Register<GPCLR1>
@RegisterBlock(offset: 0x200010)
var gpfsel4: Register<GPFSEL4>
}
let gpio = GPIO(unsafeAddress: 0xFE00_0000)
func setLedOutput() {
gpio.gpfsel4.modify {
// setFunction Select 42 (fsel42) to 001
$0.fsel42b1 = true
$0.fsel42b2 = false
$0.fsel42b3 = false
}
}
func ledOn() {
gpio.gpset1.modify {
$0.set = true
}
}
func ledOff() {
gpio.gpclr1.modify {
$0.clear = true
}
}
func delay() {
for _ in 1..<1_000_000 { nop() }
}
@main
struct Application {
static func main() {
setLedOutput()
while true {
ledOn()
delay()
ledOff()
// Sources/MMIOVolatile/MMIOVolatile.h typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef unsigned long long int uint64_t; #define VOLATILE_LOAD(type) __attribute__((always_inline)) static type mmio_volatile_load_##type( const volatile type * _Nonnull pointer) { return *pointer; } VOLATILE_LOAD(uint8_t); VOLATILE_LOAD(uint16_t); VOLATILE_LOAD(uint32_t); VOLATILE_LOAD(uint64_t); \ \ \ #define VOLATILE_STORE(type) __attribute__((always_inline)) static void mmio_volatile_store_##type( volatile type * _Nonnull pointer, type value) { *pointer = value; } VOLATILE_STORE(uint8_t); VOLATILE_STORE(uint16_t); VOLATILE_STORE(uint32_t); VOLATILE_STORE(uint64_t); \ \ \
import Support import _Volatile func setLedOutput() { let GPFSEL4 = VolatileMappedRegister<UInt32>( unsafeBitPattern: 0xFE00_0000 + 0x200010 ) var value = GPFSEL4.load() value |= (1 << 6) GPFSEL4.store(value) } func ledOn() { let GPSET1 = VolatileMappedRegister<UInt32>( unsafeBitPattern: 0xFE00_0000 + 0x200020 ) GPSET1.store(1 << 10) } func ledOff() { let GPCLR1 = VolatileMappedRegister<UInt32>( unsafeBitPattern: 0xFE00_0000 + 0x20002c ) GPCLR1.store(1 << 10) }
ラズパイ4Bでベアメタルな HDMI出力
CPUとGPUは直接やりとりできる https://www.sci-pi.org.uk/arch/soc.html より抜粋
// InlineArray (SE-0453) var mail = InlineArray<36, UInt32>(repeating: 0) mail[0] = 35 * 4 // Length of message in bytes mail[1] = MBOX_REQUEST mail[2] = MBOX_TAG_SETPHYWH // Tag identifier mail[3] = 8 // Value size in bytes mail[4] = 0 mail[5] = 1920 // Value(width) mail[6] = 1080 // Value(height) // ・・・ mail[30] = MBOX_TAG_GETPITCH mail[31] = 4 mail[32] = 0 mail[33] = 0 // Bytes per line mail[34] = MBOX_TAG_LAST let res = Mailbox.request(mail, ch: MBOX_CH_PROP)
// Integer Generic Parameters (SE-0452)
@frozen public struct InlineArray<let count : Int, Element> :
~Copyable where Element : ~Copyable {}
enum Mailbox {
static func request<let count: Int>(
_ mail: InlineArray<count, UInt32>,
ch: UInt8
) -> InlineArray<count, UInt32> {
let bufferAddress: UInt = 0x10000
for i in 0..<count {
MMIO.write(bufferAddress + UInt(i * 4), mail[i])
}
// 28-bit address (MSB) and 4-bit value (LSB)
let r = UInt32(bufferAddress & ~0xf) | UInt32(ch & 0xf)
// Wait until we can write
while MMIO.read(MBOX_STATUS) & MBOX_FULL != 0 {}
// Write the address of our buffer to the mailbox with the channel appended
MMIO.write(MBOX_WRITE, r)
while true {
// Is there a reply?
while MMIO.read(MBOX_STATUS) & MBOX_EMPTY != 0 {}
}
}
// Is it a reply to our message?
if r == MMIO.read(MBOX_READ) {
var res = InlineArray<count, UInt32>(repeating: 0)
for i in 0..<count {
res[i] = MMIO.read(bufferAddress + UInt(i * 4))
}
return res
}
enum Mailbox {
static func request<let count: Int>(
_ mail: InlineArray<count, UInt32>,
ch: UInt8
) -> InlineArray<count, UInt32> {
let bufferAddress: UInt = 0x10000
for i in 0..<count {
MMIO.write(bufferAddress + UInt(i * 4), mail[i])
}
// 28-bit address (MSB) and 4-bit value (LSB)
let r = UInt32(bufferAddress & ~0xf) | UInt32(ch & 0xf)
// Wait until we can write
while MMIO.read(MBOX_STATUS) & MBOX_FULL != 0 {}
// Write the address of our buffer to the mailbox with the channel appended
MMIO.write(MBOX_WRITE, r)
while true {
// Is there a reply?
while MMIO.read(MBOX_STATUS) & MBOX_EMPTY != 0 {}
}
}
// Is it a reply to our message?
if r == MMIO.read(MBOX_READ) {
var res = InlineArray<count, UInt32>(repeating: 0)
for i in 0..<count {
res[i] = MMIO.read(bufferAddress + UInt(i * 4))
}
return res
}
enum Mailbox {
static func request<let count: Int>(
_ mail: InlineArray<count, UInt32>,
ch: UInt8
) -> InlineArray<count, UInt32> {
let bufferAddress: UInt = 0x10000
for i in 0..<count {
MMIO.write(bufferAddress + UInt(i * 4), mail[i])
}
// 28-bit address (MSB) and 4-bit value (LSB)
let r = UInt32(bufferAddress & ~0xf) | UInt32(ch & 0xf)
// Wait until we can write
while MMIO.read(MBOX_STATUS) & MBOX_FULL != 0 {}
// Write the address of our buffer to the mailbox with the channel appended
MMIO.write(MBOX_WRITE, r)
while true {
// Is there a reply?
while MMIO.read(MBOX_STATUS) & MBOX_EMPTY != 0 {}
}
}
// Is it a reply to our message?
if r == MMIO.read(MBOX_READ) {
var res = InlineArray<count, UInt32>(repeating: 0)
for i in 0..<count {
res[i] = MMIO.read(bufferAddress + UInt(i * 4))
}
return res
}
// ・・・ let res = Mailbox.request(mail, ch: MBOX_CH_PROP) // Check call is successful and we have a pointer with depth 32 if res[1] == MBOX_RESPONSE, res[20] == 32, res[28] != 0 { return Framebuffer( width: res[10], // Actual physical width height: res[11], // Actual physical height pitch: res[33], // Number of bytes per line address: UInt(res[28] & 0x3fff_ffff) // Convert GPU address to ARM address ) } else { return nil }
struct Image<let size: Int> {
let width: Int
let height: Int
let data: InlineArray<size, UInt32>
func color(x: Int, y: Int) -> UInt32 {
let index = y * width + x
guard data.indices.contains(index) else { return 0 }
return data[index]
}
}
extension Image where size == 11236 { // 11236 == 106 x 106
static let swiftLogo = Image(
width: 106,
height: 106,
data: [
0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000,
0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000,
0x000000, 0x000000, 0x000000, 0x000000, 0xeb5233, 0xec5137, 0xed5239,
0xed4f38, 0xee5136, 0xef5038, 0xef5039, 0xef5237, 0xef5039, 0xef5039,
0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039,
0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039,
0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039,
0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039, 0xef5039,
ラズパイ4Bでベアメタルな USB制御
USB 3.0はPCIeに接続されている https://www.sci-pi.org.uk/arch/soc.html より抜粋
USBキーボード入力までの長い道のり PCIeの初期化 xHCをリスタート PCIeでxHCの検索 USBデバイス接続とポート初期化 BARの取得 スロット割り当て xHCをリセット USBデバイスへのアドレス割り当て Scratchpad Buffers のためのメモリ領域を確保 コントロール転送とデバイスディスクリプタの取得 DCBAAのためのメモリ領域を確保 コンフィグレーションディスクリプタの取得 Primary Event Ringを確保して初期化 HIDエンドポイントの有効化 Command Ring を確保して初期化 USBキーボードからの入力
最初の問い 画面描画やネットワーク通信は コンピュータでどのように 実行されるのか?
OSだけが周辺機器とMMIOする アプリ アプリ アプリ アプリ OS メモリ ストレージ ネットワーク
まとめ 任意のメモリアドレスを 読み書きできれば コンピュータを動かせる
今回のコード(未紹介のものも含む) https://github.com/bricklife/iosdc-japan-2025