ゾックス パソコンでワンセグテレビを楽しめるUSB接続ワンセグチューナー ブラック DS-DT305BK - オウルテック

オウルテック

5.0 / 5.0

ワンセグチューナーでソフトウェアラジオをやるのが流行ってるらしくて、やってみたかったので、DS-DT305BK というのを買ってやってみた。Realtek RTL2832U というチップを使っているやつならなんでもいいっぽいけど、日本で買えるのはあまりないっぽい。Mac でも簡単にいけるかな〜と思ったけど超簡単だった……

でもって以下のように rtl-sdr というのを入れる。OSS のデバイスドライバ

git clone git://git.osmocom.org/rtl-sdr.git
cd rtl-sdr/
mkdir build
cd build
cmake ../
make
make install

で /usr/local に入る。

rtl_test -s 3.2e6

とかやるとなんか動いてるふう。何の問題もなく動いてる感じ……

しかしこれだと音聞いたりがめんどうなので GUI も入れてみる。

http://gqrx.dk/ を入れると GUI でウォーターフォール画面とかが見れるようになる。これもビルド済みのがあるので入れるだけで良い。

  1. トップ
  2. tech
  3. 900円でソフトウェアラジオ入門
  1. トップ
  2. ham
  3. 900円でソフトウェアラジオ入門

去年歯医者で一通りの治療が終わったあと、フロスの必要性をかなりしつこく言われたので、最初は糸ようじを使い、途中から普通の糸フロスを使ってみたりしていた。

しかし普通の糸フロスはめちゃくちゃむずかしくて挫折、フロスホルダーという糸ようじみたいにフロスを固定するものを使いはじめたけど、これも糸をうまくピンと張れなかったり、なんか微妙に使いにくかったりして、だんだんフロスをするのがおろそかになっていった。

結果、半年後には虫歯が2つ発見される (1つは神経ギリギリ・もう1つは軽め) というハメになった。まさにこのザマである。

結局結論としては糸ようじが最強ということがわかった。糸1本1本が細いので歯並びが悪くてもだいたいの歯間に入れることができ、なおかつ複数本の糸の塊になっているので汚れがじゃんじゃかとれる感じがキモチいい。

糸ようじ 60本 - 糸ようじ

糸ようじ

5.0 / 5.0

さすがにもう歯を削ったりしたくないので、毎日やっていきたい。今のところ毎日できている。

FT4232H

http://strawberry-linux.com/catalog/items?code=50029

FT2232H か FT4232H を使って JTAG インターフェイスにするのが高コストパフォーマンスっぽいので、FT4232H を買ってみた。FTDI の一部のシリアルポート変換チップは、MPSSE という方法で JTAG や I2C, SPI を喋ることができる。

なので、UART 接続しつつ JTAG しつつ、みたいなのが1個あればできて便利そう!って感じです。ただ、他プロトコル喋らせるのは、やはりちょっとハマりどころがあった……

JTAG 専用の USB インターフェイスの中には普通にこれを使っているのも多いみたい。専用のものだとここに書いてあるようなハマり所はあまりなさそう。

この製品のピン配置

OpenOCD

JTAG をハンドリングして使えるようにしてくれる OSS として Open OCD (オンチップデバッガー) というのがある。これは gdb のプロトコルを喋るサーバーになったりしてくれる。

FT4232H を生で使った場合、2つの方法がある。

libftd2xx を使う場合

FTDI が提供しているドライバを使う場合。ドライバ自体のソースは非公開のビルド済みのものを使う。

http://www.ftdichip.com/Drivers/D2XX.htm で libftd2xx をいれる

sudo cp /Volumes/release/D2XX/bin/10.5-10.7/libftd2xx.1.2.2.dylib /usr/local/lib/
sudo ln -sf /usr/local/lib/libftd2xx.1.2.2.dylib /usr/local/lib/libftd2xx.dylib
sudo cp /Volumes/release/D2XX/Samples/ftd2xx.h /usr/local/include/
sudo cp /Volumes/release/D2XX/Samples/WinTypes.h /usr/local/include/  # Win とかついてるが必要

で、入れたら、homebrew で以下の通りインストールする

sudo brew install openocd ----enable-ft2232_ftd2xx 

このようなファイルを generic-ft4232h.cfg でつくる。

# Adapter Configuration:
# http://openocd.sourceforge.net/doc/html/Debug-Adapter-Configuration.html

interface ft2232
ft2232_device_desc "Quad RS232-HS"
ft2232_layout usbjtag
ft2232_vid_pid 0x0403 0x6011 # vendor id, product id from usb

vid_pid は system_profiler SPUSBDataType を実行して出てくるやつを入れたらよい。

ft2232_layout は、この FT4232H を使う場合 usbjtag を指定すればよい。usbjtag を指定した場合

  • ADBUS0 -> TCK
  • ADBUS1 -> TDI
  • ADBUS2 -> TDO
  • ADBUS3 -> TMS
  • ADBUS4 -> nTRST
  • ADBUS5 -> WAIT or STOPCLK
  • ADBUS6 -> nRST
  • ADBUS7 -> RTCK

になる。

libftdi を使う場合

入れるのはより簡単

sudo brew install openocd --enable-ft2232_libftdi 

とするだけで依存まで入る。ちなみに OpenOCD が最新の libftdi には対応していないので、自分でビルドしようとするとだいぶハマる。homebrew 使いましょう。

設定ファイルは以下のようになる。コメント頑張って書いた。AN_135_MPSSE_Basics.pdf で出てくるドキュメントと FT4232H のデータシートを読むとだいたい必要な情報は理解できる……

# Adapter Configuration:
# http://openocd.sourceforge.net/doc/html/Debug-Adapter-Configuration.html

# Use MPSSE adapter
interface ftdi
# USB driver name (must be matched with your device)
ftdi_device_desc "Quad RS232-HS"
# USB vendor ID, product ID pairs
ftdi_vid_pid 0x0403 0x6011

# Channel for using JTAG
ftdi_channel 0

# Initial state and direction setting (ref. AN_135_MPSSE_Basics.pdf)
# ftdi_layout_init [data] [direction]

#  name   signal  state      direction  JTAG name
# *DBUS0  TCK/SK  (low =0)   (out=1)    TDI
# *DBUS1  TDI/DO  (low =0)   (out=1)    TDO
# *DBUS2  TDO/DI  (low =0)   (in =0)    TCK
# *DBUS3  TMS/CS  (high=1)   (out=1)    TMS
# *DBUS4  GPIOL0  (low =0)   (in =0)    (nTRST)
# *DBUS5  GPIOL1  (low =0)   (in =0)    WAIT | STOPCLK
# *DBUS6  GPIOL2  (low =0)   (in =0)    (nRST)
# *DBUS7  GPIOL3  (low =0)   (in =0)    RTCK
ftdi_layout_init 0x08 0x0b

# Use GPIOL0 as nTRST
ftdi_layout_signal nTRST -data 0x10
# Use GPIOL2 as nRST
ftdi_layout_signal nRST -oe 0x40

libftdi を使う場合エラーメッセージがろくに表示されないので罠い。例えば、ftdi_device_desc が間違っていると error code -100 みたいなエラーしかでないのでさっぱりわからない。

この設定ファイルの場合、ftd2xx の usbjtag と同じピン配置になる (はず…)。すなわち以下

  • ADBUS0 -> TCK
  • ADBUS1 -> TDI
  • ADBUS2 -> TDO
  • ADBUS3 -> TMS
  • ADBUS4 -> nTRST
  • ADBUS5 -> WAIT or STOPCLK
  • ADBUS6 -> nRST
  • ADBUS7 -> RTCK

Raspberry Pi との接続

Raspberry Pi 側の配置 (リビジョンによって違うが、最近のバージョン = rev2 で)

  • 7pin TDI
  • 13pin TMS
  • 15pin TRST
  • 18pin TDO
  • 22pin TCK
  • 25pin GND


Raspi <-> JTAG

  • 7pin (TDI) 4pin (TDI)
  • 13pin (TMS) 6pin (TMS)
  • 15pin (TRST) 7pin (TRST, GPIOL0)
  • 18pin (TDO) 5pin (TDO)
  • 22pin (TCK) 3pin (TCK)
  • 25pin GND

とする。あとは同じ名前のピン同士を接続する。

Bare metal Raspbery Pi 側で JTAG を有効にする

JTAG は GPIO ピンの ALT 機能なので (JTAG を使ってる場合これらのピンはGPIOとしては使えない)、JTAG を使うようにブートする必要がある。以下のイメージを kernel.img にして SD カードにコピー

https://github.com/dwelch67/raspberrypi/blob/master/armjtag/armjtag.bin

これは単に JTAG を利用するように設定したあと、LED をチカチカさせて接続を待つプログラムになっている。

接続する

raspi.cfg を以下の内容でつくる。

# Broadcom 2835 on Raspberry Pi

telnet_port 4444
gdb_port 3333

#jtag_khz 1000
adapter_khz 1000

#jtag_nsrst_delay 400
#jtag_ntrst_delay 400

if { [info exists CHIPNAME] } {
   set  _CHIPNAME $CHIPNAME
} else {
   set  _CHIPNAME raspi
}

reset_config none

if { [info exists CPU_TAPID ] } {
   set _CPU_TAPID $CPU_TAPID
} else {
   set _CPU_TAPID 0x07b7617F
}
jtag newtap $_CHIPNAME arm -irlen 5 -expected-id $_CPU_TAPID

set _TARGETNAME $_CHIPNAME.arm
target create $_TARGETNAME arm11 -chain-position $_TARGETNAME

でもって、

openocd -d -f generic-ft4232h.cfg -f raspi.cfg 

