みつきんのメモ

組み込みエンジニアです。Interface誌で「Yocto Projectではじめる 組み込みLinux開発入門」連載中

Yocto Projectでustreamerを使う

はじめに

猫の監視体制を強化すべくウェブカメラを作ろうと思う。

その用途にはustreamerが使えるらしい。

このアプリはOpemEmbedded-Core互換のレシピが提供されていないようなので今回は自作してみる。

ビルドホストはUbuntu26.04を使用する。

また、動作確認のためターゲットとしてラズベリーパイ(初代)に、UVCのカメラを接続したものを使用する。

環境構築

作業ディレクトリ

~/yocto/wrynoseを作業ディレクトリとする。

$ mkdir -p ~/yocto/wrynose
$ cd ~/yocto/wrynose

bitbake-setupの導入

bitbake-setupをインストールしていない場合は「bitbake-setupをpip3でインストールする」を参考にインストールする。

作業環境(=セットアップ)の作成

bitbake-setup initを実行してセットアップを作成する

setup-dir-nameを「poky-wrynose-webcam」に設定する。

$ source ~/yocto/wrynose/yocto-venv/bin/activate
$ bitbake-setup \
    init --non-interactive poky-wrynose poky distro/poky machine/qemuarm64 \
    --setup-dir-name poky-wrynose-webcam
$ deactivate

bitbakeや、関連ツールが参照するPythonのライブラリの参照に支障をきたすケースがあるのでPython仮想環境から抜けておく。

BitBake環境を設定する

$ source bitbake-builds/poky-wrynose-webcam/build/init-build-env

meta-raspberrypiの追加

$ bitbake-layers layerindex-fetch -f ../layers meta-raspberrypi

MACHINEの変更

MACHINEをRaspberry Pi向けに変更する。

$ bitbake-config-build enable-fragment machine/raspberrypi

rootルートログインの許可

現在のPokyはdebug-tweaks相当の機能はデフォルトでオフになっているので、これを有効化する。

$ bitbake-config-build enable-fragment core/yocto/root-login-with-empty-password

ビルド

core-image-baseをビルドする。

$ bitbake core-image-base

ustreamのレシピ化

レイヤの作成

作業用レイヤをmeta-webcamとして作成する。

$ bitbake-layers create-layer -a -p 10 ../layers/meta-webcam

レシピの雛形の作成

ustreamerのレシピの雛形を作成する。

$ recipetool create https://github.com/pikvm/ustreamer.git

雛形の確認

生成された時点での雛形は下記。

# Recipe created by recipetool
# This is the basis of a recipe and may need further editing in order to be fully functional.
# (Feel free to remove these comments when editing.)

# WARNING: the following LICENSE and LIC_FILES_CHKSUM values are best guesses - it is
# your responsibility to verify that the values are complete and correct.
LICENSE = "GPL-3.0-only"
LIC_FILES_CHKSUM = "file://LICENSE;md5=d32239bcb673463ab874e80d47fae504"

SRC_URI = "git://github.com/pikvm/ustreamer.git;protocol=https;branch=master"

# Modify these as desired
PV = "1.0+git"
SRCREV = "296639a2acd06f8f39131e2d27170caccf8fd63a"

# NOTE: the following library dependencies are unknown, ignoring: systemd event_pthreads gpiod event speexdsp jansson opus
#       (this is based on recipes that have previously been built and packaged)
# NOTE: some of these dependencies may be optional, check the Makefile and/or upstream documentation
DEPENDS = "libbsd libjpeg-turbo alsa-lib"

# NOTE: this is a Makefile-only piece of software, so we cannot generate much of the
# recipe automatically - you will need to examine the Makefile yourself and ensure
# that the appropriate arguments are passed in.

do_configure () {
    # Specify any needed configure commands here
    :
}

do_compile () {
    # You will almost certainly need to add additional arguments here
    oe_runmake
}

do_install () {
    # This is a guess; additional arguments may be required
    oe_runmake install 'DESTDIR=${D}'
}

レイヤへの配置

作成したレシピの雛形をレイヤに配置する。

$ mkdir -p ../layers/meta-webcam/recipes-app/ustreamer
$ mv ustreamer_git.bb ../layers/meta-webcam/recipes-app/ustreamer/

ビルド

ビルドを実行してみる。

pkg-configのエラー

pkg-configがsysrootに存在しないためエラーになっている。

| Makefile:45: *** "No pkg-config found in /home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/build/tmp/sysroots-uninative/x86_64-linux/usr/bin:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/layers/openembedded-core/scripts:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/build/tmp/work/arm1176jzfshf-vfp-poky-linux-gnueabi/ustreamer/1.0+git/recipe-sysroot-native/usr/bin/arm-poky-linux-gnueabi:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/build/tmp/work/arm1176jzfshf-vfp-poky-linux-gnueabi/ustreamer/1.0+git/recipe-sysroot/usr/bin/crossscripts:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/build/tmp/work/arm1176jzfshf-vfp-poky-linux-gnueabi/ustreamer/1.0+git/recipe-sysroot-native/usr/sbin:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/build/tmp/work/arm1176jzfshf-vfp-poky-linux-gnueabi/ustreamer/1.0+git/recipe-sysroot-native/usr/bin:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/build/tmp/work/arm1176jzfshf-vfp-poky-linux-gnueabi/ustreamer/1.0+git/recipe-sysroot-native/sbin:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/build/tmp/work/arm1176jzfshf-vfp-poky-linux-gnueabi/ustreamer/1.0+git/recipe-sysroot-native/bin:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/layers/bitbake/bin:/home/mickey/yocto/wrynose/bitbake-builds/poky-wrynose-webcam/build/tmp/hosttools".  Stop.

下記で解決できる。

inherit pkgconfig

libeventのエラー

次は下記のエラーが発生する。これはsysrootにlibeventパッケージ関連のファイルがないために発生する。

| ustreamer/http/mime.c:27:10: fatal error: event2/util.h: No such file or directory
|    27 | #include <event2/util.h>
|       |          ^~~~~~~~~~~~~~~

依存関係にlibeventを追加すると解決できる。

