« ワットチェッカー的なものを自作する (その5) | トップページ | クランプメーターによる測定値の正確さ »

2014年8月30日 (土)

ワットチェッカー的なものを自作する (その6)

懸案だった BBB の PRU を使って ADE7753 からデータを取り込む方法を紹介する。

/*-----------  -----------*/

PRU の利用に必要な環境

以前の記事に書いた通り am335x_pru_package に含まれる prussdrv ドライバーと pasm アセンブラーが必要である。またデバッガーも必要で、同じ記事で紹介した Prudebug を使う。

 

McSPI0 と PRU-ICSS を有効化して P8-15 と P8-16 を使えるようにする

Device Tree Overlay を使う。McSPI0 はDebian に標準で含まれている BB-SPIDEV0 を使えばよいが PRU-ICSS と I/O ポートについては新たに作成する必要がある。

$ cat BB-PRU-P8_1516-00A0.dts
/*
* pru dts file BB-PRU-P8_1516-00A0.dts
*/

/dts-v1/;
/plugin/;
 
/ {
  compatible = "ti,beaglebone", "ti,beaglebone-black";

   /* identification */
  part-number = "BB-PRU-P8_1516";
  version = "00A0";

  exclusive-use =
    "P8.15", /* input */
    "P8.16", /* input */
    "pruss";
 
  fragment@0 {
    target = <&am33xx_pinmux>;
    __overlay__ {
      mygpio: pinmux_mygpio{
        pinctrl-single,pins = < 
                0x03C 0x36 // P8_15 = GPIO1_15
                0x038 0x36 // P8_16 = GPIO1_14
                >;
      };
    };
  };

  fragment@1 {
    target = <&pruss>;
    __overlay__ {
      status = "okay";
      pinctrl-names = "default";
      pinctrl-0 = <&mygpio>;
      pru_p8_15 {
        pin-names = "GPIO:PRU-P8.15";
        gpios   = < &gpio2 15 0 >;
      };
      pru_p8_16 {
        pin-names = "GPIO:PRU-P8.16";
        gpios   = < &gpio2 14 0 >;
      };
    };
  };
};
$ dtc -O dtb -o BB-PRU-P8_1516-00A0.dtbo -b 0 -@ BB-PRU-P8_1516-00A0.dts
$ sudo cp BB-PRU-P8_1516-00A0.dtbo /lib/firmware/
$ cat ../pru_spi.sh
#!/bin/bash

echo BB-SPIDEV0 > /sys/devices/bone_capemgr.*/slots
echo BB-PRU-P8_1516 > /sys/devices/bone_capemgr.*/slots
cat /sys/devices/bone_capemgr.*/slots
$ sudo ../pru_spi.sh 
 0: 54:PF--- 
 1: 55:PF--- 
 2: 56:PF--- 
 3: 57:PF--- 
 4: ff:P-O-L Bone-LT-eMMC-2G,00A0,Texas Instrument,BB-BONE-EMMC-2G
 5: ff:P-O-L Bone-Black-HDMI,00A0,Texas Instrument,BB-BONELT-HDMI
 7: ff:P-O-L Override Board Name,00A0,Override Manuf,cape-bone-iio
 8: ff:P-O-L Override Board Name,00A0,Override Manuf,BB-SPIDEV0
 9: ff:P-O-L Override Board Name,00A0,Override Manuf,BB-PRU-P8_1516

2つとも忘れずに Cape Manager に読ませるためシェルスクリプトを作った。

BB-PRU-P8_1516 を読ませないと PRU が使えない。例えば Prudebug を起動し、d コマンドで RAM の内容を表示しようとすると異常終了する。

$ sudo ./prudebug 
[sudo] password for debian: 
PRU Debugger v0.24
(C) Copyright 2011, 2013 by Arctica Technologies.  All rights reserved.
Written by Steven Anderson

Using /dev/mem device.
Processor type          AM335x
PRUSS memory address    0x4a300000
PRUSS memory length     0x00040000

         offsets below are in 32-bit word addresses (not ARM byte addresses)
         PRU            Instruction    Data         Ctrl
         0              0x0000d000     0x00000000   0x00008800
         1              0x0000e000     0x00000800   0x00009000

PRU0> d  
Absolute addr = 0x0000, offset = 0x0000, Len = 16
$ 

特にメッセージも出さずにシェルのプロンプトに戻ってしまう。これについては Prudebug に改善の余地がありそうだ。

一方 BB-SPIDEV0 を読ませなくても PRU も McSPI も一応動くが、外部と信号のやり取りができないので正常には動かない。

 

基本的な PRU のプログラム

ADE7753 の任意のレジスター1つを読出しまたは書き込みできるプログラムである。これを複数組み合わせれば原理的に何でもできるはずだ。SPI関連の主な処理内容は以下の通り。

  • PRU が SPI などをアクセスできるよう、OCP (On-Chip Peripheral) master ポートを有効にする
  • McSPI0 モジュールのクロックを有効にする
  • McSPI0 モジュールをリセットする
  • McSPI0 モジュールを設定する
  • McSPI0 モジュールとCS0信号を有効化する (同時に SPI クロック周波数を設定している)
  • データ転送を行う (2~4バイト)
  • McSPI0 モジュールを有効化して CS0 信号をアクティブにする
  • McSPI0 モジュールと CS0 信号を無効化する
  • McSPI0 モジュールのクロックを止める
  • 結果を RAM に保存して停止する

マルチタスクではないので割り込みは使わず全てプログラムで制御しているので、処理の内容が分かり易いのではないかと思う。複数バイト転送間隔などのタイミングもプログラムで制御するので FIFO などは使っていない。

$ cat demo0.p
// ADE7753 spi demo0

.origin 0
.entrypoint START

// MCSPI_CH0CONF param
// CLKG=1, FORCE=0, DPE0=1, WL=7, EPOL=1, CLKD=4, PHA=1
#define OFF_CONF 0x200103d1
// CLKG=1, FORCE=1, DPE0=1, WL=7, EPOL=1, CLKD=4, PHA=1
#define ON_CONF  0x201103d1