とする。-d はデバッグオプションなので、ちゃんと動けばつけなくていい。

シリアルポートドライバのアンロード

ちゃんとデバイス名とか設定してるのに! 動かない! ってときは

kextstat  | grep FTDI

してみると、シリアルポートドライバが (入れたなら) 入ってるので、それが動いている。そのポートに対して JTAG をしかけようとしても衝突するのでうまくいかない。

とりあえずアンロードして動くことを試す

sudo kextunload /System/Library/Extensions/FTDIUSBSerialDriver.kext
# ドライバを無効にしている状態で抜き挿しするとロードされてしまう? アップルがデフォルトで入れてるドライバも無効にする kextstat | grep FTDI ででてくる
sudo kextunload -b com.apple.driver.AppleUSBFTDI

ftd2xx の場合 FT_OpenEx あたりで、うまくいくはずなのに失敗している場合これが疑われる。libftdi の場合エラーがよくわからないので気付きにくい。


これで動くなら動くでいいんだけど、このままだと他のチャンネルをシリアルポートとして使えない。なので、一部のポートだけシリアルポート機能をやめたい。検索したところドライバの設定ファイルを変えればいいっぽい。以下の場所にある。

/System/Library/Extensions/FTDIUSBSerialDriver.kext/Contents/Info.plist

これに対して以下のように A ポートだけ無効にする。

--- Info.plist.orig	2014-03-02 14:58:05.000000000 +0900
+++ Info.plist	2014-03-02 14:58:35.000000000 +0900
@@ -2033,25 +2033,6 @@
 			<key>idVendor</key>
 			<integer>1027</integer>
 		</dict>
-		<key>FT4232H_A</key>
-		<dict>
-			<key>CFBundleIdentifier</key>
-			<string>com.FTDI.driver.FTDIUSBSerialDriver</string>
-			<key>IOClass</key>
-			<string>FTDIUSBSerialDriver</string>
-			<key>IOProviderClass</key>
-			<string>IOUSBInterface</string>
-			<key>bConfigurationValue</key>
-			<integer>1</integer>
-			<key>bInterfaceNumber</key>
-			<integer>0</integer>
-			<key>bcdDevice</key>
-			<integer>2048</integer>
-			<key>idProduct</key>
-			<integer>24593</integer>
-			<key>idVendor</key>
-			<integer>1027</integer>
-		</dict>
 		<key>FT4232H_B</key>
 		<dict>
 			<key>CFBundleIdentifier</key>

一応バックアップをとってから編集して、

sudo kextunload /System/Library/Extensions/FTDIUSBSerialDriver.kext
sudo kextload /System/Library/Extensions/FTDIUSBSerialDriver.kext

すると ls -l /dev/tty.usbserial-* の結果に A が出てこなくなり、JTAG もうまく動くようになる。

JTAG 経由で書きこんで実行してみる

$openocd -f generic-ft4232h.cfg -f raspi.cfg 
Open On-Chip Debugger 0.7.0 (2014-03-02-16:47)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.sourceforge.net/doc/doxygen/bugs.html
Info : only one transport option; autoselect 'jtag'
adapter speed: 1000 kHz
none separate
raspi.arm
Info : clock speed 1000 kHz
Info : JTAG tap: raspi.arm tap/device found: 0x07b7617f (mfg: 0x0bf, part: 0x7b76, ver: 0x0)
Info : found ARM1176
Info : raspi.arm: hardware has 6 breakpoints, 2 watchpoints

上記通りの設定をすると、コマンドでポート 4444 を listen するようになっている。この状態でイメージをアップロードしてみる。

$ telnet localhost 4444
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
> halt
target state: halted
target halted in ARM state due to debug-request, current mode: Supervisor
cpsr: 0x600001d3 pc: 0x000210d0
> load_image main.elf
330812 bytes written at address 0x00008000
downloaded 330812 bytes in 3.823093s (84.502 KiB/s)
> resume 0
>

でうまくいけばとりあえず JTAG 経由で何かしらする準備ができた感じ。

gdb

上記通りの設定をしていると、gdb プロトコル用のポートとして 3333 も listen している。これを使って gdb でデバッグをする。

まず先に telnet するなりなんなりして halt してないとだめっぽい?

$  arm-none-eabi-gdb main.elf
GNU gdb (32-bit ARM EABI Toolchain JBS-FLOAT_IO-SGXXLITE_ML-2013.05-23-v2013.05-20-g7e710b6) 7.4.50.20120716-cvs
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-apple-darwin13.0.2 --target=arm-none-eabi".
For bug reporting instructions, please see:
<https://github.com/jsnyder/arm-eabi-toolchain>...
Reading symbols from /Users/cho45/project/raspberrypi-mruby-bare-metal/main.elf...done.
(gdb)  target remote localhost:4445
Remote debugging using localhost:4445
0x00000000 in ?? ()
(gdb) load
Loading section .text, size 0x52c00 lma 0x8000
Load failed

しかし load がうまくいかず……

Error: 33 words out of 4060 not transferred

みたいなエラーがでる。データラインにノイズが乗ってる?

ちょっとすぐ解決できなそうなので今日はここまで……

  1. トップ
  2. tech
  3. FT4332H (FT2232) で Raspberry Pi (bare metal) を JTAG (Mac OS X)

印刷してピンヘッダにハメれば、どれがどのピンだかわかりやすくなるというツールを作った。PDF (A4) を出力するので、あとは印刷してカッターで切りとれば 100mil のピンヘッダにぴったりあう。

一行目の # からはじまる行がタイトルになり、あとは適当にそれっぽくピン番号と名前のペアを書いていく。名前の語尾に # をつけると反転する。

ほんとはフォントも変えれるようにしたかったんだけど、ブラウザ上で任意のフォントを確実に指定する方法がなくてあきらめた。

作るまで

いろいろな方法があるが、結局 PDF を JS で生成している。jsPDF というライブラリをつかっているので比較的楽に書けた。

他に検討した方法は以下の通り

HTML/CSS

できるならこれが一番良い方法だと思ったが、いまいちクロスブラウザでうまくいくか微妙なのでやめた。table を印刷しようとするとプリントするときだけ width/height が効かなくなったり、つらい。

Canvas

実寸で印刷しようと思うと結局めんどうなのでやめた。

SVG

ハマりそうなのでやめた。

  1. トップ
  2. tech
  3. ピンヘッダのレイアウトを実寸で印刷するツール


ピンヘッダのレイアウトツールに 幅狭の DIP IC のラベルを作る機能を足した。印刷して貼るだけで配線ミスが減らせるので便利

特に DIP IC の場合、文字を入れるスペースが殆どないので、自分に必要な要素だけ厳選して作る必要があって、簡単に別バージョンを作れたほうがいいと思う。ブラウザ上でやって実寸 PDF すぐ印刷できるのはかなり便利だと思う。

  1. トップ
  2. tech
  3. IC のピン配置ラベルを作る

Path::Class と Path::Tiny はほぼ同じことができるモジュールで、どっちを使っても特に変わりはないんですが「やっぱこっち〜」って感じで変えるとハマるような差異があり、今回ハマりましたのでご紹介します。

Path::Class->tempdir, Path::Tiny->tempdir

いずれのモジュールにも、tempdir というメソッドがあり、これはいずれも全く同じ引数をとり、File::Temp を使ってテンポラリディレクトリを掘ってパスオブジェクトを返すだけのメソッドです。

しかし内部で使っている File::Temp のメソッドが少々違うため、CLEANUP まわりの挙動が以下のように異ります。

  • Path::Class は File::Temp::tempdir() を使っている
    • CLEANUP => 0 がデフォルト (デフォルトで削除されない)
    • CLEANUP => 1 を指定した場合 Perl プロセス終了時にファイルが削除される
  • Path::Tiny は File::Temp->newdir() を使っている
    • CLEANUP => 1 がデフォルト (デフォルトで削除される)
    • 返ってきたオブジェクトが DESTROY されるときにファイルが削除される

File::Temp の挙動

  • File::Temp::tempdir() は文字列を返す
  • File::Temp->newdir() はオブジェクトを返す
    • 内部的には File::Temp::tempdir() を呼んでる
    • DESTROY が設定されている

ハマる例

普通ハマらないんですが、アホなことしてるとハマります。

  • Path::Class->tempdir を使っていて、Path::Tiny に変えた場合、その後の処理でファイルが存在していない場合がある
  • Path::Tiny->tempdir を使っていて、Path::Class に変えた場合、ファイルが削除されない場合がある
  1. トップ
  2. tech
  3. File::Temp, Path::Class, Path::Tiny

ZeroPlus LAP-C 16064 をというロジックアナライザを買ってみた。Windows にしか対応してないけど、一応 Mac でも動かせそうな感じなのでやってみた。ただ、ロジックアナライザの値段はソフトウェアの値段感があって、できれば Windows で使いたくなる。

  • sigrok を使う
  • VM 上で Windows を起動する

という方法を試した。結論からすると sigrok を使うほうが楽だけど、機能的にはちゃんと Windows 使ったほうがよさそう。

sigrok を使う

OSS で対応しているソフトがあるので使ってみる

$ brew tap rene-dev/sigrok
$ brew install --HEAD libserialport
$ brew install --HEAD --with-libserialport libsigrok
$ brew install --HEAD libsigrokdecode
$ brew install --HEAD --with-libserialport sigrok-cli
$ brew install --HEAD pulseview

で頑張ると入る。libsigrokdecode がうまく入らなかったので いろいろ頑張った

pulseview

pulseview コマンドで Qt で作られた GUI が起動する。あんまり機能はないので難しくはない感じ…

  • [File] -> [Connect to Device...] で LAP-C を選択する。
  • ツールバーの設定アイコンで Volatage Threshold を適切に設定する
  • サンプリング数とサンプリングレートを設定する
  • Run を押すと出てくる