DEPENDS += "libevent"

インストール場所のエラー

デフォルトのままでは/usr/localにインストールされる。これはOSのパッケージ管理外のソースをビルドする場合の一般的な挙動。

ERROR: ustreamer-1.0+git-r0 do_package: QA Issue: ustreamer: Files/directories were installed but not shipped in any package:
  /usr/local/share
  /usr/local/share/man
  /usr/local/share/man/man1
  /usr/local/share/man/man1/ustreamer-dump.1.gz
  /usr/local/share/man/man1/ustreamer.1.gz
  /usr/local/bin/ustreamer
  /usr/local/bin/ustreamer-dump
Please set FILES such that these items are packaged. Alternatively if they are unneeded, avoid installing them or delete them within do_install.

Yocto Projectのビルド環境ではパッケージを作成するので、/usrに入るようにする。

EXTRA_OEMAKE += "PREFIX=${prefix}"

最終的なレシピ

最終的なustreamer_git.bbは以下のようになる。

LICENSE = "GPL-3.0-only"
LIC_FILES_CHKSUM = "file://LICENSE;md5=d32239bcb673463ab874e80d47fae504"

SRC_URI = "git://github.com/pikvm/ustreamer.git;protocol=https;branch=master"

inherit pkgconfig

PV = "1.0+git"
SRCREV = "296639a2acd06f8f39131e2d27170caccf8fd63a"

DEPENDS = "libjpeg-turbo alsa-lib libevent libbsd"

EXTRA_OEMAKE += "PREFIX=${prefix}"

do_configure () {
    :
}

do_compile () {
    oe_runmake
}

do_install () {
    oe_runmake install 'DESTDIR=${D}'
}

local.confの修正

local.confに以下を追加してイメージにustreamerを追加する。

IMAGE_INSTALL:append = " ustreamer"

イメージのビルド

イメージをビルドする。

$ bitbake core-image-base

動作確認

ラズベリーパイにログインして以下のコマンドでustreamerを実行する。

$ ustreamer --device=/dev/video0 --format=yuyv --resolution=320x240 --host=0.0.0.0 --desired-fps=15

core-image-baseではデフォルトでzeroconfが有効化されているので、「http://raspberrypi.local:8080/stream」でアクセスできる。

まとめ

今回はustreamerをレシピ化した。

recipetool createによりレシピの雛形を作成できるが、そのままでは動かないこともあるといういい実例になった。

一からレシピを自作するよりも非常に効率が良いということがわかったと思う。

Yocto Project Ubuntu 26.04で「ERROR: PermissionError: [Errno 1] Operation not permitted」

はじめに

筆者の作業環境であるUbuntu 26.04でBitBakeを実行した時に下記のエラーが発生した。

ERROR: PermissionError: [Errno 1] Operation not permitted

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/ubuntu/yocto/wrynose/bitbake-builds-sstate/make-sstate-cache/layers/bitbake/bin/bitbake-worker", line 287, in child
    bb.utils.disable_network(uid, gid)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/home/ubuntu/yocto/wrynose/bitbake-builds-sstate/make-sstate-cache/layers/bitbake/lib/bb/utils.py", line 2062, in disable_network
    with open("/proc/self/uid_map", "w") as f:
         ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
PermissionError: [Errno 1] Operation not permitted

ERROR: Task (/home/ubuntu/yocto/wrynose/bitbake-builds-sstate/make-sstate-cache/layers/openembedded-core/meta/recipes-extended/xz/xz_5.8.2.bb:do_unpack) failed with exit code '1'

apt upgrade後にビルドホストを再起動していなかったり、元々使っていたPoky環境だったりして環境が汚染されていたのも原因の一つ。

クリーンな状態で、Poky環境を一から取得し直してbitbakeを実行すると下記のようになった。

ERROR: User namespaces are not usable by BitBake, possibly due to AppArmor.
See https://discourse.ubuntu.com/t/ubuntu-24-04-lts-noble-numbat-release-notes/39890#unprivileged-user-namespace-restrictions for more information.

これはどちらも同じ原因で発生するものであり、BitBakeが失敗すること自体は想定された挙動であるらしい。

回避方法

ここ数週間のUbuntuのパッケージ更新によるセキュリティポリシーの厳格化に起因しているらしい。

暫定的に対処するのであれば下記でもBitBakeは動作するようになる。

$ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

これを永続化するには以下のようにする。

$ echo "kernel.apparmor_restrict_unprivileged_userns=0" | sudo tee /etc/sysctl.d/60-apparmor-namespace.conf
$ sudo sysctl --system

まとめ

数日前まで動作していたBitBakeが急にエラーを吐いたら、これを疑ってみてほしい。

bitbake-setupをpip3でインストールする

はじめに

2026からbitbake-setupがpip3でインストール可能になった。

ここではインストール方法を簡単にまとめる。

ホストOSはUbuntu26.04を使用する。

インストール手順

Python3の仮想環境

PEP668が導入されて以降、Yocto Projectのドキュメントでも仮想環境(venv)を使用する手順が案内されるようになった。 本稿でもそれに合わせてvenvを使用する。

Ubuntu 26.04ではpython3.14-venvパッケージをインストールしておく必要がある。

$ sudo apt update
$ sudo apt install -y python3.14-venv

作業ディレクトリ

~/yocto/wrynoseを作業ディレクトリとする。

$ mkdir -p ~/yocto/wrynose
$ cd ~/yocto/wrynose

仮想環境の作成

~/yocto/wrynose/yocto-venvに仮想環境を作成する。

$ python3 -m venv ~/yocto/wrynose/yocto-venv

仮想環境のアクティベート

下記で仮想環境をアクティベートする。

これによりホストOSのPython環境と隔離される。

$ source ~/yocto/wrynose/yocto-venv/bin/activate

bitbake-setupのインストール

下記でbitbake-setupをインストールする。

$ pip install bitbake-setup

これでbitbake-setupが使用可能となる。

まとめ

bitbake-setupを使用するためにbitbakeリポジトリをクローンしなくても良くなった。

導入後は以下でbitbake-setupが使用可能となる。