START:
// Set C24 reg. to 0x00000000
    MOV r5, 0x00022000      // Load PRU0 CTRL base address
    MOV r1, 0
    SBBO r1, r5, 0x20, 1    // Set C24_BLK_INDEX zero
// Enable OCP master ports.
    LBCO r1, C4, 4, 4       // Load SYSCFG reg.
    CLR  r1, r1, 4          // Clear STANDBY_INIT
    SBCO r1, C4, 4, 4       // Save new content to SYSCFG reg.
// Turn-on McSPI0
    MOV r6, 0x44E00000      // Load CM_PER base address
    MOV r1, 0x00000002      // Bit pattern to enable SPI0 clock
    SBBO r1, r6, 0x4C, 4    // Write to CM_PER_SPI0_CLKCTRL
// Reset McSPI0
    MOV r6, 0x48030100      // Load MCSPI0 + 0x100
    MOV r1, 0x00000002      // Soft reset
    SBBO r1, r6, 0x10, 4    // Write into MCSPI_SYSCONFIG
WAIT4RESET:                 // Wait for reset done
    LBBO r2, r6, 0x14, 4    // Read MCSPI_SYSSTATUS 
    QBBC WAIT4RESET, r2, 0  // Check RESETDONE
// Configure McSPI0
    MOV r1, 0x00000001      // Master, 4pin & single
    SBBO r1, r6, 0x28, 4    // Write into MCSPI_MODULCTRL
    MOV r1, 0x00000011      // Smart idle & autoidle
    SBBO r1, r6, 0x10, 4    // Write into MCSPI_SYSCONFIG
// Assert CS0 signal (low)
    MOV r1, ON_CONF         // SPI configuration word
    SBBO r1, r6, 0x2C, 4    // Write r1 into MCSPI_CH0CONF
    MOV r1, 0x00000001      // Param to enable McSPI0 channel 0
    SBBO r1, r6, 0x34, 4    // Write the param to MCSPI_CH0CTRL
// Prepare for transfer
    MOV r9, 0               // Clear RX data reg. (for debug)
    LBCO r8, C24, 4, 4      // Load data word
    LBCO r4, C24, 0, 1      // Load command byte
    QBEQ XFR_2_BYTE, r4.b0, 2
    QBEQ XFR_3_BYTE, r4.b0, 3
XFR_4_BYTE:                 // Transfer 4 bytes
    MOV r1.b0, r8.b3
    CALL XFR_BYTE
    MOV r9.b3, r3.b0
    CALL WAIT4USEC
XFR_3_BYTE:                 // Transfer 3 bytes
    MOV r1.b0, r8.b2
    CALL XFR_BYTE
    MOV r9.b2, r3.b0
    CALL WAIT4USEC
XFR_2_BYTE:                 // Transfer 2 bytes
    MOV r1.b0, r8.b1
    CALL XFR_BYTE
    MOV r9.b1, r3.b0
    CALL WAIT4USEC
// Transfer last byte
    MOV r1.b0, r8.b0
    CALL XFR_BYTE
    MOV r9.b0, r3.b0
// Deassert CS0 signal (high)
CLOSE_SPI0:
    LBBO r2, r6, 0x30, 4    // Load MCSPI_CH0STAT
    QBBC CLOSE_SPI0, r2, 2  // Check EOT -- wait until set
    MOV r1, 0x0             // Param to disable channel 0 of McSPI0
    SBBO r1, r6, 0x34, 4    // Write the param to MCSPI_CH0CTRL
    MOV r1, OFF_CONF
    SBBO r1, r6, 0x2C, 4    // Write into MCSPI_CH0CONF
// Turn-off McSPI0
    MOV r6, 0x44E00000      // Load CM_PER base address
    MOV r1, 0x00000000      // Bit pattern to disable SPI0 clock
    SBBO r1, r6, 0x4C, 4    // Write the pattern to CM_PER_SPI0_CLKCTRL
// Save received data
    SBCO r9, C24, 8, 4
// Save result code to indicate that the processig ended
    MOV r1, 1
    SBCO r1, C24, 1, 1
// End of processing: halt PRU
    HALT

// Single byte transfer proc
//
XFR_BYTE:
    LBBO r2, r6, 0x18, 4    // Load MCSPI_IRQSTATUS
    QBBS READY4WRITE, r2, 0 // Check for TX0_EMPTY
    QBA XFR_BYTE            // Loop until TX0_EMPTY set
READY4WRITE:
    MOV r2, 0x00000001      // Reset TX0_EMPTY
    SBBO r2, r6, 0x18, 4    // Do it
    SBBO r1, r6, 0x38, 4    // Write r1 into MCSPI_TX0
READ2:
    LBBO r2, r6, 0x18, 4    // Load MCSPI_IRQSTATUS
    QBBS READY4READ, r2, 2  // Check for RX0_FULL
    QBA READ2               // Loop until RX0_FULL set
READY4READ:
    MOV r2, 0x00000004      // Reset RX0_FULL
    SBBO r2, r6, 0x18, 4    // Do it
    LBBO r3, r6, 0x3C, 4    // Read MCSPI_RX0
    RET

// Wait for 4us
//
WAIT4USEC:
    MOV r4, 400
WAITLOOP:
    SUB r4, r4, 1
    QBNE WAITLOOP, r4, 0
    RET
$ cat Makefile 

COMPILER=~/pru_pkg/am335x_pru_package/pru_sw/utils/pasm -V3 -blz 
FILENAME=demo0

.PHONY: clean all

all:
        $(COMPILER) $(FILENAME).p


clean: 
        rm -rf $(FILENAME).bin $(FILENAME).txt $(FILENAME).lst


$ make
~/pru_pkg/am335x_pru_package/pru_sw/utils/pasm -V3 -blz  demo0.p


PRU Assembler Version 0.84
Copyright (C) 2005-2013 by Texas Instruments Inc.

Base source directory: '.'
New source file: 'demo0.p'
Output base filename: 'demo0'
DOTCMD : Scope '_ROOT_' declared
Base source directory: '.'
New source file: 'demo0.p'
demo0.p(    8) : DEFINE : 'OFF_CONF' = '0x200103d1'
demo0.p(   10) : DEFINE : 'ON_CONF' = '0x201103d1'
demo0.p(   12) : LABEL  : 'START' = 00000
demo0.p(   29) : LABEL  : 'WAIT4RESET' = 00015
    (104行省略)
