Moiz's journal

プログラミングやFPGAなどの技術系の趣味に関するブログです

ゼロから作るRAW現像その1 - 基本的な処理

追記

このブログ記事「ゼロから作るRAW現像」を大きく再構成してより読みやすくした書籍「PythonとColabでできる-ゼロから作るRAW現像」を【技術書典6】にて頒布しました。

現在はBOOTHにて入手可能です。書籍+PDF版は2200円プラス送料、PDF版は1200円です。

moiz.booth.pm

はじめに

会社の同僚にrawpyというPython用のライブラリの存在を教えてもらいました。 これを使うと各種デジカメのRAWファイルから、BayerのRAWデータを抽出できます。以前はDCRAWのソースコードを改造してrawデータのダンプなどしていたのですが、ずいぶん良い時代になったものです。 せっかくなのでrawpyで抽出したBayerフォーマット画像データから、普通の画像ビューワーなどで表示できる画像ファイルをできるだけスクラッチから作成して見たいと思います。

今回の内容はJupyter Notebook ファイルとしてgithubにアップロードしてあります。RAWデータNotebook ファイルをダウンロードしてJupyter Notebookから開くことで同様の処理が行なえます

RAWファイルおよびRAWデータについて

RAWファイルやRAWデータというのは厳密な定義はないのですが、カメラ処理でRAWというとBayerフォーマットの画像データを指すことが多いようです。したがって多くの場合、RAWデータはBayerフォーマットの画像データ、RAWファイルはそのRAWデータを含んだファイルということになります。

まず前提として、今使われているカメラの画像センサーの殆どはBayer配列というものを使ってフルカラーを実現しています。

画像センサーは、碁盤の目状にならんだ小さな光センサーの集まりでできています。一つ一つの光センサーはそのままでは色の違いを認識できません。そこで色を認識するためには、3原色のうち一色を選択して光センサーにあてて、その光の強度を測定する必要があります。 方法としてはまず、分光器を使って光を赤、青、緑に分解して、3つの画像センサーにあてて、それぞれの色の画像を認識し、その後その3枚をあわせることでフルカラーの画像を合成するという方法がありました。これは3板方式などとよばれることがあります。この方法は手法的にもわかりやすく、また、余計な処理が含まれないためフルカラーの画像がきれい、といった特徴があり高級ビデオカメラなどで採用されていました。欠点としては分光器と3つの画像センサーを搭載するためにサイズが大きくなるという点があります。

これに対して、画像センサー上の一つ一つの光センサーの上に、一部の波長の光だけを通す色フィルターを載せ、各画素が異なる色を取り込むという方法もあります。この方法では1枚の画像センサーでフルカラー画像を取り込めるため、3板方式に対して単板方式とよばれることもあります。3版方式とくらべた利点としては分光器が不要で1枚のセンサーで済むのでサイズが小さい。逆に欠点としては、1画素につき1色の情報しか無いので、フルカラーの画像を再現するには画像処理が必要になる、という点があります。

単版方式の画像センサーの上に載る色フィルターの種類としては、3原色を通す原色フィルターと、3原色の補色(シアン・マゼンダ・イエロー)を通す補色フィルター1というものがあります。補色フィルターは光の透過率が高いためより明るい画像を得ることができます。それに対して原色フィルターは色の再現度にすぐれています。Bayer配列はこの単版原色フィルター方式のうち最もポピュラーなものです。

こういうわけでBayer配列の画像センサーの出力では1画素につき一色しか情報をもちません。Bayer配列のカラーフィルターはこの図左のように、2x2ブロックの中に、赤が1画素、青が1画素、緑が2画素ならぶようになっています。緑は対角線上にならびます。緑が2画素あるのは、可視光の中でも最も強い光の緑色を使うことで解像度を稼ぐため、という解釈がなされています。Bayerというのはこの配列の発明者の名前です。

f:id:uzusayuu:20180923132843p:plain

カメラ用センサーでは2000年代初頭までは、補色フィルターや3板方式もそれなりの割合で使われていたのですが、センサーの性能向上やカメラの小型化と高画質化の流れの中でほとんどがBayer方式にかわりました。今では、SigmaのFoveonのような意欲的な例外を除くと、DSLRやスマートフォンで使われているカラー画像センサーの殆どがBayer方式を採用しています。したがって、ほとんどのカメラの中ではBayerフォーマットの画像データをセンサーから受取り、フルカラーの画像に変換するという処理が行われている、ということになります。

こういったBayerフォーマットの画像ファイルは、すなわちセンサーの出力に近いところで出力されたことになり、カメラが処理したJPEGに比べて以下のような利点があります。

  • ビット数が多い(RGBは通常8ビット。Bayerは10ビットから12ビットが普通。さらに多いものもある)
  • 信号が線形(ガンマ補正などがされていない)
  • 余計な画像処理がされていない
  • 非可逆圧縮がかけられていない(情報のロスがない)

したがって、優秀なソフトウェアを使うことで、カメラが出力するJPEGよりもすぐれた画像を手に入れる事ができる可能性があります。