$ source ~/yocto/wrynose/yocto-venv/bin/activate
$ bitbake-setup init

Yocto Projectでベアメタル その3

はじめに

wrynoseでベアメタルシリーズ

  1. Yocto Projectでベアメタル
  2. Yocto Projectでベアメタル その2

前回はbaremetal-helloworldの内容を確認した。

今回は実機でベアメタルのコードを動かしてみる。ターゲットはmicro:bit v2を使用する。

環境構築

作業ディレクトリ

~/yocto/wrynoseを作業ディレクトリとする。

$ mkdir -p ~/yocto/wrynose
$ cd ~/yocto/wrynose

bitbakeリポジトリの取得

bitbake-setupを実行するためにbitbakeのリポジトリを取得する。

$ git clone https://git.openembedded.org/bitbake -b yocto-6.0

作業環境を構築する

bitbake-setup initを実行して作業環境(=セットアップ)を作成する。

top-dir-nameを「yocto-builds」に変更する。

setup-dir-nameを「poky-wrynose-baremetal」に設定する。

$ ./bitbake/bin/bitbake-setup \
    --setting default top-dir-name "yocto-builds" \
    init --non-interactive poky-wrynose poky distro/poky machine/qemuarm64 \
    --setup-dir-name poky-wrynose-baremetal

BitBake環境を設定する

init-build-envを実行する。

$ source yocto-builds/poky-wrynose-baremetal/build/init-build-env

作業レイヤの作成

meta-baremetal-workを作成する。

$ bitbake-layers create-layer -p 7 -a ../layers/meta-baremetal-work

マシンの作成

実機用のマシン定義がないためmeta-zephyrのnrf52.incを参考に作成する。マシン名は「bbc-microbit-v2」とする。

$ mkdir -p ../layers/meta-baremetal-work/conf/machine

meta-baremetal-work/conf/machine/bbc-microbit-v2.confを以下の内容で作成する。

#@TYPE: Machine
#@NAME: bbc-microbit-v2

#@DESCRIPTION: Machine configuration for BBC micro:bit v2 Board.

require conf/machine/include/arm/armv7m/tune-cortexm4.inc

TUNE_FEATURES = "armv7em cortexm4"

TCLIBC = "baremetal"

マシンの切り替え

OE_FRAGMENTSのマシン設定をbbc-microbit-v2に変更する。

$ bitbake-config-build enable-fragment machine/bbc-microbit-v2

プログラムの作成

ファイルの配置

今回はレシピにソースコードを同梱する。

meta-baremetal-workに以下のようにファイルを配置する。

meta-baremetal-work
└── recipes-extended
    └── baremetal-example
        ├── helloworld
        │   ├── Makefile
        │   ├── hello.c
        │   ├── hello.ld
        │   └── start.s
        └── helloworld_0.1.bb

ディレクトリの作成

レシピやプログラムを配置するためのディレクトリを作成する。

$ mkdir -p ../layers/meta-baremetal-work/recipes-extended/baremetal-example/helloworld

リンカスクリプト

meta-baremetal-work/recipes-extended/baremetal-example/helloworld/hello.ldを以下の内容で作成する。

今回はハードコードした文字列をUARTから出力するだけのプログラムなので変数を使用しない。 そのためリンカスクリプトは極限まで省略している。

MEMORY
{
  FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 512K
  RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 128K
}

ENTRY(_start)

SECTIONS
{
    .text : {
        . = ALIGN(4);
        KEEP(*(.vectors))
        *(.text)
        *(.rodata)
        . = ALIGN(4);
    } > FLASH

    .bss : {
        *(.bss COMMON)
    } > RAM

    stack_top = ORIGIN(RAM) + LENGTH(RAM);
}

このリンカスクリプトでは以下のことを行っている。

  • メモリ領域の定義
  • セクションの定義

メモリ領域の定義

micro:bit v2の実際のハードウェア構成に合わせてフラッシュROMとRAMのアドレスと範囲を定義している。

メモリ 開始アドレス 終了アドレス 長さ
Flash ROM 0x00000000 0x0007FFFF 512KiB
RAM 0x20000000 0x2001FFFF 128KiB

セクションの定義

変数を使用しないため、読み取り専用のデータをすべてフラッシュROMに配置し、 グローバル変数の領域(BSS)とスタックの領域のみRAMに配置するように定義している。

micro:bit v2のCPUであるCortex-M4では、リセットや割り込みなどのハードウェアによる自動的な処理を行うためにベクタテーブルというものが必要となっている。

ベクタテーブルはアドレスのテーブルになっており、発生したイベントに応じてどの処理を実行するかを決定する。

電源投入時にはリセットが発生するため、リセットベクタ(アドレス 0x00000004)を含んだベクタテーブルを、FLASHの先頭(0x00000000)から正しく配置しておく必要がある。そのため、.text セクションの最先頭で KEEP(*(.vectors)) を指定している。

このリンカスクリプトでは .text セクションを以下のように定義している。

セクション 役割
.vectors ベクタテーブル
.text プログラムコード
.rodata 定数のデータ

スタートアップコード

meta-baremetal-work/recipes-extended/baremetal-example/helloworld/start.sを以下の内容で作成する。

このコードでは以下のことを行っている。

  • Thumb命令モードの設定
  • ベクタテーブルの定義
  • エントリポイント(c_entry)の呼び出し
.syntax unified
.cpu cortex-m4
.thumb

.section .vectors, "a"
.balign 4             /* 4 byte alignment */

.word stack_top       /* 0x00000000: Initial stack pointer */
.word _start          /* 0x00000004: Reset vector */

.section .text
.globl _start

.thumb_func
_start:
        bl c_entry
        b .

ベクタテーブルの内容

Cortex-M4ではリセットが発生すると次のことを行う。

  • スタックポインタの初期化
  • リセットベクタの実行

これらはベクタテーブルを参照しハードウェアが自動的に処理する。

今回は次のようにベクタテーブルを設定している。

アドレス 概要 設定値
0x00000000 スタックポインタ stack_top
0x00000004 リセットベクタ _start