Pass 2 : 0 Error(s), 0 Warning(s)

Writing Code Image of 78 word(s)

1バイトを転送する処理の概要は以下の通りだ。

  • MCSPI_IRQSTATUS レジスターを読んで TX0_EMPTY ビットがセットされている事を確認し、送信するデータを MCSPI_TX0 レジスターに書き込む
  • MCSPI_IRQSTATUS レジスターを読んで RX0_FULL ビットがセットされるまで待ち、MCSPI_RX0 レジスターから受信したデータを読み込む

TX0_EMPTY ビットがセットされていなかった場合に待つロジックが入っているのは念のためで、通常ここで待つことは無い。また次回転送のために TX0_EMPTY および RX0_FULL ステータスビットをクリアしておく必要がある。

SPI クロックは始めの方で #define している ON_CONF 定数で変えられるようにしていて、今回は ADE7753 が許容する最大値に近い 48Mhz の 1/5、9.6Mhz にしている。その代わりタイミングの制約を満たすよう1バイト転送が終わって次の転送を始める前に 4us 待ち時間を設けた。これはすべての場合で最も遅いタイミングに合わた安全サイドの設計である。

デバッグの際修正とアセンブルを繰り返すことになるので Make ファイルを準備して make を使えるようにした。

このプログラムはデバッガーで試すことができる。

$ sudo ./prudebug 
[sudo] password for debian: 
PRU Debugger v0.24
(C) Copyright 2011, 2013 by Arctica Technologies.  All rights reserved.
Written by Steven Anderson

Using UIO PRUSS device.
Processor type          AM335x
PRUSS memory address    0x4a300000
PRUSS memory length     0x00040000

         offsets below are in 32-bit word addresses (not ARM byte addresses)
         PRU            Instruction    Data         Ctrl
         0              0x0000d000     0x00000000   0x00008800
         1              0x0000e000     0x00000800   0x00009000

PRU0> l 0 demo0.bin
Binary file of size 308 bytes loaded into PRU0 instruction RAM.

PRU0> wr 0 3 0x90000 0x12345678
Write to absolute address 0x0000
PRU0> d
Absolute addr = 0x0000, offset = 0x0000, Len = 16
[0x0000] 0x00000003 0x00090000 0x12345678 0x8d1df242 
[0x0004] 0x0cb83ad0 0xcfbd98ce 0xb6144e49 0xb668d1de 
[0x0008] 0xa608328d 0xdf79be77 0x9413d0cd 0x625be6ca 
[0x000c] 0x137ccf38 0x16618a47 0x9e808104 0xb00e8a30 

PRU0> g
PRU0> d
Absolute addr = 0x0000, offset = 0x0000, Len = 16
[0x0000] 0x00000103 0x00090000 0x00ff000c 0x8d1df242 
[0x0004] 0x0cb83ad0 0xcfbd98ce 0xb6144e49 0xb668d1de 
[0x0008] 0xa608328d 0xdf79be77 0x9413d0cd 0x625be6ca 
[0x000c] 0x137ccf38 0x16618a47 0x9e808104 0xb00e8a30 

PRU0> 

プログラムをロードし動作内容を指定するデータをRAMに書き込んで実行すれば結果がRAMに残される。RAMデータの内訳は以下のとおりだ。

  • ワードアドレス 0x00 の最下位バイト: 0x03 = 転送するバイト数 (入力)
  • ワードアドレス 0x00 の2番目のバイト: 0x00 → 0x01 = PRU の処理終了フラグ (出力)
  • ワードアドレス 0x01: 0x00090000 = 送信するデータ (入力)
  • ワードアドレス 0x02: 0x12345678 → 0x00ff000c = 受信したデータ (入力)

上のリストはリセット直後のADE7753のMODEレジスターを読み込んだ例である。

もし分かりにくい点があるとしたら、PRU 上の32ビットデータの扱いはリトルエンディアンだが SPI で取り扱われるストリームはビッグエンディアンだ、と言うことではないだろうか。例えばワードアドレス 0x01 のデータはバイトアドレス順で 0x00, 0x00, 0x09, 0x00 と格納されているが、このうち先頭3バイトを 0x09, 0x00, 0x00 の順でSPIに送り出す必要がある。

この PRU のプログラムを利用して、このシリーズの第2回で紹介した SPI のテストプログラムとほとんど同じ動きをするプログラムを作ることができる。なお今回 Linux 上で動くプログラムは Python で作った。

$ cat demo0.py
#!/usr/bin/python

import ctypes, prussdrv, struct, sys, time

n = len(sys.argv)
if n<3 or n>5:
    print "Usage: %s d1 d2 [d3 [d4]]" % sys.argv[0]
    print "\td1...d4: 2 digit hex value"
    sys.exit(1)

prussdrv.init()
prussdrv.open(prussdrv.PRU_EVTOUT_0)

data_ram = ctypes.POINTER(ctypes.c_ubyte)()
prussdrv.map_prumem(prussdrv.PRUSS0_PRU0_DATARAM, ctypes.byref(data_ram) )

for i in range(4):
    data_ram[i] = 0
    data_ram[i+4] = int(sys.argv[-i-1], 16) if n > (i+1) else 0
data_ram[0] = n - 1
data_ram[1] = 0
prussdrv.exec_program(0, "demo0.bin")

while not data_ram[1]:
    time.sleep(0.1)

ram_values = struct.unpack('L'*3, str(bytearray(data_ram[0:0x0c] ) ) )
for i,d in zip([0, 4, 8], ram_values):
    print "data_ram[0x%02x:0x%02x] =" % (i+3, i),
    print "0x%08x" % d

prussdrv.pru_disable(0)
prussdrv.exit()
$ sudo python demo0.py
[sudo] password for debian: 
Usage: demo0.py d1 d2 [d3 [d4]]
        d1...d4: 2 digit hex value
$ sudo python demo0.py 9 0 0
data_ram[0x03:0x00] = 0x00000103
data_ram[0x07:0x04] = 0x00090000
data_ram[0x0b:0x08] = 0x00ff000c

 

