みつきんのメモ

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

RISC-Vのベアメタル入門(自分用) UART編

はじめに

前回動かした環境でUARTを動かしてみる。

具体的には、またdwelch67/sifive_samplesを写経する。

UARTのレジスタ構成

SiFive FE310-G000 Manual v2p3から抜粋する。

f:id:mickey_happygolucky:20190304134208p:plain
UARTレジスタ

必要なレジスタは7つ。

注意する点としては、txdataのところにこんな一文がある。

Reading from txdata returns the current value of the full flag and zero in the data field. 
The full flag indicates whether the transmit FIFO is able to accept new entries; when set, writes to data are ignored.
A RISC-V amoswap instruction can be used to both read the full status and attempt to enqueue data,
with a non-zero return value indicating the character was not accepted.

どうやらtxdataの操作にはAtomic命令のamoswapを使用する必要があるらしい。

ピンファンクションの設定

UARTを有効化するには、ピンファンクションを設定する必要がある。

SiFive FE310-G000 Preliminary Datasheet v1p5のGPIOピンアサインを抜粋。

f:id:mickey_happygolucky:20190304134120p:plain
GPIOピンアサイ

GPIO16にUART0_RXが,GPIO17にUART0_TXが割り当たっているらしい。

ここでは、次のようにしている。

    ra=GET32(GPIO_IOF_SEL);
    ra&=~(1<<16); //UART0_RX
    ra&=~(1<<17); //UART0_TX
    PUT32(GPIO_IOF_SEL,ra);
    ra=GET32(GPIO_IOF_EN);
    ra|=1<<16; //UART0_RX
    ra|=1<<17; //UART0_TX
    PUT32(GPIO_IOF_EN,ra);

iof_selの16と17を落としてiof_enの16と17を立てているのは分かるんだが、このレジスタの仕様が全然見つからない。。。

レジスタのビットと機能の対応はどこで分かるんだろう。

サンプルプログラム

dwelch67/sifive_samplesのUART0を元にhello worldを作る。

Makefileとmemmapは前回と同じ。

novectors.s

    .globl _start
_start:
    lui x2, 0x80004 /* set sp to 0x80004 */
    jal notmain /* call notmain */
    j .

    .globl dummy
dummy:
    ret

    .globl PUT32
PUT32:
    sw x11, (x10)
    ret

    .globl GET32
GET32:
    lw x10, (x10)
    ret

    .globl AMOSWAP
AMOSWAP:
    amoswap.w x10, x11, (x10) /* ret = *a0; *a0 = a1 */
    ret

基本は前回と同じだが、txdata操作用にAMOSWAPの実装を追加。

notmain.c

uart0_divには、以前QEMUでHiFive1のプログラムを動かした時に、core freq at 11644342 Hzと表示されたので、ざっくりと11644342/115200=101として101を設定している。

void PUT32(unsigned int, unsigned int);
unsigned int GET32(unsigned int);
void dummy(unsigned int);
unsigned int AMOSWAP(unsigned int, unsigned int);

#define GPIOBASE 0x10012000
#define GPIO_VALUE      (GPIOBASE+0x00)
#define GPIO_INPUT_EN   (GPIOBASE+0x04)
#define GPIO_OUTPUT_EN  (GPIOBASE+0x08)
#define GPIO_PORT       (GPIOBASE+0x0C)
#define GPIO_PUE        (GPIOBASE+0x10)
#define GPIO_OUT_XOR    (GPIOBASE+0x40)
#define GPIO_IOF_EN     (GPIOBASE+0x38)
#define GPIO_IOF_SEL    (GPIOBASE+0x3C)

#define UART0BASE 0x10013000
#define UART0_TXDATA    (UART0BASE+0x00)
#define UART0_RXDATA    (UART0BASE+0x04)
#define UART0_TXCTRL    (UART0BASE+0x08)
#define UART0_RXCTRL    (UART0BASE+0x0C)
#define UART0_IE        (UART0BASE+0x10)
#define UART0_IP        (UART0BASE+0x14)
#define UART0_DIV       (UART0BASE+0x18)