Decoder の追加はそのまんまなので難しくない Threshold 設定するのが忘れがちでハマりそう。

左のラベルをクリックするとトリガーの条件をかけられる。なぜか状態トリガーしかない。ハード的にはエッジトリガもあるはずなんだけどよくわからない。

captureratio ってのがなんなのかわかりにくいが、トリガーがかかるまでのサンプリングレートを決定する割合みたい。たぶん設定したサンプリングレートに captureratio をかけた値でトリガーがかかるまでサンプルされ、その後設定したサンプリングレートでサンプリングされる、のかな。

よくわからないけど、基本、トリガかけたい信号の周波数の2倍になるように captureratio を設定して、あとは細かいタイミングを見るために高速にサンプリングする、という使いかたなのかな。

sigrok-cli
sigrok-cli  --show --driver zeroplus-logic-cube
zeroplus-logic-cube - ZEROPLUS LAP-C(16064) with 16 probes: A0 A1 A2 A3 A4 A5 A6 A7 B0 B1 B2 B3 B4 B5 B6 B7
Supported configuration options:
    samplerate - supported samplerates:
      100 Hz
      500 Hz
      1 kHz
      5 kHz
      25 kHz
      50 kHz
      100 kHz
      200 kHz
      400 kHz
      800 kHz
      1 MHz
      10 MHz
      25 MHz
      50 MHz
      80 MHz
      100 MHz
    captureratio
    voltage_threshold
    Maximum number of samples: 65536

ここに出てくるサンプリングレート以外を設定するとセグフォして死ぬので注意

 sigrok-cli --driver zeroplus-logic-cube --probes A0=SCL,A1=SDA --output-format bits --samples 1k --config samplerate=25K 

で適当に表示されるはずなんだけど、なぜか手元のだと 4bit ごとにビットが立つみたいな挙動になってしまう…

あと sigrok-cli だとvoltage_threshold は設定できない (パースできない)。エラーもでないので罠。たぶん sigrok-cli あんまり使ってる人いない。

VirtualBox 上の Windows を使う

Mac、VirtualBox 上の Win XP で動かす

接続からドライバインストールまで

VirtualBox は USB extension を入れる必要がある。

ゲスト Windows が起動したら、VirtualBox のメニューの Devices -> USB Devices -> LAP-C〜 を選ぶ (つまり Windows 側へ接続する)

Windows 側では特に何も起きていないように見えるが、デバイスマネージャを見ると不明なデバイスが1つ見える。

そのデバイスのプロパティを開き、ドライバの再インストールボタンを押す。

  • (Windows Update に)「いいえ、今回は接続しません」を選択
  • 「一覧または特定の場所からインストールする」を選択
  • 「次の場所で最適のドライバを検索する」を選択
    • リムーバブルメディアは検索しない
    • 「 次の場所を含める」に C:\Program Files\PC-Based Instrument\ZEROPLUS\DRIVER を入れる
  • 次へをやると1段階進む
  • 今度は再び自動的にドライバのインストールウィザードが開くので、同じように進める
  • ドライバの選択では一番新しそうなのを選ぶ

これで起動はするようになる。

使いかた (I2C)

A0 を SCL, A1 を SDA にする場合 (当然これらと GND を結線しておく)

  • [Bus/Signal] → [Channels Setup]
  • [Delete All] して全部消す
  • [Add Bus/Signal] を SCL, SDA 及びバスのデータ表示用に3回押す
  • SCL 用には A0 を選択
  • SDA 用には A1 を選択
  • I2C 用には A0, A1 をどっちも選択
  • [OK] で閉じる

[Tool] -> [Bus Property...] をクリックするとアナイラザを選択できるはずだが、エラって死ぬ……… ので終了。うまくいく方法はわからず。

ZeroPlus に問いあわせてみたら、これは V3.12.02 のバグらしい。Channels Setup から BUS を作ってはダメみたい。

V3.12.02 でうまくいく方法は以下

  • [Bus/Signal] → [Channels Setup]
  • [Restore Default] して元に戻す
  • [OK] で閉じる
  • メイン画面の左側のプローブ一覧から A1, A0 を Shift か Ctrl を押しながら複数選択して、右クリックから [Group into Bus] をする。
  • [Tool] -> [Bus Property...] で開く (開ける)
  • 適当に設定する

あらかじめ対応する DLL を入れる必要がある。http://www.zeroplus.com.tw/logic-analyzer_en/products.php?pdn=10&pdnex=list で、とりあえず Generic Free Protocols を入れたらいい

  • シリアルが必要なアナライザの場合入れるコードは http://www.zeroplus.com.tw/ に登録して出てきたコード
  • 入れる場所は設定画面

で、適当にセットしたらいける。



  1. トップ
  2. tech
  3. Mac OS Xで ZeroPlus LAP-C

hdiutil でできる。

hdiutil segment -o dest -segmentSize 500m  src.dmg

これで複数に分割される。分割されても、全部パートが揃っていれば、最初の一個を開けばそのままマウントできる。

2G以上あるファイルを FAT32 なフラッシュメモリに入れたいときとかで便利。

あるいは sparsebundle にしても入れれると思うけど、 sparsebundle は1つのディレクトリにだいぶたくさんエントリを作るので、しょぼいファイルシステムだとかなり重くなる。

  1. トップ
  2. tech
  3. 巨大な .dmg ファイルを複数に分割する

を作った。Togetter とかで「ああ、こういう話題に言及する人とは関わりあいたくないな」っていうことが時々あると思いますが、そういうときに使える便利なツールです。

余談

僕はホワイトリスト的な、つまり Twitter の場合プライベートアカウントだったりあるいは、そもそもフェイスブックだったり、というやりかたが好きではない。当然ホワイトリスト的なソーシャルネットワークのほうが安全ではあるが、新しい思いのよらなさ、というのがないのは詰らない。

しかし一方で、完全にオープンというのも全くよくない。表現をないがしろにするモヒカン的なゴミクズというのはそこらじゅうにいるし、一瞬でも隙を見せれば攻撃してくる人というのもいる。いろいろ面倒なことになるリスクだったり、思いもよらず傷つくリスクのほうが、思いもよらず良いことがある、というメリットを明かにうわまわる。

何らかの画期的な解決策、というのをいつも考えてはいるが、未だに完全にこれはというものは思いつかない。

そこで、ある程度現状で思いつく中で、なおかつ簡単に実行可能な方法として、関わりあいたくない人を先読みでブラックリストへ登録していく、というのがある。既存のサービスだと「ブロック」という言葉遣いなので、少し抵抗があるが、本来もっと活用すべき機能であると思っている。

  1. トップ
  2. tech
  3. Togetter でまとめられた Twitter ユーザーを一括でブロックするウェブサービス

asm だけで getenv をしてみようとしたらかなり大変で、本当はもっと先の目標があったけど、遥か遠いので、とりあえず getenv しただけで一旦まとめる。

getenv は何をするか?

そもそも、Linux のプロセスイメージをちゃんと知っていないといけない。環境変数ってどこからくるんだ? というところからして、全く知らなかった。getenv とかシステムコールになってて呼んだら出てくるのかと思っていた。

このページがわかりやすかった。

結局以下のようにすれば r0, r1, r2 それぞれに argc argv environ が入るようになる。sp にスタック位置が起動時から入っているので、そこを基準にオフセットを計算するみたい。スタックはアドレスが小さくなるほうに伸びるけど、それの逆方向に argc やら何やらが入っている。

_start:
        mov lr, #0

        /* r0 = argc */
        ldr r0, [sp]
        /* r1 = argv */
        add r1, sp, $0x04

        /* r2 = argc * 4 (skip argv) */
        mov r3, $0x04
        mul r2, r0, r3
        /* r2 += 4 (skip null word) */
        add r2, r2, $0x04
        /* r2 += offset (ok this is environ) */
        add r2, r1

gcc で普通にコンパイルすると libc がこのへんうまいことやってくれているんだなあと思って libc の大事さを感じる。

そして getenv を実装するために最低でも strncmp 的なものが必要だし、strlen もないと出力するとき困る。

environ は文字列の配列なので、レジスタに今何がロードされているのか意識するのがこんがらがってつらい。LL とか触っていると、文字列の配列は文字列のリストにしか見えないので、しばしば実際は文字列のアドレスの配列になっていることを忘れてしまう。

ARM の場合さらに、遠いアドレスにあるメモリを直接参照できないので、さらに1段参照が増えていたりしてややこしい。オペランドの = を使うとそのへんあまり意識せずにすむようになって便利 (遠い場合は自動的に近くに値プールをつくってくれるらしい)

デバッグ方法

ちょっと複雑になってくるともはやデバッガのステップ実行なしではつらい。gdb が使えるのでつかう。as に --gstabs+ オプションをつけてコンパイルしたバイナリを gdb sketch とかで普通に起動してやればよい。レジスタの値は info registers で見ることができる。

$ as --gstabs+ -o sketch.o sketch.s && ld -o sketch -e _start sketch.o
$ gdb sketch
(gdb) b main
Breakpoint 1 at 0x80ac: file sketch.s, line 46.
(gdb) r
Starting program: /home/pi/sketch/sketch 

Breakpoint 1, main () at sketch.s:46
46              bl getenv
(gdb) info registers
r0             0x80c8   32968
r1             0xbefff734       3204446004
r2             0xbefff73c       3204446012
r3             0x101cc  65996
r4             0x0      0
r5             0x0      0
r6             0x0      0
r7             0x0      0
r8             0x0      0
r9             0x0      0
r10            0x0      0
r11            0x0      0
r12            0x0      0
sp             0xbefff730       0xbefff730
lr             0x809c   32924
pc             0x80ac   0x80ac <main+4>
cpsr           0x10     16