次のステップ

今回 PRU を使っているのは ADE7753 から波形データを読み出すためだ。そのためには複数のデータをタイミングを合わせて読出し、バッファーに格納する処理を PRU のプログラムに実装する必要がある。

タイミングを合わせるのには IRQ 信号を使い、バッファーは PRU の共用 RAM を使うことにした。共用 RAM は12kB あるので、ADE7753の波形データは 24 ビットだが扱いやすいよう 32 ビットにしても 3,072 サンプルを格納できる。電力、電流、および電圧の波形データをそれぞれ 1,024 サンプルずつ一括して採取することができる勘定だ。これなら最大サンプルレート 27.9ksps で採取しても約 37ms、つまり 50Hz でも1サイクル半以上が収まるから十分と言えるだろう。

他に必要となるこまごまとした事がいくつかある。

  • 各波形データの採取は ZX (ゼロクロス) 信号に同期して開始しなければならない
  • ZX と電圧波形信号は LPF による遅れがあるので、電力と電流データの採取開始はこの分の調整が必要である
  • 速いサンプルレートに対応するため、1バイト転送が終わってから次の転送を始めるまでの待ち時間を許容される最小値に調整する必要がある
  • サブルーチンの中からさらにサブルーチンを呼べるようにする必要がある

ZX に同期する処理は ADE7753 に電圧信号が与えられていないとずっと待ちっぱなしになってしまうので、タイムアウトさせる仕組みもあった方がいい。これらを加味し、さらにここには書いていない事も盛り込んで PRU のプログラムを修正した。必要と思われる個所には全てコメントを入れておいたので参照してほしい。開発環境を日本語化していないから怪しげな英語だし、あまり親切とは言えないコメントだがご容赦願いたい。

$ cat ade7753.p
// ADE7753 spi

.macro XCALL              // Define extended call pseudo op
.mparam where             // Parameter: where to call to
    SUB r29, r29, 2         // Push stack pointer (r29)
    SBBO r30.w0, r29, 1, 2  // Save current return address
    JAL r30.w0, where       // Call
    LBBO r30.w0, r29, 1, 2  // Restore the return address
    ADD r29, r29, 2         // Pop stack pointer
.endm // XCALL

.origin 0
.entrypoint START

// MCSPI_CH0CONF param
// CLKG=1, FORCE=0, DPE0=1, WL=7, EPOL=1, CLKD=4, PHA=1
#define OFF_CONF 0x200103d1
// CLKG=1, FORCE=1, DPE0=1, WL=7, EPOL=1, CLKD=4, PHA=1
#define ON_CONF  0x201103d1

// Start address of shared RAM
#define SHRDRAM 0x00010000
// NUmber of samples, in word unit (not byte)
#define SAMPLES 1024

// *** Data RAM map (local view) ***
// 0x0000: Command from ARM
//    [2:0]==0,1,5,6 or 7: Take waveform samples
//        [7:6]==00b: take waveform samples in 27.9ksps
//        [7:6]==01b: take waveform samples in 14ksps
//        [7:6]==10b: take waveform samples in 7ksps
//        [7:6]==11b: take waveform samples in 3.5ksps
//    [3:0]==0x2,0x3 or 0x4: transfer 2-4 bytes immediately
//    [3:0]==0xa,0xb or 0xc: transfer 2-4 bytes in sync witn zero-cross
// 0x0001: Busy/done flag
//      00000000b: PRU is busy
//      00000001b: Completed normally
//      10000001b: Completed with zero-cross time-out
// 0x0002 - 0x0003: Non-WSMP IRQ count in waveform samples
// 0x0004 - 0x0007: Data to send from ARM,
//                  or delay time in number of samples for waveform
// 0x0008 - 0x000b: Data received from SPI (normal transfer)
//        "       : IRQ wait count (x15ns) (waveform samples)
// 0x000c - 0x000f: Wait count in SPI communication (x20ns)
//        - 0x1fff: Return address stack
// 0x010000 - 0x010fff (Shared RAM): Power waveform data
// 0x011000 - 0x011fff (Shared RAM): Channel 1 waveform data
// 0x012000 - 0x012fff (Shared RAM): Channel 2 waveform data

START:
    MOV r29, 0x1fff         // Set stack pointer to top of data RAM
// Enable OCP master ports.
    LBCO r1, C4, 4, 4       // Load SYSCFG reg.
    CLR  r1, r1, 4          // Clear STANDBY_INIT
    SBCO r1, C4, 4, 4       // Save new content to SYSCFG reg.
// Set C24 reg. to 0x00000000
    MOV r5, 0x00022000      // Load PRU0 CTRL base address
    MOV r1, 0
    SBBO r1, r5, 0x20, 1    // Set C24_BLK_INDEX zero
    SBCO r1, C24, 1, 1      // Clear done signature
    MOV r1, 0x80000000      // Set r31[31] as wakeup source
    SBBO r1, r5, 0x08, 4    // Save the bitmap to WAKEUP_EN

// Main loop
//
NEXT:
// Preset time-out counters to prevent false time-out error
    MOV r13, 4000000        // ZX (time-out=60ms)
// Clear debug counters
    MOV r10, 0              // SPI wait count
    MOV r11, 0              // Non-WSMP IRQ count
    MOV r15, 0              // IRQ wait count
// Reset wait counts to 400 = 4us
    MOV r20, 400            // Between b0 and b1
    MOV r21, 400            // Between b1 and b2
    MOV r22, 400            // Between b2 and b3
// Sleep until an event comes from ARM
    SLP 1                   // Sleep until an event comes from ARM
    MOV r4, 1 << 22         // ARM_PRU0_INTERRUPT event bit
    MOV r5, 0x00020280      // Setup r5 to point SECR0
    SBBO r4, r5, 0, 4       // Clear ARM_PRU0_INTERRUPT event
// Turn-on McSPI0
    MOV r6, 0x44E00000      // Load CM_PER base address
    MOV r1, 0x00000002      // Bit pattern to enable SPI0 clock
    SBBO r1, r6, 0x4C, 4    // Write to CM_PER_SPI0_CLKCTRL