#define MTIME 0x0200BFF8

void putc(char c)
{
    /* wait if queue is full. */
    while (1) {
        if ((GET32(UART0_TXDATA)&0x80000000) == 0)
            break;
    }
    AMOSWAP(UART0_TXDATA, c);
}

int notmain(void)
{
    unsigned int ra;

    //11644342/115200 = 101
    ra = GET32(GPIO_IOF_SEL);
    ra &= ~(1<<16); //UART0_RX
    ra &= ~(1<<17); //UART0_TX
    PUT32(GPIO_IOF_SEL, ra);

    ra = GET32(GPIO_IOF_EN);
    ra |= (1<<16); //UART0_RX
    ra |= (1<<17); //UART0_TX

    PUT32(UART0_DIV, 101-1);
    PUT32(UART0_TXCTRL, 0x00000003); //txen=1, nstop=1

    putc('h');
    putc('e');
    putc('l');
    putc('l');
    putc('o');
    putc(' ');
    putc('w');
    putc('o');
    putc('r');
    putc('l');
    putc('d');
    putc('\n');
    return 0;
}

華麗なるベタ書き。putc()をひたすらに並べる。

QEMUで実行

$ qemu-system-riscv32 -nographic -machine sifive_e -kernel ./notmain.elf
hello world

無事に表示された。

まとめ

UART送信は、GPIOのピンファンクションの設定を除けば、txctrlとdivを設定するだけで使えるので非常に簡単。

txdataの操作にamoswapを使用するのも注意点か。 でも、uart02uart03の実装を見るに、amoswapは必須ではないのか?

参考資料

SiFive FE310-G000 Preliminary Datasheet v1p5

SiFive E300 Platform Reference Manual

RISC-Vのベアメタル入門(自分用)

はじめに

PlatformIOなどを使用すれば、なんちゃってベアメタルプログラミングは簡単に始められるが、 そこに至るまでの知識が全く足りていないため少しだけ掘り下げてみる。

今回のターゲットはSiFiveのHiFive1(のエミュレータ)。

ツールチェイン

HiFive1に合わせてRV32IMACのツールチェインを作成する。

$ mkdir -p ~/bin/riscv
$ cd ~/bin/riscv
$ sudo apt-get install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc 
$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
$ cd riscv-gnu-toolchain
$ ./configure --prefix=${HOME}/bin/riscv --with-arch=rv32imac
$ make -j $(nproc)

手動でPATHを通すのでmake installはしない。

.bashrcに下記を追加

PATH="${HOME}/bin/riscv/bin:${PATH}"

メモリマップ

SiFive FE310-G000 Manual v2p3からメモリマップの部分を抜粋する。

f:id:mickey_happygolucky:20190303164944p:plain

ブートシーケンス

SiFive HiFive1 Getting Started Guide 1.0.2HiFive1 Boot Sequenceによると、ざっくりと次のようになっている。

Board is shipped with boot loader at the beginning of SPI Flash(0x20000000)
The core jumps to the main user portion of code at 0x20400000

HiFive1はブートローダが書き込み済みの状態で出荷され、それはは0x20000000〜0x203FFFFFの間に書き込まれているらしい事がわかる。

ユーザーがプログラムを書き込むのは0x20400000からということらしい。

サンプルプログラム

ベアメタルの基本的な知識が圧倒的に足りないため何かを写経する。

dwelch67/sifive_samplesが良さそう。

blinker00(flash版)

blinkerとはなっているけど特に何かチカチカするプログラムではないと思う。

memmap(リンカスクリプト)

メモリをフラッシュとRAMに分けてセクションを切っている。

MEMORY
{
    rom : ORIGIN = 0x20400000, LENGTH = 0x4000
    ram : ORIGIN = 0x80000000, LENGTH = 0x4000
}

SECTIONS
{
    .text : {*(.text*)} > rom
    .rodata : {*(.rodata*)} > rom
    .bss : {*(.bss*)} > ram
}

プログラムの書き込み先は0x20400000。

novectors.s

