日曜技術者のメモ

趣味でやった事のメモ書きです。

リチウムコイン電池で動くワイヤレス温湿度気圧ロガーを作ってみた-2-

前回の続き。

se.hatenablog.jp

本当は↓の連続送信テストが終了したらブログに纏めようかと思っていたが、2週間待っても電池が切れないので途中結果で書いておく。

連続送信テスト

前回の投稿から電池の持ちを調べる為に電池での連続送信テストしてみた。
実際の使用は1送信/1分の予定だがかなり時間がかかりそうなので 1送信/1秒という高頻度で送信をした。

センサーデータはシリアルポートでPCに接続されているのでTeraTermとかで適当にログを取れば済むのだが、日付や電池の電圧もログに取りたかったのでPythonを使ってプログラムを書いた。
↓が書いたプログラム

# -*- coding: utf-8 -*-

import serial
import signal
import sys
import datetime

def handler(signal, frame):
    f.close()
    dmm_ser.write(("SYSTem:LOCal\r\n").encode(sys.stdout.encoding))
    dmm_ser.close()
    sensor_ser.close()
    sys.exit(0)


f = open('test.txt','w')

signal.signal(signal.SIGINT, handler)
sensor_ser = serial.Serial(port="COM4", timeout=3,baudrate=19200)
dmm_ser = serial.Serial(port="COM3", timeout=3,baudrate=9600)

dmm_ser.write(("SYSTem:REMote\r\n").encode(sys.stdout.encoding))
i = 0;

while(1):
    line = sensor_ser.readline()
    dmm_ser.write(("READ?\r\n").encode(sys.stdout.encoding))
    dmm_val = dmm_ser.readline()
    d = datetime.datetime.today()
    ss = d.strftime("%Y-%m-%d %H:%M:%S")
    print( str(i) + "," + ss + "," + line.strip().decode('utf-8').replace(':', ',')+"," + dmm_val.decode('utf-8').strip())
    f.write( str(i) + "," + ss + "," + line.strip().decode('utf-8').replace(':', ',')+","+dmm_val.decode('utf-8').strip()+"\n")
    i = i + 1

f.close()
dmm_ser.close()
sensor_ser.close()

今使っているHP 34401Aはシリアルポートでリモート測定ができるので pyserialで両方のシリアルポートに接続し、日付を追加してファイルに保存している。
ファイルの中は↓のようになっている。

0,2017-08-02 00:22:08,00,XXXX,46,29,64,10,08,78,49,88,00,+3.15603450E+00
1,2017-08-02 00:22:09,00,XXXX,46,29,64,10,08,78,49,65,00,+3.16726560E+00
2,2017-08-02 00:22:10,00,XXXX,46,29,63,10,08,79,49,51,00,+3.17619240E+00
3,2017-08-02 00:22:11,00,XXXX,46,29,61,10,08,77,49,44,00,+3.18370380E+00
4,2017-08-02 00:22:12,00,XXXX,46,29,60,10,08,79,49,41,00,+3.18991540E+00
5,2017-08-02 00:22:13,00,XXXX,46,29,59,10,08,78,49,41,00,+3.19537430E+00
6,2017-08-02 00:22:14,00,XXXX,46,29,57,10,08,74,49,40,00,+3.20004990E+00
7,2017-08-02 00:22:15,00,XXXX,46,29,55,10,08,79,49,39,00,+3.20418880E+00

ファイルの中身はカンマ区切りで先頭から

0.受信回数
1.受信日時
2.送信モジュールのノード番号 3.送信モジュールの固有ID(↑のログでは一応XXXXに変えています)
4.受信データの RSSI 値
5.温度(整数部)
6.温度(少数部)
7.気圧(千・百の位)
8.気圧(十・一の位)
9.気圧(小数部)
10.湿度(整数部)
11.湿度(少数部)
12.空き
13.電圧
と並んでいる。

IM315TXは一度に8byteしか送れないので詰めて送信している。

温度・気圧・湿度の推移

Pythonスクリプトから出力されたファイルをExcelで読み、グラフ化したのが以下ツイートの画像

これをツイートした時点では気づかなかったが、原因は台風が近づいていたので気圧が1000hPaを下回り、 3桁になってので気圧とその後ろの湿度が一文字ずつずれていたから。

これもスクリプトをちゃちゃっと書いて修正

# coding: UTF-8

import sys
 
f = open('test2.txt','r')
fw = open('test3.txt','w')
lines2 = f.readlines()
f.close()
cnt = 0
for line in lines2:
    

    if (cnt % 10 == 0):
        tmp = line.split(",")
        #0,2017-08-02 00:22:08 ,00,XXXX,46,29,64,10,08,78,49,88,00,+3.15603450E+00
        #0|           1        |2 |   3| 4| 5| 6| 7| 8| 9|10|11|12|     13        |

        log_cnt = tmp[0];
        log_date = tmp[1];
        log_temp = int(tmp[5])+int(tmp[4])/100
        if (int(tmp[7]) < 20):
            log_press = int(tmp[7]) * 100 + int(tmp[8]) + int(tmp[9])/100
            log_hum = (int(tmp[10]) * 1000 + int(tmp[11])*10) / 1024
        else:
            #490011,2017-08-07 16:30:52 ,00,XXXX,46,36,39,99,95,15,58,45,00,+2.92671440E+00
            #  0   |           1        |2 |   3| 4| 5| 6| 7| 8| 9|10|11|12|     13        |
            log_press = int(tmp[7][0]) * 100 + int(tmp[7][1] + tmp[8][0]) + int(tmp[8][1] + tmp[9][0])/100
            log_hum = (int(tmp[9][1] + tmp[10][0] ) * 1000 + float(tmp[10][1] + tmp[11][0] + tmp[11][1])) / 1024
        fw.write(log_cnt + "," + log_date + "," + str(log_temp) + "," + str(log_press) + "," +  str(log_hum) + "\n" )
    cnt = cnt + 1

fw.close()

気圧(千・百の位) が20より大きければ一桁ずらして計算している。
その結果、正常っぽい値になった。

