tmct-web

電子工作・DIYの製作例やノウハウを公開しています

【Arduino対応!】FlashROMに大きな固定値データを入れ込む

プログラム用Flash ROMに大きなデータを埋め込む!

C言語による記述で、Arduinoなどのプログラムが格納されたFlashROMに数十〜数百キロバイト程度の大きな固定データを埋め込んで配列として参照する方法を紹介します。

たとえばArduinoなどファイルシステムを持たないマイコンシステムで、グラフィック液晶に表示するビットマップを保持したい!とか、FPGAのコンフィグレーションデータをファームウェアに保持したい!…なんて場合に使えます。

また、ランレングス・エンコーディングによってデータを圧縮して格納する方法も紹介しますよ。

ROM領域にデータを埋め込む方法は、これ以外にもセクションを区切ってデータのバイナリファイル自体をリンクする方法があります。
プログラムからはセクション先頭アドレスから配列として参照することができますが、以下のデメリットがあるためお勧めしません。

  • リンカースクリプトの修正が必要になり統合開発環境との相性が悪い
  • リンカースクリプトを修正した時点で対象デバイスや開発環境に強く依存する記述となり、ソースコードの移植性が大変悪くなる
  • 配列名がリンカースクリプト内に記述されるため、ソースコードだけ見ても一見定義がないように見える
  • データの参照単位によってはもとデータのエンディアンと対象システムのエンディアンが不一致を起こすことがある

データを埋め込む手順

C言語で記述できる範囲でデータを埋め込むためには、少々手順を踏む必要があります。

  1. 埋め込むデータをInclude可能な形式に変換する。
    配列の初期値として利用するために、バイナリデータからカンマ区切りの数値データ列に変換します。
  2. C言語のソースコードにIncludeしてコンパイルする。

データ変換は少々手間ですが、専用ツール bsgen_tmct を使えば簡単です。
その辺りも含めて各手順を示していきます。

まずはデータ形式を変換する

埋め込むデータはC言語からは配列の初期値として参照することになります。
配列の初期値は一般的に以下のようにカンマ区切りで定義されます。

const unsigned char sample_data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };

…この要領で元データを1バイトずつ定義していけば良いのですが、さすがに手動で変換するのは辛いものがありますね。
そんなわけでフォーマット変換を自動で行うためのツールを用意しました。以下のGitHubのリンクからダウンロードして利用できます。(Windows以外で動作させるには .NET Coreのインストールが必要です。)

bsgen_tmct https://github.com/tmct-web/bsgen_tmct

 

詳しい使い方はGithubに記載しているのでそちらを見てください。
とりあえず source.txt をカンマ区切りのファイルに変換して dist.inc を生成させる場合はコマンドラインから以下のように呼び出します。
※コンソールアプリのためGUIはありません。

bsgen -src source.txt -dst dist.inc

 

MacOSのターミナルから実行した例は以下。「xxxx piece(s) of data have been written.」と表示されればエラーなく終了して、source.txt と同じフォルダに dist.inc が生成されているはずです。

% ./bsgen -src source.txt -dst dest.inc         
Bit Stream Generator - Version 1.0.0.0
©2021 tmct-web All rights reserved.
https://ss1.xrea.com/tmct.s1009.xrea.com/
 
Source file: source.txt
Destination file: dest.inc
Data byte width: 1 (Single byte)
 - Destination format: Include
Compression type: No encode
[INFO] The source file has been opened.
[INFO] 3737 piece(s) of data have been read.
[INFO] No data will be encoded.
[INFO] The destination file has been opened.
[INFO] 3737 piece(s) of data have been written.
% 

できたデータをC言語のソースコードにIncludeする

フォーマット変換後のファイルが生成できたら、その内容をファームウェアやプログラムに含めます。

以下に’dist.inc’の中身をsample_data[]の初期値としてプログラム内に含め、標準出力に1バイトずつ出力するコードの例を示します。

const unsigned char sample_data[] = {
    #include "dist.inc"
};
unsigned long ptr = 0;

while (ptr < sizeof(sample_data))
{
  printf("%x¥r¥n", sample_data[ptr]);
  ptr++;
}

 

このように、includeで含めた内容は配列として参照することができます。簡単ですね。
ただし、必ず「const」で定義するなど「プログラム実行時は固定値であり変更しない(≒だからROM領域に配置しなさい)」ということをリンカーに伝える記述をする必要があります。この辺りのコードは残念ながら開発環境に依存するので注意が必要です。

Arduinoで使う場合の注意!

ATMEGA系のマイコンが使われているエントリーレベルのArduinoの場合、たとえconstで定義しても変数の内容はSRAM上に確保されます。

それは困るので、データの実体をFlash ROM上に配置する専用の変数型で定義してそこを参照してもらうようにします。
上記のコードをエントリレベルArduino向けに書き換えた例を以下に示します。

以下のコードはSerialを使うので、予め初期化しておくことが必要です。
また、Arduinoの開発環境は拡張子が .h のファイルのみ include できる仕様なので、dest.inc は dest.h にファイル名を変更しておく必要があることに注意してください。

#include <avr/pgmspace.h>


const PROGMEM unsigned char sample_data[] = {
  #include "dest.h"
};

unsigned long ptr = 0;

while (ptr < sizeof(sample_data))
{
  Serial.println(pgm_read_byte_near(sample_data + ptr), HEX);
  ptr++;
}

 

既述の通りPROGMEMで定義することにより、Flash ROM上に置かれるデータ配列になります。この配列を参照するには pgm_read_byte_near() マクロ間数を使います。