// Reset McSPI0
    MOV r6, 0x48030100      // Load MCSPI0 + 0x100
    MOV r1, 0x00000002      // Soft reset
    SBBO r1, r6, 0x10, 4    // Write into MCSPI_SYSCONFIG
WAIT4RESET:                 // Wait for reset done
    LBBO r2, r6, 0x14, 4    // Read MCSPI_SYSSTATUS 
    QBBC WAIT4RESET, r2, 0  // Check RESETDONE
// Configure McSPI0
    MOV r1, 0x00000001      // Master, 4pin & single
    SBBO r1, r6, 0x28, 4    // Write into MCSPI_MODULCTRL
    MOV r1, 0x00000011      // Smart idle & autoidle
    SBBO r1, r6, 0x10, 4    // Write into MCSPI_SYSCONFIG
// Check for waveform command
    LBCO r4, C24, 0, 1      // Load command byte
    AND r4, r4, 0x07        // Take lower 3 bit
    QBEQ WF_SAMPLES, r4.b0, 0
    QBEQ WF_SAMPLES, r4.b0, 1
    QBEQ WF_SAMPLES, r4.b0, 5
    QBEQ WF_SAMPLES, r4.b0, 6
    QBEQ WF_SAMPLES, r4.b0, 7
    CALL OPEN_SPI0          // If not WF, assert CS0 and continue
// Prepare for transfer
    MOV r9, 0               // Clear RX data reg.
    LBCO r8, C24, 4, 4      // Load TX data word
    CALL SYNCIF             // ZX sync if data_ram@0x0[3]==1
    QBEQ TRANSFER3, r4.b0, 2
    QBEQ TRANSFER3, r4.b0, 3
    CALL XFR_4_BYTE
    QBA XFR_DONE
TRANSFER3:
    CALL XFR_3_BYTE
    QBA XFR_DONE
TRANSFER2:
    CALL XFR_2_BYTE
XFR_DONE:
    CALL CLOSE_SPI0         // Deassert CS0
    SBCO r9, C24, 8, 4      // Save received data
XFR_END:                    // ALL done!
// Turn-off McSPI0
    MOV r6, 0x44E00000      // Load CM_PER base address
    MOV r1, 0x00000000      // Bit pattern to disable SPI0 clock
    SBBO r1, r6, 0x4C, 4    // Write the pattern to CM_PER_SPI0_CLKCTRL
// Set the result code
    MOV r4, 0x01            // Result code for normal end
    QBNE NO_ZX_TO, r13, 0   // Check if ZX time-out?
    OR r4, r4, 0x80         // Set ZX time-out bit
NO_ZX_TO:
    SBCO r4, C24, 1, 1      // Save result code
// Save debug counters
    SBCO r10, C24, 0xc, 4   // Save SPI wait count
    SBCO r11, C24, 2, 2     // Save non-WSMP IRQ count
    QBA NEXT  // The end of mainloop -- go to sleep until next event

// Take waveform samples
//
WF_SAMPLES:
// Clear entire shared ram (debug purpose)
    MOV r12, SHRDRAM + 0x3000  // 0x3000 = 12k
    MOV r4, 0
    MOV r14, SHRDRAM
CLRLOOP:
    SBBO r4, r14, 0, 4
    ADD r14, r14, 4
    QBGT CLRLOOP, r14, r12
// Cleare done, then prepare sps parameter
    LBCO r4, C24, 0, 1      // Load command byte
    AND r7, r4, 0xc0        // Get sps value
    LSL r7, r7, 5           // Aligne bit position
// Start taking power waveform samples
    MOV r8, 0x89008C        // Code to choose power waveform samples
    OR r8, r8, r7           // Apply sps value
    MOV r14, SHRDRAM        // Start address of data area
    LBCO r16, C24, 4, 4     // Load data word as delay count
    CALL GET_SAMPLE         // Do it
// Start taking channel 1 waveform samples
    MOV r8, 0x89408C        // Code to choose channel 1 waveform samples
    OR r8, r8, r7
    MOV r14, SHRDRAM + 0x1000
    LBCO r16, C24, 4, 4     // Load data word as delay count
    CALL GET_SAMPLE
// Start taking channel 2 waveform samples
    MOV r8, 0x89608C        // Code to choose channel 2 waveform samples
    OR r8, r8, r7
    MOV r14, SHRDRAM + 0x2000
    MOV r16, 0              // Delay count = zero for ch2 (voltage)
    CALL GET_SAMPLE
// Save WF specific debug counter
    SBCO r15, C24, 8, 4     // Save IRQ wait count
// Take waveform samples done
    QBA XFR_END             // Goto end-of-mainloop

// Unit transfer proc for reg read/write
//
XFR_4_BYTE:                 // Transfer 4 bytes
    MOV r1.b0, r8.b3
    XCALL XFR_BYTE
    MOV r9.b3, r3.b0
    MOV r4, r22
    XCALL WAIT10XR4NS
XFR_3_BYTE:                 // Transfer 3 bytes
    MOV r1.b0, r8.b2
    XCALL XFR_BYTE
    MOV r9.b2, r3.b0
    MOV r4, r21
    XCALL WAIT10XR4NS
XFR_2_BYTE:                 // Transfer 2 bytes
    MOV r1.b0, r8.b1
    XCALL XFR_BYTE
    MOV r9.b1, r3.b0
    MOV r4, r20
    XCALL WAIT10XR4NS
// Transfer the last byte
    MOV r1.b0, r8.b0
    XCALL XFR_BYTE
    MOV r9.b0, r3.b0
    RET

// Proc to get waveform samples from a channel
//
GET_SAMPLE:
    XCALL WF_WR_3          // Send channel selection
    MOV r8, 0x8A0048       // Enable sampling
    XCALL WF_WR_3
    MOV r12, SAMPLES << 2  // Get number of bytes to store
    ADD r12, r12, r14      // Get the end address of data area
    XCALL ZXSYNC           // Sync with zero-cross
WAIT4WSMP:
    QBBC GOTIRQ, r31, 14    // Wait for IRQ signal
    ADD r15, r15, 1         // Wait count
    QBA WAIT4WSMP
