2015年07月08日

RPi - Raspberry Pi 2 で周波数変調してミニ FM 局を楽しむ方法

ミニ FM 局を作って遊ぶ。 実用的かは大いに疑問でも科学が大好きな男の子にはたまらないハックですよね。 今日ご紹介するのはジャンパーワイヤと Raspberry Pi 2 さえあれば FM 電波で興奮することができるぜってハックです。

fm_radio

pifm ってなーに?


Raspberry Pi を使って FM(周波数変調)電波を発信する。 僕の記憶が正しければこのハックが公開されたのは「Turning the Raspberry Pi Into an FM Transmitter」が最初で、これを契機に PiFmRds やその他の派生プロジェクトが雨後の筍のように出てきたって流れだと思います。 この pifm が生まれた 2012 年頃の僕といえば、アンテナ代わりのジャンパーワイヤが手元に無く、すぐに試すってわけにはいきませんでした。 その後、国内でもブログや「トランジスタ技術2014年7月号」でチラホラと紹介されるようになり、忘れては思い出しながら「いつか Raspberry Pi 2 でチャレンジしてみるか」って思うようになったんです。




僕は組み込み開発、BROADCOM 製 SoC(System on Chip)、高周波そのどれにも詳しくないのですが、誤解を恐れずにいえば pifm の概要はこうです。 Raspbyerry Pi の持つディジタル入出力端子(GPIO)には PLL(Phase Locked Loop, 位相同期ループ)と呼ばれる入力したリファレンス信号に位相が同期した信号を出力する回路につながった端子(GPIO 4)が存在します。 この PLL へ Raspberry Pi のシステム・クロック(500 MHz)を使い指定した周波数を中心に、音声データに応じてその周波数を変調(分周, Divisor)した信号を入力すると GPIO/ジャンパーワイヤを経由して Raspberry Pi から安定した FM 電波を出力できる!ってワケです。

Raspberry Pi 2 PLL


で、当然この興味深い FM トランスミッターを知れば、知的好奇心豊富な男の子は俺の Raspberry Pi 2 でも動かそう!って思いますよね。 でも残念ながら簡単には動かないです。 一番大きな問題は GPIO を使って周辺機器(これをペリフェラル・デバイスって言います)を制御する為に物理メモリ上にマップされたレジスタって領域の開始アドレスが 0x20000000 (Raspberry Pi)から 0x3F000000 (Raspberry Pi 2)に変更になったからです(これは SoC に依存します)。

実際にはこれ以外にも細かな問題は残っていて、ただ単純に pifm.c やその他の派生コードを編集してペリフェラル・デバイスのアドレスを 0x20xxxxxx から 0x3F0xxxxxx に変えたってだけではうまく動きません。 それでみんな試行錯誤するんですが大体途中でココロが折れることになります。


pi2fm のビルドと wav ファイルのストリーミング


まあ、動かなくても良いかって考え始めていた今日このごろ。 よっちさんのサイトで RaspberryPi 2 でも動くぜ!って記事を見つけて「スゲー」って興奮しました。 ただ、よっちさんのコードは標準入力(パイプ)経由でデータを受け取れなかったり、動的に中心周波数を指定できなかったりと少し気になるところがありました。 それで僕は pifm の派生コードで最もシンプルであろう dpiponi さんの pifm.c をベースに僕が欲しかった新しい pifm を書く(2つのコードをマージする)ことにしました。

名前は pi2fm.c で中身(コード)は次の通りです。

// FM Transmitter for Raspberry Pi 2.
// modified from code at https://github.com/dpiponi/pifm by netbufalo.
//
// page numbers in comments refer to
// http://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf
//
// build:
//   $ gcc -lm -std=c99 -g pi2fm.c -o pi2fm
//

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <math.h>
#include <fcntl.h>
#include <sys/mman.h>

#include <unistd.h>

volatile unsigned char *allof7e;

#define BCM2836_PERI_BASE        0x3F000000 // register physical address.
#define GPIO_BASE (BCM2836_PERI_BASE + 0x200000) // GPIO offset (0x200000).
#define CM_GP0CTL (0x7e101070) // p.107
#define GPFSEL0   (0x7E200000) // p.90
#define CM_GP0DIV (0x7e101074) // p.108

#define ACCESS(offset, type) (*(volatile type*)(offset+(int)allof7e-0x7e000000))

