続いて、デバッガーを起動してPS(ARM CPUコア)のプログラムを転送しますが、ちょっとコツがあります。私の環境では、Debgug Configurationをいきなり作って起動しようとするとエラーが出てプログラムの起動に失敗することが多いです。そのため、Project ExplorerのI2C_Testプロジェクトを右クリックし、Debug As → Launc on Hardware (GDB)を選択してまずプログラムを起動します。無事プログラムが起動するとmainの最初の行でプログラムがブレークします。
この段階ではDebug Configurationをしていないため、"STDIO not connected”の警告がConsoleに出力され、print文の実行結果は表示されません。ここで一旦、ツルーバーのTerminateボタンを押してプログラムを終了します。デバッグPerspectiveから一旦C/C++ Perspectiveに戻ってProject Explorer → I2C_Testプロジェクトを右クリック → Debug As → Debug Configurations..を選択。STDIO ConnectionにCOMポート番号と通信速度(115200)を設定してDebugをクリック。ツールバーのResumeボタンをクリックするとプログラムが動き出します。
購入した機種はRIGOL DS1054Z(中国製)でAmazonから購入しました。同価格帯(5万円台)で買える、Tektronix TBS1052Bとどちらにしようかと少し悩んだのですが、4ch vs 2chでRIGOLの方がch数が多いこと、その他測定機能もRIGOLの方が充実しているように見えたのと、Amazonや他のBlogのレビューを見ても、RIGOLの評価は概ね良かったので、ブランド的にはTextronixですがコスパを取ってRIGOLにしました。
Video Out IPに画像を流し込むためには、カメラ入力側にAXI4-Stream Masterインタフェースを作りこむ必要があります。今にして思うと、いきなりAIX4を作るというのも無謀な試みでした。You TubeにAXI-Stram Masterを作るチュートリアルビデオがあり、(英語ですが平易な語りなのでなんとか理解できますが、量は結構多い)これを参考にカメラデータの取り込みとAXI4-Stram出力のIPを作ってみました。単体のテストベンチではAIX4の制御信号を出力できているように見えていたのですが、Video OutやVTCと結合していきなり実機で動かそうとしても全く画像が表示されず。
当初、OV7670の解像度がVGA(640 x 480)なのでVGAモニターにスルーでデーターを流せると思っていたのですが、オシロで出力波形を観測すると垂直同期(リフレッシュ周期)が24Hz〜30Hz, 水平同期が12.5KHz位で、VGAのリフレッシュレート60Hz, 水平同期31KHzに対して速度が大きく異なるため、そもそもスルーで出力できないということが判明(考えてみれば、3000円以下のカメラモジュールで60Hzのリフレッシュレートを期待するのが間違っている)。Video OutやVTC IPもブラックボックスで中身が分からないため、問題の切り分けができず、表示部分も自分で作った方がよいと方針転換。
カメラモジュールとVGAの動作速度が異なるため、速度差を吸収するために、VRAM(Dual Port RAM)を間に入れて、カメラ入力に同期して画像データをVRAMに書き込み、VGAのPixelレートで画像データーを読み出せるようにする必要があります。
VGAのフル解像度を使うためには、必要なメモリー容量的にZYBOのDDR3-SDRAMを使う必要がありますが、いきなりDDR3-SDRAMを使うのはハードルが高いので、先ずはFPGA内に作れるBRAM(Block RAM)を使うことにしました。ZYBOに搭載されているZYNQ XC7Z010-1CLG400CのBRAM容量の制約から、VGAのフル解像度を使うのは諦めて、QVGA(320 x 240)で画像を取り込んで、VGA画面(640 x 480)の中央に320 x 240の画像を表示できるようにすることを目標に再設定。
VGA(640 x 480) x 16bitカラーの画像を扱うためには、600KBのVRAM容量が必要となり、QVGAのようにFPGAのBRAMには格納できないため、ZYBOのPS側に搭載されているDDR3 RAMをVRAMとして使用する必要があります。今回はPL(FPGA部)からDDR3 RAMへのアクセスを行うことがテーマでした。
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer: Kenshi Kamiya
//
// Create Date: 2016/10/11 15:00:13
// Design Name: ov7670_camera
// Module Name: ov7670_camera
// Project Name: ov7670 camera data capture and output data wht ap_hs
// Target Devices: Zybo
// Tool Versions: Vivado 2016.3
//
// Revision: 01
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
/* This is a bit tricky href starts a pixel transfer that takes 3 cycles for first pixel
then 2 cycles after 2nd pixcel
Input | state after clock tick
href | wr_hold data_in data_out we col col_next
cycle -1 x | xx xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx x xxxx xxx
cycle 0 1 | 00 xxxxxxxxRRRRRGGG xxxxxxxxxxxxxxxx x xxxx col
cycle 1 0 | 01 RRRRRGGGGGBBBBB xxxxxxxxRRRRRGGG x col col
cycle 2 x | 10 GGGBBBBBxxxxxxxx RRRRRGGGGGGBBBBB 1 col col+1
*/
module ov7670_camera_hs(
input wire clk,
input wire resetN,
input wire pclk,
input wire vsync,
input wire href,
input wire [7:0] data,
output reg [9:0] line,
output reg [9:0] col,
output reg [15:0] data_out,
output wire ap_start,
input wire ap_idle,
input wire ap_done,
output wire col_vld,
output wire line_vld,
output wire data_vld,
input wire col_ack,
input wire line_ack,
input wire data_ack
);
reg [9:0] col_next;
reg [9:0] line_next;
reg [7:0] data_s;
reg [15:0] data_in;
reg [1:0] wr_hold;
reg we;
reg [2:0] state;
reg [3:0] control;
// Registers for synchronizer
reg pclk_d, pclk_s;
reg href_d, href_s;
parameter IDLE=0, AP_START=1, OUT_VLD=2, OUT_ACK=3, AP_DONE=4, WAITE_NEXT=5;
parameter MAXCOL=640, MAXLINE=480;
// Synchronizer for pclk
always @(posedge clk)
begin
pclk_d <= pclk;
pclk_s <= pclk_d;
end
// Synchronizer for href
always @(posedge clk)
begin
href_d <= href;
href_s <= href_d;
end
// Synchronizer for input data
always @(posedge clk)
begin
data_s <= data;
end
// FSM for ap_hs protocol
always @(posedge clk)
if (resetN == 0)
state <= IDLE;
else
begin
case(state)
IDLE: begin
if (we == 1)
begin
state <= AP_START;
end
else
begin
state <= IDLE;
end
end
AP_START: begin
state <= OUT_VLD;
end
OUT_VLD: begin
state <= OUT_ACK;
end
OUT_ACK: begin
// ap_ack retun immediatry, so no chcke ap_ack
// To catch when ap_done return one clclk after ap_ack
if (ap_done == 1)
begin
state <= WAITE_NEXT;
end
else
begin
state <= AP_DONE;
end
end
AP_DONE: begin
if (ap_done == 1)
begin
state <= WAITE_NEXT;
end
else
begin
state <= AP_DONE;
end
end
WAITE_NEXT: begin
// Wait to end current WE cycle
if(we == 1)
begin
state <= WAITE_NEXT;
end
else
begin
state <= IDLE;
end
end
default: begin
state <= IDLE;
end
endcase
end
// Generate control signal
always @(*)
case(state)
IDLE: control = 4'b0000;
AP_START: control = 4'b1000;
OUT_VLD: control = 4'b1111;
OUT_ACK: control = 4'b0000;
AP_DONE: control = 4'b0000;
WAITE_NEXT: control = 4'b0000;
default: control = 4'b0000;
endcase
assign {ap_start, col_vld, line_vld, data_vld} = control;
// Camera data capture FSM
always @(posedge pclk_s)
begin
if (vsync)
wr_hold <= 2'd0;
else
begin
we <= wr_hold[1];
wr_hold <= {wr_hold[0], (href_s & ~wr_hold[0])};
data_out <= data_in;
data_in <= {data_in[7:0], data_s};
end
end
always @(posedge pclk_s)
begin
if (vsync)
begin
col <= 10'd0;
col_next <= 10'd0;
end
else
begin
col <= col_next;
if (wr_hold[1] == 1)
begin
col_next <= col_next + 1;
if (col_next == MAXCOL - 1)
col_next <= 10'd0;
end
end
end
always @(posedge pclk_s)
begin
if (vsync)
begin
line <= 10'd0;
line_next<= 10'd0;
end
else
begin
line <= line_next;
if (wr_hold[1] == 1 && col_next == MAXCOL - 1)
begin
line_next <= line_next + 1;
if (line_next == MAXLINE - 1)
line_next <= 10'd0;
end
end
end
endmodule
<VGA IF>
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer: Kenshi Kamiya
//
// Create Date: 2016/11/06 16:33:59
// Design Name: VGA Driver with ap_hs protocol
// Module Name: vga_hs
// Project Name: OV7670_VGA
// Target Devices: Zybo
// Tool Versions: Vivado 2016.3
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module vga_hs(
input wire clk,
input wire clk25,
input wire resetN,
output reg [4:0] vo_r_data,
output reg [5:0] vo_g_data,
output reg [4:0] vo_b_data,
output reg vo_hsync,
output reg vo_vsync,
output reg [9:0] vga_line,
output reg [9:0] vga_col,
input wire [15:0] frame_pixel,
output wire ap_start,
input wire ap_idle,
input wire ap_done,
output wire line_vld,
output wire col_vld,
input wire line_ack,
input wire col_ack
);
parameter hRez = 640, hFrontPorch = 16, hSyncPluse = 96, hBackPorch = 48, hMaxCount = 800;
parameter vRez = 480, vFrontPorch = 10, vSyncPluse = 2, vBackPorch = 33, vMaxCount = 525;
parameter hDispStart = hFrontPorch + hSyncPluse + hBackPorch; // 160
parameter vDispStart = vFrontPorch + vSyncPluse + vBackPorch; // 45
parameter IDLE=0, AP_START=1, OUT_VLD=2, AP_DONE=3, WAITE=4;
reg [9:0] hCounter;
reg [9:0] vCounter;
reg [9:0] cur_hCounter;
reg blank;
reg [2:0] state;
reg [15:0] pixelData;
reg [2:0] control;
wire cke;
assign cke = ~blank & clk25;
// hCounter
always @(posedge clk25)
begin
if (resetN == 0)
hCounter <= 10'd0;
else if (hCounter == (hMaxCount - 1))
hCounter <= 10'd0;
else
hCounter <= hCounter + 1;
end
//vCounter
always @(posedge clk25)
begin
if (resetN == 0)
vCounter <= 10'd0;
else if (hCounter == (hMaxCount - 1))
if (vCounter == (vMaxCount - 1))
vCounter <= 10'd0;
else
vCounter <= vCounter + 1;
end
// FSM for ap_hs protocol
always @(posedge clk)
if (resetN == 0)
state <= 0;
else
case(state)
IDLE: begin // State 0
if ( (cke && vga_col >= 1) || (vCounter >= vDispStart && hCounter == 11'd10))
state = AP_START;
else
state = IDLE;
end
AP_START: begin // State 1
state = OUT_VLD;
end
OUT_VLD: begin // State 2
state = AP_DONE;
end
AP_DONE: begin // State 3
// ap_ack retun immediately, so no chcke ap_ack
if (ap_done && cke)
state <= AP_START; // Sthort cut to execute next cycle
else if (ap_done )
state = IDLE;
else if (ap_done && hCounter < hDispStart)
state = WAITE;
else
state= AP_DONE;
end
WAITE: begin // State 4
// Waite to finish hBlanking in case read first block at line start
if (hCounter < hDispStart)
state = WAITE;
else
state = IDLE;
end
default: begin
state = IDLE;
end
endcase
always @(*)
case(state)
IDLE: control = 3'b000;
AP_START: control = 3'b100;
OUT_VLD: control = 3'b111;
AP_DONE: control = 3'b000;
WAITE: control = 3'b000;
default control = 4'b000;
endcase
assign {ap_start, line_vld, col_vld} = control;
// Generate HSYNC
always @(posedge clk25)
begin
if (resetN == 0)
vo_hsync <= 1;
else if ((hCounter > hFrontPorch) && (hCounter <= hFrontPorch + hSyncPluse))
vo_hsync <= 0;
else
vo_hsync <= 1;
end
// Generate VSYNC
always @(posedge clk25)
begin
if (resetN == 0)
vo_vsync <= 1;
else if ((vCounter > vFrontPorch) && (vCounter <= vFrontPorch + vSyncPluse))
vo_vsync <= 0;
else
vo_vsync <= 1;
end
// Generate blank signal
always @(posedge clk25)
begin
if (resetN == 0)
blank <= 1;
else if ((vCounter >= vDispStart) && (vCounter < vMaxCount ) && (hCounter >= hDispStart) && (hCounter < hMaxCount) )
blank <= 0;
else
blank <= 1;
end
// VGA column address
always @(posedge clk25)
begin
if (resetN == 0)
vga_col <= 10'd0;
else if (blank == 0)
if (vga_col == hRez - 1)
vga_col <= 10'd0;
else
vga_col <= vga_col + 1;
end
// VGA line address
always @(posedge clk25)
begin
if (resetN == 0)
vga_line <= 10'd0;
else if (blank == 0)
if (vga_col == (hRez - 1))
if (vga_line == (vRez -1))
vga_line <= 10'd0;
else
vga_line <= vga_line +1;
end
// Generate color signal
always @(posedge clk25)
begin
if (blank == 0)
begin
vo_r_data <= pixelData[15:11];
vo_g_data <= pixelData[10:5];
vo_b_data <= pixelData[4:0];
end
else
begin
vo_r_data <= 5'd0;
vo_g_data <= 6'd0;
vo_b_data <= 5'd0;
end
end
always @(posedge ap_done)
begin
pixelData <= frame_pixel;
end
endmodule
PYNQ-Z1はZYBOとよく似たZYNQ SoCを使ったFPGAボードですが、Pythonを使ってLinuxからFPGAのリソースにアクセスできることが特徴です。PYNQではFPGAのConfiguration Data (bitsteam) をOverlayと呼んでおり、標準でPYNQのI/Oやビデオ関係の処理ができるOverlayが提供されているのですが、ドキュメントを読んでいると、カスタムOverlayも作成できるとあります。
class Bitstream(PL):
"""This class instantiates a programmable logic bitstream.
Attributes
----------
bitfile_name : str
The absolute path of the bitstream.
timestamp : str
Timestamp when loading the bitstream. Format:
year, month, day, hour, minute, second, microsecond
"""
def __init__(self, bitfile_name):
"""Return a new Bitstream object.
Users can either specify an absolute path to the bitstream file
(e.g. '/home/xilinx/src/pynq/bitstream/base.bit'),
or only a relative path.
(e.g. 'base.bit').
Note
----
self.bitstream always stores the absolute path of the bitstream.
Parameters
----------
bitfile_name : str
The bitstream absolute path or name as a string.
"""
super().__init__()
if not isinstance(bitfile_name, str):
raise TypeError("Bitstream name has to be a string.")
if os.path.isfile(bitfile_name):
self.bitfile_name = bitfile_name
elif os.path.isfile(general_const.BS_SEARCH_PATH + bitfile_name):
self.bitfile_name = general_const.BS_SEARCH_PATH + bitfile_name
else:
raise IOError('Bitstream file {} does not exist.'\
.format(bitfile_name))
self.timestamp = ''
def download(self):
"""The method to download the bitstream onto PL.
Note
----
The class variables held by the singleton PL will also be updated.
Parameters
----------
None
Returns
-------
None
"""
# Compose bitfile name, open bitfile
with open(self.bitfile_name, 'rb') as f:
buf = f.read()
# Set is_partial_bitfile device attribute to 0
with open(general_const.BS_IS_PARTIAL, 'w') as fd:
fd.write('0')
# Write bitfile to xdevcfg device
with open(general_const.BS_XDEVCFG, 'wb') as f:
f.write(buf)
t = datetime.now()
self.timestamp = "{}/{}/{} {}:{}:{} +{}".format(t.year,t.month,t.day,\
t.hour,t.minute,t.second,t.microsecond)
# Update PL information
PL._client_request()
PL._bitfile_name = self.bitfile_name
PL._timestamp = self.timestamp
PL._ip_dict = {}
PL._gpio_dict = {}
PL._server_update()
class MMIO:
""" This class exposes API for MMIO read and write.
Attributes
----------
virt_base : int
The address of the page for the MMIO base address.
virt_offset : int
The offset of the MMIO base address from the virt_base.
base_addr : int
The base address, not necessarily page aligned.
length : int
The length in bytes of the address range.
debug : bool
Turn on debug mode if it is True.
mmap_file : file
Underlying file object for MMIO mapping
mem : mmap
An mmap object created when mapping files to memory.
array : numpy.ndarray
A numpy view of the mapped range for efficient assignment
"""
def __init__(self, base_addr, length=4, debug=False):
"""Return a new MMIO object.
Parameters
----------
base_addr : int
The base address of the MMIO.
length : int
The length in bytes; default is 4.
debug : bool
Turn on debug mode if it is True; default is False.
"""
if base_addr < 0 or length < 0:
raise ValueError("Negative offset or negative length.")
euid = os.geteuid()
if euid != 0:
raise EnvironmentError('Root permissions required.')
# Align the base address with the pages
self.virt_base = base_addr & ~(mmap.PAGESIZE - 1)
# Calculate base address offset w.r.t the base address
self.virt_offset = base_addr - self.virt_base
# Storing the base address and length
self.base_addr = base_addr
self.length = length
self.debug = debug
self._debug('MMIO(address, size) = ({0:x}, {1:x} bytes).',
self.base_addr, self.length)
# Open file and mmap
self.mmap_file = os.open(general_const.MMIO_FILE_NAME,
os.O_RDWR | os.O_SYNC)
self.mem = mmap.mmap(self.mmap_file, (self.length + self.virt_offset),
mmap.MAP_SHARED,
mmap.PROT_READ | mmap.PROT_WRITE,
offset=self.virt_base)
self.array = np.frombuffer(self.mem, np.uint32,
length >> 2, self.virt_offset)
def __del__(self):
"""Destructor to ensure mmap file is closed
"""
os.close(self.mmap_file)
def read(self, offset=0, length=4):
"""The method to read data from MMIO.
Parameters
----------
offset : int
The read offset from the MMIO base address.
length : int
The length of the data in bytes.
Returns
-------
list
A list of data read out from MMIO
"""
if not length == 4:
raise ValueError("MMIO currently only supports 4-byte reads.")
if offset < 0 or length < 0:
raise ValueError("Negative offset or negative length.")
idx = offset >> 2
if idx << 2 != offset:
raise MemoryError('Read operation unaligned.')
self._debug('Reading {0} bytes from offset {1:x}',
length, offset)
# Read data out
return int(self.array[idx])
def write(self, offset, data):
"""The method to write data to MMIO.
Parameters
----------
offset : int
The write offset from the MMIO base address.
data : int / bytes
The integer(s) to be written into MMIO.
Returns
-------
None
"""
if offset < 0:
raise ValueError("Negative offset.")
idx = offset >> 2
if idx << 2 != offset:
raise MemoryError('Write operation not aligned.')
if type(data) is int:
self._debug('Writing 4 bytes to offset {0:x}: {1:x}',
offset, data)
self.array[idx] = np.uint32(data)
elif type(data) is bytes:
length = len(data)
num_words = length >> 2
if num_words << 2 != length:
raise MemoryError('Need an integer number of words')
buf = np.frombuffer(data, np.uint32, num_words, 0)
self.array[offset:offset + num_words] = buf
else:
raise ValueError("Data type must be int or bytes.")
from pynq import MMIO
from pynq import PL
LEDS_OFFSET0 = 0
class MyLED(object):
"""This class controls the onboard LEDs vi axi_gpio_0. """
_mmio = None
_leds_value = 0
def __init__(self):
"""Create a new MyLED object. """
if MyLED._mmio is None:
MyLED._mmio = MMIO(int(PL.ip_dict["SEG_axi_gpio_0_Reg"][0],16),16)
MyLED._mmio.write(LEDS_OFFSET0, 0x0)
def set(self, value):
"""Turn on a LED.
Parameters
----------
Value = GPIO out data
Returns
-------
None
"""
MyLED._mmio.write(LEDS_OFFSET0, value)