GOTIRQ:
    MOV r8, 0x0C0000        // Read RSTATUS reg
    XCALL WF_RD_3
    QBBS WSMPOK, r9, 3      // Got WSMP IRQ, let's go on
    ADD r11, r11, 1         // Non-WSMP IRQ count
    QBA WAIT4WSMP           // Ignore non-WSMP IRQ
WSMPOK:
    MOV r8, 0x01000000      // Read WAVEFORM reg
    XCALL WF_RD_4
    QBEQ SAVEWSMP, r16, 0   // Check if not to skip
    SUB r16, r16, 1         // 1 sample skipped
    QBA WAIT4WSMP           // and try again
SAVEWSMP:
    QBBC WSMP_POSITIVE, r9, 23 // Check sign bit
    MOV r9.b3, 0xff         // Negative value
    QBA WSMP_SIGDONE
WSMP_POSITIVE:
    MOV r9.b3, 0            // Positive value
WSMP_SIGDONE:
    SBBO r9, r14, 0, 4
    ADD r14, r14, 4
    QBGT WAIT4WSMP, r14, r12  // Loop until the end
    RET

// Unit transfer proc for waveform samples
//
WF_RD_4:                    // Transfer 4 bytes (read)
// Change wait count
    MOV r22, 50             // 500ns between b2 and b3
    MOV r21, 0              // 0ns between b1 and b2
    MOV r20, 0              // 0ns between b0 and b1
    XCALL OPEN_SPI0
    XCALL XFR_4_BYTE
    XCALL CLOSE_SPI0
    RET
WF_WR_3:                    // Transfer 3 bytes (write)
// Change wait count
    MOV r21, 400            // 4us between b1 and b2
    MOV r20, 400            // 4us between b0 and b1
    QBA WF_3_BYTE
WF_RD_3:                    // Transfer 3 bytes (read)
// Change wait count
    MOV r21, 400            // 4us between b1 and b2
    MOV r20, 0              // 0ns between b0 and b1
WF_3_BYTE:
    XCALL OPEN_SPI0
    XCALL XFR_3_BYTE
    XCALL CLOSE_SPI0
    RET

// Single byte transfer proc
//
XFR_BYTE:
    LBBO r2, r6, 0x18, 4    // Load MCSPI_IRQSTATUS
    QBBS READY4WRITE, r2, 0 // Check for TX0_EMPTY
    ADD r10, r10, 1         // Wait count
    QBA XFR_BYTE            // Loop until TX0_EMPTY set
READY4WRITE:
    MOV r2, 0x00000001      // Reset TX0_EMPTY
    SBBO r2, r6, 0x18, 4    // Do it
    SBBO r1, r6, 0x38, 4    // Write r1 into MCSPI_TX0
READ2:
    LBBO r2, r6, 0x18, 4    // Load MCSPI_IRQSTATUS
    QBBS READY4READ, r2, 2  // Check for RX0_FULL
    QBA READ2               // Loop until RX0_FULL set
READY4READ:
    MOV r2, 0x00000004      // Reset RX0_FULL
    SBBO r2, r6, 0x18, 4    // Do it
    LBBO r3, r6, 0x3C, 4    // Read MCSPI_RX0
    RET

// Wait timer
//
WAIT10XR4NS:                // Wait for (10 x r4)ns
    QBNE WAITLOOP, r4, 0    // Do wait if r4 != 0
    RET                     // Do nothing if r4 == 0
WAITLOOP:
    SUB r4, r4, 1
    QBNE WAITLOOP, r4, 0
    RET

// Open & Close McSPI0 (assert & deassert CS0)
//
OPEN_SPI0:
// Assert CS0 signal (make it low)
    MOV r1, ON_CONF         // SPI configuration word
    SBBO r1, r6, 0x2C, 4    // Write r1 into MCSPI_CH0CONF
    MOV r1, 0x00000001      // Param to enable McSPI0 channel 0
    SBBO r1, r6, 0x34, 4    // Write the param to MCSPI_CH0CTRL
    RET
CLOSE_SPI0:
// Deassert CS0 signal (make it high)
    LBBO r2, r6, 0x30, 4    // Load MCSPI_CH0STAT
    QBBS EOT_OK, r2, 2      // Check EOT
    QBA CLOSE_SPI0          // Loop until EOT set
EOT_OK:
    MOV r1, 0x0             // Param to disable channel 0 of McSPI0
    SBBO r1, r6, 0x34, 4    // Write the param to MCSPI_CH0CTRL
    MOV r1, OFF_CONF
    SBBO r1, r6, 0x2C, 4    // Write into MCSPI_CH0CONF
    RET

// Sync with rising edge of zero-cross signal (ZX)
//
SYNCIF:  // conditional
    LBCO r4, C24, 0, 1      // Load command byte
    QBBS SYNC, r4, 3        // Do sync if bit 3 is set
    RET
ZXSYNC:  // always
    QBNE DO_SYNC, r13, 0    // Do sync if not timed-out previously
    RET
DO_SYNC:
    MOV r13, 4000000        // Reset tmieout -- 60ms / 15ns = 4e6
SYNC:
    QBBC SYNC_LO, r31, 15   // Check if ZX signal is lo
    SUB r13, r13, 1         // Check time-out
    QBLT SYNC, r13, 0
SYNC_LO:
    QBBS SYNC_HI, r31, 15   // Check if ZX signal is hi
    QBEQ SYNC_HI, r13, 0    // Check time-out
    SUB r13, r13, 1
    QBA SYNC_LO
SYNC_HI:                    // Sync done if r13 != 0 else time-out
    RET
debian@arm:~/spi/pru/rel_cand_1$ make
~/pru_pkg/am335x_pru_package/pru_sw/utils/pasm -V3 -blz  ade7753.p


PRU Assembler Version 0.84
Copyright (C) 2005-2013 by Texas Instruments Inc.

Base source directory: '.'
New source file: 'ade7753.p'
Output base filename: 'ade7753'
DOTCMD : Scope '_ROOT_' declared
    (中略)

Writing Code Image of 291 word(s)

デバッガーを使ってこのプログラムを試すことはできるが、波形データがちゃんととれているかどうかまで調べるのは無理である。インターフェイス用の Python  モジュールを準備した。なお今回準備したモジュールは 50Hz 専用である。60Hz 対応するには get_wf メソッドを修正する必要がある。