逆に欠点としては

  • データ量が多い(ビット数が多い。通常非可逆圧縮がされていない)
  • 手を加えないと画像が見れない
  • 画像フォーマットの情報があまりシェアされていない
  • 実際にはどんな処理がすでに行われているのか不透明

などがあります。最後の点に関して言うと、RAWデータといってもセンサー出力をそのままファイルに書き出すことはまずなく、欠陥画素除去など最低限の前処理が行われいるのが普通です。 しかし、実際にどんな前処理がおこなわれているのかは必ずしも発表されていません。

カメラ画像処理

Bayerからフルカラーの画像を作り出すカメラ画像処理のうち、メインになる部分の例はこんな感じになります2f:id:uzusayuu:20180923122237p:plain このうち、最低限必要な処理は、以下のものです。

  • ブラックレベル補正
  • ホワイトバランス補正(デジタルゲイン補正も含む)
  • デモザイク(Bayerからフルカラー画像への変換)
  • ガンマ補正

これらがないと、まともに見ることのできる画像を作ることができません。

さらに、最低限の画質を維持するには、通常は、

  • 線形性補正
  • 欠陥画素補正
  • 周辺減光補正
  • カラーマトリクス

が必要です。ただし、線形性補正や欠陥画素補正は、カメラがRAWデータを出力する前に処理されていることが多いようです。また、センサーの特性がよければ線形性補正やカラーマトリクスの影響は小さいかもしれません。

次に、より良い画質を実現するものとして、

  • ノイズ除去
  • エッジ強調・テクスチャ補正

があります。RGB->YUV変換はJPEGMPEGの画像を作るのには必要ですが、RGB画像を出力する分にはなくてもかまいません。

この他に、最近のカメラでは更に画質を向上させるために

  • レンズ収差補正
  • レンズ歪補正
  • 偽色補正
  • グローバル・トーン補正
  • ローカル・トーン補正
  • 高度なノイズ処理
  • 高度な色補正
  • ズーム
  • マルチフレーム処理

などの処理が行われるのが普通です。今回はベーシックな処理のみとりあげるので、こういった高度な処理は行いません。

結局、今回扱うのは次の部分のみです3

f:id:uzusayuu:20180923160440p:plain

準備

まずRAW画像を用意します。今回はSony α 7 IIIで撮影したこの画像を使います。 f:id:uzusayuu:20180923124230p:plain

使用するRAWファイルはこちらからダウンロードできます。 https://github.com/moizumi99/raw_process/blob/master/sample.ARW

次にPython3が実行できる環境を用意します。今回はUbuntu18.04上のPython3.6を使用しました。

さらにPythonのライブラリとしてrawpyが必要です。pipが導入してあれば、次のコマンドでインストールできます。

pip install rawpy

他に、以下ののライブラリも必要なので導入済みでなければインストールしておいてください。

  • matplotlib
  • PIL
  • numpy
  • imageio
  • math

また必ずしも必須ではありませんが、Jupyter Notebookが使えれば以下で解説する内容を実行するのが楽になると思います。

今回の実行例はすべてJupyter Notebook上でのもので、githubにアップロードしてあります。 ローカルにダウンロード後、Jupyter上からファイルを開けば同様の内容が実行できるはずです。

なお処理自体に関係ありませんが、いくつかのパラメータを取得するのにexiftoolsを使っています。

RAW画像読み込み

では、raw画像を読み込んでみましょう。まず、sample.ARWをダウンロードしたディレクトリで、Jupyterを起動します。 JupyterがなければPythonの対話ウィンドウで同様の事ができると思います。

%matplotlib inline
import rawpy
raw = rawpy.imread('sample.ARW')

これでrawpyを使って画像が読み込めます。簡単ですね。 ちゃんと読めたか確認しましょう。

from matplotlib.pyplot import imshow
img_preview = raw.postprocess(use_camera_wb=True)
imshow(img_preview)

f:id:uzusayuu:20180923140802p:plain

実はrawpyにはLibRaw(内部的にはdcraw)を利用したraw画像現像機能があるので、このようにして現像後の画像を見ることができます。 ただ、今回の目的はBayerから最終画像フォーマットまでの処理を一つ一つ追いかけていくことなので、これはあくまで参考とします。 最終的にこの画像と似たようなものが得られれば成功としましょう。

画像データ変換

扱いやすいように画像をnumpyのarrayに変換しておきましょう。まず、RAWデータのフォーマットを確認しておきます。

print(raw.sizes)

ImageSizes(raw_height=4024, raw_width=6048, height=4024, width=6024, top_margin=0, left_margin=0, iheight=4024, iwidth=6024, pixel_aspect=1.0, flip=0)

RAWデータのサイズは4024 x 6048のようです。numpy arrayにデータを移しましょう。

import numpy as np
h, w = raw.sizes.raw_height, raw.sizes.raw_width
raw_image = raw.raw_image.copy()
raw_array = np.array(raw_image).reshape((h, w)).astype('float')

このデータを無理やり画像として見ようとするとこうなります。