Thumb命令を使用する場合、リセットベクタのアドレスは最下位ビットを1にする必要があるが、コード中で .thumb_func を指定しているため、アセンブラが自動的に調整するようになっている。

_start

スタックポインタの初期化はハードウェアによって自動で行われるため、ここではプログラムのエントリポイントとなるc_entryの読み出しのみ行っている。

呼び出しから戻ってきた場合は、b . によってその場で無限ループさせている。

プログラム本体

meta-baremetal-work/recipes-extended/baremetal-example/helloworld/hello.cを以下の内容で作成する。

typedef unsigned int uint32_t;
typedef unsigned char uint8_t;

// nRF52833 レジスタベースアドレス
#define NRF_P1_BASE             0x50000300UL
#define NRF_UARTE0_BASE         0x40002000UL

// GPIOレジスタオフセット (ポート1用)
#define P1_DIRSET               (*(volatile uint32_t *)(NRF_P1_BASE + 0x518))

// UARTE0 レジスタ定義 (EasyDMA付きUARTだが、通常のUART互換モードで使用)
#define UARTE0_ENABLE           (*(volatile uint32_t *)(NRF_UARTE0_BASE + 0x500))
#define UARTE0_PSEL_TXD         (*(volatile uint32_t *)(NRF_UARTE0_BASE + 0x50C))
#define UARTE0_BAUDRATE         (*(volatile uint32_t *)(NRF_UARTE0_BASE + 0x524))
#define UARTE0_TASKS_STARTTX    (*(volatile uint32_t *)(NRF_UARTE0_BASE + 0x008))
#define UARTE0_TASKS_STOPTX     (*(volatile uint32_t *)(NRF_UARTE0_BASE + 0x00C))
#define UARTE0_EVENTS_TXDRDY    (*(volatile uint32_t *)(NRF_UARTE0_BASE + 0x11C))
#define UARTE0_TXD              (*(volatile uint32_t *)(NRF_UARTE0_BASE + 0x51C))

// 設定値
#define UARTE_ENABLE_ENABLE     4UL          // UART互換モード
#define UART_BAUDRATE_115200    0x01D7E000UL // 115200 Baud

// micro:bit v2 UARTピン配置
#define PIN_TXD_P0_06           6UL          // ポート0の6番ピン

void uart_init(void) {
    UARTE0_PSEL_TXD = PIN_TXD_P0_06;
    UARTE0_BAUDRATE = UART_BAUDRATE_115200;
    UARTE0_ENABLE = UARTE_ENABLE_ENABLE;
}

void uart_putchar(char c) {
    UARTE0_EVENTS_TXDRDY = 0;             // イベントクリア
    UARTE0_TASKS_STARTTX = 1;             // 送信タスク開始
    UARTE0_TXD = (uint8_t)c;              // データを書き込む

    while (UARTE0_EVENTS_TXDRDY == 0) {
        // 送信完了を待機
    }
    
    UARTE0_TASKS_STOPTX = 1;              // 送信タスク停止
}

void uart_putstring(const char *str) {
    while (*str) {
        uart_putchar(*str++);
    }
}

int c_entry(void) {
    uart_init();
    uart_putstring("Hello, World!\r\n");
}

このプログラムはUART(シリアル通信)で文字列を送信(TX)する。 受信(RX)に関しては設定を省略している。

UARTの制御は特定のアドレスに割り当てあられたレジスタを読み書きすることで行う。

重要なのは次の2つの関数。

  1. uart_init()
  2. uart_putchar()

uart_init()

この関数はUARTペリフェラルの初期化を行っている。

具体的には以下のような処理をしている。

  • TXDピンの割り当て:micro:bit v2の仕様に合わせ、ポート0の6番ピンを指定
  • ボーレートの設定:通信速度を115200 bpsに設定
  • UARTのモード設定:EasyDMAを使用しない「UART互換モード」で機能を有効化

uart_putchar()

この関数はUARTで引数の文字を1文字送信する。以下の手順でハードウェアを制御している。

  • TXDRDY(送信完了)イベントのクリア
  • 送信タスクの開始(TASKS_STARTTX)
  • TXDへの書き込み(データ送信開始)
  • 送信完了待ち(TXDRDYの監視)
  • 送信タスクの停止(TASKS_STOPTX)

TXDRDYレジスタは通常のメモリ(RAM)とは異なり、ハードウェアによって値が自動的に書き換えられることがある。 送信が完了し、次の送信が可能状態になるとハードウェア側が自動的に1を設定する。 プログラム側では、その状態変化が起こるまでループでレジスタを監視するという制御を行っている。

Makefile

meta-baremetal-work/recipes-extended/baremetal-example/helloworld/Makefileを以下の内容で作成する。

LDSCRIPT ?= hello.ld
LDFLAGS  ?= -T $(LDSCRIPT)
override LDFLAGS += -T $(LDSCRIPT)

TARGET   = kernel.bin
ELF      = kernel.elf
SRCS     = start.s hello.c

all: $(TARGET)

$(TARGET): $(ELF)
    $(OBJCOPY) -O binary $(ELF) $(TARGET)

$(ELF): $(SRCS) $(LDSCRIPT)
    $(CC) $(CFLAGS) $(LDFLAGS) $(SRCS) -o $(ELF)

clean:
    rm -f $(ELF) $(TARGET)

ビルドした後、objcopyコマンドを使用してELF形式からバイナリ形式への変換も行っている。

レシピの作成

OpenEmbedded-Coreのbaremetal-helloworldとレシピ名を分けるためにシンプルにhelloworld_0.1.bbとする。

meta-baremetal-work/recipes-extended/baremetal-example/helloworld_0.1.bbを以下の内容で作成する。

SUMMARY = "The helloworld work with BBC micro:bit v2 board."

LICENSE = "CLOSED"

SRC_URI = " \
        file://Makefile \
        file://hello.c \
        file://hello.ld \
        file://start.s \
"
S = "${UNPACKDIR}"

TARGET_CFLAGS:append = " -ffreestanding"
TARGET_LDFLAGS:append = " -nostdlib -nostartfiles"

