0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry Pi、JUCE、ALSAの限界を知る

Last updated at Posted at 2025-05-27

今までちゃんと調べてなかったけど、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を見ると、ドライバの最小値に補正するようになっている。

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を使う場合も似たようなことをしている。

juce_ALSA_linux.cpp
        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を使ってレイテンシを計算している。

juce_ALSA_linux.cpp
        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, &param) == 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にやらせた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?