エントリとなる_startではスタックポインタの初期化しかしていない。 その後いきなりC言語の関数を呼び出している。

    .globl _start
_start:
    lui x2, 0x80004 /* set sp to 0x80004 */
    jal notmain /* call notmain(jump and link) */
    j . /* infinit loop */

    .globl dummy
dummy:
    ret

    .globl PUT32
PUT32:
    sw x11,(x10) /* *a0 = a1(store word) */
    ret

    .globl GET32
GET32:
    lw x10,(x10) /* ret = *(a0) */
    ret

ちなみにコメントは追加した。

notmain.c

C言語のエントリポイント。 0x800010000x800020000x80003000に値を書き込んでいる。

void PUT32(unsigned int, unsigned int);
unsigned int GET32(unsigned int);
void dummy(unsigned int);

int notmain(void)
{
    unsigned int ra;

    for (ra = 0; ra < 20; ra+=4)
        PUT32(0x80001000+ra, ra);
    PUT32(0x80002000, 0x12345678);
    PUT32(0x80003000, GET32(0x80002000));
    return 0;
}

Makefile

QEMUで実行する場合、elfを使用するためbin形式への変換は不要。

RISCVGNU = riscv32-unknown-elf

AOPS = -march=rv32imac -g
COPS = -march=rv32imac -Wall -O2 -nostdlib -nostartfiles -ffreestanding -g

all : notmain.bin

clean :
  rm -f *.o
  rm -f *.elf
  rm -f *.bin
  rm -f *.list

novectors.o : novectors.s
  $(RISCVGNU)-as $(AOPS) novectors.s -o novectors.o

notmain.o : notmain.c
  $(RISCVGNU)-gcc $(COPS) -c notmain.c -o notmain.o

notmain.bin : memmap novectors.o notmain.o
  $(RISCVGNU)-ld novectors.o notmain.o -T memmap -o notmain.elf
  $(RISCVGNU)-objdump -D notmain.elf > notmain.list
  $(RISCVGNU)-objcopy notmain.elf -O binary notmain.bin

QEMUで実行

HiFive1がないのでQEMUで実行する。

バイナリの実行

$ qemu-system-riscv32 -nographic -machine sifive_e -kernel ./notmain.elf

GDB

$ qemu-system-riscv32 -nographic -machine sifive_e -kernel ./notmain.elf -S -s

-SGDBコマンドまでプログラムを実行状態にしない。

-s-gdb tcp::1234と同じ。

gdbgui

$ gdbgui -g riscv32-unknown-elf-gdb notmain.elf

デバッグコマンド

target remote localhost:1234

ここからgdbguiでデバッグが可能。CPUの先頭で止まっているのでデバッグし放題となる。

まとめ

写経だいじ。

参考資料

自分の備忘録も兼ねてお世話になったところをまとめておく。

SiFive HiFive1 Getting Started Guide

SiFive FE310-G000 Manual v2p3

RISC-Vのアセンブリ記述

RISC-V Assembly Programmer's Manual

QEMUにGDBを繋げてhariboteOSをデバッグする方法

RISC-V QEMU

はじめに

RISC-VのQEMUを動かしてみる。

依存パッケージのインストール

$ sudo apt-get install gcc libc6-dev pkg-config bridge-utils uml-utilities zlib1g-dev libglib2.0-dev autoconf automake libtool libsdl1.2-dev

ビルド

ここを参考にビルド

$ git clone --recursive https://github.com/riscv/riscv-qemu.git
$ cd riscv-qemu
$ ./configure --target-list=riscv64-softmmu,riscv32-softmmu
$ make -j$(nproc)
$ sudo make install

HiFive1のバイナリを実行してみる

platformio

platformioでHiFive1向けのバイナリを作成する。

$ platformio init --board freedom-e300-hifive1

src/hello.cを次の内容で作成する。

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("hello world\n");
    return 0;
}

次のコマンドでビルド

$ platformio run

.pioenvs/freedom-e300-hifive1/firmware.elfがビルドされたバイナリ。

これを実行してみる。