BAREMETAL_BINNAME ?= "hello_baremetal_${MACHINE}"
IMAGE_LINK_NAME ?= "baremetal-helloworld-image-${MACHINE}"
IMAGE_NAME_SUFFIX ?= ""

inherit baremetal-image

COMPATIBLE_MACHINE = "bbc-microbit-v2"

do_install(){
    install -d ${D}/${base_libdir}/firmware
    install -m 755 ${B}/kernel.bin ${D}/${base_libdir}/firmware/${BAREMETAL_BINNAME}.bin
    install -m 755 ${B}/kernel.elf ${D}/${base_libdir}/firmware/${BAREMETAL_BINNAME}.elf
}

FILES:${PN} += " \
    ${base_libdir}/firmware/${BAREMETAL_BINNAME}.bin \
    ${base_libdir}/firmware/${BAREMETAL_BINNAME}.elf \
"

baremetal-imageクラスの動作に合わせて、do_installタスクを定義している。

freestandingのコンパイルフラグ

ベアメタルのコードをビルドする場合下記の設定が必須になる。 これがないとlibcやデフォルトのスタートアップ(crt0.oなど)を自動的にリンクしようとするからだ。

TARGET_CFLAGS:append = " -ffreestanding"
TARGET_LDFLAGS:append = " -nostdlib -nostartfiles"

一見下記の設定でこれを設定してくれそうだが、実際にはそうはならない。

TCLIBC = "baremetal"

しかし、OpenEmbedded-Coreが提供しているbaremetal-helloworld_git.bbをビルドする際にも明示的にTARGET_CFLAGSやTARGET_LDFLAGSを指定しなくてもビルドが成功する。

これはソースコード側のMakefileに仕掛けがある。 下記のようにリンカ(LD)を独立して実行しているため、自動処理が行われない状態になっているためである。

# Create a raw binary from ELF
build: link
    ${OBJCOPY} -O binary ${executable}.elf ${executable}.bin

# Link objects using the provided linker script
link: compile
    sed -i 's~startup.o~$(call contains,startup,${objs})~g' ${lscript}.ld
    ${LD} -T${lscript}.ld ${objs} -o ${executable}.elf

# Compile source but dont link
compile: assemble
    ${CC} ${CFLAGS} ${LDFLAGS} -c $(filter hello%,${sources}).c -o $(call contains,hello,${objs})

本来はこのような場合でもfreestanding環境でのビルドであることを示すためにもTARGET_CFLAGSやTARGET_LDFLAGSの設定はしておくべきだと考える。

ビルド

BitBake

次のコマンドでビルドする。

$ bitbake helloworld

プログラムの書き込み

本来はバイナリ形式のファイルもDAPLinkでの転送に対応しているのだが、ファイルサイズが小さすぎると転送に失敗してしまう。 今回のプログラムのサイズはバイナリ形式だと228バイトしかないため。DAPLinkでの転送ができない。

この場合、ddなどを駆使して無理やりファイルサイズを大きくする(0でパディングする)か、pyocdなどのツールで書き込む必要がある。

pyocd

今回はpyocdを使用してプログラムをターゲットに転送する。

venv環境にpyocdをインストール

$ python -m venv ./.env
$ source .env/bin/activate
$ pip install pyocd
$ pyocd pack install nrf52833

pyocdのudevルールを作成し有効化する。

$ sudo tee /etc/udev/rules.d/49-pyocd.rules << 'EOF'
# Amazon Kindle Fire
SUBSYSTEM=="usb", ATTR{idVendor}=="1949", ATTR{idProduct}=="0186", MODE="0666"

# CMSIS-DAP / micro:bit
SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", ATTR{idProduct}=="0204", MODE="0666", GROUP="plugdev"
KERNEL=="hidraw*", ATTRS{idVendor}=="0d28", ATTRS{idProduct}=="0204", MODE="0666", GROUP="plugdev"
EOF

$ sudo udevadm control --reload-rules
$ sudo udevadm trigger

バイナリの書き込み

$ pushd ./tmp/deploy/images/bbc-microbit-v2
$ pyocd flash -t nrf52833 ./baremetal-helloworld-image-bbc-microbit-v2.bin@0x0

これで書き込みができる。

実機での動作

ターゲットのリセットボタンを押すごとに「hello world」が表示される。

まとめ

Yocto Projectで実機向けのベアメタルプログラムとそのレシピを作成した。 freestanding環境のコンパイルオプションについて悩んだが、それ以外は割と素直に動かすことができた。

ベアメタルプログラミングをするのにわざわざPokyの環境を構築するのは大仰に思うかもしれないが、 手順を統一できることや、ツールチェーンの準備もBitBakeに任せることができるので、 大局的に見るとメリットになることが多いかもしれない。

Yocto Projectでベアメタル その2

はじめに

wrynoseでベアメタルシリーズ

  1. Yocto Projectでベアメタル

前回はwrynose環境でベアメタルのレシピであるbaremetal-helloworldをビルドした。

今回はそのレシピが取り扱うベアメタルのソースコード周りを確認する。

ベアメタル

ソースコードの取得

せっかくのPokyの環境なのでソースコードの取得はdevtoolを使用するのが手っ取り早い。

下記のコマンドを実行しworkspaceにソースコードを展開する。

$ devtool modify baremetal-helloworld

ソースコード

下記にソースコードが展開されている。

$ pushd ./workspace/sources/baremetal-helloworld

ベアメタルの根幹

C言語を勉強すると「プログラムはmain関数から開始されます」と解説されることがほとんどだが、 ベアメタルではmain関数が呼び出されるまでのコードも自作する必要がある。

PC上のリッチなOSの上で実行されるプログラムとは異なり、 ベアメタル環境では、MCU(CPU)に対して、どのアドレスにどんなペリフェラルが接続されており、 プログラムがどこに配置されるべきかを、プログラマがすべて指示する必要がある。

ベアメタルに必要なもの

ベアメタルに最低限必要なものは以下の3つ。

  1. リンカスクリプト(プログラムの配置場所を指示する設定ファイル)
  2. スタートアップコード(電源ONからmain関数に繋ぐための最初のコード。アセンブリ言語で書くことが多い)
  3. アプリケーションのソースコード(main関数以降の、いわゆる普通のプログラム)