コード全文

/*#!as --gstabs+ -o sketch.o sketch.s && ld -o sketch -e _start sketch.o && objdump -d -j .text -j .data sketch && ./sketch
 */

.global _start

.macro sys_exit
    mov r7, $0x01 /* set system call number to 1 (exit) */
    svc $0x00     /* supervisor call  */
.endm

.macro sys_write
    mov r7, $0x04
    svc $0x00     /* supervisor call  */
.endm

_start:
        mov lr, #0

        /* r0 = argc */
        ldr r0, [sp]
        /* r1 = argv */
        add r1, sp, $0x04

        /* r2 = argc * 4 (skip argv) */
        mov r3, $0x04
        mul r2, r0, r3
        /* r2 += 4 (skip null word) */
        add r2, r2, $0x04
        /* r2 += offset (ok this is environ) */
        add r2, r1

        /* save char** environ to global variable */
        ldr r3, =environ
        str r2, [r3]

        /* r0 = argc, r1 = argv, r2 = environ */
        bl main

        /* not reached */
        mov r0, $0xff
        sys_exit

main:
        /* getenv("USER") */
        adr r0, USER
        bl getenv
        /* if "USER" is not in ENV */
        cmp r0, $0x00
        bleq error

        bl puts

        mov r0, $0x00 /* set exit status to 0 */
        sys_exit

USER:
        .asciz "USER"
        .align 2

error:
        mov r0, $0x01
        sys_exit

strncmp: /* char* s1, char* s2, size_t len -> 1|0 */
        stmfd sp!, {v1-v5, lr} /* save variable resistors and returning address */
        mov r3, $0x00 /* result */
1:      cmp r2, $0x00
        beq 2f  /* if (r2 == 0) goto 2 */
        sub r2, r2, $0x01 /* len-- */
        ldrb r4, [r0], $0x01 /* r4 = *s1++ */
        ldrb r5, [r1], $0x01 /* r5 = *s2++ */
        cmp r4, r5
        beq 1b /* if (r4 == r5) goto 1 */
        add r3, $0x01 /* r3++ (this function always returns 1 when the comparing fails) */
2:
        mov r0, r3
        ldmfd sp!, {v1-v5, pc} /* restore variable resistors and set pc to returning address */

strlen: /* char* str* -> uint */
        mov r1, $0x00 /* r1 = result */
        /* r2 = *str++ (ldrb = load byte, and r0 increment after) */
1:      ldrb r2, [r0], $0x01
        cmp r2, $0x00
        addne r1, r1, $0x01 /* if (r2 != 0) r1++ */
        bne 1b  /* if (r2 != 0) goto 1; */
        mov r0, r1
        mov pc, lr

getenv: /* char* name -> char* */
        stmfd sp!, {v1-v5, lr}
        /* v1 = name */
        mov v1, r0
        /* v2 = strlen(r0) */
        bl strlen
        mov v2, r0
        /* v3 = environ char** */
        ldr v3, =environ
        ldr v3, [v3]

1:      /* if (strncmp(name, *environ, len) == 0) { */
        mov r0, v1
        ldr r1, [v3]
        /* *environ != NULL */
        cmp r1, $0x00
        beq 2f
        mov r2, v2
        bl strncmp
        cmp r0, $0x00
            /* if (*environ)[len] == '=') { */
            ldreq r0, [v3]
            ldreqb r0, [r0, v2]
            cmpeq r0, #'=
            beq 3f
            /* } */

        /* environ++ */
        add v3, $0x04
        b 1b

2:
        /* not found return NULL */
        mov r0, $0x00
        ldmfd sp!, {v1-v5, pc}

3:      /* found and return address */
        ldreq r0, [v3]
        add r0, r0, v2
        add r0, r0, $0x01 /* skip '=' */
        ldmfd sp!, {v1-v5, pc}

puts:
        stmfd sp!, {v1-v5, lr}
        mov v1, r0
        bl strlen
        mov r2, r0
        mov r1, v1
        mov r0, $0x01
        sys_write
        mov r0, $0x01
        adr r1, linefeed
        mov r2, $0x01
        sys_write
        ldmfd sp!, {v1-v5, pc}

        linefeed:
            .byte '\n
            .align 2



.section .bss
    .align 2

environ: .word 0
  1. トップ
  2. tech
  3. ARM Linux EABI の asm で getenv する

いい時代なので、実機がなくても qemu で環境をつくることができる。

qemu を入れる

brew install qemu

で入る

イメージを用意する

ここにある debian のイメージを例にすると、適当に必要なファイルをダウンロードするだけ

  • vmlinuz-3.2.0-4-versatile
  • initrd.img-3.2.0-4-versatile
  • debian_wheezy_armel_standard.qcow2

起動

qemu-system-arm -M versatilepb -kernel vmlinuz-3.2.0-4-versatile -initrd initrd.img-3.2.0-4-versatile -hda debian_wheezy_armel_standard.qcow2 -append "root=/dev/sda1" -m 256 -redir tcp:2200::22

で起動させる。中から外のネットワークには出られるが中に繋ぐ方法がないっぽい?ので -redir tcp:2200::22 でポートフォワード的なことをしている。

ssh

ssh -p2200 root@localhost
password: root

Debian だと開発ツールがデフォルトで入っていないのがうざいけど、しかたない


ref.

  1. トップ
  2. tech
  3. ARM Linux EABI on QEMU

ngResource は単にAPIのラッパーという感じではなくて、JS でサーバ側のモデルとうまく同期するように作られている。

最も簡単な例だと以下のように使うが、Entry.get は XHR が完了する前に、とりあえず空のオブジェクトが返るようになっており、XHR の完了とともに破壊的に書きかえられる。これにより、entry の変更がすぐ全体に伝わるようになっている。

var Entry = $resource('/entry/:id');
$scope.entry = Entry.get({ id : 0 });

デフォルトで定義されている query/get/save/delete だけを見ると単に REST API のラッパーのように見えるが、独自のメソッドを追加するとより理解しやすいコードを書ける。

以下のコードは、デフォルトで下書き状態で生成される Entry オブジェクトを、後から publish 状態に変えるような挙動を想定している。単に $save() とかを使ってもいいが、専用のメソッドを生やすことでやりたいことを明確にできる。

var Entry = $resource('/entry/:id', {}, {
	'publish': { method: 'PUT', params : { publish : 1 } }
});
$scope.entry = Entry.get({ id : 0 });

....
$scope.doPublish = function () {
	$scope.entry.$publish(function () {
		alert('entry published!');
	});
}

この独自に定義したメソッドの場合も XHR が完了すると、API のレスポンスで元の entry インスタンスは破壊的に変更がかかる。すなわち $scope.entry を改めて自分で更新する必要はない。Angular の場合、オブジェクトの変更がうまいことビューに反映されるようになっているので、これだけでビューの更新までかかるコードになっている。

ngResource とサーバ側 API とうまく協調させることで、自動的にビューの更新までできるようになる。

ngResource の挙動とサーバサイドのAPIのインターフェイスをあわせる方法は前に書いた。ngResource は定義方法がいまいちわかりにくいし、挙動も若干マジカルだが、うまく使えば余計なことを気にせずにかっこよくビューまで一体したコードが書ける。

  1. トップ
  2. tech
  3. ngResource は何が便利なのか?

単に実行したディレクトリのファイル名を表示するだけのプログラムを asm で書いてみる。

普段全くディレクトリエントリの構造を意識しないけど、システムコールを直接呼ぼうと思うと意識せざるを得ない。

使うシステムコールは以下の通り

  • open
  • getdents
  • close
  • write (表示用)
  • exit

libc レベルだと opendir/closedir というふうにディレクトリ対象の open 操作は分かれているので、システムコールもそうなのかと思っていたけど、そうではなく普通の open/close で統一されている。ディレクトリ内容を読むには readdir というシステムコールもあるが、getdents が現代版らしいので、最初からこちらを使う。

getdents

open/close はともかく、getdents の挙動を理解するのに苦労した。

struct linux_dirent {
    unsigned long  d_ino;     /* Inode number */
    unsigned long  d_off;     /* Offset to next linux_dirent */
    unsigned short d_reclen;  /* Length of this linux_dirent */
    char           d_name[];  /* Filename (null-terminated) */
                      /* length is actually (d_reclen - 2 -
                         offsetof(struct linux_dirent, d_name)) */
    /*
    char           pad;       // Zero padding byte
    char           d_type;    // File type (only since Linux
                              // 2.6.4); offset is (d_reclen - 1)
    */

}
http://linuxjm.sourceforge.jp/html/LDP_man-pages/man2/getdents.2.html

以上のような構造体を渡したバッファに書きこんでくれるのだけれど、なんで構造体の後ろのほうコメントアウトになってるの?って感じ。よく定義を読んだら d_name[] は文字列へのポインタではなく文字列そのものなので、ここは可変長になっていて、コメントアウトされている分は d_reclen から実際の位置を計算する必要があることがわかる。また、このシステムコールはバッファが許す限りこのエントリを連続で書いてくるので、d_reclen をポインタに足しながら全部読み出す必要がある。

挙動さえ理解できれば難しくないので、とりあえず C レベルで1回書いたほうが早かったかもしれない。

ソートとかしていないので、表示される順番は ls -1f したときと同じになる。

errno

libc レベルだと errno というグローバル変数にエラー番号が入るが、システムコールを直で呼ぶ場合、r0 にエラー番号の符号を反転させた値が返ってくる。