$ qemu-system-riscv32 -nographic -machine sifive_e -kernel .pioenvs/freedom-e300-hifive1/firmware.elf
core freq at 11644342 Hz
hello world
Progam has exited with code:0x00000000

Ctrl+a xqemuを終了。

STM32F4Discovery NuttXでC++(libc++)をためす

はじめに

make menuconfigで見てみると、NuttXでもlibc++uClibc++などが使えるようになっているらしい。 ただ、ライブラリのソースはカーネルやアプリからは分離されているようで、別途取得する必要がある。

具体的なやり方はAlan C. Assisさんが紹介してくれているのでなぞってみる。

コミットログを見るにこの人はコアなコントリビュータっぽい。

ソースの取得

NuttX

$ git clone https://bitbucket.org/nuttx/nuttx

Apps

$ git clone https://bitbucket.org/nuttx/apps

libc++

$ git clone https://bitbucket.org/acassis/libcxx

libc++のインストール

libc++のソースをNuttXのソースツリーにインストールする。

$ cd libcxx
$ ./install.sh ../nuttx
Installing LLVM/libcxx in the NuttX source tree
Installation suceeded

./install.shの中身

基本的にはNuttXのソースツリーの構造をチェックして、ファイルをコピーしているだけっぽい。 ここではチェック部分を省略したが、実際はかなり細かくチェックしている。

#!/bin/bash

usage="USAGE: $0 <full path to the NuttX directory>"

# Get the single, required command line argument

nuttx_path=$1
if [ -z "${nuttx_path}" ]; then
  echo "ERROR: Missing path to the NuttX directory"
  echo $usage
  exit 1
fi

# Lots of sanity checking so that we do not do anything too stupid

if [ ! -d src ]; then
  echo "ERROR: Directory src/ does not exist in this directory"
  echo "       Please CD into the libcxx directory and try again"
  echo $usage
  exit 1
fi

...(snip)...

libxx_srcdir=${nuttx_path}/libs/libxx

if [ ! -d "${libxx_srcdir}" ]; then
  echo "ERROR: Directory ${libxx_srcdir} does not exist"
  echo $usage
  exit 1
fi

...(snip)...

libcxx_incdir=${nuttx_incdir}/libcxx

if [ -d "${libcxx_incdir}" ]; then
  echo "ERROR: Directory ${libcxx_incdir} already exists"
  echo "       Please remove the  ${libcxx_incdir} directory and try again"
  echo $usage
  exit 1
fi

...(snip)...

# Installation

echo "Installing LLVM/libcxx in the NuttX source tree"

filelist=`find src -type f`