リンカスクリプト

マシンをqemuarmにしたのでlinkerscript_arm.ldが適用される。

/* Use the _start symbol as entry point */
ENTRY(_start)
SECTIONS{
        /* Ram starts at 0x40000000 */
        /*
        QEMU when given -kernel adds an offset of
        0x00010000 for 32-bit ARM (KERNEL_LOAD_ADDR)
        */
        . = 0x40010000 ;
        /* Put startup code at the beginning */
        /* Only put the text section of  build/startup_arm.o */
        /* Put all other sections from others */
        .stub : {  build/startup_arm.o(.text) }
        .text : { *(.text) }
        .rodata : { *(.rodata) }
        .data : { *(.data) }
        /* Put common symbols from bss on the bss section */
        .bss : { *(.bss COMMON) }

        /* Use 4KB for stack */
        . = ALIGN(8);
        stack_bottom = .;
        . += 0x1000;
        stack_top = .;
}

このリンカスクリプトでは大きく2つのものを定義している。

  1. エントリポイント
  2. セクション

エントリポイント

エントリポイントとは、プログラムが実行された時に一番最初に処理を開始する位置のこと。 これが"main"とならないのは、main関数を実行する前にメモリの初期化など、いくつかの処理を実行する必要があるため。

セクション

セクションとはプログラムが使用する変数領域やコードをメモリ上のどこに配置するかを決定するためのもの。今回は、QEMU(ARM32)の仕様に合わせて、RAMの開始アドレス(0x40000000)にオフセットを加えた 0x40010000 を起点として、以下の順番でメモリを割り当てている。

セクション 概要 備考
.stub スタートアップコード startup_arm.o のプログラムコード(最優先で先頭に配置)
.text プログラムコード それ以外のすべてのプログラムコード
.rodata 定数データ 読み出し専用の定数や文字列など
.data 初期値ありの変数 グローバル変数など
.bss 初期値なしの変数 起動時に0で初期化する変数領域
(スタック領域) ローカル変数 4KB(0x1000)分のサイズを確保

スタートアップコード

この環境ではstartup_arm.sが採用される。

/* Declare the _start symbol as global */
.globl _start
_start:
        /* No main function, jump to c_entry
        as the beginning of our program */
        ldr r14, =stack_top
        mov sp, r14
        bl c_entry
        b .

このコードでは以下のことを行っている。

  1. エントリポイント(_start)の定義
  2. スタックポインタの初期化
  3. c_entry(mainの代わり)の呼び出し

このコードは実装を簡単にするために意図的に不完全なコードになっている。 不完全な部分としては以下のセクションの初期化処理が実装されていない。

  • .data(初期値あり変数の配置)
  • .bss(初期値なし変数のゼロクリア)

スタックポインタ

スタックはローカル変数の領域であり、関数の呼び出しごとに使用する領域が切り替わる。 そのため領域自体の初期化は必要なく、現在のスタックの基準アドレスさえ決まればいい。電源投入直後のMCUはスタックポインタの位置が決まっていないため、このコードではリンカスクリプトで定義した stack_top を使って位置を設定している。

c_entryについて

mainではなく "c_entry" としているのは、スタートアップが不完全であるため。通常のC言語プログラムのつもりでmain関数を呼び出してしまうと、グローバル変数やスタティック変数がまともに動かないという不具合が発生する。

また、関数名をmainにすると、コンパイラがOS上での動作を前提とした標準ライブラリの初期化などの処理を意図せず追加してしまうことがあり、エラーや不具合の原因になる。そのため、コンパイラの自動処理を避ける目的であえて異なる関数名にしている。

アプリケーションのソースコード

この環境ではhello_baremetal_arm.cが採用される。

/* QEMU virt machine memory map sets VIRT_UART as 0x90000000 */
#define UART0_BASE 0x09000000

volatile unsigned int * const UART0DR = (unsigned int *)UART0_BASE;

/* Until we reach to the end of the string, put each char on UART0 */
void print_uart0(const char *str) {
  while(*str != '\0') {
    *UART0DR = (unsigned int)(*str);
    str++;
  }
}

/* Entry function from startup.s */
void c_entry() {
  print_uart0("Hello OpenEmbedded on ARM!\n");
}

ペリフェラルの制御

ペリフェラルは周辺機器のことでMCUのコアの周辺に配置された補助回路で、接続される外部機器の制御に特化している。 ペリフェラルを制御するにはMCUに割り当てられたレジスタのアドレスのデータを読み書きする。

レジスタへのアクセスはMCUからはただのアドレスへの読み書きにしか見えないので、メモリアクセスと区別がつきにくいが、 レジスタはハードウェアが制御しているため、MCUから書き換えなくても値が変化することがあるなど明確な違いがある。

このようなレジスタの性質をコンパイラに正しく伝えるため、変数定義には volatile キーワードを付与し、意図しない最適化による処理の省略などを防ぐ必要がある。

QEMU環境では実際のハードウェア回路が存在しないため、ソフトウェアでレジスタアクセスを擬似的にエミュレートする。

UART

UARTは非同期シリアル通信方式及びその回路のこと。 PCとMCUの間でシリアルコンソール接続を行う際によく使用される。

データ送信用のレジスタに文字データを書き込むと、UARTのペリフェラルにより、信号ピンの電圧を変化させて物理的な通信を行う。受信した開発用PC側のコンソールアプリケーションがその信号を文字として画面に描画する。

qemuarmの環境でUARTで送信するためのレジスタは0x09000000となっている。 このアドレスをポインタ変数UART0DRに割り当てて、 UART0DRを介してデータをレジスタに書き込むことで文字列を送信している。

QEMUではコンソール機能もエミュレートしており、書き込まれたデータの画面表示まで行っている。

c_entryの実装

c_entry関数では、print_uart0関数を呼び出し"Hello OpenEmbedded on ARM!\n"と出力している。

まとめ

qemuarm(32ビット)向けのベアメタルのコードを調査した。 中身を読んでみるときちんとベアメタルだった。

Yocto Projectでベアメタル

はじめに