outimg = raw_array.copy()
outimg = outimg.reshape((h, w))
outimg[outimg < 0] = 0
outimg = outimg / outimg.max()
imshow(outimg, cmap='gray')

f:id:uzusayuu:20180925140131p:plain

拡大するとこんな感じです

f:id:uzusayuu:20180925125658p:plain

明るいところが緑、暗いところが赤や青の画素のはずです。でも、これじゃなんのことやらさっぱりわかりませんね。 そこで、デモザイク処理でフルカラーの画像を作る必要があるわけです。

ブラックレベル補正

RAWデータの黒に対応する値は通常0より大きくなっています。画像を正常に表示するにはこれを0にもどす必要があります。これをやって置かないと黒が十分黒くない、カスミがかかったような眠い画像になってしまいますし、色もずれてしまいます。

まず、rawpyの機能でブラックレベルを確認しましょう。

blc = raw.black_level_per_channel
print(blc)

[512, 512, 512, 512]

どうやら全チャンネルでブラックレベルは512のようですが、他のRAWファイルでもこのようになっているとは限りません。各画素ごとのチャンネルに対応した値を引くようにしておきましょう。

さて、先程とりだしたrawデータの配列は以下の方法で確認できます。

bayer_pattern = raw.raw_pattern
print(bayer_pattern)

[[0 1]
 [1 2]]

どうやら0が赤、1が緑、2が青をしめしているようです。つまり、画像データの中から2x2のブロックを重複なくとりだすと、その中の左上が赤、右下が青、その他が緑、という事のようです。 このチャンネルに合わせて正しいブラックレベルを引きます。

print(raw_array.min(), raw_array.max())
blc_raw = raw_array.copy()
for y in range(0, h, 2):
    for x in range(0, w, 2):
        blc_raw[y + 0, x + 0] -= blc[bayer_pattern[0, 0]]
        blc_raw[y + 0, x + 1] -= blc[bayer_pattern[0, 1]]
        blc_raw[y + 1, x + 0] -= blc[bayer_pattern[1, 0]]
        blc_raw[y + 1, x + 1] -= blc[bayer_pattern[1, 1]]
print(blc_raw.min(), blc_raw.max())

0.0 8180.0
-512.0 7668.0

処理後の画像は、最大値と最小値が512小さくなっているのが確認できました。

outimg = blc_raw.copy()
outimg = outimg.reshape((h, w))
outimg[outimg < 0] = 0
outimg = outimg / outimg.max()
imshow(outimg, cmap='gray')

f:id:uzusayuu:20180925140209p:plain

画像も少し暗くなっています。

簡易デモザイク

次にとうとうBayer配列からフルカラーの画像を作ります。これが終わるとやっと画像がまともに確認できるようになります。

この処理はデモザイクと呼ばれることが多いです。本来デモザイクはカメラ画像処理プロセス(ISP)の肝になる部分で、画質のうち解像感や、偽色などの不快なアーティファクトなどを大きく左右します。 したがって手を抜くべきところではないのですが、今回は簡易処理なので、考えうる限りでもっとも簡単な処理を採用します。

その簡単な処理というのは、ようするに3色の情報を持つ最小単位の2x2のブロックから、1画素のみをとりだす、というものです。

f:id:uzusayuu:20180923134843p:plain

結果として得られる画像サイズは1/4になりますが、もとが24Mもあるので、まだ6M残っています。今回の目的には十分でしょう。 なお、解像度低下をともなわないデモザイクアルゴリズムは次回以降とりあげようと思います。

では、簡易デモザイク処理してみましょう。2x2ピクセルの中に2画素ある緑は平均値をとります。