つまり、libc はシステムコールから負の値が返ってくると、符号を反転して errno にセットして、C レベルの関数では -1 を返すという挙動をするみたい。

コード全文

/*#!as --gstabs+ -o ls.o ls.s && ld -o ls -e _start ls.o && objdump -d -j .text -j .data ls && ./ls
 */

.global _start
.macro sys_exit
    mov r7, $0x01 /* set system call number to 1 (exit) */
    svc $0x00     /* supervisor call  */
.endm

O_RDONLY = 0x0000
.macro sys_open
    mov r7, $0x05
    svc $0x00     /* supervisor call  */
.endm

.macro sys_write
    mov r7, $0x04
    svc $0x00     /* supervisor call  */
.endm

.macro sys_close
    mov r7, $0x06
    svc $0x00     /* supervisor call  */
.endm

.macro sys_getdents
    mov r7, $0x8d
    svc $0x00     /* supervisor call  */
.endm

.section .text
_start:
        bl main

        /* not reached */
        mov r0, $0xff
        sys_exit

main:
        /* open */
        ldr r0, =current_dir
        mov r1, #O_RDONLY
        sys_open
        cmp r0, $0x00
        rsble r0, r0, #0
        blle error
        mov v1, r0

1:
        /* getdents */
        mov r0, v1
        ldr r1, =dentry_buffer
        mov r2, #dentry_buffer_len
        sys_getdents
        cmp r0, $0x00
        beq 2f
        mov v2, r0 /* read bytes */
        rsblt r0, r0, #0
        bllt error


        ldr v3, =dentry_buffer