f:id:ginnyu-tei:20170816005148p:plain:w600
↑でも触れているがこの時台風5号が近づいたので気圧がかなり下がっている。
温湿度はこのグラフに室外の状況やエアコンの稼働状況なんかも入れると良さそう。

電圧の推移

↓は電圧のグラフ

f:id:ginnyu-tei:20170816005913p:plain:w600
初日は電圧が一気に下がったのですぐにテストは終了するかと思ったが一旦下がった後2、3日は電圧がほぼ変わらなかった。消費による電圧降下より室温による電圧変化の方が大きいくらい。その後緩やかに下がっていったが、ここ数日は電圧が落ちるペースが早くなってきた。
このブログを書いている現時点では1212000回送信で2.48V位である。
かなり下がってきたとはいえ、1.8Vまでは余裕がある。

テスト中間結果

1送信/1秒で1212000回送信という事は1送信/1分だと60倍なので約2.3年の換算になる。
元々1年位電池交換なしで動けばいいと思っていたので予想以上である。
ただし、これは室温の高い夏のテストなので冬場はどうかわからない。

電圧グラフを見ると室温で電圧が上下しているのでかなり影響を受ける様である。
これについては冬にテストして確かめたいと思う。

次回はさらに消費電力を減らすためにTI storeで色々仕入れたのでそれについて書く予定。

リチウムコイン電池で動くワイヤレス温湿度気圧ロガーを作ってみた-1-

秋月で温度・湿度・気圧をまとめて測定できるBME280というセンサーを買ったので
リチウムコイン電池で動くワイヤレスな装置を作ってみた。

f:id:ginnyu-tei:20170801215519j:plain:w400
(左:センサー・送信 右:受信)

主な部品リスト

名前 価格
MSP430G2553 210円
BME280モジュール 1080円
315MHz無線送信モジュール 2160円
315MHz無線受信モジュール 2160円
32.768kHz 水晶発振子 30円
ボタン電池基板取付用ホルダー CR2032用 50円
1.27mmピッチユニバーサル基板 100円
47kΩ
1.5kΩ
LED

リチウムコイン電池でマイコンを動かすのは初めてなので色々試行錯誤してやってみたい。
マイコンはTiの超低消費電力マイコンMSP430をチョイス。使った事のないマイコンなのでMSP430 Value Line LaunchPad Development Toolと追加でMSP430G2553を購入。 ユニバーサル基板はハーフピッチなのはIM315のコネクタが1.27mmピッチなのでそれに合わせた。

回路概要

今回はUEWを使って少しずつ動作確認しながら作成したので回路図は書かなかった。
裏側はこんなかんじ↓
f:id:ginnyu-tei:20170801231856j:plain:w400
なので、ここでは接続の概要だけ記載しておく。

送信側

BM280はSPI4線でMSP430のUSCI_B0に接続、CSはP1.3を使った。
IM315TXとはUART(P1.2、P1.3)で接続。(送信頻度が少ないのでBusyは配線してない)
32.768kHz 水晶発振子はLPM3からの復帰用にXIN、XOUTに接続。

受信側

受信側はPCでロギングする為にIM315RXとFT232xを接続した。

MSP430プログラム

ソフトはmain.cとUARTでの文字送信用関数群のsimple_uart.hとBM280を制御するbm280.hに分かれている。

BM280の制御プログラムは以下ページのコードを使わせて頂きました。
www.mgo-tec.com

main.c

#include <msp430.h> 
#include <stdint.h>
#include <simple_uart.h>
#include <bme280.h>

/*
 * main.c
 */
int main(void)
{
    //WDTCTL = WDTPW + WDTHOLD; // Stop WDT
    WDTCTL = WDT_ADLY_1000;
    IE1 |= WDTIE;

    //クロック設定
    DCOCTL = 0;
    BCSCTL1 = CALBC1_1MHZ;
    DCOCTL = CALDCO_1MHZ;

    //P1.5 SCLK
    P1SEL |= BIT5;
    P1SEL2 |= BIT5;
    //P1.6 SOMI
    P1SEL |= BIT6;
    P1SEL2 |= BIT6;
    //P1.7 SIMO
    P1SEL |= BIT7;
    P1SEL2 |= BIT7;
    //P1.3 SS
    P1DIR |= BIT3;
    P1OUT |= BIT3;

    //P1.1 RXD
    P1SEL |= BIT1;
    P1SEL2 |= BIT1;
    //P1.2 TXD
    P1SEL |= BIT2 ;
    P1SEL2 |= BIT2;

    //UART
    //enable software reset
    UCA0CTL1 = UCSWRST;

    //UART設定
    UCA0CTL1 |= UCSSEL_2; // SMCLK
    UCA0BR0 = 104; // 1MHz 9600
    UCA0BR1 = 0; // 1MHz 9600
    UCA0MCTL = UCBRS0; // Modulation UCBRSx = 1
    //clear software reset
    UCA0CTL1 &= ~UCSWRST;

    //SPI
    //enable software reset
    UCB0CTL1 = UCSWRST;
    //SPI設定
    UCB0CTL0 = UCCKPL|UCMODE0|UCMSB|UCMST|UCSYNC;
    //SCK設定
    UCB0CTL1 = UCSSEL1;
    UCB0BR0 = 1;
    UCB0BR1 = 0;
    //clear software reset
    UCB0CTL1 &= ~UCSWRST;

    //BME280
    uint8_t t_sb = 5; //stanby 1000ms
    uint8_t filter = 0; //filter O = off
    uint8_t spi3or4 = 0; //SPI 3wire or 4wire, 0=4wire, 1=3wire
    uint8_t osrs_t = 4; //OverSampling Temperature x4
    uint8_t osrs_p = 4; //OverSampling Pressure x4
    uint8_t osrs_h = 4; //OverSampling Humidity x4
    uint8_t Mode = 3; //Normal mode
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | Mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3or4;
    uint8_t ctrl_hum_reg  = osrs_h;
    writeReg(0xF2,ctrl_hum_reg);
    writeReg(0xF4,ctrl_meas_reg);
    writeReg(0xF5,config_reg);
    readTrim();

    while (1) {
        __bis_SR_register(LPM3_bits+GIE); //LPM3に移行

        volatile double temp_act = 0.0, press_act = 0.0, hum_act = 0.0, altitude_act = 0.0;
        int32_t temp_cal;
        uint32_t press_cal,hum_cal;

        readData();

        temp_cal = calibration_T(temp_raw);
        press_cal = calibration_P(pres_raw);
        hum_cal = calibration_H(hum_raw);
        //temp_act = (double)temp_cal / 100.0;
        //press_act = (double)press_cal / 100.0;
        //hum_act = (double)hum_cal / 1024.0;
        //IM315TXへ送信
        uart_puts("TXDT ");
        uart_putdec(temp_cal);
        uart_putdec(press_cal);
        uart_putdec(hum_cal);
        uart_puts("\r\n");
        //_delay_cycles(1000000);  // 1sec

    }
}