$ cat pru_spi.py
#!/usr/bin/python

import ctypes, prussdrv, time, struct

class pru_spi(object):
    def __init__(self, binpath="ade7753.bin", verbose=False, ts=False):
        self.ZX_TO_ERROR = IOError("Zero-cross timeout.")
        self.PRU_NOT_RUNNING = IOError("PRU is not running (stopped already.)")
        self.NO_SUCH_KSPS = ValueError("No such ksps (3.5|7|14|27.9).")
        self.NO_WF_DATA = Exception("No waveform data available.")
        self.ksps_dict = {3.5:0xc0, 7:0x80, 14:0x40, 27.9:0x00}
        self.verbose = verbose
        self.ts = ts
        self.ts0 = time.time()
        self.last_num = 0
        self.last_cmd = 0
        self.last_ret = 0
        self.last_val = 0
        self.tot = [0] * 3 * 1024
        self.results = [0] * 4
        self.wf_done = False
        prussdrv.init()
        prussdrv.open(prussdrv.PRU_EVTOUT_0)
        prussdrv.pruintc_init(prussdrv.constants.getPRUSS_INTC_INITDATA() )
        self.data_ram = ctypes.POINTER(ctypes.c_ubyte)()
        self.shrd_ram = ctypes.POINTER(ctypes.c_ubyte)()
        prussdrv.map_prumem(prussdrv.PRUSS0_PRU0_DATARAM,
                            ctypes.byref(self.data_ram) )
        prussdrv.map_prumem(prussdrv.PRUSS0_SHARED_DATARAM,
                            ctypes.byref(self.shrd_ram) )
        prussdrv.exec_program(0, binpath)
        self.pru_ok = True
        
    def xfer(self, num, cmd):
        self.last_num = num
        self.last_cmd = cmd
        if self.pru_ok:
            self.data_ram[0] = num
            cmdarray = bytearray(struct.pack('L', cmd) )
            for i in range(4):
                self.data_ram[i+4] = cmdarray[i]

            # Send host to PRU event
            prussdrv.pru_send_event(22) # prussdrv.ARM_PRU0_INTERRUPT = 21?
            r = self.data_ram[1]
            while not r:
                time.sleep(.01)         # sleep 10ms
                r = self.data_ram[1]    # try again
            self.last_ret = r

            self.results = struct.unpack('L'*4,
                                         str(bytearray(self.data_ram[0:16] ) ) )
            self.data_ram[1] = 0
            n = num&0x07 if (num&0x07) > 0 and (num&0x07) <= 4 else 4
            mask = 0x0000ff if n==2 else 0x00ffff if n==3 else 0xffffff
            self.last_val = self.results[2] & mask
            if self.verbose:
                self.dump()
            if r & 0x80:
                raise self.ZX_TO_ERROR
            else:
                return r, self.last_val
        else:
            raise self.PRU_NOT_RUNNING

    def get_wf(self, ksps=14):
        if not self.ksps_dict.has_key(ksps):
            raise self.NO_SUCH_KSPS
        elif not self.pru_ok:
            raise self.PRU_NOT_RUNNING
        else:
            self.xfer(self.ksps_dict[ksps],
                      int(round(float(ksps)*20*(1.-19.7/360) ) ) )
            self.wf_done = True
            self.tot = struct.unpack(
                'i' * 3 * 1024, str(bytearray(self.shrd_ram[0:4*3*1024] ) ) )
            return self.wf_data()

    def wf_data(self):
        if not self.wf_done:
            raise self.NO_WF_DATA
        else:
            p = self.tot[0:1024]
            c = self.tot[1024:2048]
            v = self.tot[2048:3072]
            return p, c, v

    def dump(self):
        if self.ts:
            print "PRU_SPI dump (@time=%.3f):" % (time.time() - self.ts0)
        else:
            print "PRU_SPI dump:"
        for i in range(4):
            print "\tData_RAM[0x%02x-0x%02x] = 0x%08x" % (4*i+3,
                                                          4*i,
                                                          self.results[i] )
    def stop(self):
        if self.pru_ok:
            if self.verbose:
                if self.ts:
                    print "Stopping PRU @time=%.3f" % (time.time() - self.ts0)
                else:
                    print "Stopping PRU..."
            prussdrv.pru_reset(0)
            prussdrv.pru_disable(0)
            prussdrv.exit()
            self.pru_ok = False

if __name__ == "__main__":
    import os, sys

    n = len(sys.argv)
    if n<3 or n>5:
        print "Usage: %s d1 d2 [d3 [d4]]" % sys.argv[0]
        print "\td1...d4: 2 digit hex value"
        sys.exit(1)

    binpath = os.path.join(sys.path[0], "ade7753.bin")
    pru = pru_spi(binpath)
    try:
        pru.verbose = True
        pru.ts = True

        d = 0
        for a in sys.argv[1:]:
            d = int(a, 16) + (d<<8)

        pru.xfer(n-1, d)
    except:
        raise
    finally:
        pru.stop()
$ sudo python pru_spi.py 9 0 0
PRU_SPI dump (@time=0.013):
        Data_RAM[0x03-0x00] = 0x00000103
        Data_RAM[0x07-0x04] = 0x00090000
        Data_RAM[0x0b-0x08] = 0x00ff000c
        Data_RAM[0x0f-0x0c] = 0x00000000
Stopping PRU @time=0.017

モジュールをプログラムとして実行すると最初の方で紹介した Python のプログラムとほぼ同じ動きをするようにしておいた。これは簡単な動作確認用だ。波形データを目視チェックするにはプロットしてみるのが一番である。これは Python で対話的に行うことができる。

$ sudo python
Python 2.7.3 (default, Mar 14 2014, 17:55:54) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pru_spi
>>> import matplotlib.pyplot as pl
>>> import numpy as np
>>> 
>>> pru = pru_spi.pru_spi(verbose=True)
>>> ksps = 27.9
>>> t = np.arange(1024) / ksps
>>> t20 = int(20 * ksps)
>>> p,a,v = [np.array(i) for i in pru.get_wf(ksps)]
PRU_SPI dump:
        Data_RAM[0x03-0x00] = 0x00000100
        Data_RAM[0x07-0x04] = 0x0000020f
        Data_RAM[0x0b-0x08] = 0x0048e18e
        Data_RAM[0x0f-0x0c] = 0x00000000