3:
        ldrh v5, [v3, #8] /* linux_dirent d_reclen */

        mov r0, $0x01
        add r1, v3, #10
        sub r2, v5, #12
        sys_write
        mov r0, $0x0a

        push {r0}
        mov r0, $0x01
        mov r1, sp
        mov r2, #1
        sys_write
        pop {r0}

        sub v2, v2, v5 /* len -= d_reclen */
        add v3, v3, v5 /* buffer += d_reclen */
        cmp v2, $0x00
        bne 3b

        b 1b

2:
        /* close */
        mov r0, v1
        sys_close

        mov r0, $0x00 /* set exit status to 0 */
        sys_exit

current_dir:
    .asciz "."
    .align 2

error:
        cmp r0, $0x00
        moveq r0, $0x01
        sys_exit

.section .bss
    .align 2

buffer: .skip 4096
dentry_buffer:
    .skip 4096
    dentry_buffer_len = . - dentry_buffer
  1. トップ
  2. tech
  3. ARM Linux EABI の asm で簡単な ls を作る

asm で動的にメモリ取得をしたいと思っても、malloc は libc の関数であるので、libc 依存しないなら自分で書かねばらない。

ということで、動的なメモリ確保を書いてみる。malloc を書く、というと荷が重すぎるので、単にプロセスが確保するメモリを動的に増やしていく、という大変基礎的な部分だけをやってみる。

brk

まず brk システムコールを試す。brk は program break の意味らしい。program break とはプログラムの data セクションの最後のことで、brk システムコールはこの data セクションの最後の位置を変更するというシステムコールになっている。これにより、プロセスにメモリを割りあてたり、OS に返したりできる。いわゆるヒープ領域というやつ。

program break の初期値

brk システムコールの引数は変更後の program break のアドレスになっている。つまり、初期値をどこかから持ってきて、自分で必要なサイズ分インクリメントして渡さなければならない。

libc レベルでは sbrk という関数も提供されていて、こちらの引数は単に increment (または decrement) する数だけを指定する。要は sbrk 内でアドレス計算をやってくれている。malloc では内部的には直接 brk を使わず sbrk を使ってメモリの取得開放を行っている。

最初この初期値は ld script で定義されている _end (bss セクションの最後を示す) でいいのかなと思い、

brk: .word _end

としてリンク時解決にしてみたけど、gdb でステップ実行しながら brk の返り値を確かめてみると、実際の program break と _end は違うことがわかった。

どこからもってくればいいのかと思ったけど、brk システムコールを 0 で呼んで、現在の brk アドレスを取得すればいいようだ (引数が不正だったり、メモリがない場合 brk は単に現在の break アドレスを返す)。ほんとかと思ったので libc のコードを読んでみたが、libc の sbrk 実装もそのようになっていたので由緒正しい。

実装

機能的には sbrk 相当のものなので sbrk という名前にしてある。r0 に欲しいサイズを入れて bl sbrk すると、要求したバイト数確保して、先頭アドレスを返す。

.macro sys_brk
    mov r7, $0x2d
    svc $0x00     /* supervisor call  */
.endm

sbrk: /* uint size -> void* */
        push {lr}
        ldr r3, =brk /* r3 = prev_brk */
        ldr r1, [r3]

        cmp r1, $0x00 /* if prev_brk == 0 */
        bleq sbrk_init

        add r0, r0, r1
        sys_brk
        cmp r0, r1
        blt sbrk_nomem /* curr_brk == prev_brk */
        str r0, [r3] /* update heap_start */
        mov r0, r1
        pop {pc}
sbrk_init:
        push {r0}
        mov r0, $0x00
        sys_brk
        mov r1, r0
        pop {r0}
        mov pc, lr
sbrk_nomem:
        mov r0, $0x00
        pop {pc}

.section .data
brk: .word 0

mmap システムコール

プロセスに動的にメモリを割り当てる方法としては mmap を使う方法もある。mmap の匿名マッピングは単に指定したサイズの連続した領域を確保する。ある程度大きいメモリを確保する場合はこちらのほうが管理が楽みたい。glibc の malloc は 128KB 以上一気に確保しようとすると mmap を使うらしい。

mmap のデメリットはページサイズ単位でしか割当られないことで、1KB だけ欲しい場合でも4KB 程度は割当される。以下の例では 8byte だけリクエストしているが 4096バイトまでは書きこめ、4097バイト目に書こうとすると Segmentation fault になる。

mmap の返り値もエラーの場合は負の数が返るのだけれど、最上位ビットが立っていても有効なメモリアドレスという場合もあるので、単に負かどうかでは判定できない。なんかよくわからないけど、-4096よりも大きい場合だけエラーとして扱いのがセオリーっぽい?

PROT_NONE = 0x00
PROT_READ = 0x01
PROT_WRITE = 0x02
PROT_EXEC = 0x04
MAP_ANONYMOUS = 0x20
MAP_PRIVATE = 0x02
.macro sys_mmap
    mov r7, $0xc0 /* sys_mmap_pgoff */
    svc $0x00     /* supervisor call  */
.endm

.macro sys_munmap
    mov r7, $0x5b
    svc $0x00     /* supervisor call  */
.endm

main:
        /* mmap */
        mov r0, #0 /* start */
        mov r1, #8 /* length */
        mov r2, #PROT_READ /* prot */
        orr r2, r2, #PROT_WRITE
        mov r3, #MAP_ANONYMOUS
        orr r3, r3, #MAP_PRIVATE /* flags */
        mov r4, #-1 /* fd */
        mov r5, #0 /* page offset */
        sys_mmap
        cmn r0, #4096 /* if (r0 > -4096) */
        rsbhs r0, r0, #0
        blhs error
        mov v1, r0

        /* write 4096 bytes (SEGV on 4097) */
        mov r2, r0                  
        ldr r3, =4096

1:
        mov r0, $0x2e
        strb r0, [r2], $0x01

        sub r3, r3, #1
        cmp r3, #0
        bne 1b

        mov r0, $0x01
        mov r1, v1
        mov r2, #4096
        sys_write

        mov r0, v1 /* start */
        mov r1, #8 /* length */
        sys_munmap

        mov r0, $0x00 /* set exit status to 0 */
        sys_exit

error:
        cmp r0, $0x00
        moveq r0, $0x01
        sys_exit

brk か mmap か

malloc はどっちも使っているようだけど、いまいち brk を使うメリットがわからない。

  • 小さいデータの場合 brk で段階的に伸ばしてシステムコール呼ぶ回数を減らすほうがよい?
    • mmap である程度メモリ確保しちゃえばいいんじゃ
  • メモリ再割当のコストが減らせる?
    • 足りなくなったら mremap したらだめ? mremap は Linux 限定
  • mmap が返すアドレスは毎回適当なので、管理コストが増える?
    • brk なら全部必ず連続するのが便利?

よくわからなかった。

glibc 以外の malloc 実装を軽く調べた感じだと、OpenBSD は mmap しか使わず、jemalloc もオプションで指定しない限り mmap だけを使うようだった。

  1. トップ
  2. tech
  3. ARM Linux EABI の asm で動的メモリ確保

前文

たとえ全く使ったことがない言語であっても、それがスクリプト言語であれば1日もかからない内容をアセンブリ言語で実装するということは、時間リソースを大変富豪的に使うプログラミングである。

レポジトリ

仕様

あまり仕様が多いとつらいので絞りに絞り以下だけ実装することにした

  • インデックスでのリスト表示
  • パーマリンクページ表示

制限事項など

  • たどるディレクトリは1階層だけに制限
  • エントリファイルサイズには制限はない
  • テンプレートファイルサイズは4KBまで
  • ファイル数に制限はない
  • テンプレートはhtml 1種類のみ (flavour 機能はない)
  • プラグイン機能もない

設計

せっかくなので覚えたことを使おうと以下のようにしてある。大変メモリとシステムコールを富豪的に使う

  • エントリのファイル名などを持つ構造体の配列は brk で段々広げてヒープ領域に入れる
  • ファイル本文・タイトルは mmap で確保した領域に入れる
    • すなわちどんなに小さいファイルでも 4KB 確保される
    • 直接ファイルを mmap してないのは後述

コード全文

/*#!as --gstabs+ -o blosxasm.o blosxasm.s && ld -o blosxasm -e _start blosxasm.o && objdump -d -j .text -j .data blosxasm && ./blosxasm
 */

.global _start

.macro sys_exit
    mov r7, $0x01 /* set system call number to 1 (exit) */
    svc $0x00     /* supervisor call  */
.endm

.macro sys_read
    mov r7, $0x03
    svc $0x00     /* supervisor call  */
.endm

.macro sys_write
    mov r7, $0x04
    svc $0x00     /* supervisor call  */
.endm

O_RDONLY = 0x0000
.macro sys_open
    mov r7, $0x05
    svc $0x00     /* supervisor call  */
.endm

.macro sys_close
    mov r7, $0x06
    svc $0x00     /* supervisor call  */
.endm

.macro sys_getdents
    mov r7, $0x8d
    svc $0x00     /* supervisor call  */
.endm

.macro sys_stat
    mov r7, $0x6a
    svc $0x00     /* supervisor call  */
.endm

.macro sys_brk
    mov r7, $0x2d
    svc $0x00     /* supervisor call  */
.endm

PROT_NONE = 0x00
PROT_READ = 0x01
PROT_WRITE = 0x02
PROT_EXEC = 0x04
MAP_ANONYMOUS = 0x20
MAP_PRIVATE = 0x02
.macro sys_mmap
    mov r7, $0xc0 /* sys_mmap_pgoff */
    svc $0x00     /* supervisor call  */
.endm

.macro sys_munmap
    mov r7, $0x5b
    svc $0x00     /* supervisor call  */
.endm

.macro sys_mremap
    mov r7, $0xa3
    svc $0x00     /* supervisor call  */
.endm


.section .data
brk: .word 0

config:
    var_title:
        .ascii "blosxasm-arm-linux-eabi"
        var_title_len = . - var_title
        .align 2
    var_home:
        .ascii "/blosxasm.cgi/"
        var_home_len = . - var_home
        .align 2
    var_data_dir:
        .asciz "data/"
        .align 2
    var_head_path:
        .asciz "head.html"
        .align 2
    var_story_path:
        .asciz "story.html"
        .align 2
    var_foot_path:
        .asciz "foot.html"
        .align 2


.section .text
_start:
        mov lr, #0

        /* r0 = argc (not used) */
        ldr r0, [sp]
        /* r1 = argv (not used) */
        add r1, sp, $0x04

        /* r2 = argc * 4 (skip argv) */
        mov r3, $0x04
        mul r2, r0, r3
        /* skip null word */
        add r2, r2, $0x04
        add r2, r1

        /* save char** environ */
        ldr r0, =environ
        str r2, [r0]

        bl main

        mov r0, $0xff
        sys_exit

main:
        adr r0, PATH_INFO
        bl getenv
        cmp r0, $0x00 /* if env is not set */
        adreq r0, PATH_INFO_default
        mov v5, r0 /* v5 = PATH_INFO */
        bl strlen
        cmp r0, $0x00 /* if env is set but empty */
        adreq v5, PATH_INFO_default


        ldr r0, =var_head_path
        bl template

        /**
         * append entry to brk (like dynamic array)
         */
        mov r0, $0x00
        bl sbrk
        mov v1, r0 /* v1 = first brk */

        ldr r0, =var_data_dir
        ldr r1, =file_callback
        bl dentries

        /**
         * loop each entry
         */
        ldr v2, =entries_count
        ldr v2, [v2] /* v2 = entry count */

        ldr v3, =current_entry /* v3 = pointer to current entry address */

        main_entry_loop:
            str v1, [v3]

            /* findstr(entry.name, path_info+1, strlen(path_info)-1) */
            mov r0, v5
            bl strlen
            sub r2, r0, #1
            add r0, v1, #entry_path
            add r1, v5, #1
            bl findstr
            cmp r0, $0x00

            ldreq r0, =var_story_path
            bleq template

            sub v2, v2, $0x01
            cmp v2, $0x00
            add v1, v1, #entry_buffer_len
            bne main_entry_loop

        ldr r0, =var_foot_path
        bl template


        mov r0, $0x00 /* set exit status to 0 */
        ldr r1, =buffer
        sys_exit

file_callback:
        stmfd sp!, {r1-r3, v1-v5, lr}

        mov v1, r0 /* v1 = name */
        mov v2, r1 /* v2 = name_len */

        /* skip . files */
        ldrb r0, [v1]
        cmp r0, #'.
        bleq 1f

        mov r0, v1
        bl read_entry

1:
        ldmfd sp!, {r1-r3, v1-v5, pc}

USER:
        .asciz "USER"
        .align 2

PATH_INFO:
        .asciz "PATH_INFO"
        .align 2

PATH_INFO_default:
        .asciz "/"
        .align 2

error:
        cmp r0, $0x00
        moveq r0, $0x01
        sys_exit

divmod: /* uint numerator, uint devider -> quo, rem */
        stmfd sp!, {v1-v5, lr}
        mov v1, r0 /* num */
        mov v2, r1 /* div */

        mov r0, $0x00 /* quo */
        mov r1, $0x00 /* rem */
        mov r2, #32 /* i */
1:
        sub r2, r2, $0x01
        /* rem = rem << 1*/
        mov r1, r1, LSL #1
        /* num >> i */
        mov r3, v1, LSR r2
        /* num & 1 */
        and r3, r3, #1
        /* rem[0] = num[i] */
        orr r1, r1, r3
        /* rem >= div */
            cmp r1, v2
            subge r1, r1, v2
            movge r3, #1
            orrge r0, r0, r3, LSL r2
        cmp r2, $0x00
        bne 1b


        ldmfd sp!, {v1-v5, pc}

base10: /* int numerator, char* buffer -> int length */
        stmfd sp!, {v1-v5, lr}
        mov v1, r1
        mov v2, $0x00 /* length */

1:
        mov r1, #10
        bl divmod
        push {r1} /* for getting digit from top */
        add v2, v2, $0x01
        cmp r0, $0x00
        bne 1b

        mov r2, v2
2:
        sub r2, r2, $0x01
        pop {r0}
        add r0, r0, $0x30
        strb r0, [v1], $0x01
        cmp r2, $0x00
        bne 2b

        mov r0, $0x00
        strb r0, [v1]

        mov r0, v2
        ldmfd sp!, {v1-v5, pc}

base16: /* int numerator, char* buffer -> int length */
        stmfd sp!, {v1-v5, lr}
        mov v1, r1
        mov v2, $0x00 /* length */
1:
        and r1, r0, $0x0f
        cmp r1, $0x09
        addle r1, r1, $0x30
        addgt r1, r1, $0x57
        push {r1}

        add v2, v2, $0x01
        mov r0, r0, LSR #4
        cmp r0, $0x00
        bne 1b

        mov r2, v2
2:
        sub r2, r2, $0x01
        pop {r0}
        strb r0, [v1], $0x01
        cmp r2, $0x00
        bne 2b

        mov r0, $0x00
        strb r0, [v1]

        mov r0, v2
        ldmfd sp!, {v1-v5, pc}

strncmp: /* char* s1, char* s2, size_t len -> 1|0 */
        stmfd sp!, {v1-v5, lr}
        mov r3, $0x00 /* result */
1:      cmp r2, $0x00
        beq 2f  /* if (r2 == 0) goto 2 */
        sub r2, r2, $0x01 /* len-- */
        ldrb r4, [r0], $0x01
        ldrb r5, [r1], $0x01
        cmp r4, r5
        addne r3, $0x01
        beq 1b
2:
        mov r0, r3
        ldmfd sp!, {v1-v5, pc}

strlen: /* char* str -> uint */
        stmfd sp!, {r1-r2, lr}
        mov r1, $0x00
        /* r2 = *str++ (ldrb = load byte, and r0 increment after) */
1:      ldrb r2, [r0], $0x01
        cmp r2, $0x00
        addne r1, r1, $0x01 /* if (r2 != 0) r1++ */
        bne 1b  /* if (r2 != 0) goto 1; */
        mov r0, r1
        ldmfd sp!, {r1-r2, pc}

strcpy: /* char* dest, char* src -> dest */
        stmfd sp!, {r0-r2, lr}
1:
        ldrb r2, [r1], $0x01
        strb r2, [r0], $0x01
        cmp r2, $0x00
        bne 1b

        ldmfd sp!, {r0-r2, pc}

strcat: /* char* dest, char* src -> dest */
        stmfd sp!, {r0-r2, v1-v5, lr}

1:
        ldrb r2, [r0], $0x01
        cmp r2, $0x00
        bne 1b
        sub r0, r0, $0x01
2:
        ldrb r2, [r1], $0x01
        strb r2, [r0], $0x01
        cmp r2, $0x00
        bne 2b

        ldmfd sp!, {r0-r2, v1-v5, pc}

findstr: /* char* str, char* search, int len_of_search -> uint */
        stmfd sp!, {v1-v5, lr}

        mov v1, r0
        mov v2, r1
        mov v3, r2

        mov v4, v1 /* save original address */

        cmp v3, $0x00
        moveq r0, $0x00
        ldmeqfd sp!, {v1-v5, pc}

1:
        ldrb r0, [v1]
        cmp r0, $0x00
        beq 2f
        mov r0, v1
        mov r1, v2
        mov r2, v3
        bl strncmp
        cmp r0, $0x00
        addne v1, $0x01
        bne 1b

        sub r0, v1, v4
        ldmfd sp!, {v1-v5, pc}

2:
        mov r0, #-1
        ldmfd sp!, {v1-v5, pc}


sbrk: /* uint size -> void* */
        push {lr}
        ldr r3, =brk /* r3 = prev_brk */
        ldr r1, [r3]

        cmp r1, $0x00 /* if prev_brk == 0 */
        bleq sbrk_init

        add r0, r0, r1
        sys_brk
        cmp r0, r1
        blt sbrk_nomem /* curr_brk == prev_brk */
        str r0, [r3] /* update heap_start */
        mov r0, r1
        pop {pc}
sbrk_init:
        push {r0}
        mov r0, $0x00
        sys_brk
        mov r1, r0
        pop {r0}
        mov pc, lr
sbrk_nomem:
        mov r0, $0x00
        pop {pc}

getenv: /* char* name -> char* */
        stmfd sp!, {v1-v5, lr}
        /* v1 = name */
        mov v1, r0
        /* v2 = strlen(r0) */
        bl strlen
        mov v2, r0
        /* v3 = environ char** */
        ldr v3, =environ
        ldr v3, [v3]

1:      /* if (strncmp(name, *environ, len) == 0) { */
        mov r0, v1
        ldr r1, [v3]
        mov r2, v2
        bl strncmp
        cmp r0, $0x00
            /* if (*environ)[len] == '=') { */
            ldreq r0, [v3]
            ldreqb r0, [r0, v2]
            cmpeq r0, #'=
            beq 2f
            /* } */

        /* environ++ */
        add v3, $0x04
        /* *environ != NULL */
        ldr r1, [v3]
        cmp r1, $0x00
        bne 1b

        /* not found return NULL */
        mov r0, $0x00
        ldmfd sp!, {v1-v5, pc}

2:      /* found and return address */
        ldreq r0, [v3]
        add r0, r0, v2
        add r0, r0, $0x01 /* skip '=' */
        ldmfd sp!, {v1-v5, pc}

dentries: /* char* path, (void)(callback(name, name_len)) */
        stmfd sp!, {r1-r3, v1-v5, lr}

        mov ip, r1

        /* open */
        mov r1, #O_RDONLY
        sys_open
        cmp r0, $0x00
        rsble r0, r0, #0
        blle error
        mov v1, r0

1:
        /* getdents */
        mov r0, v1
        ldr r1, =dentry_buffer
        mov r2, #dentry_buffer_len
        sys_getdents
        cmp r0, $0x00
        beq 2f
        mov v2, r0 /* read bytes */
        rsblt r0, r0, #0
        bllt error


        ldr v3, =dentry_buffer
3:
        ldrh v5, [v3, #8] /* linux_dirent d_reclen */

        add r0, v3, #10
        mov r1, v5
        sub r1, r1, #12
        blx ip /* callback */

        sub v2, v2, v5 /* len -= d_reclen */
        add v3, v3, v5 /* buffer += d_reclen */
        cmp v2, $0x00
        bne 3b

        b 1b

2:
        /* close */
        mov r0, v1
        sys_close
        ldmfd sp!, {r1-r3, v1-v5, pc}

read_stat: /* char* name */
        stmfd sp!, {r0-r3, v1-v5, lr}
        ldr r1, =stat_buffer
        sys_stat
        cmp r0, $0x00
        rsblt r0, r0, #0
        bllt error
        ldmfd sp!, {r0-r3, v1-v5, pc}

read_entry: /* char* name, int name_len */
        stmfd sp!, {r1-r3, v1-v5, lr}

        mov v1, r0

        /* expand heap */
        mov r0, #entry_buffer_len
        bl sbrk
        mov v3, r0 /* v3 = entry_address */

        /* copy path */
        add r0, v3, #entry_path
        mov r1, v1
        bl strcpy

        /* r0 = ["data/" + name] */
        ldr r0, =buffer
        ldr r1, =var_data_dir
        bl strcpy
        mov r1, v1
        bl strcat
        mov v1, r0 /* v1 = path adr */

        mov r0, v1
        bl read_stat

        /* copy mtime */
        ldr r0, =st_mtime
        ldr r0, [r0]
        str r0, [v3, #entry_mtime]

        ldr v2, =st_size
        ldr v2, [v2] /* v2 = file size */

        push {r4, r5}
        /* mmap for file contents */
        mov r0, #0
        mov r1, v2
        mov r2, #PROT_READ
        orr r2, r2, #PROT_WRITE
        mov r3, #MAP_ANONYMOUS
        orr r3, r3, #MAP_PRIVATE
        mov r4, #-1
        mov r5, #0
        sys_mmap
        cmn r0, #4096
        rsbhs r0, r0, #0
        blhs error
        mov r3, r0 /* r3 = mmapped address */
        pop {r4, r5}

        str r3, [v3, #entry_title]

        /* open */
        mov r0, v1
        mov r1, #O_RDONLY
        sys_open
        cmp r0, $0x00
        rsble r0, r0, #0
        blle error
        mov v5, r0 /* v5 = fd */

        /* read */
1:
        mov r0, v5
        mov r1, r3
        mov r2, #4096
        sys_read
        cmp r0, $0x00
        rsblt r0, r0, #0
        bllt error
        add r1, r0
        bne 1b

        /* close */
        mov r0, v5
        sys_close

        
        /* find first \n */
        mov r0, r3
2:
        ldrb r1, [r0], $0x01
        cmp r1, $0x0a
        cmpne r1, $0x00
        bne 2b
        /* replace first \n to \0 to splitting title and body */
        mov r1, $0x00
        strb r1, [r0, #-1]
        str r0, [v3, #entry_body]

        ldr r0, =entries_count
        ldr r1, [r0]
        add r1, r1, $0x01
        str r1, [r0]

        ldmfd sp!, {r1-r3, v1-v5, pc}

template: /* char* name */
        stmfd sp!, {r0-r3, v1-v5, lr}

        /* open() */
        mov r1, #O_RDONLY
        sys_open
        cmp r0, $0x00
        beq error
        mov v1, r0 /* v1 = fd */

        ldr v2, =buffer /* v2 = buffer */

        mov r1, v2
        mov r2, #4096
        sys_read
        mov v3, r0 /* v3 = length */
        mov r0, $0x00
        strb r0, [v2, v3] /* null terminate */

1:
        mov r0, v2
        adr r1, open_variable
        mov r2, #open_variable_len
        bl findstr
        /* check found */
        cmp r0, #-1
        beq 2f

        /* output prev chars */
        mov r2, r0 /* set output length */
        mov r0, $0x01
        mov r1, v2
        sys_write

        /* increment buffer */
        add v2, v2, r2
        add v2, v2, #open_variable_len

        /* find close */
        mov r0, v2
        adr r1, close_variable
        mov r2, #close_variable_len
        bl findstr
        /* check found */
        cmp r0, #-1
        beq 2f
        mov v5, r0 /* variable name length */

        /* replaces */
            /* replace title */
            mov r0, v2
            adr r1, variable_name_blogtitle
            mov r2, v5
            bl strncmp
            cmp r0, $0x00
            bleq variable_blogtitle
            beq 3f

            /* replace home */
            mov r0, v2
            adr r1, variable_name_home
            mov r2, v5
            bl strncmp
            cmp r0, $0x00
            bleq variable_home
            beq 3f

            /* replace path */
            mov r0, v2
            adr r1, variable_name_path
            mov r2, v5
            bl strncmp
            cmp r0, $0x00
            bleq variable_path
            beq 3f

            /* replace title */
            mov r0, v2
            adr r1, variable_name_title
            mov r2, v5
            bl strncmp
            cmp r0, $0x00
            bleq variable_title
            beq 3f

            /* replace body */
            mov r0, v2
            adr r1, variable_name_body
            mov r2, v5
            bl strncmp
            cmp r0, $0x00
            bleq variable_body
            beq 3f

            /* replace time */
            mov r0, v2
            adr r1, variable_name_time
            mov r2, v5
            bl strncmp
            cmp r0, $0x00
            bleq variable_time
            beq 3f

            /* replace environ */
            mov r0, v2
            adr r1, variable_name_environ
            mov r2, v5
            bl strncmp
            cmp r0, $0x00
            bleq variable_environ
            beq 3f

3:
        /* increment buffer */
        add v2, v2, v5
        add v2, v2, #close_variable_len

        ldrb r0, [v2]
        cmp r0, $0x00
        bne 1b

2:
        /* output rest */
        ldr r2, =buffer
        sub r2, v2, r2
        sub r2, v3, r2
        mov r0, $0x01
        mov r1, v2
        sys_write

        /* close() */
        mov r0, v1
        sys_close

        ldmfd sp!, {r0-r3, v1-v5, pc}

        open_variable:
            .ascii "#{"
        open_variable_len = . - open_variable
            .align 2
        close_variable:
            .ascii "}"
        close_variable_len = . - close_variable
            .align 2

        variable_name_blogtitle:
            .asciz "blogtitle"
            .align 2
        variable_name_home:
            .asciz "home"
            .align 2
        variable_name_title:
            .asciz "title"
            .align 2
        variable_name_body:
            .asciz "body"
            .align 2
        variable_name_path:
            .asciz "path"
            .align 2
        variable_name_time:
            .asciz "time"
            .align 2
        variable_name_environ:
            .asciz "environ"
            .align 2

        variable_blogtitle:
            stmfd sp!, {r0-r3, lr}
            mov r0, $0x01
            ldr r1, =var_title
            mov r2, #var_title_len
            sys_write
            ldmfd sp!, {r0-r3, pc}
        variable_home:
            stmfd sp!, {r0-r3, lr}
            mov r0, $0x01
            ldr r1, =var_home
            mov r2, #var_home_len
            sys_write
            ldmfd sp!, {r0-r3, pc}
        variable_path:
            stmfd sp!, {r0-r3, v1-v5, lr}
            ldr r0, =current_entry
            ldr r0, [r0]

            add r1, r0, #entry_path
            bl strlen
            mov r2, r0

            mov r0, $0x01
            sys_write
            ldmfd sp!, {r0-r3, v1-v5, pc}
        variable_title:
            stmfd sp!, {r0-r3, v1-v5, lr}
            ldr r0, =current_entry
            ldr r0, [r0]

            add r0, r0, #entry_title
            ldr r0, [r0]
            mov r1, r0

            bl strlen
            mov r2, r0

            mov r0, $0x01
            sys_write
            ldmfd sp!, {r0-r3, v1-v5, pc}
        variable_body:
            stmfd sp!, {r0-r3, v1-v5, lr}
            ldr r0, =current_entry
            ldr r0, [r0]

            add r0, r0, #entry_body
            ldr r0, [r0]
            mov r1, r0

            bl strlen
            mov r2, r0

            mov r0, $0x01
            sys_write
            ldmfd sp!, {r0-r3, v1-v5, pc}
        variable_time:
            stmfd sp!, {r0-r3, v1-v5, lr}
            ldr r0, =current_entry
            ldr r0, [r0]

            add r0, r0, #entry_mtime
            ldr r0, [r0]
            ldr r1, =buffer
            bl base10

            mov r2, r0
            mov r0, $0x01
            ldr r1, =buffer
            sys_write

            ldmfd sp!, {r0-r3, v1-v5, pc}
        variable_environ:
            stmfd sp!, {r0-r3, v1-v5, lr}
            ldr r0, =environ
            ldr r0, [r0]
            ldr r1, =buffer
            bl base16

            mov r2, r0
            mov r0, $0x01
            ldr r1, =buffer
            sys_write

            ldmfd sp!, {r0-r3, v1-v5, pc}

.section .bss
    .align 2

environ: .word 0
buffer: .skip 4096
dentry_buffer:
    .skip 4096
    dentry_buffer_len = . - dentry_buffer
stat_buffer: /* /usr/include/arm-linux-gnueabihf/asm/stat.h  */
    st_dev: .skip 4
    st_ino: .skip 4
    st_mode: .skip 2
    st_nlink: .skip 2
    st_uid: .skip 2
    st_gid: .skip 2
    st_rdev: .skip 4
    st_size: .skip 4
    st_blksize: .skip 4
    st_blocks: .skip 4
    st_atime: .skip 4
    st_atime_nsec: .skip 4
    st_mtime: .skip 4
    st_mtime_nsec: .skip 4
    st_ctime: .skip 4
    st_ctime_nsec: .skip 4
    .skip 4
    .skip 4
    .align 2
    stat_buffer_len = . - stat_buffer
entry_buffer: /* not used: just calculate offsets */
    entry_buffer_path: .skip 256
    entry_buffer_mtime: .skip 4
    entry_buffer_title: .skip 4
    entry_buffer_body: .skip 4
    .align 2
    entry_buffer_len = . - entry_buffer
    entry_path       = entry_buffer_path - entry_buffer
    entry_mtime      = entry_buffer_mtime - entry_buffer
    entry_title      = entry_buffer_title - entry_buffer
    entry_body       = entry_buffer_body - entry_buffer
entries_count: .word 0
current_entry: .word 0

全体の流れ

_start

エントリポイント、スタックポインタから環境変数のアドレスを計算して初期化する処理だけして main を呼ぶ

main

全体の流れが書いてあるサブルーチン

  • PATH_INFO の初期化
  • ヘッダの出力 (template サブルーチンを呼ぶ)
  • brk を準備してエントリリストを構築 (file_callback を渡して dentries サブルーチンを呼ぶ)
  • (ソートをやるつもりで配列にしてあるけどめんどくて実装してない)
  • エントリの出力 (template サブルーチンを呼ぶ)
  • フッタの出力

dentries

設定したディレクトリ以下のファイルを列挙してコールバックを呼ぶサブルーチンになっている。getdents してるだけ

file_callback

dentries から呼ばれる。ファイル名をうけとって、. から始まるファイルを無視しつつ read_entry サブルーチンを呼ぶ。

read_entry サブルーチン

構造体を構築してもろもろ書きこむ。

  • ファイル名 (パスではなく)
  • 更新日時 (ソート用・表示用)
  • タイトル文字列の先頭アドレス
  • 本文文字列への先頭アドレス

更新日時は stat した結果をとって書きこんでいるだけ

タイトル文字列・本文文字列はちょっとめんどうくさい。stat した結果でファイルサイズはわかっているので、必要な分 mmap して確保して、sys_read で全部読みこんでいる。

blosxom は1行目をタイトルとして扱うという仕様になっているけど、最初の \n を \0 に変えるだけで null ターミネートの文字列にわけられ、あとはコピーする必要がないので、そのままアドレスを計算して構造体に入れている。元のファイルは書き換えたくないので直接マッピングはしてない。

template サブルーチン

単に #{varname} みたいな形式を置換している。

その他

依存なしで書いているので、基本的なサブルーチンも自分で書く必要があり、かなり面倒だった。

  • base10
    • 数値を文字列にする
    • 割り算が必要
  • divmod
  • base16
    • ビットシフトだけでいけるので簡単
  • strncmp
    • 文字列が等しいかを判定する
    • libc の実装では 1, 0, -1 で出力されるが必要ないので 0/1 だけ返す
  • strcpy
    • \0 を考慮しながらメモリコピーをする
  • strcat
    • 文字列連結
  • findstr
    • 指定した文字列が見つかった場所を返す
    • 見つからなかったら -1

ほかに書いたもの

デモ

arm で動いてて外部公開できるサーバがないのでない。VPS で qemu で立ちあげればできるけどどこまでやることでもない。

インデックス

パーマリンク

単に1つになるだけ

まとめ

低レベルのコードを書く意義は、低レベルのデバッグ方法に慣れることができる点だと思う。コードリーディングするだけではデバッグ能力はつかない。

参考文献

http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/linux/syscalls.h?id=HEAD
http://strace.git.sourceforge.net/git/gitweb.cgi?p=strace/strace;a=blob;f=linux/arm/syscallent.h;h=132b22ad8aea66c434983e99426c8f6eaed5d114;hb=HEAD
http://linuxjm.sourceforge.jp/html/LDP_man-pages/man2/readdir.2.html


getenv
http://asm.sourceforge.net/articles/startup.html

  1. トップ
  2. tech
  3. アセンブリ (arm-linux-eabi) で富豪的にCGIブログ (blosxom) を作る。

前文

我々は、ブラウザの WebAudio サポートによって、空気をメディアに使う方法を手に入れた。これにより、またひとつクロスドメイン通信の方法が増えたといえよう。

知っている人は少ないと思うが、古代人は空気によって伝送できる程度の周波数を用いてデータ伝送を行っていたという記録が残っている。これは失われた暗黒の技術であって、現在ではもはや ITU という国連の機関によってその存在の記録が残されているのみである。

デモ

小さな画像を音声帯域を使って送受信する

実際にマイク入力を使う場合、

マイク入力はやはり難しくて、周囲の環境によって波形が乱れるとうまくいかなかったりする。

V.21

ITU-T 勧告 V.21 は全二重 300bps の通信プロトコルで、変調方式は単純な FSK (周波数変移変調) になってる。

FSK

FSK (周波数変移変調) は、2つの周波数をそれぞれ 1 と 0 とに割当てて、切替えながら送信することでデータを送信する変調方法。単純でノイズに強いのが特徴。

かなり面倒くさがって WebAudio だけに依存するように書いたので、いちいち全サンプルに対して処理をしていてとても重い。アルゴリズム自体にも、もっと根本的にいい方法がありそうだけれど、知らないので直交信号をそれぞれヘテロダインする方法にしてある。

このへん、ノウハウがなさすぎて精度良く検出するのが僕にはかなり難しかった。

FSK 部分は以下

V.21 の周波数

既設の電話回線をデータ通信に応用する技術なので、可聴帯域の周波数を搬送波に使っており、全二重なのでチャンネルが2つある。つまり使う周波数は4つ。

  • channel No.1: FA=1180Hz, FB=980Hz (200Hz shift)
  • channel No.2: FA=1850Hz, FB=1650Hz (200Hz shift)

WebAudio + 空気の場合、別にこの帯域に拘る必要はなくて、可聴域限界ぎりぎりまで周波数をあげてもいいと思う。実際そのようにしても動くし、速度もあがるしモスキート音なのであまり煩くなくなる。今回はそれだとモデムっぽさがなくなってカッコよくないのでやめた。

プロトコル

プロトコルは以下のようにした

  • 非同期
    • スタートビット1bit
    • ストップビット1.5bit
    • データ8bit

でバイト単位のやりとりをする。1バイトにつき10.5bit使うので、最大で 28.6bytes/sec ぐらい。

さらにまとまったデータ送信のプロトコルとして以下のような xmodem ライクな実装を書いた

  • 送信側が待ちうけ
  • 受信側が NACK 送信
  • 送信側がデータブロック (128bytes) を送信
  • 受信側が ACK 送信
  • ... 繰り返し
  • 送信側が EOT を送信
  • 受信側は ACK を送信して終了

データ部は「SOH, データブロック番号, ビット反転したデータブロック番号」をヘッダにして、最後に CRC8 を付与している。これらが意図した値でなかった場合 NAK を返し、該当ブロックの再送要求をする。また、途中でバイト単位のやりとり自体が失敗した場合にそなえてタイムアウトによる再送もやってある。

まとめ

ポエティックでフィクションな前文を書くのを広めたい。

  1. トップ
  2. tech
  3. WebAudio FSK モデム