void setup_fm(int state) {
    int mem_fd = open("/dev/mem", O_RDWR|O_SYNC);
    if (mem_fd < 0) {
        printf("can't open /dev/mem\n");
        exit(-1);
    }
    allof7e = (unsigned char *)mmap(
                  NULL,
                  0x01000000,  // len
                  PROT_READ|PROT_WRITE,
                  MAP_SHARED,
                  mem_fd,
                  BCM2836_PERI_BASE // base
              );

    if (allof7e == (unsigned char *)-1) {
        exit(-1);
    }

    // set up GPIO 4 to pulse regularly at a given period.
    struct GPFSEL0_T {
        char FSEL0 : 3;
        char FSEL1 : 3;
        char FSEL2 : 3;
        char FSEL3 : 3;
        char FSEL4 : 3;
        char FSEL5 : 3;
        char FSEL6 : 3;
        char FSEL7 : 3;
        char FSEL8 : 3;
        char FSEL9 : 3;
        char RESERVED : 2;
    };

    // note sure why i can't use next line in place of following three.
    // this is a pure C issue, not a hardware issue.
    //ACCESS(GPFSEL0, struct GPFSEL0_T).FSEL4 = 4; // alternative function 0 (see p.92)
    int tmp = ACCESS(GPFSEL0, int);
    tmp = (tmp | (1<<14)) & ~ ((1<<12) | (1<<13));
    ACCESS(GPFSEL0, int) = tmp;

    struct GPCTL {
        char SRC         : 4;
        char ENAB        : 1;
        char KILL        : 1;
        char             : 1;
        char BUSY        : 1;
        char FLIP        : 1;
        char MASH        : 2;
        unsigned int     : 13;
        char PASSWD      : 8;
    };
    char clock_src_plld = 6; // p.107
    ACCESS(CM_GP0CTL, struct GPCTL) = (struct GPCTL) {clock_src_plld, state, 0, 0, 0, state, 0x5a };
}


void shutdown_fm() {
    static int shutdown = 0;
    if (!shutdown) {
        shutdown = 1;
        printf("\nShutting down\n");
        setup_fm(0);
        exit(0);
    }
}


void modulate(int period) {
    struct CM_GP0DIV_T {
        unsigned int DIV : 24;
        char PASSWD : 8;
    };

    ACCESS(CM_GP0DIV, struct CM_GP0DIV_T) = (struct CM_GP0DIV_T) { period, 0x5a };
}

// set square wave period. See p. 105 and 108
// although DIV is 24 bit the period can only be set to an accuracy of 12 bits.
// the first 12 bits control the pulse length in units of 1/500MHz.
// the next 12 bits are used to dither the period so it averages at the chosen 24 bit value.
// the resulting quare wave is then filtered using MASH.
// see p.105 and http://en.wikipedia.org/wiki/MASH_(modulator)#Decimation_structures
// the 0x5a is a "password"
void playWav(char *filename, int mod, float bandwidth) {
    int fp = STDIN_FILENO;
    if (filename[0]!='-') fp = open(filename, 'r');
    lseek(fp, 22, SEEK_SET); // Skip 44 bytes wave header.
    int len = 512;
    short *data = (short *)malloc(len);
    printf("now broadcasting: %s ...\n", filename);

    int speed = 270; // you can play faster by decreasing this value.
    unsigned int lfsr = 1;
    int readBytes;
    while (readBytes = read(fp, data, len)) {
        for (int j = 0; j<readBytes/2; j++) {
            // compute modulated carrier period.
            float dval = (float)(data[j])/65536.0 * bandwidth;
            int intval = (int)(floor(dval));
            float frac = dval - (float)intval;
            unsigned int fracval = (unsigned int)(frac*((float)(1<<16))*((float)(1<<16)));
            for (int i=0; i<speed; i++) {
                lfsr = (lfsr >> 1) ^ (-(lfsr & 1u) & 0xD0000001u); // Galois LFSR
                modulate(intval + (fracval>lfsr?1:0) + mod);
            }
        }
    }
}


int main(int argc, char **argv) {
    if (argc>1) {
        signal(SIGTERM, &shutdown_fm);
        signal(SIGINT, &shutdown_fm);
        atexit(&shutdown_fm);
        setup_fm(1);
        float freq_out = argc>2?atof(argv[2]):77.7; // center freq
        float bandwidth = argc>3?atof(argv[3]):8; // a.k.a volume
        int freq_pi = 500; // 500 MHz (RPi core_freq?).
        int mod = (freq_pi/freq_out)*4096; // divisor * PAGE_SIZE
        modulate(mod); // initialize carrier.
        printf("starting...\n -> carrier freq: %3.1f MHz\n -> band width: %3.1f\n", freq_out, bandwidth);
        playWav(argv[1], mod, bandwidth);
    } else {
         fprintf(stderr,
                "usage: %s wavfile.wav [freq] [A.K.A volume]\n\n"
                "where wavfile is 16 bit 22.050 kHz Mono.\n"
                "set wavfile to '-' to use stdin.\n"
                "band width will default to 16 if not specified. it should only be lowered!", argv[0]);
    }

    return 0;
}


簡単にいうと 500 MHz のシステム・クロックを使い、引数で指定された周波数(デフォルトは 77.7 MHz)を中心にファイル又は標準入力から読み込んだ wav データ(音声信号)に応じて分周(1 / R)した FM 電波を作り出しています。

さて、このコードをコピーするかダウンロード(pi2fm.tar.gz)して次のコマンドで pi2fm.c をビルドしましょう。 僕は OS には Raspbin を使っていますよ。

pi@raspberrypi $ gcc -lm -std=c99 -g pi2fm.c -o pi2fm


ビルドが終わったら Raspberry Pi 2 の GPIO 4 にジャンパーワイヤ(でなくても良いですが)をつなげ、

Raspberry Pi 2 with jumper wire