>>> 
>>> print p[:t20].mean()
78178.0681004
>>> print a[:t20].std()
311652.471269
>>> print v[:t20].std()
3878.05186451
>>> 
>>> ax1 = pl.subplot(111)
>>> ax2 = pl.twinx()
>>> ax1.plot(t,a,'r',label="Amp")
[<matplotlib.lines.Line2D object at 0x11488f0>]
>>> ax2.plot(t,p,'g',label="Pow")
[<matplotlib.lines.Line2D object at 0x1166f10>]
>>> ax2.plot(t,v,'b',label="Vol")
[<matplotlib.lines.Line2D object at 0x1166e70>]
>>> ax1.legend(loc="upper left")
<matplotlib.legend.Legend object at 0x1166550>
>>> ax2.legend(loc="upper right")
<matplotlib.legend.Legend object at 0x1153d10>
>>> ax1.grid()
>>> pl.show()

もちろんファイルに書いてスクリプトとして実行するのも可能だ。多分その方が作業は楽である。上の実行例は 300W のヘアードライヤーを負荷にしている。この時のプロットは以下の通りだ。

20140829_300w

値は ADE7753 内部表現のままなので少し分かりにくいかもしれないが、一応それらしくプロットされていて動作にこれと言った異常は無さそうである。

電力の平均値および電流と電圧の標準偏差を表示させているが、これらはそれぞれ有効電力と rms 値に相当するものである。これらにこのシリーズ第3回目で求めた係数を (電圧については第5回目の修正も) 適用すると、

  • 電力平均値 = 78180 x 9.460e-06 x (4.844e-5 / 0.001953) x 1.626e04 = 298.3
  • 電流 rms 値 = 311700 x 1.892e-7 ÷ 0.02 = 2.949
  • 電圧 rms 値 = 3878 x 1.062 x 4.844e-05 x 6.1 x 100/1.211 = 100.5

電流値に関してアナログ回路の誤差を無視しているが、かなりそれらしい値が得られた。正確な値を得るには電流の係数を校正する必要がある。それを別にすれば正常に動作していると見て良いだろう。

 

今後の展開

BBB の Linux 上のプログラムから ADE7753 の全ての機能が使えるようになったので、かなり高機能な「ワットチェッカー的なもの」を容易に実現する手段が整ったことになる。Web サーバーを動かしてアプリを組むのが最もありがちなパターンではないかと思う。しかしこの方向に進めるのには若干の躊躇があるのも事実だ。

「ワットチェッカー的なもの」を自作しようとした理由は比較的長期間の消費電力推移を記録したかったからである。多分常時動かしておくことになる。そう考えると BBB は少し消費電力が多過ぎる。商用電源の消費電力で考えるので AC アダプターの効率が影響するが、大体 3W 位消費するはずである。サーバーと考えれば十分小さな値だが、常時動かしておくなら 1W を切るのが望ましいと思っている。

今あるアイデアは ADE7753 と PIC32MX を組み合わせてラトックシステムの Bluetooth ワットチェッカーと同じようなことができるのではないか、と言うことである。

この製品の仕様によれば消費電力は 0.3W だ。ここまで小さくするには電源回路をかなり工夫する必要がある。以前電圧信号の取り出しに使っていた電源トランスを使おうかと思っていたのだが、これだとトランスだけで 0.6W 消費してしまう可能性があるのでアウトだ。

今後 BBB よりも小規模な実装を試そうと思っているが、少し時間がかかりそうだ。なのでこのシリーズは一旦終了して、何か成果が出たらその時にまた報告するつもりである。実際のところ BBB を使うのが楽なので、結局その方向に落ち着く可能性が結構高いのかもしれない。

 

今回のまとめ

今までに検討した方式の比較をまとめておく。

方式 PC オーディオ
利点
  • PC を除けばハードウエア規模最少
  • 容易に高機能・高性能を実現できる
  • 短い時間スケールの解析が可能 (電流・電圧両方)
  • 比較的長時間の波形データを一旦記録してオフライン解析が容易
  • 課題
  • 消費電力大 (少なくとも 15W 程度)
  • 装置サイズ大
  • 信号レベルの調整とそれに応じた校正が必要 (自動化可能?)
  • 主な用途・目的
  • 一時な (常設でない) 数時間程度の消費電力データ記録
  • 短い時間スケールの現象の解析
  • 発生頻度の低い現象の解析
  •  
    方式 ADE7753 + BBB
    利点
  • 装置サイズ小
  • 消費電力が比較的小さい (3W 程度)
  • 比較的容易に高機能・高性能を実現できる
  • 短い時間スケールの解析が可能 (電流のみ)
  • 課題
  • BBB の価格と入手性
  • PRU を使った開発スキル
  • 主な用途・目的
  • 一時的な (常設でない) 数日間程度の消費電力データ記録
  • 常設の高機能電力計測サーバー
  •  
    方式 ADE7753 + 小規模マイクロコントローラー (PIC32MX など)
    利点
  • 装置サイズ小
  • 消費電力最少 (1W 未満)
  • 課題
  • 開発スキル
  • 専用の開発環境 (プログラマー、In-Circuit Debugger など)
  • 既製品との競合
  • 主な用途・目的
  • 自作することによる技術的な満足感
  • 既製品に無い機能の実現
  • 常設の継続的な消費電力記録
  • |

    « ワットチェッカー的なものを自作する (その5) | トップページ | クランプメーターによる測定値の正確さ »

    BeagleBone Black」カテゴリの記事

    趣味」カテゴリの記事

    コメント

    コメントを書く



    (ウェブ上には掲載しません)


    コメントは記事投稿者が公開するまで表示されません。



    トラックバック

    この記事のトラックバックURL:
    http://app.cocolog-nifty.com/t/trackback/543745/60232152

    この記事へのトラックバック一覧です: ワットチェッカー的なものを自作する (その6):

    « ワットチェッカー的なものを自作する (その5) | トップページ | クランプメーターによる測定値の正確さ »