久々にベアメタル熱が上がってきた。 メガマニュアルには下記のような記述があり、baremetalのビルドができそう。

TCLIBC
Specifies the GNU standard C library (libc) variant to use during the build process.

You can select “glibc”, “musl”, “newlib”, or “baremetal”.

OpenEmbedded-Coreにはbaremetal-helloworldというレシピが存在する。 この大本となるリポジトリには下記のようにビルド手順が示されており、上記の機能を使用しているようだ。

$ echo "MACHINE = \"qemuarm64\"" >> ./conf/local.conf
$ echo "TCLIBC = \"baremetal\"" >> ./conf/local.conf

このビルド手順はScarthgap以前(具体的にはdunfell or kirkstoneの時代)のものなので、今回はwrynose環境でビルドしてみる。

環境構築

作業ディレクトリ

~/yocto/wrynoseを作業ディレクトリとする。

$ mkdir -p ~/yocto/wrynose
$ cd ~/yocto/wrynose

bitbakeリポジトリの取得

bitbake-setupを実行するためにbitbakeのリポジトリを取得する。

$ git clone https://git.openembedded.org/bitbake -b yocto-6.0

作業環境を構築する

bitbake-setup initを実行して作業環境(=セットアップ)を作成する。

top-dir-nameを「yocto-builds」に変更する。

setup-dir-nameを「poky-wrynose-baremetal」に設定する。

$ ./bitbake/bin/bitbake-setup \
    --setting default top-dir-name "yocto-builds" \
    init --non-interactive poky-wrynose poky distro/poky machine/qemuarm64 \
    --setup-dir-name poky-wrynose-baremetal

BitBake環境を設定する

init-build-envを実行する。

$ source yocto-builds/poky-wrynose-baremetal/build/init-build-env

MACHINEの切り替え

OE_FRAGMENTSのマシン設定をqemuarmに変更する。

$ bitbake-config-build enable-fragment machine/qemuarm

local.confの修正

conf/local.confに以下の内容を記述する。

TCLIBC = "baremetal"

ビルド

$ bitbake baremetal-helloworld

動作確認

世界に挨拶した。

$ runqemu nographic nonetwork
runqemu - INFO - Running bitbake -e  ...
runqemu - INFO - Continuing with the following parameters:
KERNEL: [/home/mickey/yocto/wrynose/yocto-builds/poky-wrynose-baremetal/build/tmp/deploy/images/qemuarm/baremetal-helloworld-image-qemuarm.bin]
MACHINE: [qemuarm]
FSTYPE: [bin]
ROOTFS: [/home/mickey/yocto/wrynose/yocto-builds/poky-wrynose-baremetal/build/tmp/deploy/images/qemuarm/baremetal-helloworld-image-qemuarm.bin]
Disabled. Changes on rootfs will be kept after QEMU shutdown.
CONFFILE: [/home/mickey/yocto/wrynose/yocto-builds/poky-wrynose-baremetal/build/tmp/deploy/images/qemuarm/baremetal-helloworld-image-qemuarm-20260801075737.qemuboot.conf]

runqemu - INFO - Running /home/mickey/yocto/wrynose/yocto-builds/poky-wrynose-baremetal/build/tmp/work/x86_64-linux/qemu-helper-native/1.0/recipe-sysroot-native/usr/bin/qemu-system-arm -nic none -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0 -drive id=disk0,file=/home/mickey/yocto/wrynose/yocto-builds/poky-wrynose-baremetal/build/tmp/deploy/images/qemuarm/baremetal-helloworld-image-qemuarm.bin,if=none,format=raw -device virtio-blk-device,drive=disk0 -device qemu-xhci -device usb-tablet -device usb-kbd -nographic  -machine virt,highmem=off -cpu cortex-a15 -smp 4 -m 256 -serial mon:stdio -serial null -nographic -device virtio-gpu-pci -kernel /home/mickey/yocto/wrynose/yocto-builds/poky-wrynose-baremetal/build/tmp/deploy/images/qemuarm/baremetal-helloworld-image-qemuarm.bin -append 'root=/dev/vda rw  mem=256M console=ttyAMA0 console=hvc0 swiotlb=0 '

runqemu - INFO - Host uptime: 603285.94

Hello OpenEmbedded on ARM!

レシピ

このレシピはOpenEmbedded-Coreで提供しているQEMU関連のマシン全てに対応している。

実際にレシピを読むとわかるが、マシンでのオーバーライドがたくさん記述されている。

baremetal-helloworld_git.bb

今回はqemuarmのマシンを設定しているので関連する部分だけ抜粋する

... (snip) ...
 7 SRCREV = "db2bf750eaef7fc0832e13ada8291343bbcc3afe"
 8 PV = "0.1+git"
 9 
10 SRC_URI = "git://github.com/ahcbb6/baremetal-helloqemu.git;protocol=https;branch=master"
11 UPSTREAM_CHECK_COMMITS = "1"
12 
13 # The following variables should be set to accomodate each application
14 BAREMETAL_BINNAME ?= "hello_baremetal_${MACHINE}"
15 IMAGE_LINK_NAME ?= "baremetal-helloworld-image-${MACHINE}"
16 IMAGE_NAME_SUFFIX ?= ""
17 
18 # Baremetal-Image creates the proper wiring, assumes the output is provided in
19 # binary and ELF format, installed on ${base_libdir}/firmware/ , we want a
20 # package to be created since we might have some way of updating the baremetal
21 # firmware from Linux
22 inherit baremetal-image
... (snip) ...
27 # These parameters are app specific for this example
28 # This will be translated automatically to the architecture and
29 # machine that QEMU uses on OE, e.g. -machine virt -cpu cortex-a57
30 # but the examples can also be run on other architectures/machines
31 # such as vexpress-a15 by overriding the setting on the machine.conf
32 COMPATIBLE_MACHINE = "qemuarmv5|qemuarm|qemuarm64|qemuriscv64|qemuriscv32|qemux86|qemux86-64"
33 
... (snip) ...
36 BAREMETAL_QEMUARCH:qemuarm = "arm"
... (snip) ...
43 EXTRA_OEMAKE:append = " QEMUARCH=${BAREMETAL_QEMUARCH} V=1"
... (snip) ...
50 # Install binaries on the proper location for baremetal-image to fetch and deploy
51 do_install(){
52     install -d ${D}/${base_libdir}/firmware
53     install -m 755 ${B}/build/hello_baremetal_${BAREMETAL_QEMUARCH}.bin ${D}/${base_libdir}/firmware/${BAREMETAL_BINNAME}.bin
54     install -m 755 ${B}/build/hello_baremetal_${BAREMETAL_QEMUARCH}.elf ${D}/${base_libdir}/firmware/${BAREMETAL_BINNAME}.elf
55 }
56 
57 FILES:${PN} += " \
58     ${base_libdir}/firmware/${BAREMETAL_BINNAME}.bin \
59     ${base_libdir}/firmware/${BAREMETAL_BINNAME}.elf \
60 "
... (snip) ...