次のコマンドで wav ファイルを再生してみて下さい。きっとあなたの受信機からサウンドが流れてくるはずです(最後の周波数は省略可能です)。

pi@raspberrypi $ sudo ./pi2fm sound.wav 77.7


僕のスマートフォン(ZenFone 2)で受信した FM 電波を(買った直後に後継機が発売され全米が泣いた)Bose SoundLink Mini を使って音声出力している動画がこちら。


 
 
ファイル名に - を指定すると標準入力から wav データを読み込むので avconv(libav.org 版 ffmpeg)と組み合わせて mp3 ファイルの変換データを動的に再生(放送)することも可能です。

pi@raspberrypi $ avconv -i sound.mp3 -f s16le -ar 22.05k -ac 1 - | sudo ./pi2fm -


ね、ちょっと遅れ気味かもしれませんが、mp3 ファイルを動的に変換・配信できましたよね。 じゃあ、これでハッピーかっていうとそうじゃなくて僕がパイプを実装した理由はマイク入力をリアルタイムに配信したかったから。 まず適当な USB サウンド・カードを用意して Raspberry Pi に差し込んで下さい。 arecord を使えば録音(キャプチャ)に利用できるデバイスの card 及び device 番号を知ることができます。

pi@raspberrypi $ arecord -l
**** List of CAPTURE Hardware Devices ****
card 1: Device [USB PnP Sound Device], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0


ちなみに僕が使ったサウンド・カードは iBUFFALO USB オーディオ変換ケーブル(BSHSAU01BK)

Raspberry Pi 2 with sound card


これにマイクを接続して先ほど調べたキャプチャ・デバイスの card, device 番号を plughw で指定、サンプル・ビットを 16 bit リトル・エンディアン(PCM)、サンプリング・レートは 22050 Hz で音声入力をキャプチャしながら、そのデータを pi2fm にパイプします。

pi@raspberrypi $ arecord -D plughw:1,0 -f S16_LE -r 22050 | sudo ./pi2fm -


ちょっと大きな声が必要かもしれませんが、マイクでキャプチャした音声をストリーミングできるはずです。 ただ、僕が試した限りでは pi2fm が CPU パワーのほぼ全て使ってしまうので mp3 ファイル、マイク入力どちらもダイナミックに FM するってのはちょっと厳しい(音声が遅延する)って感じました。


pi2fm ではできないこと、注意事項、その他 FM 発信プロジェクト


pi2fm はサンプル・ビットが 16 bit でサンプリング・レートが 22.05 KHz のモノラル、つまり AM 相当の品質で動いています。 できればサプリング・レートを 32 KHz にしたり、ステレオにしたいって思いましたが物理層周りの知識が乏しい僕にはこのへんが限界。

あと、最近は Raspberry Pi 2 に対応したその他の FM トランスミッター・コードも幾つか出てきていて、僕がブログを下書きしている最中(数日前)にも SandPox さんの fm_transmitter って C++ 実装が Raspberry Pi 2 に対応してました。 もう記事もコードもその大半を書き終えたところだったので「ウゲー」って思いましたが、fm_transmitter はまだ改善の余地ありらしいので参考にビルド、プレイ方法だけご紹介しておきます(こっちの方が CPU 負荷は低そうです)。

pi@raspberrypi $ git clone https://github.com/SandPox/fm_transmitter.git
pi@raspberrypi $ cd fm_transmitter
pi@raspberrypi $ g++ -Wall -c main.cpp transmitter.cpp wave_reader.cpp
pi@raspberrypi $ g++ -Wall -o fm_transmitter main.o transmitter.o wave_reader.o
pi@raspberrypi $ sudo ./fm_transmitter star_wars.wav 77.7


注意事項は電波法を守るってことです。 電波法では免許が不要な微弱電波(ここでいうミニ FM 局)の条件として「発射する電波が著しく微弱な無線局(第4条第1項)」、具体的には「無線設備から 3 メートルの距離において、その電界強度が毎メートル 500 マイクロボルト以下のもの」って定義があります。 これは正確な測定器が無ければ判断できないのですが、今回紹介したコードであれば微弱な範囲に収まっているハズです(受信機にもよりますが 10 〜 15 m の距離で聞こえなくなります)。

それでは、また今度。

Arduinoをはじめようキット
スイッチサイエンス
売り上げランキング: 1,283


Posted by netbuffalo at 04:00│Comments(2)TrackBack(0) Raspberry Pi | プログラミング


この記事へのトラックバックURL

この記事へのコメント
ココロが折れた一人です
結局DMAはあきらめたのでしょうか?
Posted by 通りすがり at 2015年07月13日 17:10
通りすがりさん、コメントありがとうございます。
はい、DMA(Direct Memory Access)は諦めました。 DMA のコードも見たのですが、チャレンジしたらいつ終わるかわからない・・・と感じ記事の内容で区切りにしました。 次の世代・チャレンジャーに託したいと思ってます(笑)。 今後何か新しい情報を仕入れたら記事にします。
Posted by netbuffalo at 2015年07月13日 18:05

コメントする

名前
 
  絵文字