今までちゃんと調べてなかったけど、Raspberry Piを使ったシンセを作る際にどこが限界なのかを調べてみた。これはレイテンシと安定性をどこまで詰められるのかの話。前提としてRaspberry Pi 4、PCM5102A(hifiberryのドライバ)を使う。
聴力によるなんとなくの限界値チェック
JUCEで簡単なシンセサイザーを作ってRaspberry Pi上でGUI付きのスタンドアロンアプリを作って確認。PCM5102Aなどの枯れているDACを使ってレイテンシがどこまで下げられるかチェックすると、だいたい32〜64サンプル@44.1kHzくらいのところに限界がある。このあたりからバッファアンダーランが起こりやすくなる。
ALSAの限界
ALSAの限界というかドライバの限界。これは割と簡単に調べられた。
# socをインストール
sudo apt update
sudo apt install sox
# 120秒のサイン波を作る
sox -n -r 44100 -c 2 -b 32 test.wav synth 120 sine 440
# バッファサイズ 2 サンプルで予備バッファ無しの設定を指定する
aplay -D hw:0,0 --period-size=2 --buffer-size=2 -v test.wav
Playing WAVE 'test.wav' : Signed 32 bit Little Endian, Rate 44100 Hz, Stereo
Hardware PCM card 0 'snd_rpi_hifiberry_dac' device 0 subdevice 0
Its setup is:
stream : PLAYBACK
access : RW_INTERLEAVED
format : S32_LE
subformat : STD
channels : 2
rate : 44100
exact rate : 44100 (44100/1)
msbits : 32
buffer_size : 64
period_size : 32
period_time : 725
tstamp_mode : NONE
tstamp_type : MONOTONIC
period_step : 1
avail_min : 32
period_event : 0
start_threshold : 64
stop_threshold : 64
silence_threshold: 0
silence_size : 0
boundary : 4611686018427387904
appl_ptr : 0
hw_ptr : 0
ここでわかるのは、どんなに小さいバッファサイズを指定してもperiod_sizeが32になるし、buffer_sizeが64になる。これは32サンプルのサイズのバッファを2つ持ってダブルバッファで処理をしている、ということになる。aplay.cを見ると、ドライバの最小値に補正するようになっている。
if (period_time > 0)
err = snd_pcm_hw_params_set_period_time_near(handle, params,
&period_time, 0);
else
err = snd_pcm_hw_params_set_period_size_near(handle, params,
&period_frames, 0);
なので、アプリのAudio設定でバッファサイズを最小値の16に設定するのはあまり意味がないと思う。
JUCEの処理
JUCEでALSAを使う場合も似たようなことをしている。
int dir = 0;
unsigned int periods = 4;
snd_pcm_uframes_t samplesPerPeriod = (snd_pcm_uframes_t) bufferSize;
if (JUCE_ALSA_FAILED (snd_pcm_hw_params_set_rate_near (handle, hwParams, &sampleRate, nullptr))
|| JUCE_ALSA_FAILED (snd_pcm_hw_params_set_channels (handle, hwParams, (unsigned int ) numChannels))
|| JUCE_ALSA_FAILED (snd_pcm_hw_params_set_periods_near (handle, hwParams, &periods, &dir))
|| JUCE_ALSA_FAILED (snd_pcm_hw_params_set_period_size_near (handle, hwParams, &samplesPerPeriod, &dir))
|| JUCE_ALSA_FAILED (snd_pcm_hw_params (handle, hwParams)))
{
return false;
}
ソースから見て取れるのはJUCEではperiodsを4設定していること。ダブルバッファではなく4つもバッファを持っている。これは経験則から安定性を考えてのことだと思う。そのあとでレイテンシの計算をしているけど、JACKがやっているように(らしい)periods - 1を使ってレイテンシを計算している。
if (JUCE_ALSA_FAILED (snd_pcm_hw_params_get_period_size (hwParams, &frames, &dir))
|| JUCE_ALSA_FAILED (snd_pcm_hw_params_get_periods (hwParams, &periods, &dir)))
latency = 0;
else
latency = (int) frames * ((int) periods - 1); // (this is the method JACK uses to guess the latency..)
JUCEのAudio設定画面では32サンプルだと0.7msと出るけど、実際にはlatency = 32 × (4-1) = 96 samples (2.18ms@44.1kHz) と内部では計算している。これを実際にどこで使っているのかは知らないけど。しかしこれを踏まえると、例えば256サンプルの場合は 256 x (4 - 1) / 44100 * 1000 = 17.4(ms)となり、かなりのレイテンシになる。
Raspberry Pi OSの限界
Raspberry Pi OSはLinuxなわけで、汎用なOSであってAudioに特化したものではない。製造元が提供しているのはリアルタイム対応ではない"PREEMPT"な状態。これをPREEMPT-RTにすることでAudio処理の安定化は期待できると思う。
Raspberry Pi OSをPREEMPT-RTにする
注) これは必要ないかも。やってみたけど、正直差がわからない。それともやり方の問題?
いわゆるリアルタイムLinuxにするために、カーネルを自分で用意して入れ替えなければならない。MacでDockerが走っているという前提で下記の3つのファイルをローカルに用意する。
Dockerfile
FROM ubuntu:22.04
# 非対話的インストールのため
ENV DEBIAN_FRONTEND=noninteractive
# 必要なパッケージをインストール
RUN apt-get update && apt-get install -y \
git \
bc \
bison \
flex \
libssl-dev \
make \
libc6-dev \
libncurses5-dev \
crossbuild-essential-arm64 \
gcc-aarch64-linux-gnu \
wget \
xz-utils \
device-tree-compiler \
python3 \
rsync \
kmod \
&& rm -rf /var/lib/apt/lists/*
# 作業ディレクトリを設定
WORKDIR /build
# クロスコンパイル用の環境変数(64bit ARM用)
ENV ARCH=arm64
ENV CROSS_COMPILE=aarch64-linux-gnu-
ENV KERNEL=kernel8
# Raspberry Pi カーネルソースをクローン(6.12系 - RPi4対応)
RUN git clone --depth=1 --branch rpi-6.12.y https://github.com/raspberrypi/linux
# カーネル設定(6.12系メインラインPREEMPT_RT - RPi4用)
RUN cd linux && \
echo "Configuring PREEMPT_RT kernel for Raspberry Pi 4 (6.12+)" && \
make bcm2711_defconfig && \
echo "Enabling mainline PREEMPT_RT features..." && \
# エキスパートモードを有効化
scripts/config --enable CONFIG_EXPERT && \
# プリエンプションモデルを確実に設定
scripts/config --disable CONFIG_PREEMPT_NONE && \
scripts/config --disable CONFIG_PREEMPT_VOLUNTARY && \
scripts/config --disable CONFIG_PREEMPT && \
scripts/config --enable CONFIG_PREEMPT_RT && \
# 基本RT設定
scripts/config --enable CONFIG_HIGH_RES_TIMERS && \
scripts/config --set-val CONFIG_HZ 1000 && \
scripts/config --enable CONFIG_IRQ_FORCED_THREADING && \
# RPi4では安全なRCU設定も有効化可能
scripts/config --enable CONFIG_RCU_BOOST && \
scripts/config --enable CONFIG_NO_HZ_FULL && \
# 設定を適用
make olddefconfig && \
echo "=== RPi4 PREEMPT_RT Configuration Check ===" && \
if grep -q "CONFIG_PREEMPT_RT=y" .config && ! grep -q "CONFIG_PREEMPT=y" .config; then \
echo "✓ SUCCESS: PREEMPT_RT is properly configured for RPi4!"; \
else \
echo "✗ FAILED: PREEMPT_RT configuration failed!"; \
grep -E "CONFIG_PREEMPT.*=" .config; \
exit 1; \
fi
# ビルドスクリプトをコピー
COPY docker_build.sh /build/
RUN chmod +x /build/docker_build.sh
CMD ["/build/docker_build.sh"]
build_rt_kernel.sh
#!/bin/bash
# Raspberry Pi 4 64-bit PREEMPT_RT Kernel Build Script for Docker
# 6.12+ Mainline PREEMPT_RT Version (Raspberry Pi 4 optimized)
# Usage: ./build_rt_kernel.sh
set -e
# 色付きの出力用
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}Raspberry Pi 4 64-bit PREEMPT_RT Kernel Builder${NC}"
echo "6.12+ Mainline PREEMPT_RT (RPi4 optimized!)"
echo "============================================="
# 出力ディレクトリを作成
OUTPUT_DIR="$(pwd)/rpi_rt_output"
echo -e "${YELLOW}Cleaning previous build...${NC}"
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
echo -e "${YELLOW}Output directory: $OUTPUT_DIR${NC}"
# 必要なファイルの確認
if [ ! -f "Dockerfile" ]; then
echo -e "${RED}Error: Dockerfile not found in current directory${NC}"
exit 1
fi
if [ ! -f "docker_build.sh" ]; then
echo -e "${RED}Error: docker_build.sh not found in current directory${NC}"
exit 1
fi
# Dockerイメージをビルド
echo -e "${YELLOW}Building Docker image for RPi4 6.12+ kernel...${NC}"
docker build -t rpi4-rt-builder-6.12 .
if [ $? -ne 0 ]; then
echo -e "${RED}Docker image build failed!${NC}"
exit 1
fi
echo -e "${GREEN}Docker image built successfully!${NC}"
# カーネルをビルド
echo -e "${YELLOW}Starting RPi4 PREEMPT_RT kernel build process...${NC}"
echo "This may take 30-60 minutes..."
docker run --rm \
-v "$OUTPUT_DIR:/output" \
rpi4-rt-builder-6.12
if [ $? -ne 0 ]; then
echo -e "${RED}Kernel build failed!${NC}"
exit 1
fi
echo -e "${GREEN}RPi4 PREEMPT_RT kernel build completed successfully!${NC}"
echo ""
echo -e "${YELLOW}Installation Instructions for Raspberry Pi 4:${NC}"
echo "1. Mount your RPi4 SD card"
echo "2. Backup current kernel:"
echo " sudo cp /Volumes/bootfs/kernel8.img /Volumes/bootfs/kernel8.img.backup"
echo " sudo cp /Volumes/bootfs/config.txt /Volumes/bootfs/config.txt.backup"
echo ""
echo "3. Copy the new files:"
echo " sudo cp $OUTPUT_DIR/boot/kernel8.img /Volumes/bootfs/"
echo " sudo cp $OUTPUT_DIR/boot/*.dtb /Volumes/bootfs/"
echo " sudo cp $OUTPUT_DIR/boot/overlays/*.dtb* /Volumes/bootfs/overlays/"
echo ""
echo "4. Install kernel modules:"
echo " sudo cp -r $OUTPUT_DIR/modules/lib/modules/* /Volumes/rootfs/lib/modules/"
echo ""
echo "5. Update /boot/config.txt by adding:"
echo " cat $OUTPUT_DIR/boot/config_snippet.txt"
echo ""
echo "6. Update cmdline.txt with RT optimizations:"
echo " # Replace PARTUUID with your actual partition UUID"
echo " sudo cp $OUTPUT_DIR/boot/cmdline_rt.txt /Volumes/bootfs/cmdline.txt"
echo ""
echo "7. Reboot your Raspberry Pi 4"
echo ""
echo -e "${YELLOW}Verification commands (after reboot):${NC}"
echo " uname -r # Should show RT version"
echo " uname -m # Should show 'aarch64'"
echo " cat /sys/kernel/realtime # Should show '1'"
echo " sudo cyclictest -t1 -p 80 -i 1000 -l 10000 -h 100 -m"
echo ""
echo -e "${GREEN}This kernel uses mainline PREEMPT_RT (6.12+)${NC}"
echo -e "${GREEN}Optimized for Raspberry Pi 4 - No RCU issues!${NC}"
echo ""
echo -e "${GREEN}Files are ready in: $OUTPUT_DIR${NC}"
docker_build.sh
#!/bin/bash
set -e
cd /build/linux
echo "============================================"
echo "Building Raspberry Pi 4 PREEMPT_RT Kernel"
echo "Target: 64-bit ARM (kernel8.img)"
echo "Kernel: 6.12+ with mainline PREEMPT_RT"
echo "============================================"
# カーネルバージョンを表示
KERNEL_VERSION=$(make kernelversion)
echo "Kernel version: $KERNEL_VERSION"
# PREEMPT_RT設定を確認
echo "=== Checking PREEMPT_RT Configuration ==="
if grep -q "CONFIG_PREEMPT_RT=y" .config; then
echo "✓ CONFIG_PREEMPT_RT=y is enabled"
else
echo "✗ CONFIG_PREEMPT_RT is not enabled"
echo "Current preemption settings:"
grep -E "CONFIG_PREEMPT.*=" .config || echo "No PREEMPT config found"
fi
# その他のRT設定も確認
echo "Other RT settings:"
grep -E "CONFIG_HIGH_RES_TIMERS|CONFIG_NO_HZ_FULL|CONFIG_HZ=" .config | head -5
echo "Building kernel for Raspberry Pi 4..."
echo "This may take 30-60 minutes depending on your system..."
# カーネルビルド
make -j$(nproc) Image modules dtbs
echo "Creating output directories..."
mkdir -p /output/boot/overlays
mkdir -p /output/modules
echo "Copying kernel image..."
cp arch/arm64/boot/Image /output/boot/${KERNEL}.img
# カーネルサイズを表示
KERNEL_SIZE=$(stat -c%s /output/boot/${KERNEL}.img)
echo "Kernel size: $(( KERNEL_SIZE / 1024 / 1024 ))MB"
echo "Copying device tree files..."
cp arch/arm64/boot/dts/broadcom/*.dtb /output/boot/
cp arch/arm64/boot/dts/overlays/*.dtb* /output/boot/overlays/ 2>/dev/null || echo "No overlay files found"
echo "Installing kernel modules..."
make INSTALL_MOD_PATH=/output/modules modules_install
# モジュール数を表示
MODULE_COUNT=$(find /output/modules/lib/modules -name "*.ko" | wc -l 2>/dev/null || echo "0")
echo "Kernel modules installed: $MODULE_COUNT modules"
echo "Creating enhanced config.txt snippet..."
cat > /output/boot/config_snippet.txt << 'EOF'
# PREEMPT_RT kernel configuration (64-bit, mainline 6.12+ for RPi4)
kernel=kernel8.img
arm_64bit=1
# Memory configuration for RT performance
gpu_mem=64
# RT-optimized hardware settings
# Disable features that can cause jitter
dtparam=audio=off
disable_splash=1
# Hardware interfaces (enable if needed)
dtparam=i2c_arm=on
dtparam=spi=on
# Display settings
hdmi_safe=1
hdmi_force_hotplug=1
# Camera support (enable if needed)
camera_auto_detect=1
display_auto_detect=1
EOF
echo "Creating RT-optimized cmdline.txt snippet..."
cat > /output/boot/cmdline_rt.txt << 'EOF'
console=serial0,115200 console=tty1 root=PARTUUID=PLACEHOLDER rootfstype=ext4 fsck.repair=yes rootwait isolcpus=1,2,3 nohz_full=1,2,3 rcu_nocbs=1,2,3 irqaffinity=0 threadirqs quiet
EOF
echo "============================================"
echo "Build completed successfully for RPi4!"
echo "============================================"
echo
echo "Generated files:"
echo " Kernel: /output/boot/${KERNEL}.img ($(( KERNEL_SIZE / 1024 / 1024 ))MB)"
echo " Modules: /output/modules/lib/modules/$(ls /output/modules/lib/modules/ | head -1)"
echo " Config: /output/boot/config_snippet.txt"
echo " RT cmdline: /output/boot/cmdline_rt.txt"
echo
echo "Installation instructions for Raspberry Pi 4:"
echo "1. Copy kernel and files to RPi4"
echo "2. Update config.txt and cmdline.txt"
echo "3. Reboot and enjoy stable PREEMPT_RT!"
echo
echo "This kernel uses mainline PREEMPT_RT (6.12+)"
echo "Optimized for Raspberry Pi 4 - no RCU issues!"
この3つのファイルが置いてあるフォルダの中でbuild_rt_kernel.shを実行すると、カーネルがrpi_rt_outputというフォルダにできあがる。ちなみにこれはRaspberry Pi 4用なので、他のモデルでは動かない。ビルドしたあと、SDカードにファイルをコピーするのだがこれまた手動でやるのはめんどくさいので下記のスクリプトを作った。これはMacのParallels上で動いているUbuntuで使うように作られている。 なぜならMacではext4は読み書きできないから。
install_rt_kernel.sh
#!/bin/bash
# Raspberry Pi 4 PREEMPT_RT Kernel Installation Script
# For 6.12+ Mainline PREEMPT_RT Kernels
# Usage: ./install_rt_kernel.sh
set -e
# 色付きの出力用
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# スクリプトの場所を取得
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo -e "${GREEN}Raspberry Pi 4 PREEMPT_RT Kernel Installer${NC}"
echo -e "${BLUE}6.12+ Mainline PREEMPT_RT Version${NC}"
echo "============================================"
# RTカーネルの出力ディレクトリを確認
RT_OUTPUT_DIR="$SCRIPT_DIR/rpi_rt_output"
if [ ! -d "$RT_OUTPUT_DIR" ]; then
echo -e "${RED}Error: $RT_OUTPUT_DIR not found!${NC}"
echo "Please run the build script first: ./build_rt_kernel.sh"
exit 1
fi
# 必要なファイルの存在確認
echo -e "${YELLOW}Checking RT kernel files...${NC}"
REQUIRED_FILES=(
"boot/kernel8.img"
"boot/config_snippet.txt"
"boot/cmdline_rt.txt"
)
MISSING_FILES=()
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$RT_OUTPUT_DIR/$file" ]; then
MISSING_FILES+=("$file")
fi
done
if [ ${#MISSING_FILES[@]} -gt 0 ]; then
echo -e "${RED}Error: Missing required files:${NC}"
for file in "${MISSING_FILES[@]}"; do
echo " - $file"
done
echo "Please rebuild the kernel with: ./build_rt_kernel.sh"
exit 1
fi
if [ ! -d "$RT_OUTPUT_DIR/modules/lib/modules" ]; then
echo -e "${RED}Error: kernel modules not found!${NC}"
exit 1
fi
echo -e "${GREEN}RT kernel files found!${NC}"
# カーネル情報を表示
KERNEL_SIZE=$(stat -c%s "$RT_OUTPUT_DIR/boot/kernel8.img" 2>/dev/null || echo "0")
MODULE_COUNT=$(find "$RT_OUTPUT_DIR/modules/lib/modules" -name "*.ko" 2>/dev/null | wc -l)
KERNEL_VERSION=$(ls "$RT_OUTPUT_DIR/modules/lib/modules/" | head -1)
echo "Kernel size: $(( KERNEL_SIZE / 1024 / 1024 ))MB"
echo "Kernel version: $KERNEL_VERSION"
echo "Module count: $MODULE_COUNT"
# SDカードの自動検出(Parallels/Mac環境対応)
echo ""
echo -e "${YELLOW}Detecting SD card...${NC}"
BOOT_MOUNT=""
ROOT_MOUNT=""
# 環境変数で指定されている場合はそれを使用
if [ -n "$BOOT_MOUNT" ] && [ -n "$ROOT_MOUNT" ]; then
echo "Using environment variables:"
echo " BOOT_MOUNT=$BOOT_MOUNT"
echo " ROOT_MOUNT=$ROOT_MOUNT"
elif [ -d "/media/parallels/bootfs" ] && [ -d "/media/parallels/rootfs" ]; then
# Parallels環境の場合
BOOT_MOUNT="/media/parallels/bootfs"
ROOT_MOUNT="/media/parallels/rootfs"
echo "Parallels environment detected"
else
# その他の環境での検出
MOUNT_PATTERNS=("/Volumes/bootfs" "/Volumes/boot" "/media/*/bootfs" "/media/*/boot")
for pattern in "${MOUNT_PATTERNS[@]}"; do
for mount_point in $pattern; do
if [ -d "$mount_point" ]; then
# bootfsらしいディレクトリの確認
if [ -f "$mount_point/config.txt" ] || [ -f "$mount_point/kernel8.img" ]; then
BOOT_MOUNT="$mount_point"
break 2
fi
fi
done
done
ROOT_PATTERNS=("/Volumes/rootfs" "/Volumes/root" "/media/*/rootfs" "/media/*/root")
for pattern in "${ROOT_PATTERNS[@]}"; do
for mount_point in $pattern; do
if [ -d "$mount_point" ]; then
# rootfsらしいディレクトリの確認
if [ -d "$mount_point/lib/modules" ] || [ -d "$mount_point/home" ]; then
ROOT_MOUNT="$mount_point"
break 2
fi
fi
done
done
fi
if [ -z "$BOOT_MOUNT" ] || [ -z "$ROOT_MOUNT" ]; then
echo -e "${RED}Error: SD card not detected or not mounted!${NC}"
echo ""
echo "Please ensure your Raspberry Pi 4 SD card is connected and mounted."
echo "Current mount points detected:"
echo "Available mounts:"
df -h | grep -E "(bootfs|rootfs|boot|root)"
echo ""
echo "Expected mount points:"
echo " - Boot partition: /media/parallels/bootfs or /Volumes/bootfs"
echo " - Root partition: /media/parallels/rootfs or /Volumes/rootfs"
echo ""
echo "You can run with manual specification:"
echo " BOOT_MOUNT=/your/boot/path ROOT_MOUNT=/your/root/path $0"
exit 1
fi
echo -e "${GREEN}SD card detected:${NC}"
echo " Boot partition: $BOOT_MOUNT"
echo " Root partition: $ROOT_MOUNT"
# Raspberry Pi OS の確認
if [ ! -f "$BOOT_MOUNT/config.txt" ]; then
echo -e "${RED}Error: This doesn't appear to be a Raspberry Pi OS SD card${NC}"
echo "Missing config.txt file"
exit 1
fi
# アーキテクチャの確認(64-bit推奨)
if grep -q "arm_64bit=1" "$BOOT_MOUNT/config.txt" 2>/dev/null || [ -f "$BOOT_MOUNT/kernel8.img" ]; then
echo -e "${GREEN}64-bit Raspberry Pi OS detected${NC}"
else
echo -e "${YELLOW}Warning: May not be 64-bit Raspberry Pi OS${NC}"
echo "This RT kernel is optimized for 64-bit RPi4"
fi
# 確認プロンプト
echo ""
echo -e "${YELLOW}This script will install PREEMPT_RT kernel for Raspberry Pi 4:${NC}"
echo "1. Backup current kernel and configuration files"
echo "2. Install RT kernel (kernel8.img)"
echo "3. Copy device tree files and kernel modules"
echo "4. Update config.txt for RT kernel with optimizations"
echo "5. Create RT-optimized cmdline.txt template"
echo ""
echo -e "${RED}Warning: This will replace your current kernel!${NC}"
echo "Make sure you have backups and can access the SD card if issues occur."
echo ""
read -p "Continue with installation? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Installation cancelled."
exit 1
fi
# バックアップの作成
echo ""
echo -e "${YELLOW}Creating backups...${NC}"
BACKUP_DIR="$BOOT_MOUNT/backup_rt_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
# 元のカーネルファイルをバックアップ
if [ -f "$BOOT_MOUNT/kernel8.img" ]; then
cp "$BOOT_MOUNT/kernel8.img" "$BACKUP_DIR/"
echo "Backed up kernel8.img"
fi
if [ -f "$BOOT_MOUNT/config.txt" ]; then
cp "$BOOT_MOUNT/config.txt" "$BACKUP_DIR/"
echo "Backed up config.txt"
fi
if [ -f "$BOOT_MOUNT/cmdline.txt" ]; then
cp "$BOOT_MOUNT/cmdline.txt" "$BACKUP_DIR/"
echo "Backed up cmdline.txt"
fi
echo -e "${GREEN}Backups created in: $BACKUP_DIR${NC}"
# RTカーネルのインストール
echo ""
echo -e "${YELLOW}Installing RT kernel...${NC}"
cp "$RT_OUTPUT_DIR/boot/kernel8.img" "$BOOT_MOUNT/"
echo "RT kernel installed ($(ls -lh "$BOOT_MOUNT/kernel8.img" | awk '{print $5}'))"
# デバイスツリーファイルのコピー
echo -e "${YELLOW}Installing device tree files...${NC}"
cp "$RT_OUTPUT_DIR/boot/"*.dtb "$BOOT_MOUNT/" 2>/dev/null || echo "No DTB files to copy"
cp "$RT_OUTPUT_DIR/boot/overlays/"*.dtb* "$BOOT_MOUNT/overlays/" 2>/dev/null || echo "No overlay files to copy"
echo "Device tree files installed"
# カーネルモジュールのインストール
echo -e "${YELLOW}Installing kernel modules...${NC}"
cp -r "$RT_OUTPUT_DIR/modules/lib/modules/"* "$ROOT_MOUNT/lib/modules/"
echo "Kernel modules installed"
# config.txtの更新
echo -e "${YELLOW}Updating config.txt...${NC}"
cat "$RT_OUTPUT_DIR/boot/config_snippet.txt" >> "$BOOT_MOUNT/config.txt"
echo "config.txt updated for RT kernel"
# cmdline.txtテンプレートの作成
echo -e "${YELLOW}Creating RT cmdline.txt template...${NC}"
# 元のPARTUUIDを保持
ORIGINAL_PARTUUID=$(grep -o 'root=PARTUUID=[^ ]*' "$BACKUP_DIR/cmdline.txt" 2>/dev/null || echo 'root=PARTUUID=YOUR_PARTUUID')
sed "s/root=PARTUUID=PLACEHOLDER/$ORIGINAL_PARTUUID/" "$RT_OUTPUT_DIR/boot/cmdline_rt.txt" > "$BOOT_MOUNT/cmdline_rt_template.txt"
echo "RT cmdline template created: cmdline_rt_template.txt"
echo "Review and replace cmdline.txt if you want RT optimizations"
# 同期と完了メッセージ
echo -e "${YELLOW}Syncing files...${NC}"
sync
echo ""
echo -e "${GREEN}Installation completed successfully for Raspberry Pi 4!${NC}"
echo ""
echo -e "${BLUE}Installation Summary:${NC}"
echo "- RT kernel: $(ls -lh "$BOOT_MOUNT/kernel8.img" | awk '{print $5}')"
echo "- Modules: $(ls "$ROOT_MOUNT/lib/modules/" | grep -E '^6\.' | tail -1)"
echo "- Backup: $BACKUP_DIR"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Safely eject the SD card"
echo "2. Insert into Raspberry Pi 4 and boot"
echo "3. After boot, verify RT kernel with:"
echo " uname -r"
echo " cat /sys/kernel/realtime"
echo " sudo cyclictest -t1 -p 80 -i 1000 -l 1000"
echo ""
echo -e "${YELLOW}Optional RT optimizations:${NC}"
echo "Replace /boot/cmdline.txt with /boot/cmdline_rt_template.txt for:"
echo "- CPU isolation (cores 1,2,3 for RT tasks)"
echo "- IRQ affinity (core 0 for interrupts)"
echo "- Tickless operation on isolated cores"
echo ""
echo -e "${YELLOW}If boot fails, restore from backup:${NC}"
echo " cp $BACKUP_DIR/kernel8.img $BOOT_MOUNT/"
echo " cp $BACKUP_DIR/config.txt $BOOT_MOUNT/"
echo ""
echo -e "${GREEN}Happy real-time computing on Raspberry Pi 4!${NC}"
Raspberry Imagerで焼いたSDカードをMacに挿してParallels上のUbuntuで認識させてマウントされた状態でこのスクリプトを走らせれば、PREEMPT-RTのOSが出来上がる。
パフォーマンスを引き出すための設定 (OS)
SWAP完全無効化
# SWAP無効化
sudo swapoff -a
sudo dphys-swapfile swapoff
sudo systemctl stop dphys-swapfile
sudo systemctl disable dphys-swapfile
# 恒久設定
sudo nano /etc/dphys-swapfile
# CONF_SWAPSIZE=0 に変更
CPU性能最大化
# CPUを最高性能固定
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# 1.0GHz → 1.8GHz (80%性能向上)
リアルタイム権限設定
# /etc/security/limits.conf に下記を追加
username - rtprio 99
username - memlock unlimited
@audio - rtprio 99
@audio - memlock unlimited
# usernameの部分は実際のユーザー名
ALSA設定
# ~/.asoundrc を下記の内容で作成
pcm.!default {
type hw
card 0
device 0
}
ctl.!default {
type hw
card 0
}
pcm.!default の部分:
- PCM(Pulse Code Modulation)デバイスのデフォルト設定
- type hw = ハードウェア直接アクセス
- card 0 = サウンドカード0番
- device 0 = そのカード内のデバイス0番
ctl.!default の部分:
- コントロールデバイス(音量調整など)のデフォルト設定
- 同じくハードウェア直接アクセスでカード0を指定
この設定により、ALSAを使うアプリケーションは全て、サウンドカード0のデバイス0に直接音声を送るようになります。PulseAudioやPipeWireなどの音声サーバーを経由せず、ハードウェアに直接アクセスする設定です。
音声の遅延を最小限にしたい場合や、他の音声システムとの競合を避けたい場合によく使われる設定ですね。
パフォーマンスを引き出すための設定 (アプリ)
OS側の設定ができても、アプリ内側で対応しないと効果が発揮できない。
JUCEソース最適化
4つ持っているバッファを2つに減らす。バッファアンダーランのリスクは増えるがレイテンシを優先する。
// modules/juce_audio_devices/native/juce_linux_ALSA.cpp の下記の部分
// 修正前
unsigned int periods = 4;
// 修正後
unsigned int periods = 2; // 4→2に変更
リアルタイムスレッド設定
PREEMPT-RTの恩恵を受けるにはスレッドのスケジューリングがSHED_FIFOでなくてはならない。また優先度も最高レベルに引き上げてCPUコアも占有する。ここはJUCEのソースをいじるのが嫌なのでprepareToPlay()が呼ばれた後一発目のprocessBlock()で行う。
// 下記のメソッドをAudioProcessorを継承したクラスに実装
#if JUCE_LINUX
void XXXAudioProcessor::configureLinuxRealtimeThread()
{
struct sched_param param;
param.sched_priority = 99; // 最高優先度
if (sched_setscheduler(0, SCHED_FIFO, ¶m) == 0) {
std::cout << "[IFW3] MAXIMUM priority scheduler: SUCCESS (priority 99)" << std::endl;
}
// メモリロック
mlockall(MCL_CURRENT | MCL_FUTURE);
// CPU専有
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(3, &cpuset);
sched_setaffinity(0, sizeof(cpu_set_t), &cpuset);
pthread_setname_np(pthread_self(), "IFW3-AudioRT");
}
#endif
prepareToPlay()とprocessBlock()内で適用
void XXXAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) override
{
#if JUCE_LINUX
rtConfigured.store(false); // フラグリセット
#endif
// 他の処理...
}
void XXXAudioProcessor::processBlock(AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
{
#if JUCE_LINUX
if (!rtConfigured.load()) {
configureLinuxRealtimeThread();
rtConfigured.store(true);
}
#endif
// オーディオ処理...
}
ヘッダに追加
// 必要なインクルードファイル
#if JUCE_LINUX
#include <sys/mman.h> // mlockall, MCL_CURRENT, MCL_FUTURE
#include <sched.h> // sched_setscheduler, SCHED_FIFO
#include <pthread.h> // pthread_setname_np
#include <errno.h> // errno
#include <cstring> // strerror
#include <unistd.h> // sysconf
#include <sys/resource.h> // getrlimit, RLIMIT_RTPRIO
#include <sys/syscall.h> // syscall, __NR_gettid
#endif
// これらはクラスのメンバとして追加
#if JUCE_LINUX
std::atomic<bool> rtConfigured{false};
std::atomic<bool> configurationAttempted{false};
#endif
どうやってこれをやった?
だいたいClaude Sonnet 4にやらせた。