for y in range(0, h, 2):
    for x in range(0, w, 2):
        colors = [0, 0, 0, 0]
        colors[bayer_pattern[0, 0]] += blc_raw[y + 0, x + 0]
        colors[bayer_pattern[0, 1]] += blc_raw[y + 0, x + 1]
        colors[bayer_pattern[1, 0]] += blc_raw[y + 1, x + 0]
        colors[bayer_pattern[1, 1]] += blc_raw[y + 1, x + 1]
        dms_img[y // 2, x // 2, 0] = colors[0]
        dms_img[y // 2, x // 2, 1] = (colors[1] + colors[3])/ 2
        dms_img[y // 2, x // 2, 2] = colors[2]

さてこれでフルカラーの画像ができたはずです。見てみましょう。

outimg = dms_img.copy()
outimg = outimg.reshape((h // 2, w //2, 3))
outimg[outimg < 0] = 0
outimg = outimg / outimg.max()
imshow(outimg)

f:id:uzusayuu:20180923154414p:plain

でました。画像が暗く、色も変ですが、それは予定通りです。そのあたりをこれから直していきます。

ホワイトバランス補正

次にホワイトバランス補正をかけます4。これは、センサーの色ごとの感度や、光のスペクトラムなどの影響を除去して、本来の白を白として再現するための処理です。 そのためには各色の画素に、別途計算したゲイン値をかけてあげます。今回はカメラが撮影時に計算したゲイン値をRAWファイルから抽出して使います。

ますはどんなホワイトバランス値かみてみましょう。RAWファイルの中に記録されたゲインを見てみましょう。

wb = np.array(raw.camera_whitebalance)
print(wb)

[2288. 1024. 1544. 1024.]

これは赤色にかけるゲインがx2288/1024、緑色がx1.0、青色がx1544/1024、という事のようです。処理してみましょう。

img_wb = dms_img.copy().flatten().reshape((-1, 3))
for index, pixel in enumerate(img_wb):
    pixel = pixel * wb[:3] /max(wb)
    img_wb[index] = pixel

f:id:uzusayuu:20180923154505p:plain

色がだいぶそれらしくなりました。

カラーマトリクス補正

次にカラーマトリクス補正を行います。カラーマトリクスというのは処理的には3x3の行列に、3色の値を成分としたベクトルをかけるという処理になります。

f:id:uzusayuu:20180925135315p:plain

なぜこんな事をするかというと、カメラのセンサーの色ごとの感度が人間の目とは完全には一致しないためです。例えば人間の目はある光の周波数の範囲を赤、青、緑、と感じるのですが、センサーが緑を検知する範囲は人間が緑と感じる領域とは微妙に異なっています。同じように青や赤の範囲も違います。これは、センサーが光をなるべく沢山取り込むため、だとか、製造上の制限、などの理由があるようです。 さらに、人間の目には、ある色を抑制するような領域まであります。これはセンサーで言えばマイナスの感度があるようなものですが、そんなセンサーは作れません。

こういったセンサー感度と人間の目の間隔とがなるべく小さくなるように、3色を混ぜて、より人間の感覚に近い色を作り出す必要があります。この処理を通常は行列を使って行い、これをカラーマトリクス処理と呼びます。

さて、ここでrawpyを使ってRAWデータの中に含まれる、マトリクスを調べるとこんなふうになってしまいます。

print(raw.color_matrix)

[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

仕方がないのでexiftoolsでマトリクスを元のARWファイルから取り出したところ、次のような値のようです。

Color Matrix                    : 1141 -205 88 -52 1229 -154 70 -225 1179

この値を使って処理を行いましょう。

color_matrix = np.array([[1141, -205, 88], [-52, 1229, -154], [70, -225, 1179]]) / 1024

img_ccm = np.zeros_like(img_wb)
for index, pixel in enumerate(img_wb):
    pixel = np.dot(color_matrix, pixel)
    img_ccm[index] = pixel

f:id:uzusayuu:20180923154644p:plain

あまり影響が感じられません。元のセンサーが良いので極端な補正はかけなくてよいのかもしれません。

ガンマ補正

最後にガンマ補正をかけます。 ガンマ補正というのは、もともとテレビがブラウン管だった頃にテレビの出力特性と信号の強度を調整するために使われていたものです。 今でも残っているのは、ガンマ補正による特性が結果的に人間の目の非線形的な感度と相性が良かったからのようです。 そんなわけで現在でもディスプレイの輝度は信号に対してブラウン管と似たような信号特性を持って作られており、画像にはガンマ補正をかけておかないと出力は暗い画像になってしまいます。

ガンマ特性自体は次の式で表されます

 y = x^{2.2}

f:id:uzusayuu:20180925135153p:plain

ガンマ補正はこれを打ち消す必要があるので、このようになります。

 y = x^{\frac{1}{2.2}}

f:id:uzusayuu:20180925135208p:plain

やってみましょう。

import math

img_gamma = img_ccm.copy().flatten()
img_gamma[img_gamma < 0] = 0
img_gamma = img_gamma/img_gamma.max()
for index, val in enumerate(img_gamma):
    img_gamma[index] = math.pow(val, 1/2.2)
img_gamma = img_gamma.reshape((h//2, w//2, 3))

f:id:uzusayuu:20180923154725p:plain

だいぶきれいになりました。

まとめ

今回は簡易的な処理でしたが、元のRAW画像のデータが高品質なのでこの程度の画質を実現することができました。

最後にセーブしておきます。

import imageio

outimg = img_gamma.copy().reshape((h // 2, w //2, 3))
outimg[outimg < 0] = 0
outimg = outimg * 255
imageio.imwrite("sample.png", outimg.astype('uint8'))

最後に

今回はrawpyを使ってカメラのRAWファイルからBayerデータを取り出し、Pythonでできるだけスクラッチから簡易的なカメラ画像処理を作成して、RAW現像を行いました。 今の所、周辺減光補正、ノイズ処理、エッジ強調がない、など主要な処理が抜けていますし、デモザイクは簡易的なものですので、次回以降こういった処理を追加していこうと思います。

今回使用したRAWファイル、Jupyter notebookでの実行例、出力したPNGファイルはすべてGitHubにアップロードしてあります。

github.com

次の記事

uzusayuu.hatenadiary.jp

改定履歴

2018-10-28: @karaage氏のご厚意により、記事中ブラックレベル補正の余分なコードとデモザイクのバグを修正しました。 2018-10-31: 再び@karaage氏の指摘に基づきデモザイクのコードをより汎用性の高いものに変更しました。


  1. 実際にはこの他に緑色の画素もあり、2x2の4画素のパターンになっているのが普通

  2. これはあくまで一例です。実際のカメラ内で行われる処理はメーカーや機種ごとに異なる可能性があります。

  3. 周辺減光補正は本来必要な処理ですが、今回は影響が少ない事やメタデータの解析が必要な事もあり、対象から省きました。

  4. ダイアグラムでの処理の順番に比べて、デモザイクとホワイトバランスの順番が逆になっていますが、今回採用した簡易デモザイクでは影響ありません。

Raspberry Piのベアメタル環境からSDCARDにアクセスする(BUSモード編)

はじめに

Raspberry Piのベアメタル環境からGPIOを経由してSD CARDをアクセスする方法について、BUSモードでも成功したので紹介します

BUSモード

前回のエントリーではRaspberry PiでOSなどを介さず、EMMC 1も使わず、GPIO機能のみを使ってSD CARDにSPIモードでアクセスする方法を紹介しました。 しかし、SPIモードの使用には

  1. 一旦BUSモードで動作すると、SPIモードには再遷移できない
  2. Raspberry PiブートローダーはSD CARDにBUSモードでアクセスる
  3. したがって、デモでSPIモードを使用する前にユーザーがカードを一度抜き差しして、カードをリセットする必要がある

という問題がありました。 また、SD CARDの規格としても、SPIモードはあくまで簡易的にデータにアクセスするための方法です。したがってできることならBUSモードでSD CARDにアクセスしたいと思っていました。

例えば、SPIモードの特徴は以下のとおりです

  1. 使用するピンが少ない。電源とグラウンドを除くと、クロック1ビット、コマンド1ビットとデータ1ビットの、3本の信号線のみ2
  2. 制御が比較的簡単
  3. ただしデータが1ビットしかないので遅い

それに対してBUSモードは

  1. 最大4ビットのデータ線が使える
  2. SD CARDの最近の高速モードに対応している
  3. したがって速い
  4. ただし、制御はSPIモードに比べて若干面倒

となります3。 このように、BUSモードこそがSD CARD本来のモードと言えると思います。

今回は高速化を期待してBUSモードに対応させてみました。

リポジトリと使用方法

今回のデモの内容はGithubで公開してあります。

github.com

使用方法は

  1. 必要ないSD CARDを用意する4
  2. SD CARDにラズビアンOSなどをインストールしてRaspberry Piが起動するようにしておく
  3. SD CARDのboot領域のkernel.imgをリポジトリのkernel.imgで置き換える
  4. SD CARDをRaspberry Piに挿して起動する

なお、デモの結果を確認するにはSerialのTX/RXをRaspberry PiのGPIO#14/15(PIN8とPIN10)に接続し、PCなどのターミナルコンソールに信号の内容を表示させる必要があります。

デモの出力結果は以下のようになります。SD CARDのFAT32領域にアクセスして、ルートディレクトリのファイルを表示しています f:id:uzusayuu:20180730070201p:plain

前回との違い

SPIモードとBUSモードの違いは上記のようにいろいろありますが、今回影響を強く受けたのは以下のような点です

  1. コマンド(CMD)が双方向になった。SPIモードではCMDはホストからカードへの一方通行だったのですが、BUSモードではカードからホストにレスポンスを返すのにも使います
  2. データビットが1ビットから4ビットに増えた。BUSモードでは4本あるデータ線をすべて使うことができます
  3. コマンドやレスポンスのフォーマットがSPIモードと違う。

とくに、コマンドに対するレスポンスがデータと同時に帰ってくることがあるため、両者を同時に処理する必要がある点に注意が必要です。

なお、今回は読み込みまでしか実装していません。

実装の詳細についてはgithubで公開しているソースコードを参照ください5

次への課題

この実験は、もともとは、既存のライブラリで動作していた「Haribote OS」のSD CARD読み込み部分を、自分の実装で置き換えるというのが目的でした。これからそのあたりやっていこうと思っていたのですが、一時帰国で日本に帰った際にこんな本を見つけてしまいました。FatFSという組み込み向けFATライブラリーの解説本です。

shop.cqpub.co.jp

Haribote OSではFATの扱いはごく簡単なもので、残りの実装は読者への宿題になっています。こちらの本で解説されているFatFSを使えば読み書きやディレクトリのアクセスも含めたファイルシステムをサポートすることができそうです。ちょっと気になってきました。

まとめ

Raspberry Piのベアメタル環境でGPIOを経由してSD CARDをアクセスするデモについて紹介しました。 なお今回はリードのみの実装です。また、UHSなどの高速化モードはいっさいサポートしていません。


  1. Raspberry PiのSoC(BCM2835)のSD CARDインターフェース機能

  2. 規格としてはCS(チップセレクト)信号もあるが、Raspberry PiではSD CARDは一つしか繋がらないのでCSは必要ない

  3. 詳しいことはSD Associationが発行している簡易版規格書のパート1をご覧ください。

  4. デモでSD CARDの内容が破壊される可能性もありますので、必ず必要ないSD CARDを使用してください

  5. うまくまとまったらブログに書くことも考えています

Raspberry Piのベアメタル環境からSDCARDにアクセスする(SPIモード編)

はじめに

Raspberry Piのベアメタル環境からSD CARDをアクセスする方法について、限定的ながらある程度成功したので紹介します

周辺機器アクセスはベアメタルの鬼門

ベアメタルというのはOSなしの環境なので、当然ながらプログラミングにあたって各種OSの便利な機能を使うことができません。その中には当然、周辺機器のアクセスも含まれます。
そんな中ベアメタルで周辺機器にアクセスしようとする場合、

  1. ベアメタルで動作するライブラリを見つける
  2. 自分で何とかする
  3. 他の代替手段を探す

といった対策が必要になります。このうち1は一番簡単ではあるのですが、そもそも見つからない、見つかっても自分の目的と微妙に違う、ドキュメントが少ない、などの状況に出会うことがあり、常に万全の状態で使えるとは限りません。実際私が以前OSを移植した時は、Raspberry Piのフォーラムで見つけたSDCARDアクセスコードを許可を得て使わせてもらったのですが、その後バグの存在を指摘されたにもかかわらず動作の詳細が不明なため完全には修正することができませんでした。 また、3は逃げに近いのでなるべく避けたいところです。そうなった場合、「自分でなんとか」しようとするわけですが、次に出会う問題点がベアメタル特有のドキュメントの少なさです。場合によっては情報がまったく無く、詰んでしまう事もめずらしいことではありません。もちろんガチ勢の場合、既存のソフトのリバース・エンジニアリングや対象デバイスの解析などを駆使してどうとでもしてしまうわけですが、われわれ一般人にはかなりつらいところです。

Raspberry PiのSDCARDコントローラー(EMMC)はドキュメントが微妙

数ある周辺機器の中でもストレージは最も基本的なものです。これにアクセスできなければとたんにできる事の範囲が極端にせまくなってしまいます。
Raspberry Piの場合メインのストレージは言わずとしれたSDCARDで、Raspberry PiのCPU(BCM2835)にはEMMC(External Mass Media Controller)というIPが搭載されており、SDCARDとのアクセスをサポートしてくれます。そのEMMCの使い方は、と探してみるとBCM2835 ARM PeripheralsマニュアルにEMMCの項が見つかります(P.65 5 External Mass Media Controller)。
ここで、これは楽勝か?と思いつつ読み進めていくと、どうも勝手が違い読みにくいことに気が付きます。記述が断片的でどのように設定すればばSDCARDにアクセスできるのかわかりません。さらに読みすすめるとこのような文に出会います。

For detailed information about the EMMC internals please refer to the ArasanTM document SD3.0_Host_AHB_eMMC4.4_Usersguide_ver5.9_jan11_10.pdf but make sure to read the following chapter which lists the changes made to ArasanTM’s IP.

どうやらEMMCの使い方はArasanのドキュメントに書いてあり、BCM2835のマニュアルにはそこからの差分しか載っていないようです。記述が断片的なのはそういう理由があったわけです。 それでは、と、Arasanのドキュメントを探すと見つかりません。どこにも見つかりません。インターネット上をさんざん探しましたが、どうやら非公開のようです。やられた...。

まとめると

  • SD CARDへのアクセスが必要
  • BCM2835のEMMCの情報が必要
  • ArasanのIPの情報が必要
  • ArasanのIPのマニュアルは非公開
  • 詰んだ

困りました。

SDCARDのピンはGPIOにつながっている

このように途方にくれながらRaspberry Piのスキマティクスを見ているとSDCARDのピンはGPIOにつながっている事に気が付きました。 f:id:uzusayuu:20180520131226p:plain このあたり専用ピンにつながっているUSBとは事情が違うわけです。さて、ここでこんな考えが頭にうかびます。

「GPIOにつながっているんなら、GPIO経由でソフトでアクセスできるんじゃね?」

これは何も無茶な考えではなく、マイコンなどでは普通に行われる方法です。たとえばCQ出版の「フラッシュ・メモリ・カードの徹底研究」にはソフトウエアコントロールでMMCにアクセスする方法が、同社の「FPGAスタータ・キットで初体験!オリジナル・マイコン作り」にはNIOSからSD CARDにアクセスする方法が記載されています。 やるべきことは、

  1. EMMCを止める
  2. GPIO48から53を、ALT0(EMMC)からInputまたはOutputにつなぎ替える
  3. ソフトウェアからGPIOを制御しSDCARDをSPIモードにする
  4. 同様にソフトウェアでSDCARDの初期化を行う
  5. 同様にSDCARDからの読み出し、書き込みを行う

以上です。ソフトウェアは上記の2書籍に例があるので参考にしつつすすめる事ができます。また、SDCARD自体のコントロールについては、SDのスペック(SD Specifications Part 1 Physical Layer Simplified Specification)に十分な内容の情報があります。なんとかなりそうですね。

SDCARDにアクセスできた、が...

詳細は次回に譲るとして、試行錯誤のすえ、SDCARDの初期化と読み書きアクセスに成功しました。次の図は、SDCARDのFAT領域をベアメタル環境で読みだして、ファイルのリストを表示した結果です f:id:uzusayuu:20180520132857p:plain

今回のデモプログラムはgithubにて公開してあります。

github.com

ただ、ここで大問題があります。
どうやら、次のスペックの記述によると、SDモードからSPIモードへの変更は電源投入後の一回しかチャンスがないようです。SDのスペックの7 SPIの最初の項7.1 Introductionに次のような記述があります。

The interface is selected during the first reset command after power up (CMD0) and cannot be changed once the part is powered on.

Raspberry Piは本体のROMに記録されているブートロジックが最初のブートローダーをSDCARDから読み出すので、この時点でSDCARDはSDモードに設定されています。したがってSPIモードに変更し直すことはできないという事のようです。(痛恨)。 デモプログラムではユーザーにカードの抜き差しをしてもらうことでこの問題点を回避していますが、ちょっと実用的ではありません。やはりSDモードでのコントロールを考えないといけないようです。

あともうひとつ、ソフトウェアコントロールでシリアル通信なので読み・書き・コントロールすべて遅いです。デモではルートディレクトリのファイルを表示するのに1分くらいかかっています。*1

まとめ

以上、Raspberry Piのベアメタル環境からGPIO経由でSPIモードでSDCARDにアクセスすることにある程度成功したので報告しました。 実用性はあまりありませんが、SDCARDにコントロールIPをはさまず直接アクセスできるので、SDCARDの動作を理解するのには役に立つのではないかと思います。 次回以降、処理の詳細について説明したいと思います。

*1:これはさすがにちょっと遅すぎるので、どこか他の部分に問題があるのかもしれません。そのうちどこが本当にボトルネックになっているか調べてみたいと思います。

プロセッサの中のプロセッサとインストラクション・セットの多様性

プロセッサの中にも多数のプロセッサがいる

最近出版された「コンピュータ・アーキテクチャ 定量的アプローチ 第六版」に新設された第7章のドメイン・スペシフィック・アーキテクチャ(DSA)の章を読んでいたところ、面白い記述があった。まず前提としてライセンスの面倒さなどから、DSAには独自のRISCプロセッサを導入して、コンパイラやライブラリを移植することが多いという前置きのあとで、このように書かれている。

One AMD engineer estimated that there were 12 instruction sets in a modern microprocessor! (あるAMDのエンジニアの見積もりでは最近のマイクロプロセッサには12種類のインストラクションセットが含まれているという)

AMDのエンジニア、最近の、という言葉からおそらくRyzenやその関係のプロセッサではないかと思うが、実際にどのプロセッサなのかかは定かではない。 私自身も半導体の会社で働いているのでSOCに*1はメインのCPU以外にも多数のプロセッサが搭載されていることは事実として知ってはいたが、12種類のインストラクションというのは随分多いと感じる。なにしろインストラクション・セットが12種類ということは、搭載されているプロセッサは少なくとも12個以上で、そのうち命令が異なるものが12個ある、ということだ。 しかし同時に、いや大規模なSOCならそれくらいあってもおかしくないぞ、とも思う。

プロセッサ中のプロセッサの例

こういったSOCの中にメインのCPU以外に含まれるプロセッサというのは、内部の制御用だったりハードウェアの動作の一部または全部を引き受けたりするものなので、基本的にエンドユーザーやアプリケーション作成者からは見えないようになっている。したがって開発関係者以外には、仕様も場合によっては存在自体も隠されていることが多いのだが、ときどきその存在が表に現れる。

まず例としてAMD自身がEPYC中にX86とは別のプロセッサをセキュリティ用に搭載していることを発表している。

pc.watch.impress.co.jp

記事中「ARMのCortex-A5をセキュリティエンジンとして搭載しており、OSインディペンデントなセキュリティを提供する。」とある。つまり、EPYCにはOS上の操作しかしない一般ユーザーからは見えないプロセッサが最低でも一個搭載されていることになる。当然このプロセッサ用の開発環境、ライブラリ、OSはメインCPUとは別に作られ、チップの開発チームやパートナーのみに提供されているのだろう。

また、Tensilica*2のXtensa DSPは、Wikipediaの記述を信用するなら、PS4のプロセッサや、AMDのAPUなどに採用されているようだ。

Tensilica - Wikipedia

AMD TrueAudio - Wikipedia

これはWikipediaの記述だが、私自身もXtensaがオーディオ用として他のSOCに採用された例を見ているし、Tensilicaによるとこれまでに1400以上の採用実績があるということなので、多数のSOCで搭載されているのはおそらく間違いない。

ip.cadence.com

AMD以外に目を向けると、以前私も開発に関わっていた携帯用SOCのOMAPの画像処理プロセッサにはARMのCoretex-Mが制御用として搭載されていた。

http://www.ti.com/pdfs/wtbu/OMAP4470_07-05-v2.pdf

http://www.ti.com/lit/wp/spry242/spry242.pdf

OMAPシリーズにはこの他に画像処理アクセラレータのiMX、さらには当然ながらTIのDSPも搭載されており、GPUと合わせて今流行りのHeterogeneous Computingの先駆けを進んでいたと言っていいだろう。このあたりはTI自身が公開している上記の資料に詳しい。

ISAの多様性とRISV-V

ここで例にだしたのはセキュリティ、オーディオ、あとは画像処理という3種類のIPだが、現代のSOCにはこれ以外にも多種多様のIPコアが含まれている。 ちょっと考えただけでネットワーク、ディスプレイ、USB、電源管理、ストレージ、などなど。それぞれに専用のIPがありそれぞれ制御が必要な事を考えると、いくつかには専用プロセッサが搭載されていると推測するのは大きく間違ったことではないだろう。IPごとに必要な機能に過不足ないプロセッサを採用することを考えると、それぞれインストラクションが異なるということは十分にありそうな話だ。そうなれば最初の「最近のマイクロプロセッサには12種類のインストラクションセットが含まれている」という話もあながち大げさというわけではなさそうだ。さらに考えると、このプロセッサ群は当然コンパイラやライブラリーと一緒に提供され、多くの場合OSが動作しているはずだ。またデバッガなどの開発支援環境も当然それぞれ存在するのだろう。こういった想像は、これまで複数の会社でSOC開発に関わってきた私自身の過去の経験ともそんなに乖離していない。

また、SOCというと携帯や組み込み機器の中に入っているもの、というイメージがあるのかもしれないが、実は最近のPCのCPUはほとんどの場合SOCだ。 これはタブレット型や2in1タイプはもちろんのこと、ラップトップ、さらには多くのデスクトップまであてはまる。こういったPCに搭載されるCPUは、電源やUSB、メモリインターフェースといった昔からある機能はもちろんのこと、オーディオ、ビデオ、カメラ、などなど、PCに必要な機能の多くをオンチップで搭載している。例えば最近のノートPC用のチップを考えると、ダイ上の面積的に一番大きいのは大概GPUで、X86のコードを実行するいわゆるCPUと呼ばれる部分の面積はさらに残りの部分の半分ほどの事が多い(実際の面積比はSKUによって大きく異なります)。つまり、CPUの占める面積は意外なほど小さいのだ。

そして、残ったの面積は多数のIP*3が占めている。我々が普段使っているPCのCPUには、私達が存在も知らずアクセスすることもできないプロセッサが多数搭載されていて、その上で専用のコードやOSがユーザーの意志とはまったく関係なしに動きまわっている可能性が高いわけだ。

そう考えると、x86系とARM系の寡占化が極まったと考えられているCPUのインストラクションセットも、実は意外なほどの多様性を備えていることがわかるだろう。同様に、CPUの構造(マイクロアーキテクチャ)や、その上で動作するアセンブラ、OS、また開発に使用するコンパイラなども同様に多様なのだ。

では、これからもその多様性が広がっていくか?と考えると、少し疑問が残る。まず、明らかな問題として無駄が多い。たった一つのSOCを開発するのに12個も開発環境を用意し、それぞれノウハウもスキルベースも異なるコードを開発しメンテナンスするのはどう考えてもコスト高だ。さらに、一個一個のプロセッサに割ける人員も時間も予算も限られるので一つ一つが十分に洗練されていない、という可能性も出てくる。当然、ちょっと数を減らそうか、という話はでてくるはずだ。

ここで最初の「コンピュータアーキテクチャ」に話を戻すと、この本では「だからRISC-V*4」を使おう、という主張になっている。統一されたインストラクション、共通の開発環境を使えばこういったムダが一層されるわけだ。

さて、私自身はこれを読んだあとも「そううまくいくかね?」と思っていたのだが、先日こんなニュースが飛び込んできた。

www.itmedia.co.jp

このヘネシー氏は「コンピュータアーキテクチャ」の著者の一人。もう一人のデビッド・パターソン氏もGoogleに在籍していることを考えると、今後GoogleRISC-Vに力を入れてくることは予想に難くない。そうなると、意外とRISC-Vが広がってインストラクションセットの多様性を終焉させる、ということもありえないわけではないのかな、という気がしている。

さて、どうなりますかね?

*1:システムオンチップ, SoCとも

*2:現在はCadenceの一部

*3:Intellectual Property、半導体分野では機能ブロックやその設計資産を示す

*4:RISC-VはオープンでフリーなCPUインストラクションセット。最近盛り上がりをみせている

「実践コンピュータビジョン」の全演習問題をやってみた、というブログを書いた

夏頃にオライリージャパンの「実践コンピュータビジョン」という本を読み、全演習問題に挑戦しました。もともとこのブログにそのことを書こうと思っていたのですが、はてなブログでは大量のコードが出てくる記事を書くのがなんともおっくうで、延ばし延ばしになってきました。

先日思い立って、コードなどを書くのに楽なQiitaの方でそのことをブログに書きました。まとめ一つと、各10章に対応するエントリー、合計11本です。

 

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

ブログが分散するのは管理も大変だし、あまり好ましくないんですが、はてなでブログでコード挿入するのが面倒なのはなんともならず。いまさらはてなダイアリー表記に戻す気にもならず。