Arduinoに使われているATMEGA系マイコンはROM領域とRAM領域が完全に独立しています。(制御バスが完全に分かれています)
しかしArduinoのコンパイル環境(avr-gcc)は通常の変数に対するアクセスはすべてRAM領域に対するアクセスとして解釈するので、C言語の範疇ではROM領域にアクセスする方法がありません。
よってこのような特殊な関数(マクロ)を介してアクセスします。

さらに大きなデータを埋め込むには?

さて、ここまででFlash ROMが許す限りのデータを埋め込むことはできるようになりましたが、「データを圧縮したら、もっとたくさんデータが詰め込めるんじゃないの?」と思いますね。

じつは bsgen_tmct には「ランレングス・エンコーディング」という圧縮ビットストリームを生成する機能も入っています。この機能を使って圧縮・伸張してみましょう。

ランレングス・エンコーディング(ランレングス圧縮)は、たとえば「AAAAABBBCCCCCCCCCDDDD」という文字列があった場合、「A×4,B×3,C×9,D×4」という風に「◯が△個」の情報を羅列することでデータ量を削減します。
この方式は伸張処理(データをもとに戻す処理)が単純なのでよく利用されています。ただし、もとのデータによって圧縮効率が著しく左右される…要するに不向きなデータにはとことん不向きである、という欠点があります。
とくに元データで同じデータが3バイト以上連続する箇所が少ない場合、圧縮すると元のデータより大きくなってしまいます。

まずは圧縮されたデータ列を生成する(エンコード)

無圧縮の時と同様に source.txt をカンマ区切りのファイルに変換して dist.inc を生成させる場合はコマンドラインから以下のように呼び出します。
コマンドの最後の「-enc rle」というオプションが「ランレングス・エンコーディングしなさい」という意味を示します。

bsgen -src source.txt -dst dist.inc -enc rle

 

MacOSのターミナルから実行した例は以下。「xxxx piece(s) of data have been written.」と表示されればエラーなく終了して、source.txt と同じフォルダに dist.inc が生成されます。
「3737 piece(s) of data have been read.」に対して「3060 piece(s) of data have been written.」と表示されていることから、元の3737バイトのデータを圧縮することで3060バイトになったことがわかります。

% ./bsgen -src source.txt -dst dest.inc -enc rle        
Bit Stream Generator - Version 1.0.0.0
©2021 tmct-web All rights reserved.
https://ss1.xrea.com/tmct.s1009.xrea.com/
 
Source file: source.txt
Destination file: dest.inc
Data byte width: 1 (Single byte)
 - Destination format: Include
Compression type: Run-length encode
[INFO] The source file has been opened.
[INFO] 3737 piece(s) of data have been read.
[INFO] Begin Run-length encoding.
[INFO] 3737 piece(s) of data have been encoded.
[INFO] The data has been encoded into 3060 piece(s).
[INFO] The destination file has been opened.
[INFO] 3060 piece(s) of data have been written.
% 

 

生成されたデータは、(データの個数),(データ),(データの個数),(データ),……というフォーマットになります。
たとえば元のデータが「AAAAABBBCCCCCCCCCDDDD」なら

0x04, 0x41, 0x03, 0x42, 0x09, 0x43, 0x04, 0x44
(0x41・0x42・0x43・0x44はそれぞれA・B・C・DのASCIIコード)

…というデータ列が生成されます。
もとのデータは21バイトですが圧縮後は8バイトになっており38%程度に圧縮できていることがわかりますね。

圧縮したデータをC言語から利用する(デコード)

圧縮(エンコード)したデータは解凍(デコード)して利用する必要があります。
圧縮されたデータを元に戻すコードの例を示します。(Arduinoのコードサンプルです。)

既述のサンプルコードと同様、Serialを使うので予め初期化しておくことが必要です。
また、Arduinoの開発環境は拡張子が .h のファイルのみ include できる仕様なので、dest.inc は dest.h にファイル名を変更しておく必要があることに注意してください。

#include <avr/pgmspace.h>


const PROGMEM unsigned char binary[] = {
  #include "dest.h"
};


  unsigned int dataPointer;
  unsigned int i;
  unsigned char runLength;
  unsigned char runData;

  dataPointer = 0;

  while (sizeof(binary) > dataPointer)
  {
    runLength = pgm_read_byte_near(binary + dataPointer);
    dataPointer++;
    if (sizeof(binary) == dataPointer)
    {
      // Invalid data
      Serial.println(F("!?"));
    }
    else
    {
      runData = pgm_read_byte_near(binary + dataPointer);
      dataPointer++;
      for (i = 0; i < runLength; i++)
      {
        Serial.write(runData);
      }
    }
  }

 

このコードは dest.h の内容がそのままシリアルモニタに出力されるだけです。
もしシリアルモニタに「!?」と表示された場合、もとのデータに異常があります。サンプルデータをそのまま使用した場合、シリアルモニタは以下のようになるはずです。

Sample result

まとめ

C言語による記述でFlash ROMに固定データを埋め込んで参照する方法を紹介しました。
ランレングス・エンコーディングも併用することで、たとえばArduinoに接続したグラフィックLCDにビットマップ画像を表示したりと、「どうやってるのコレ?!」と思われるようなことにも応用できるでしょう。

最終更新: 2021-11-07
作成: 2021-11-07
  • Arduino

© 2025 tmct-web All Rights Reserved.
Powered by Hugo. Theme by TechDoc. Designed by Thingsym.