懸案だった 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 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 など)既製品との競合 |
---|
主な用途・目的 | 自作することによる技術的な満足感既製品に無い機能の実現常設の継続的な消費電力記録 |
---|