重要なポイントにはしっかりとコメントが記述されている。

最も重要なポイントとしては下記。

  • 13〜16行目の変数の定義
  • 22行目のbaremetal-imageクラスの継承

残りの部分はお約束の部分かアップストリームのソースコードの作りに依存する部分となる。

ただし、baremetal-imageクラスではbinとelfのバイナリが出力されるのを期待している。

baremetal-imageクラス

実装の詳細は省くが、やっていることは下記。

  1. binとelfをDEPLOY_DIR_IMAGEにコピー
  2. binを使ってrunqemuできるようにパラメータを調整
  3. imageクラスのタスク構成に準拠するように各種調整(image.bbclassの継承はしていない)

ベアメタルをやっていく上では1の機能が一番重要。 LinuxのカーネルやルートFSを作らずに、OpenEmbedded-Coreの仕掛けの上でbinとelfをDEPLOY_DIR_IMAGEに出力してくれるのは助かる。

まとめ

baremetal-imageクラスを使えばYocto Project環境でもベアメタルができることがわかった。 ツールチェーンを自前で用意しなくても良いのはかなり楽。 bitbakeでベアメタルのbinとelfが出力されるようになるのは私としてはありがたい。

本来の目的であるベアメタル熱のためにbaremetal-helloworldの実際のプログラムについては今後触れる。

Yocto Project Wrynoseのmeta-zephyrでmicro:bit v2のサンプルを動かす

はじめに

Zephyrでmicro:bit v2のサンプルを動かす

シリーズもの

  1. Yocto Project Wrynoseでmeta-zephyrを試す
  2. Yocto Project Wrynoseのmeta-zephyrでmicro:bit v2を動かす

作業環境

前回の環境をベースに作業する。

BitBake環境の設定

作業場所への移動とBitBakeの環境設定を行う

$ cd ~/yocto/wrynose
$ source ./yocto-builds/zephyr-master/build/init-build-env

マシン定義

作業レイヤの作成

meta-zephyr-workを作成する。

$ bitbake-layers create-layer -p 7 -a ../layers/meta-zephyr-work

マシン定義の作成

micro:bit v2は「bbc_microbit_v2」として定義されている。

Yocto Projectの環境ではマシン名は"_"ではなく"-"で区切るので「bbc-microbit-v2」とする。

$ mkdir -p ../layers/meta-zephyr-work/conf/machine

meta-zephyr-work/conf/machine/bbc-microbit-v2.confを以下の内容で作成する。

#@TYPE: Machine
#@NAME: bbc_microbit_v2

#@DESCRIPTION: Machine configuration for BBC micro:bit v2 Board.

require conf/machine/include/nrf52.inc
ZEPHYR_BOARD = "bbc_microbit_v2"

micro:bit v2ではDAPLinkが使用できるので、pyocdなどの設定は特に行わない。

マシンの切り替え

bitbake-setupの環境ではマシンはOE_FRAGMENTで設定するのでこれをbbc-microbit-v2に切り替える。

$ bitbake-config-build enable-fragment machine/bbc-microbit-v2

変更できたかの確認は下記のようにする。

$ bitbake-config-build list-fragments
... (snip) ...
Enabled built-in fragments:
distro/zephyr   Sets DISTRO = "zephyr"
machine/bbc-microbit-v2 Sets MACHINE = "bbc-microbit-v2"
... (snip) ...

sourdのレシピの作成

$ mkdir -p ../layers/meta-zephyr-work/recipes-kernel/zephyr-kernel
$ touch ../layers/meta-zephyr-work/recipes-kernel/zephyr-kernel/zephyr-microbit-sound.bb

../layers/meta-zephyr-work/recipes-kernel/zephyr-kernel/zephyr-microbit-sound.bbを以下の内容で作成する。

inherit zephyr-sample

ZEPHYR_SRC_DIR = "${ZEPHYR_BASE}//samples/boards/bbc/microbit/sound"

do_configure:prepend:bbc-microbit-v2() {
    bbnote "Injecting custom Kconfig options"
    echo "CONFIG_ENTROPY_GENERATOR=y" >> ${ZEPHYR_SRC_DIR}/prj.conf
}

pongのレシピの作成

$ mkdir -p ../layers/meta-zephyr-work/recipes-kernel/zephyr-kernel
$ touch ../layers/meta-zephyr-work/recipes-kernel/zephyr-kernel/zephyr-microbit-pong.bb

../layers/meta-zephyr-work/recipes-kernel/zephyr-kernel/zephyr-microbit-pong.bbを以下の内容で作成する。

inherit zephyr-sample

ZEPHYR_SRC_DIR = "${ZEPHYR_BASE}//samples/boards/bbc/microbit/pong"

ビルド

作成したレシピをビルドする。これはsoundの例

$ bitbake zephyr-microbit-sound

書き込み

Ubuntu 26.04ではこうなる。

cp ./tmp/deploy/images/bbc-microbit-v2/zephyer-microbit-sound-bbc-microbit-v2.bin /run/media/${USER}/MICROBIT/

まとめ

micro:bit v2のsoundのサンプルは簡単に動かせた。 pongの様なゲームとかも動かしてみると楽しみが増える。

今度はZephyrのソースツリーの外のアプリもトライする。