#pragma vector=WDT_VECTOR
__interrupt void watchdog_timer (void)
{
    __bic_SR_register_on_exit(LPM3_bits); // LPMを解除
}


// スタートアップルーチンから呼び出される初期化処理
void __low_level_init(void)
{
    WDTCTL = WDT_ADLY_1000; // WDTのクロック・ソースをACLK、
                            // 1sのインターバルタイマに設定
}

simple_uart.h

/*
 * simple_uart.h
 *
 */

#ifndef SIMPLE_UART_H_
#define SIMPLE_UART_H_

#include <stdint.h>

void uart_putc(char c)
{
    while (!(IFG2&UCA0TXIFG));
    UCA0TXBUF = c;
}

int uart_puts(const char *s) {
    int n;
    for (n = 0; *s; s++, n++) {
        uart_putc(*s);
    }
    return n;
}


void uart_putdec(uint32_t value) {
    int         i;          // General purpose counter.
    uint32_t    m;          // Mask for decimal number parser.
    char        nbuf[12];   // Buffer to store the decimal number.

    // Maximum power of 10 number in 32-bit.
    m = 1000000000;
    // Find the first masking number.
    while ((m > 1) && (value < m)) {
        m /= 10;
    }
    // Parse the value into a decimal number.
    i = 0;
    while (m >= 1) {
        nbuf[i++] = '0' + (value / m);
        value %= m;
        m /= 10;
    }
    nbuf[i++] = 0;      // End of string.
    uart_puts(nbuf);    // Put the decimal number.
}




#endif /* SIMPLE_UART_H_ */

bme280.h

/*
 * bme280.h
 *
 */

#ifndef BME280_H_
#define BME280_H_

#include <stdint.h>

uint16_t read16bit(uint8_t reg);
uint8_t read8bit(uint8_t reg);
void SpiWrite(uint8_t data);
uint8_t SpiRead();

uint32_t hum_raw, temp_raw, pres_raw;
 int32_t t_fine;

uint16_t dig_T1;
 int16_t dig_T2;
 int16_t dig_T3;

uint16_t dig_P1;
 int16_t dig_P2;
 int16_t dig_P3;
 int16_t dig_P4;
 int16_t dig_P5;
 int16_t dig_P6;
 int16_t dig_P7;
 int16_t dig_P8;
 int16_t dig_P9;

uint8_t  dig_H1;
 int16_t dig_H2;
uint8_t  dig_H3;
 int16_t dig_H4;
 int16_t dig_H5;
 int8_t  dig_H6;

 double SeaLevelPressure_hPa = 1013.25; //標準は1013.25