for file in $filelist; do
  if [ "${file##*.}" = "cpp" ]; then
    endfile="`basename ${file} .cpp`.cxx"
  else
    endfile=${file}
  fi
  install -D ${file} ${libxx_srcdir}/libcxx/${endfile#src/}
done

mkdir -p ${libcxx_incdir}

filelist=`find include -type f`

for file in $filelist; do
  install -D ${file} ${nuttx_path}/include/libcxx/${file#include/}
done

filelist=`find machine -type f`

for file in $filelist; do
  install -D ${file} ${nuttx_path}/include/${file}
done

echo "Installation suceeded"
echo ""

拡張子cppをすべてcxxに変換し、NuttXのlibs/libxx/libcxxにコピー。

include以下のファイルはそのままNuttXのinclude/libcxxにコピー。

machine以下のファイルもそのままで、NuttXのincludeにコピーしている。

コンフィグレーションとビルド

今回はstm32f4discovery/testlibcxxを使用する。

元記事ではこの前にいくつかパッチを適用しているが、今回の手順で試す場合は必要なさそうだった。

$ ./tools/configure.sh ./configs/stm32f4discovery/testlibcxx
$ make -j4

動作確認

出来上がったnuttx.binを書き込む。

$ st-flash write nuttx.bin 0x8000000

ボードをリセットするとnshが起動する。 helpコマンドを実行するとhelloxxが見つかるので実行する。

nsh>helloxx
helloxx_main: Saying hello from the dynamically constructed instance
CHelloWorld::HelloWorld: Hello, World!!
helloxx_main: Saying hello from the instance constructed on the stack
CHelloWorld::HelloWorld: Hello, World!!
helloxx_main: Saying hello from the statically constructed instance
CHelloWorld::HelloWorld: Hello, World!!

ちゃんと動いている。

C++11

例えば、ラムダ式や一様初期化 (uniform initialization)とかは動くのか。

helloxxのソースを変更して試してみる。

diff --git a/examples/helloxx/helloxx_main.cxx b/examples/helloxx/helloxx_main.cxx
index 4be4bf6b..3bc4865b 100644
--- a/examples/helloxx/helloxx_main.cxx
+++ b/examples/helloxx/helloxx_main.cxx
@@ -43,6 +43,9 @@
 #include <debug.h>
 
 #include <nuttx/init.h>
+#include <iostream>
+#include <vector>
+#include <algorithm>
 
 #include "platform/cxxinitialize.h"
 
@@ -161,6 +164,8 @@ extern "C"
     printf("helloxx_main: Saying hello from the statically constructed instance\n");
     g_HelloWorld.HelloWorld();
 #endif
+    std::vector<char> vec{'h','e','l','l','o',' ','w','o','r','l', 'd', '\n'};
+    std::for_each(vec.begin(), vec.end(), [](char c){std::cout << c;});
 
     delete pHelloWorld;
     return 0;

再度makeコマンドでnuttx.binを作り、helloxxを実行する。

nsh>helloxx
helloxx_main: Saying hello from the dynamically constructed instance
CHelloWorld::HelloWorld: Hello, World!!
helloxx_main: Saying hello from the instance constructed on the stack
CHelloWorld::HelloWorld: Hello, World!!
helloxx_main: Saying hello from the statically constructed instance
CHelloWorld::HelloWorld: Hello, World!!
hello world
nsh>

最後にhello worldが追加されている。 ちゃんと動くようだ。

AndroidStudioでTensorflowデモを動かす。

はじめに

TensorflowのAndroidのデモを動かそうとしたところ、 いろいろハマった上にまとまった情報がなかったのでメモっておく。

ソースの取得

まず--recurse-submodules忘れた。これは完全に自分のミス。

$ git clone --recurse-submodules https://github.com/tensorflow/tensorflow.git

修正点

手で修正したファイルは次のとおり。

  • tensorflow/examples/android/AndroidManifest.xml
  • tensorflow/examples/android/build.gradle

AndroidManifest.xml

基本的にはAndroidStudioが色々やってくれるのでそれに従う。

diff --git a/tensorflow/examples/android/AndroidManifest.xml b/tensorflow/examples/android/AndroidManifest.xml
index 5c47ce6b67..bea3683102 100644
--- a/tensorflow/examples/android/AndroidManifest.xml
+++ b/tensorflow/examples/android/AndroidManifest.xml
@@ -16,23 +16,29 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="org.tensorflow.demo">
-
     <uses-permission android:name="android.permission.CAMERA" />
-    <uses-feature android:name="android.hardware.camera" />
-    <uses-feature android:name="android.hardware.camera.autofocus" />
+    <uses-feature android:name="android.hardware.camera"/>
+    <uses-feature android:name="android.hardware.camera.autofocus"/>
+    <uses-feature android:name="android.software.leanback"/>
+    <uses-feature android:name="android.hardware.touchscreen"/>
+
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
 
     <uses-sdk
         android:minSdkVersion="21"
-        android:targetSdkVersion="23" />
+        android:targetSdkVersion="27" />
 
-    <application android:allowBackup="true"
+    <application
+        android:hardwareAccelerated="true"
+        android:allowBackup="true"
         android:debuggable="true"
         android:label="@string/app_name"
         android:icon="@drawable/ic_launcher"
-        android:theme="@style/MaterialTheme">
+        android:theme="@style/MaterialTheme"
+        tools:ignore="GoogleAppIndexingWarning,HardcodedDebugMode,MissingTvBanner">
 
         <activity android:name="org.tensorflow.demo.ClassifierActivity"
             android:screenOrientation="portrait"

一番ハマったのはandroid:hardwareAccelerated="true"

手持ちのスマートフォンのカメラがしょぼく、CameraAPI2が使用できなかったためレガシーにフォールバックするのだが、 これがハードウェアアクセラレーションを有効化していないと、画面が真っ黒になってしまうという問題がある。

TextureViewでonSurfaceTextureAvailableが呼ばれないことに気づいたのでわかった。

build.gradle

diff --git a/tensorflow/examples/android/build.gradle b/tensorflow/examples/android/build.gradle
index 0767726aa9..20c4e4df9b 100644
--- a/tensorflow/examples/android/build.gradle
+++ b/tensorflow/examples/android/build.gradle
@@ -24,23 +24,25 @@ getProject().setBuildDir('gradleBuild')
 
 buildscript {
     repositories {
+        google()
         jcenter()
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.0.1'
+        classpath 'com.android.tools.build:gradle:3.3.1'
         classpath 'org.apache.httpcomponents:httpclient:4.5.4'
     }
 }
 
 allprojects {
     repositories {
+        google()
         jcenter()
     }
 }
 
 // set to 'bazel', 'cmake', 'makefile', 'none'
-def nativeBuildSystem = 'bazel'
+def nativeBuildSystem = 'none'
 
 // Controls output directory in APK and CPU type for Bazel builds.
 // NOTE: Does not affect the Makefile build target API (yet), which currently
@@ -76,14 +78,14 @@ project.ext.TMP_DIR   = project.buildDir.toString() + '/downloads'
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 23
-    buildToolsVersion '26.0.2'
+    compileSdkVersion 27
+
 
     if (nativeBuildSystem == 'cmake') {
         defaultConfig {
             applicationId = 'org.tensorflow.demo'
             minSdkVersion 21
-            targetSdkVersion 23
+            targetSdkVersion 27
             ndk {
                 abiFilters "${cpuType}"
             }
@@ -189,6 +191,6 @@ apply from: "download-models.gradle"
 
 dependencies {
     if (nativeBuildSystem == 'cmake' || nativeBuildSystem == 'none') {
-        compile 'org.tensorflow:tensorflow-android:+'
+        implementation 'org.tensorflow:tensorflow-android:+'
     }
 }

ポイントとなる修正点は次のとおり。

  1. リポジトリへのgoogleの追加(2ヶ所)
  2. def nativeBuildSystem = 'none'
  3. implementation 'org.tensorflow:tensorflow-android:+'

これらの修正を行わないとビルドが通らなかった。

APKのインストールに失敗してアプリが開始できない

アプリやデバッグを開始しようとすると次のメッセージがでてアプリが開始できない。 APKのインストールで失敗するようだった。

java.io.IOException: 接続が相手からリセットされました
Error while Installing APK

たぶん、ここDisable Instant Runが効いたと思う。

Disable Instant Run (Android Document)

To disable Instant Run:

1. Open the Settings or Preferences dialog. (For Mac, Android Studio -> Preferences)
2. Navigate to Build, Execution, Deployment > Instant Run.
3. Uncheck the box next to Enable Instant Run.

ただ、この操作をしてもしばらくうまく行かなくて、.gradle.ideaを削除して、AndroidStudioを再起動したらうまくいった記憶がある。(既に曖昧)

Yoctoターゲットのセルフコンパイル環境でカーネルモジュールをビルド

はじめに

Yoctoのセルフコンパイル環境でout-of-treeのカーネルモジュールをビルドする。

普通にビルドしようとエラーが発生するのでその回避方法をメモしておく。

hello-mod

今回はrecipe-skeleton/recipes-kernelにあるhello-modラズベリーパイ3上でビルドする。

Yoctoのイメージ

今回使用するセルフコンパイル環境を作成するには、local.confに次の内容を追加する。

# for raspberrypi3
MACHINE = "raspberrypi3"
DL_DIR ?= "${TOPDIR}/../downloads"

# enable uart
ENABLE_UART = "1"

# enable self-compile
EXTRA_IMAGE_FEATURES_append = " dev-pkgs tools-debug tools-sdk"

# add kernel soruce
IMAGE_INSTALL_append = "kernel-devsrc"

カーネルソースを含むパッケージkernel-devsrcを追加する。

作成するモジュール

~/helloにソースとMakefileを作成する。

$ mkdir ~/hello
$ cd hello

~/hello/hello.cを次の内容で作成する。

#include <linux/module.h>

int init_module(void)
{
    printk("Hello World!\n");
    return 0;
}

void cleanup_module(void)
{
    printk("Goodbye Cruel World!\n");
}

MODULE_LICENSE("GPL");

~/hello/Makefileを次の内容で作成する。

obj-m := hello.o

SRC := $(shell pwd)

all:
    $(MAKE) -C $(KERNEL_SRC) M=$(SRC)

modules_install:
    $(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install

clean:
    rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c
    rm -f Module.markers Module.symvers modules.order
    rm -rf .tmp_versions Modules.symvers

このMakefileではKERNEL_SRCを変数にしているので、make実行に次のようにするか

# cd ~/hello
# make KERNEL_SRC=/lib/modules/`uname -r`/build

予め環境変数に登録しておく。

# export KERNEL_SRC=/lib/modules/`uname -r`/build
# make

発生するエラーの内容

初期状態でモジュールをビルドすると次のようなエラーが発生する。

# make KERNEL_SRC=/lib/modules/`uname -r`/build
make -C /lib/modules/4.14.79/build M=/home/root/hello
make[1]: Entering directory '/lib/modules/4.14.79/build'
make[1]: Warning: File 'scripts/Makefile.ubsan' has modification time 601 s in the future
make[2]: Warning: File 'scripts/Makefile.lib' has modification time 601 s in the future
  AR      /home/root/hello/built-in.o
  CC [M]  /home/root/hello/hello.o
In file included from ./include/asm-generic/int-ll64.h:11,
                 from ./arch/arm/include/uapi/asm/types.h:5,
                 from ./include/uapi/linux/types.h:5,
                 from ./include/linux/types.h:6,
                 from ./include/linux/list.h:5,
                 from ./include/linux/module.h:9,
                 from /home/root/hello/hello.c:1:
./include/uapi/asm-generic/int-ll64.h:12:10: fatal error: asm/bitsperlong.h: No such file or directory
 #include <asm/bitsperlong.h>
          ^~~~~~~~~~~~~~~~~~~
compilation terminated.
make[2]: *** [scripts/Makefile.build:335: /home/root/hello/hello.o] Error 1
make[1]: *** [Makefile:1527: _module_/home/root/hello] Error 2
make[1]: Leaving directory '/lib/modules/4.14.79/build'
make: *** [Makefile:6: all] Error 2

fatal error: asm/bitsperlong.h: No such file or directorybitsperlong.hが見つからないというエラーが出る。

エラー回避の方法

まずは、ラズベリーパイの時刻が狂っているので現在時刻に設定する。

$ date -s "MM/DD hh:mm YYYY"

日付を合わせておかないとwarning: Clock skew detected. Your build may be incomplete.という警告が出る。 実害はなさそうだが気持ち悪いので対策しておく。

次にカーネルソースのscriptsをビルドする。

$ cd /lib/modules/`uname -r`/build
$ make scripts

bitsperlong.hのエラーはこちらで対策できる。

再びビルド

再度モジュールをビルドしてみる。

# cd ~/hello
# make KERNEL_SRC=/lib/modules/`uname -r`/build
make -C /lib/modules/4.14.79/build M=/home/root/hello
make[1]: Entering directory '/lib/modules/4.14.79/build'
  AR      /home/root/hello/built-in.o
  CC [M]  /home/root/hello/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/root/hello/hello.mod.o
  LD [M]  /home/root/hello/hello.ko
make[1]: Leaving directory '/lib/modules/4.14.79/build'

hello.koが生成される。

ほかのヘッダファイルでエラー

たとえば、hello.cの先頭の方にに次の行を追加する。

#include <linux/module.h>
#include <linux/unistd.h>

...(snip)...

すると、makeで次のエラーが発生する。

# make KERNEL_SRC=/lib/modules/`uname -r`/build
make -C /lib/modules/4.14.79/build M=/home/root/hello
make[1]: Entering directory '/lib/modules/4.14.79/build'
  CC [M]  /home/root/hello/hello.o
In file included from ./arch/arm/include/asm/unistd.h:16,
                 from ./include/uapi/linux/unistd.h:8,
                 from /home/root/hello/hello.c:2:
./arch/arm/include/uapi/asm/unistd.h:21:10: fatal error: asm/unistd-eabi.h: No such file or directory
 #include <asm/unistd-eabi.h>
          ^~~~~~~~~~~~~~~~~~~
compilation terminated.
make[2]: *** [scripts/Makefile.build:335: /home/root/hello/hello.o] Error 1
make[1]: *** [Makefile:1527: _module_/home/root/hello] Error 2
make[1]: Leaving directory '/lib/modules/4.14.79/build'
make: *** [Makefile:6: all] Error 2

これは次のコマンドで回避できる。

# cd /lib/modules/`uname -r`/build
# make prepare

最終的な回避策

Yoctoで作成したセルフコンパイル環境でout-of-treeのカーネルモジュールをビルドするためには、 カーネルソースで予めscriptsprepareをmakeしておく必要がある。

# cd /lib/modules/`uname -r`/build
# make scripts
# make prepare

hello.cに次のようなincludeを追加したが、とりあえずのところエラーは発生していない。

#include <linux/module.h>
#include <linux/unistd.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <asm/uaccess.h>
#include <asm/io.h>

...(snip)...

PlatformIOでSTM32F4Discoveryのプログラムを書く

はじめに

組み込み向けの開発環境を構築する手段としてPlatformIOというものがある。

PlatformIOはPythonスクリプトで、必要に応じてクロスコンパイル用のツールチェインなどダウンロードしてくれたり、 それぞれのボード向けのスタートアップコードなんかも提供してくれる。

またVSCodeAtomなどのIDEとも連携でき、IDE上でデバッグをすることもできる。

CLionとの連携もできるがこちらはIDE上でのデバッグには対応していない様子だった。

インストール

PlatformIOはpipでインストールできる。

$ sudo pip install platformio

STM32F4Discovery向けの環境を構築

まず、次のコマンドでPlatformIOがSTM32F4Discoveryに対応しているかを確認する。

$ platformio boards | grep -i 'stm32f4disco'
disco_f407vg          STM32F407VGT6  168MHz    1MB     192KB  ST STM32F4DISCOVERY

disco_f407vgという名前で使えるらしいので、環境をつくってみる。

$ mkdir stm32f4disco_pio
$ cd stm32f4disco_pio
$ platformio init --board disco_f407vg

作成されたもの

プロジェクトを作成すると次のものが作成されます。

生成物 概要
platformio.ini プロジェクト設定ファイル
src ソースを格納するディレクト
lib プロジェクトして使用する(プライベートな)ライブラリを格納するディレクト

とりあえず何か動かしてみる

とりあえずsrc/hello.cppを次の内容で作成する。

#include <Arduino.h>

void setup() {
    Serial.begin(115200);
}

void loop() {
    Serial.print("Hello world\r\n");
    delay(1000);
}

ST-LINK経由で書き込むので、platformio.iniを次のようにする。

[env:disco_f407vg]
platform = ststm32
board = disco_f407vg
framework = arduino

upload_protocol = stlink

次のコマンドでプログラムをボードに書き込む。

$ platformio run --target upload

UARTの出力を確認

STM32F4DiscoveryとPCをUSB-TTL変換ケーブルで接続する。

ケーブル側 STM32F4Discovery
5V(赤) 5V
GND(黒) GND
TXD(緑) PA10(RX1)
RXD(白) PA9(TX1)

Ubuntuでは/dev/ttyUSB0として見えるのでminicomなどで開く。

ボードのリセットボタンを押した後、こんな感じで1秒ごとに表示されていればOK。

Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world