//*****************初期値読み込み**************************************
void readTrim(void) {
    dig_T1 = read16bit(0x88);
    dig_T2 = (int16_t)read16bit(0x8A);
    dig_T3 = (int16_t)read16bit(0x8C);

    dig_P1 = read16bit(0x8E);
    dig_P2 = (int16_t)read16bit(0x90);
    dig_P3 = (int16_t)read16bit(0x92);
    dig_P4 = (int16_t)read16bit(0x94);
    dig_P5 = (int16_t)read16bit(0x96);
    dig_P6 = (int16_t)read16bit(0x98);
    dig_P7 = (int16_t)read16bit(0x9A);
    dig_P8 = (int16_t)read16bit(0x9C);
    dig_P9 = (int16_t)read16bit(0x9E);

    dig_H1 = read8bit(0xA1);
    dig_H2 = (int16_t)read16bit(0xE1);
    dig_H3 = read8bit(0xE3);
    dig_H4 = (int16_t)((read8bit(0xE4) << 4) | (read8bit(0xE5) & 0x0F));
    dig_H5 = (int16_t)((read8bit(0xE6) << 4) | (read8bit(0xE5) >> 4));
    dig_H6 = (int8_t)read8bit(0xE7);
}
//***************BME280へ初期レジスタ書き込み関数****************************
void writeReg(uint8_t reg_address, uint8_t data) {
  P1OUT &= ~BIT3;
  SpiWrite(reg_address & 0x7F); // write, bit 7 low
  SpiWrite(data);
  P1OUT |= BIT3;
}
//***************BME280からの温度、湿度、気圧データ読み込み関数****************************
void readData() {

  uint32_t data[8];
  uint8_t i;

  P1OUT &= ~BIT3;
  SpiWrite(0xF7 | 0x80); //0xF7 pressure msb read, bit 7 high
  for(i=0; i<8; i++){
    data[i] = SpiRead();
  }
  P1OUT |= BIT3;
  pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4); //0xF7, msb+lsb+xlsb=19bit
  temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4); //0xFA, msb+lsb+xlsb=19bit
  hum_raw  = (data[6] << 8) | data[7];  //0xFD, msb+lsb=19bit(16:0)
}
//***************温度キャリブレーション関数****************************
int32_t calibration_T(int32_t adc_T) {
    int32_t var1, var2, T;
    var1 = ((((adc_T >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
    var2 = (((((adc_T >> 4) - ((int32_t)dig_T1)) * ((adc_T>>4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;

    t_fine = var1 + var2;
    T = (t_fine * 5 + 128) >> 8;
    return T;
}
//***************気圧キャリブレーション関数****************************
uint32_t calibration_P(int32_t adc_P) {
    int32_t var1, var2;
    uint32_t P;
    var1 = (((int32_t)t_fine)>>1) - (int32_t)64000;
    var2 = (((var1>>2) * (var1>>2)) >> 11) * ((int32_t)dig_P6);
    var2 = var2 + ((var1*((int32_t)dig_P5))<<1);
    var2 = (var2>>2)+(((int32_t)dig_P4)<<16);
    var1 = (((dig_P3 * (((var1>>2)*(var1>>2)) >> 13)) >>3) + ((((int32_t)dig_P2) * var1)>>1))>>18;
    var1 = ((((32768+var1))*((int32_t)dig_P1))>>15);
    if (var1 == 0) {
        return 0;
    }
    P = (((uint32_t)(((int32_t)1048576)-adc_P)-(var2>>12)))*3125;
    if(P<0x80000000) {
       P = (P << 1) / ((uint32_t) var1);
    }else{
        P = (P / (uint32_t)var1) * 2;
    }
    var1 = (((int32_t)dig_P9) * ((int32_t)(((P>>3) * (P>>3))>>13)))>>12;
    var2 = (((int32_t)(P>>2)) * ((int32_t)dig_P8))>>13;
    P = (uint32_t)((int32_t)P + ((var1 + var2 + dig_P7) >> 4));
    return P;
}
//***************湿度キャリブレーション関数****************************
uint32_t calibration_H(int32_t adc_H) {
    int32_t v_x1;

    v_x1 = (t_fine - ((int32_t)76800));
    v_x1 = (((((adc_H << 14) -(((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1)) +
              ((int32_t)16384)) >> 15) * (((((((v_x1 * ((int32_t)dig_H6)) >> 10) *
              (((v_x1 * ((int32_t)dig_H3)) >> 11) + ((int32_t) 32768))) >> 10) + ((int32_t)2097152)) *
              ((int32_t) dig_H2) + 8192) >> 14));
   v_x1 = (v_x1 - (((((v_x1 >> 15) * (v_x1 >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4));
   v_x1 = (v_x1 < 0 ? 0 : v_x1);
   v_x1 = (v_x1 > 419430400 ? 419430400 : v_x1);
   return (uint32_t)(v_x1 >> 12);
}
//***************標高計算関数****************************************************
//double ReadAltitude(double SeaLevel_Pres, double pressure) {
//  double altitude = 44330.0 * (1.0 - pow(pressure / SeaLevel_Pres, (1.0/5.255)));
//  return altitude;
//}
//***************BME280から16bitデータ読み込み関数****************************
uint16_t read16bit(uint8_t reg) {
  uint16_t d1, d2;
  uint16_t data;
  P1OUT &= ~BIT3;
  SpiWrite(reg | 0x80); // read, bit 7 high
  d1 = SpiRead();
  d2 = SpiRead();
  data = (d2 << 8) | d1;
  P1OUT |= BIT3;
  return data;
}
//***************BME280から8bitデータ読み込み関数****************************
uint8_t read8bit(uint8_t reg) {
  uint8_t data;
  P1OUT &= ~BIT3;
  SpiWrite(reg | 0x80); // read, bit 7 high
  data = SpiRead();
  P1OUT |= BIT3;
  return data;
}
//***************BME280へSPI信号データ送信関数****************************
void SpiWrite(uint8_t data) {
    volatile char hoge;
    while (!(IFG2 & UCB0TXIFG));
    UCB0TXBUF = data;
    while (!(IFG2 & UCB0RXIFG));
    hoge = UCB0RXBUF;
}
//***************BME280からのSPI信号データ読み込み関数****************************
uint8_t SpiRead() {
  uint8_t r_data = 0;
  UCB0TXBUF = 0x00;
  while (!(IFG2 & UCB0RXIFG));
  r_data = UCB0RXBUF;
  return r_data;
}

#endif /* BME280_H_ */

電圧・電流測定

作成後、電圧をどこまで下げても動作するかとその際の電流値を測定した。
下の様に安定化電源で電源供給してDMMで電流測定している。 f:id:ginnyu-tei:20170801234005j:plain:w400
電流値は送信中とスリープ中で全く異なる値になるのでしばらく動かしてMaxを確認する。

電圧 Imax Imin
3.3V 957uA 541uA
3.0V 920uA 531uA
2.5V 865uA 516uA
2.0V 820uA 502uA
1.8V 799uA 496uA

1.8Vまでは起動・送信が問題なかったが1.8Vを下回ると不安定になった。
これで動作確認もできたので次回は長時間のロギングをやってみる。

参考にしたサイト

BME280 – スイッチサイエンス
MSP430 Tutorial and Resources · Argenox Technologies
MSP430班(2006/05/02) ウォッチドッグタイマ2

Vivado IP Integratorでap_fifoとfifoを簡単に接続する

ap_fifofifoを接続するときの小ネタ。

ap_fifofifoと接続されない

Vivado HLSでap_fifoを多用する私ですが、Vivado IP Integrator上でFIFO Generatorと接続しようとしてもそのままでは繋がりません。
f:id:ginnyu-tei:20170717120816j:plain:w400
↑の様に接続候補に現れない。

ap_fifoは何故かfullやempty信号が負論理になっているので反転する必要があります。

f:id:ginnyu-tei:20170717121804j:plain:w400
↑この様な形で配線が必要。

この配線を毎回するのは面倒な上にせっかくIP IntegratorでFIFO I/Fがまとめてられているのに毎度ばらして配線するのは見た目が悪いので変換するIPを作成した。
これを使うと↓の様に配線がすっきりする。
f:id:ginnyu-tei:20170717123204j:plain:w400

ap_fifo2fifo

まずIPにするRTLだが↓の様にFULLを反転するコードを用意する。
これでIPを作成すれば良い。

module ap_fifo2fifo(
    WR_DATA_o,
    FULL_N_o,
    WR_EN_o,
    WR_DATA_i,
    FULL_i,
    WR_EN_i
);

parameter WIDTH = 32;

//ap_fifo
input [WIDTH-1:0] WR_DATA_o;
output FULL_N_o;
input WR_EN_o;

//fifo
output [WIDTH-1:0] WR_DATA_i;
input FULL_i;
output WR_EN_i;


assign WR_DATA_i = WR_DATA_o;
assign FULL_N_o = ~FULL_i;
assign WR_EN_i = WR_EN_o;

endmodule

このままIPを生成すると各ポートがそのままになるのでI/F定義が必要になる。
f:id:ginnyu-tei:20170717124414j:plain:w400

din側はInterface Definitionを"acc_fifo_write_rtl"のslaveにして各ポートを割り当てる。
f:id:ginnyu-tei:20170717124525j:plain:w400
f:id:ginnyu-tei:20170717124718j:plain:w400
dout側はInterface Definitionを"fifo_write_rtl"のmasterにして各ポートを割り当てる。
f:id:ginnyu-tei:20170717124838j:plain:w400 f:id:ginnyu-tei:20170717124851j:plain:w400

fifo2ap_fifo

ap_fifo2fifoもemptyを反転するRTLを用意してIPを作成する。

module fifo2ap_fifo(
    RD_DATA_i,
    EMPTY_i,
    RD_EN_i,
    RD_DATA_o,
    EMPTY_N_o,
    RD_EN_o
);

parameter WIDTH = 32;

//fifo
input [WIDTH-1:0] RD_DATA_i;
input EMPTY_i;
output RD_EN_i;

//ap_fifo
output [WIDTH-1:0] RD_DATA_o;
output EMPTY_N_o;
input RD_EN_o;

assign RD_DATA_o = RD_DATA_i;
assign EMPTY_N_o = ~EMPTY_i;
assign RD_EN_i = RD_EN_o;

endmodule

こちらもI/F定義をやっていく。
f:id:ginnyu-tei:20170717130240j:plain:w400

din側はInterface Definitionを"fifo_read_rtl"のmasterにして各ポートを割り当てる。 f:id:ginnyu-tei:20170717130343j:plain:w400
f:id:ginnyu-tei:20170717130354j:plain:w400
dout側はInterface Definitionを"acc_fifo_read_rtl"のslaveにして各ポートを割り当てる。
f:id:ginnyu-tei:20170717130425j:plain:w400
f:id:ginnyu-tei:20170717130439j:plain:w400

後はVivadoのプロジェクト上から各IPをインスタンスすれば簡単に配線できる。

VivadoHLSで射影変換を実装してみた-3-

↓の続きで今回が多分射影変換の最終回

se.hatenablog.jp

原因追求

前回射影変換をお手軽に実装したがかなり遅かったのでまずは原因を確かめる。
と、言っても原因は↓のAXI HP3の波形を見れば明らか。
f:id:ginnyu-tei:20170716121548j:plain:w400
Write/Read共にシングルアクセスなので1pix読み書きに時間がかかっている。なのでピクセルクロックの3倍で動作させても遅いのである。

しかし、射影変換は座標を変換するのでアドレスが飛び飛びになる。これ自体はどうしようもないので他の方法で速度アップを図る。

クロックアップ(150MHz→250MHz)

まず、手抜き手軽な対応としてバス周りのクロックアップをやってみる。homography_dmaとHP3バス周りを250MHzまで上げてみた。
ちなみにhomography_dmaは300MHzまで合成できたが、PSからPLへ供給できるクロックが250MHzまでだったので250MHzにした。
その結果が約33fpsまで上がった。
f:id:ginnyu-tei:20170716211315j:plain:w400

ただし、AXI HP3の波形はあまり変わっていない。
f:id:ginnyu-tei:20170716215304j:plain:w400

ライトアクセス改善

次にライトアクセスを改善してみる。
上でも書いたとおり射影変換のアドレスが飛び飛びになるが今回の実装では、画像データライトは連続している。
そこで、homography_dmaを改造してライトはバースト転送にする。

f:id:ginnyu-tei:20170716214224j:plain
↑の様にリードしたデータを一旦FIFOにプッシュして別モジュールからバーストライトを実行する。
これでDRAMへの負荷軽減を期待している。

では、コードを記載していく。
まずはhomography_dma_write_fifoここはo_dataのm_axiをap_fifo変えるだけ。この様にI/Fを手軽に変更できるのも高位合成の良い所。

void homography_dma_write_fifo(
        unsigned int *i_data,
        unsigned int *o_data,
        unsigned int *addr
        ){
#pragma HLS INTERFACE ap_fifo depth=786432 port=addr
#pragma HLS INTERFACE m_axi depth=786432 port=i_data
#pragma HLS INTERFACE ap_fifo depth=786432 port=o_data

    unsigned int tmp;
    for (int i = 0 ; i < 1024*768; i++){
#pragma HLS PIPELINE
        tmp = *(addr + i);
        if (tmp > 786432){
            o_data[i] = 0;
        } else {
            o_data[i] = i_data[tmp];
        }
    }
}

次にfifo2axi_xgaのコード

void fifo2axi_xga(
        unsigned int *i_data,
        unsigned int *o_data
        ){
#pragma HLS INTERFACE ap_fifo depth=786432 port=i_data
#pragma HLS INTERFACE m_axi depth=786432 port=o_data

    for (int i = 0 ; i < 1024*768; i++){
        o_data[i] = i_data[i];
    }
}

ただfifoのデータをaxiに書き込むだけである。ここはバーストlengthを指定できないか色々やってみたが、この書き方が16バースト固定で一番速度が早かった。
Vivado HLSでCosimした結果が↓
f:id:ginnyu-tei:20170716223136j:plain:w400

実際にはFIFOにライトする速度の方が早いのでfifoにある程度データが溜まってからライトを開始するようにすれば効率よくAXIライト出来ると思う。
(これ書いてて思いついたが、ap_fifoのempty信号にAlmost Fullを使ってバーストlength分溜まったら信号を出す様にすれば必ず1バースト分は連続で出力できる。でも、1フーレムのデータ数と1バーストの転送量の関係によってはFIFOに半端なデータが残りそう)

これで必要なモジュールができたのでIP Integratorで配線した結果が↓
f:id:ginnyu-tei:20170717001902j:plain:w400

ブロックがかなり増えたのでCreate Hierarchyで階層化した。
この状態でZedboardでhomography_dma_write_fifoのap_doneを観察した結果が↓
f:id:ginnyu-tei:20170717004256j:plain:w400
約47FPSとかなり改善した。
実際はap_doneの後fifo2axi_xgaがまだ動いているのがパイプライン的な動作になっているのでFPSには影響しない(はず)。
(例えパイプライン動作でなくとも残っているは1バースト+α位で多少増えるだけ)
AXI HP3の波形を確認するとリード側はあまり変化していないが、ライト側はバースト転送になっているのがわかる。
f:id:ginnyu-tei:20170717010613j:plain:w400

さらなる高速化

以上で簡単にできそうな改良をして2倍位早くなった。
これ以上早くするアイディアはいくつかあるが実装に時間がかかりそうなのでメモだけしておく。

  1. ブロックRAMを使う
    フレームデータをDRAMでなくブロックRAMに格納する。これならRead/Writeが1サイクルで可能なので高速化できる。でも、ブロックRAMが多いFPGAじゃないと足りなくなる。
  2. 先にアドレス計算をしておく
    一度パラメータを決めたらしばらく変えなくて良いアプリケーション向けだが、先に1フレーム分座標を計算してバースト転送出来るように並び替えをする。
  3. 変換する範囲をなるだけ狭くする
    今回の様にフレーム分の一部のみ変換する場合はフレーム全て変換せず、変換前/変換後の各4座標から変換に必要な範囲だけを転送するようにする。
  4. ブロックサイズでリードする。
    変換する次のアドレスは今のアドレスの近くにある可能性が高いので8×8や16×16のサイズでリードしてデータを貯めておく。

とりあえず、この位思いついた。実装は3以外大変そうなので誰かこれを試してみてほしい。
また思いついたら書き足す。

Zynqのベアメタルアプリケーションで画像をメモリに書き込む

Zynqでベアメタルアプリケーション(OSなし)を走らせる際
テスト入力として画像データをメモリ上に展開したくなる時がある。
その際GIMPを使えば簡単に画像データをソースコードに変換できるので
blogに書いておく。

GIMP

まず、GIMPをインストール&起動する。
そしてメモリに展開したい画像を開く

開いた後、ファイル→名前をつけてエクスポートを選ぶ、
そして、右下のエクスポートされた全ての画像から
「Cソースコードヘッダ」を選んで適当に名前をつけてエクスポートする。
f:id:ginnyu-tei:20170709005400j:plain

(かなり大きなヘッダファイルが出力されるので開く時は注意!)

出力されたファイルを確認すると以下の様なコードが出力される。

static unsigned int width = 1024;
static unsigned int height = 768;

/*  Call this macro repeatedly.  After each use, the pixel data can be extracted  */

#define HEADER_PIXEL(data,pixel) {\
pixel[0] = (((data[0] - 33) << 2) | ((data[1] - 33) >> 4)); \
pixel[1] = ((((data[1] - 33) & 0xF) << 4) | ((data[2] - 33) >> 2)); \
pixel[2] = ((((data[2] - 33) & 0x3) << 6) | ((data[3] - 33))); \
data += 4; \
}
static char *header_data =
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
~以下画像データが続く~

SDK

ヘッダファイルができたらXilinx SDKでヘッダファイルをincludeする。
HEADER_PIXELを呼び出す毎にラスタースキャンでRGB色が取得できるので
それを直接メモリに書き込む。

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"

#include "test.h" //GIMPでエクスポートしたヘッダファイル


int main()
{
    int i,j;
    char pixel[3]; //RGB格納配列
    unsigned int R,G,B;

    volatile unsigned int *Addr;
    volatile unsigned int *reg_base;

    Addr = (unsigned int*)0x10000000;

    init_platform();

    Xil_DCacheDisable();


    print("Hello World\n\r");

    for (j = 0 ; j < 768;j++){
        for (i = 0 ; i < 1024;i++){
            HEADER_PIXEL(header_data,pixel); //1pix読み込む
            R = pixel[0] & 0xFF;
            G = pixel[1] & 0xFF;
            B = pixel[2] & 0xFF;
            Addr[j*1024+i]= (R << 16) | (G << 8) | (B); //メモリへ書き込み
            //Addr[j*1024+i]=j*1024+i;
        }
    }
~以下省略~

この手法はjtagで画像をFPGAに転送するので大きな画像の場合
少し時間がかかるのが難点だが手軽に使えるのが利点。

VivadoHLSで射影変換を実装してみた-2-

前回の続き

se.hatenablog.jp

Vivado HLS

射影変換は以下図のように2モジュールに分けて実装をした。
f:id:ginnyu-tei:20170708185251j:plain:w200

fifo使ってモジュールを小分けにすると
実装が楽になったり速度が出やすくなるので良く使います。
ここら辺の話は需要があればblogかどっかで纏めるかも。

homography

まずはhomographyのC++コード

void homography(
        int a,
        int b,
        int c,
        int d,
        int e,
        int f,
        int g,
        int h,
        unsigned int *addr)
{
#pragma HLS INTERFACE s_axilite port=a bundle=reg
#pragma HLS INTERFACE s_axilite port=b bundle=reg
#pragma HLS INTERFACE s_axilite port=c bundle=reg
#pragma HLS INTERFACE s_axilite port=d bundle=reg
#pragma HLS INTERFACE s_axilite port=e bundle=reg
#pragma HLS INTERFACE s_axilite port=f bundle=reg
#pragma HLS INTERFACE s_axilite port=g bundle=reg
#pragma HLS INTERFACE s_axilite port=h bundle=reg

   #pragma HLS INTERFACE ap_fifo port=addr

    unsigned int u,v;

    for (int y = 0 ; y < 768 ; y++){
        for (int x = 0;x < 1024; x++){
#pragma HLS PIPELINE
            u = (x*a + y*b + c) / (x*g + y*h + 65536);
            v = (x*d + y*e + f) / (x*g + y*h + 65536);
            int tmp = v*1024+u;
            if ((v >= 0) & (v < 768) & (u >= 0) & (u < 1024)){
                *addr = tmp;
            } else {
                *addr = 786433; //786432 + 1
            }
        }
    }
}

↑の図では書いていなかったがlパラメータa~hはAXI LiteのレジスタにしてCPUから設定。
射影変換の式はC++コードそのままで、変換後座標を画像左上から順に変化させ
変換前座標を計算し、そこからメモリアドレスを計算してFIFOに入れる。
変換後座標値の値によっては範囲外の変換前座標になる事があるので
その時は範囲外という意味で1024*768+1の値をFIFOに入れる。
ここではパイプラインオプションを入れているので毎サイクル計算結果が出力される。

modelsimでシミュレーションしてみると毎サイクル出てくるのが確認できる。
f:id:ginnyu-tei:20170708190530j:plain:w200

そして回路規模
f:id:ginnyu-tei:20170708203559j:plain
積和除算があるのでDSPはそこそこ使ってる。

homography_dma

次はhomography_dmaのコード

void homography_dma(
        unsigned int *i_data,
        unsigned int *o_data,
        unsigned int *addr
        ){
#pragma HLS INTERFACE ap_fifo port=addr
#pragma HLS INTERFACE m_axi depth=786432 port=i_data
#pragma HLS INTERFACE m_axi depth=786432 port=o_data

    unsigned int tmp;
    for (int i = 0 ; i < 1024*768; i++){
#pragma HLS PIPELINE
        tmp = *addr;
        if (tmp > 786432){
            o_data[i] = 0;
        } else {
            o_data[i] = i_data[tmp];
        }
    }
}

homographyで計算したアドレスを使ってo_dataから出力するだけ。
元画像の範囲外座標は黒色を出力する。
なお、今回は実装していないがi_data[tmp]だけリードせず、
例えば2x2pixを読み込みしてバイリニアで補完したり、
4x4pixでバイキュービックで補完すると画質が良くなるはずである。
(この際はhomographyから1次元アドレスではなく座標値を送る用変更が必要)

↓がSim結果
AXIリード・ライト共にシングルアクセスである。
f:id:ginnyu-tei:20170708214451j:plain:w200

↓回路規模
f:id:ginnyu-tei:20170708214833j:plain

Vivado

これでモジュールが完成したので実機で試す。
TOPの配線は全てIP Integratorで済ます。
↓が全て配線したTOP
f:id:ginnyu-tei:20170708215611j:plain:w200

↓概略図
f:id:ginnyu-tei:20170708221823j:plain:w200

まず、0x10000000にCPUから画像データを書き込む。
その後、0x10000000からhomography_dmaモジュールが画像を読みこみ0x11000000に書き出す。
アナログRGBモジュールは0x10000000にある画像データを出力する。
なお、上図には書いていないが書くモジュールのap_startはVIOで制御している。
テスト稼働時はVIOを使うと非常に楽である。

本当は射影変換とアナログRGBモジュールのFPSが異なるので
フレームの同期が取れていないが今回はそのままにしておく。
静止画であれば気にならないと思う。

SDK

以下がPSで動かすプログラムである。
テンプレートにあるHello Worldを改変して使っている。

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"

#include "test.h"

int main()
{
    int i,j;
    char pixel[3];
    unsigned int R,G,B;

    volatile unsigned int *Addr;
    volatile unsigned int *reg_base;

    Addr = (unsigned int*)0x10000000;
    reg_base   =  (unsigned int*)0x43C00000;


    init_platform();

    Xil_DCacheDisable();

    print("Hello World\n\r");

    for (j = 0 ; j < 768;j++){
        for (i = 0 ; i < 1024;i++){
            HEADER_PIXEL(header_data,pixel);
            R = pixel[0] & 0xFF;
            G = pixel[1] & 0xFF;
            B = pixel[2] & 0xFF;
            Addr[j*1024+i]= (R << 16) | (G << 8) | (B);
            //Addr[j*1024+i]=j*1024+i;
        }
    }

    /射影変換パラメータ設定
    reg_base[4]  = 0x2C6C6;
    reg_base[6]  = 0xF6EE;
    reg_base[8]  = 0xFCD32CF7;
    reg_base[10] = 0x248A;
    reg_base[12] = 0x2C492;
    reg_base[14] = 0xFE1BDF57;
    reg_base[16] = 0x49;
    reg_base[18] = 0x2A;

    cleanup_platform();
    return 0;
}

ここで画像データをCPUに書き込むが、
その画像データはGIMPで作成している。 これについては別途blogに書く予定。

Zedboardでテスト

Zedboardで動かした結果が↓
f:id:ginnyu-tei:20170705210623j:plain:w200
前回のソフトで動作させた結果とほぼ同じ形に変形している。

この状態でフレームレートを測定した。
フレームレートの測定はhomography_dmaのap_doneを
Pmodに出力しオシロスコープで間隔を測定した。
↓がオシロスコープの画面
f:id:ginnyu-tei:20170708225509j:plain:w200
ap_doneは1パルスしか出なくオシロで取り逃す可能性がある
そこでap_done毎に信号を反転するモジュールを入れている。
その結果、約24fps出ている事が分かった。
Twitterでは約11fpsとツイートしたが(↓)あれは測定ミスでした。)

以上で射影変換の実装&実機テストは完了である。
今回は射影変換をお手軽に実装するという脳内テーマで
スタートしたので速度や規模はあまり気にしていなかった。
(無事動いたのでOKとする)
とは言え、homography_dmaは150MHz(ピクセルクロックの3倍)で動作させているのに
24フーレムしか出ていないので、次回は原因についてと高速化について言及する。

VivadoHLSで射影変換を実装してみた-1-

台形補正のアルゴリズムを調べてたら射影変換でやるみたいなのでFPGAに実装してみた。

OpenCV

まずはOpenCVでさくっとテスト

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
int main(void)
{
    //cv::Mat src_img;
    //src_img = cv::imread("test.jpg", 1);
    IplImage *pImg;
    pImg = cvLoadImage("test.jpg");
    //IplImage -> Mat
    cv::Mat mat(pImg);
    cv::Mat src_img;
    src_img = pImg;

    cv::Mat dst_img = src_img.clone();

    if (src_img.empty()){
        return -1;
    }

    const cv::Point2f src_pt[]={
        cv::Point2f(256, 128),
        cv::Point2f(768, 128),
        cv::Point2f(768, 640),
        cv::Point2f(256, 640) };

    const cv::Point2f dst_pt[]={
        cv::Point2f(356, 228),
        cv::Point2f(768, 228),
        cv::Point2f(668, 640),
        cv::Point2f(256, 540)};

        //パラメータ計算
    cv::Mat homography_matrix = cv::getPerspectiveTransform(src_pt,dst_pt);
        //射影変換
    cv::warpPerspective( src_img, dst_img, homography_matrix,src_img.size());

    cv::namedWindow("Image", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);
    cv::imshow("Image", dst_img);
    cv::waitKey(0);
}

f:id:ginnyu-tei:20170705220240j:plain f:id:ginnyu-tei:20170705220131j:plain こんな風に変換前の4点座標と変換後の4点座標を指定することで
座標に合うように画像を変形してくれます。

C++で実装

C++での実装は↓のページを丸パクリ参考にしました。
mf-atelier.sakura.ne.jp

C++で実装する際、ついでに16ビットシフトして整数演算化しました。
多分小数点そこまでなくても問題ないと思う

void homography(
    int x,
    int y,
    int a,
    int b,
    int c,
    int d,
    int e,
    int f,
    int g,
    int h,
    int *u,
    int *v)
{
    *u = (x*a + y*b + c) / (x*g + y*h + 65536);
    *v = (x*d + y*e + f) / (x*g + y*h + 65536);
}


int main(void)
{

    double a, b, c, d, e, f, g, h;
    int i_a, i_b, i_c, i_d, i_e, i_f, i_g, i_h;
    int u, v;

    int    naOrig[4][2] = { { 356, 228 }, { 768, 228 }, { 668, 640 }, { 256, 540 } };
    int    naTran[4][2] = { { 256, 128 }, { 768, 128 }, { 768, 640 }, { 256, 640 } };

    //cv::Mat src_img;
    //src_img = cv::imread("test.jpg", 1);

    IplImage *pImg;
    pImg = cvLoadImage("test.jpg");
    //IplImage -> Mat
    cv::Mat mat(pImg);
    cv::Mat src_img;
    src_img = pImg;

    cv::Mat dst_img = src_img.clone();

    if (src_img.empty()){
        return -1;
    }

    homography_param
        (
        naOrig,
        naTran,
        &a,
        &b,
        &c,
        &d,
        &e,
        &f,
        &g,
        &h
        );

    std::cout <<
        "a=" << a << std::endl <<
        "b=" << b << std::endl <<
        "c=" << c << std::endl <<
        "d=" << d << std::endl <<
        "e=" << e << std::endl <<
        "f=" << f << std::endl <<
        "g=" << g << std::endl <<
        "h=" << h <<
        std::endl;

    i_a = (int)(a * 65536);
    i_b = (int)(b * 65536);
    i_c = (int)(c * 65536);
    i_d = (int)(d * 65536);
    i_e = (int)(e * 65536);
    i_f = (int)(f * 65536);
    i_g = (int)(g * 65536);
    i_h = (int)(h * 65536);

    std::cout <<
        "i_a=" << i_a << std::endl <<
        "i_b=" << i_b << std::endl <<
        "i_c=" << i_c << std::endl <<
        "i_d=" << i_d << std::endl <<
        "i_e=" << i_e << std::endl <<
        "i_f=" << i_f << std::endl <<
        "i_g=" << i_g << std::endl <<
        "i_h=" << i_h <<
        std::endl;

    cv::Vec3b *src = src_img.ptr<cv::Vec3b>(0);
    cv::Vec3b *dst = dst_img.ptr<cv::Vec3b>(0);

    for (int y = 0; y < 768; y++){
        for (int x = 0; x < 1024; x++){
            homography(x, y, i_a, i_b, i_c, i_d, i_e, i_f, i_g, i_h, &u, &v);
            if ((u >= 0) & (u < 1024) &
                (v >= 0) & (v < 768)){
                dst[y * 1024 + x] = src[v * 1024 + u];
            }
            else {
                dst[y * 1024 + x] = cv::Vec3b(0, 0, 0);
            }
        }
    }
    cv::namedWindow("Image", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);
    cv::imshow("Image", dst_img);
    cv::waitKey(0);
}

パラメータ変換式は参考元のコードをほぼそのまま使用した。 (関数名をhomography_paramに変更)
逆行列計算はOpenCVを使って↓の用に実装

 // 逆行列
    //matinv(8, dATA, dATA_I);
    cv::Mat matA = (cv::Mat_<double>(8, 8));

    for (int j = 0; j < 8; j++){
        for (int i = 0; i < 8; i++){
            matA.at<double>(i, j) = dATA[i][j];
        }
    }

    matA = matA.inv();

    for (int j = 0; j < 8; j++){
        for (int i = 0; i < 8; i++){
            dATA_I[i][j] = matA.at<double>(i, j);
        }
    }

パラメータをどうやって計算するか知りたかったので数式を見たら
8次元連立一次方程式で自分で解くのは諦めました・・・

それと大事な事なんですが、ここでは変換後座標から変換前座標を計算したいので
変換前座標と変換後座標を入れ替えて計算しています。

こうする事で変換後画像内に変換前画像が当てはまらず
ピクセルが抜けてしまう事が防げます。
その為、ピクセルを補完をしなくても良くなります。
(正確には一番近い画素を変換後座標にコピーしているので
ニアレストネイバーを使ってる言える。)

余談 射影変換の逆計算方法が初め分からず逆行列の計算を
するのかとげんなりしていましたが以下ページを見て解決しました。

detail.chiebukuro.yahoo.co.jp

長くなったのでVivado HLSのコードは次回