Has anyone successfully created a 2-input, 1-output RFNoC block that
they could share, or at least help me figure out what I'm doing
incorrectly?

I had set out to create a 2-input, 1-output RFNoC block: Complex
Multiply.  There's already a 'cmul' module in the uhd-fpga source, all
it needed was a proper RFNoC wrapper.

I reviewed the source of the addsub block, as it has two inputs. 
However, its a little odd in its implementation as it doesn't follow
the design pattern using an AXI wrapper between the the RFNoC shell and
the module IP.  It uses its own chdr deframer (which is what
axi_wrapper seems like it should do instead).

I had also found this list post from 2017:
http://lists.ettus.com/pipermail/usrp-users_lists.ettus.com/2017-June/053156.html

There didn't appear to be any published resolution on that thread.

It turns out, finding information on 2-input, 1-output blocks is a bit
of a challenge, some other relevant posts:
http://lists.ettus.com/pipermail/usrp-users_lists.ettus.com/2018-January/055504.html

  I had also found this post that featured some example code and some
discussion about a "combiner" block.  It's very similar to what I
needed to do, but different arithmetic required:
http://lists.ettus.com/pipermail/usrp-users_lists.ettus.com/2017-October/054654.html

Another list post about 2-in 1-out, but no clear resolution (other than
physical limitations):
http://lists.ettus.com/pipermail/usrp-users_lists.ettus.com/2017-September/054460.html


So, armed with some information I followed 
https://kb.ettus.com/Getting_Started_with_RFNoC_Development#Starting_a_custom_RFNoC_block_using_RFNoC_Modtool

and created a module. 

The skeleton loopback code worked just fine with the testbench.  I then
modified the testbench similar to the addsub testbench and added code
for the cmul IP, and modified the noc block and noc shell settings to
support 2 inputs, 1 output.  Running that testbench resulted in an
unexpected result: it seemed that the second AXI stream wasn't
receiving data from the testbench.

I figured my best course of action then was to use Andy's combiner
block, because it allegedly runs in a testbench.  If it runs with my
testbench, then I have some verilog problems to debug.  However, I was
unable to get Andy's combiner working in my testbench.  His code has
been renamed "multiplycomplex" for the sake of in-situ testing with my
multiply-complex testbench.  This leads me to believe I'm doing
something wrong with how I'm presenting the test vectors in the
testbench.

Thanks!

-Mitch




//
/* 
 * Copyright 2017 <+YOU OR YOUR COMPANY+>.
 * 
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3, or (at your option)
 * any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this software; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

//
module noc_block_multiplycomplex #(
  parameter NOC_ID = 64'hCF4CE20A208DF91B,
  parameter STR_SINK_FIFOSIZE = 11)
(
  input bus_clk, input bus_rst,
  input ce_clk, input ce_rst,
  input  [63:0] i_tdata, input  i_tlast, input  i_tvalid, output i_tready,
  output [63:0] o_tdata, output o_tlast, output o_tvalid, input  o_tready,
  output [63:0] debug
);

  localparam MTU = 10;

  ////////////////////////////////////////////////////////////
  //
  // RFNoC Shell
  //
  ////////////////////////////////////////////////////////////
  wire [31:0] set_data;
  wire [7:0]  set_addr;
  wire        set_stb;
  reg  [63:0] rb_data;
  wire [7:0]  rb_addr;

  wire [63:0] cmdout_tdata, ackin_tdata;
  wire        cmdout_tlast, cmdout_tvalid, cmdout_tready, ackin_tlast, ackin_tvalid, ackin_tready;

  wire [127:0] str_sink_tdata;
  wire [1:0]   str_sink_tlast, str_sink_tvalid, str_sink_tready;
  
  wire [63:0] str_src_tdata;
  wire        str_src_tlast, str_src_tvalid, str_src_tready;

  wire [31:0] src_sid;
  wire [15:0] next_dst_sid, resp_out_dst_sid;
  wire [31:0] resp_in_dst_sid;

  wire        clear_tx_seqnum;

  noc_shell #(
    .NOC_ID(NOC_ID),
    .STR_SINK_FIFOSIZE({2{STR_SINK_FIFOSIZE[7:0]}}),
    .INPUT_PORTS(2),
    .OUTPUT_PORTS(1))
  noc_shell (
    .bus_clk(bus_clk), .bus_rst(bus_rst),
    .i_tdata(i_tdata), .i_tlast(i_tlast), .i_tvalid(i_tvalid), .i_tready(i_tready),
    .o_tdata(o_tdata), .o_tlast(o_tlast), .o_tvalid(o_tvalid), .o_tready(o_tready),
    // Computer Engine Clock Domain
    .clk(ce_clk), .reset(ce_rst),
    // Control Sink
    .set_data(set_data), .set_addr(set_addr), .set_stb(set_stb),
    .rb_stb(1'b1), .rb_data(rb_data), .rb_addr(rb_addr),
    // Control Source
    .cmdout_tdata(cmdout_tdata), .cmdout_tlast(cmdout_tlast), .cmdout_tvalid(cmdout_tvalid), .cmdout_tready(cmdout_tready),
    .ackin_tdata(ackin_tdata), .ackin_tlast(ackin_tlast), .ackin_tvalid(ackin_tvalid), .ackin_tready(ackin_tready),
    // Stream Sink
    .str_sink_tdata(str_sink_tdata), .str_sink_tlast(str_sink_tlast), 
    .str_sink_tvalid(str_sink_tvalid), .str_sink_tready(str_sink_tready),
    // Stream Source
    .str_src_tdata(str_src_tdata), .str_src_tlast(str_src_tlast), .str_src_tvalid(str_src_tvalid), .str_src_tready(str_src_tready),
    // Stream IDs set by host
    .src_sid(src_sid),  			// SID of this block
    .next_dst_sid(next_dst_sid),         	// Next destination SID
    .resp_in_dst_sid(resp_in_dst_sid),		// Response destination SID for input stream responses / errors
    .resp_out_dst_sid(resp_out_dst_sid), 	// Response destination SID for output stream responses / errors
    // Misc
    .vita_time('d0), .clear_tx_seqnum(clear_tx_seqnum),
    .debug(debug));

  ////////////////////////////////////////////////////////////
  //
  // AXI Wrapper
  // Convert RFNoC Shell interface into AXI stream interface
  //
  ////////////////////////////////////////////////////////////
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [63:0]  m_axis_data_tdata;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [1:0]   m_axis_data_tlast;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [1:0]   m_axis_data_tvalid;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [1:0]   m_axis_data_tready;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [255:0] m_axis_data_tuser;

  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [31:0]  s_axis_data_tdata;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire         s_axis_data_tlast;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire         s_axis_data_tvalid;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire         s_axis_data_tready;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [127:0] s_axis_data_tuser;

  // Handle headers 
  cvita_hdr_modify cvita_hdr_modify_data_0 (
    .header_in(m_axis_data_tuser[127:0]),
    .header_out(s_axis_data_tuser),
    .use_pkt_type(1'b0), .pkt_type(),
    .use_has_time(1'b1), .has_time(1'b0),
    .use_eob(1'b0), .eob(),
    .use_seqnum(1'b0), .seqnum(),
    .use_length(1'b0), .length(),
    .use_src_sid(1'b1), .src_sid(src_sid[15:0]),
    .use_dst_sid(1'b1), .dst_sid(next_dst_sid),
    .use_vita_time(1'b0), .vita_time());

  axi_wrapper #(
    .SIMPLE_MODE(0))
  axi_wrapper_0 (
    .clk(ce_clk), .reset(ce_rst),
    .clear_tx_seqnum(clear_tx_seqnum),
    .next_dst(next_dst_sid),
    .set_stb(set_stb), .set_addr(set_addr), .set_data(set_data),
    .i_tdata(str_sink_tdata[63:0]), .i_tlast(str_sink_tlast[0]), .i_tvalid(str_sink_tvalid[0]), .i_tready(str_sink_tready[0]),
    .o_tdata(str_src_tdata), .o_tlast(str_src_tlast), .o_tvalid(str_src_tvalid), .o_tready(str_src_tready),
    .m_axis_data_tdata(m_axis_data_tdata[31:0]),
    .m_axis_data_tlast(m_axis_data_tlast[0]),
    .m_axis_data_tvalid(m_axis_data_tvalid[0]),
    .m_axis_data_tready(m_axis_data_tready[0]),
    .m_axis_data_tuser(m_axis_data_tuser[127:0]),
    .s_axis_data_tdata(s_axis_data_tdata),
    .s_axis_data_tlast(s_axis_data_tlast),
    .s_axis_data_tvalid(s_axis_data_tvalid),
    .s_axis_data_tready(s_axis_data_tready),
    .s_axis_data_tuser(s_axis_data_tuser),
    .m_axis_config_tdata(),
    .m_axis_config_tlast(),
    .m_axis_config_tvalid(),
    .m_axis_config_tready(),
    .m_axis_pkt_len_tdata(),
    .m_axis_pkt_len_tvalid(),
    .m_axis_pkt_len_tready());

  axi_wrapper #(
    .SIMPLE_MODE(0))
  axi_wrapper_1 (
    .clk(ce_clk), .reset(ce_rst),
    .clear_tx_seqnum(),
    .next_dst(),
    .set_stb(), .set_addr(), .set_data(),
    .i_tdata(str_sink_tdata[127:64]), .i_tlast(str_sink_tlast[1]), .i_tvalid(str_sink_tvalid[1]), .i_tready(str_sink_tready[1]),
    .o_tdata(), .o_tlast(), .o_tvalid(), .o_tready(),
    .m_axis_data_tdata(m_axis_data_tdata[63:32]),
    .m_axis_data_tlast(m_axis_data_tlast[1]),
    .m_axis_data_tvalid(m_axis_data_tvalid[1]),
    .m_axis_data_tready(m_axis_data_tready[1]),
    .m_axis_data_tuser(m_axis_data_tuser[255:128]),
    .s_axis_data_tdata(),
    .s_axis_data_tlast(),
    .s_axis_data_tvalid(),
    .s_axis_data_tready(),
    .s_axis_data_tuser(),
    .m_axis_config_tdata(),
    .m_axis_config_tlast(),
    .m_axis_config_tvalid(),
    .m_axis_config_tready(),
    .m_axis_pkt_len_tdata(),
    .m_axis_pkt_len_tvalid(),
    .m_axis_pkt_len_tready());

  ////////////////////////////////////////////////////////////
  //
  // User code
  //
  ////////////////////////////////////////////////////////////
  // NoC Shell registers 0 - 127,
  // User register address space starts at 128
  localparam SR_USER_REG_BASE = 128;

  // Control Source Unused
  assign cmdout_tdata  = 64'd0;
  assign cmdout_tlast  = 1'b0;
  assign cmdout_tvalid = 1'b0;
  assign ackin_tready  = 1'b1;

  // Settings registers
  //
  // - The settings register bus is a simple strobed interface.
  // - Transactions include both a write and a readback.
  // - The write occurs when set_stb is asserted.
  //   The settings register with the address matching set_addr will
  //   be loaded with the data on set_data.
  // - Readback occurs when rb_stb is asserted. The read back strobe
  //   must assert at least one clock cycle after set_stb asserts /
  //   rb_stb is ignored if asserted on the same clock cycle of set_stb.
  //   Example valid and invalid timing:
  //              __    __    __    __
  //   clk     __|  |__|  |__|  |__|  |__
  //               _____
  //   set_stb ___|     |________________
  //                     _____
  //   rb_stb  _________|     |__________     (Valid)
  //                           _____
  //   rb_stb  _______________|     |____     (Valid)
  //           __________________________
  //   rb_stb                                 (Valid if readback data is a constant)
  //               _____
  //   rb_stb  ___|     |________________     (Invalid / ignored, same cycle as set_stb)
  //
  localparam [7:0] SR_TEST_REG_0 = SR_USER_REG_BASE;
  localparam [7:0] SR_TEST_REG_1 = SR_USER_REG_BASE + 8'd1;

  wire [31:0] test_reg_0;
  setting_reg #(
    .my_addr(SR_TEST_REG_0), .awidth(8), .width(32))
  sr_test_reg_0 (
    .clk(ce_clk), .rst(ce_rst),
    .strobe(set_stb), .addr(set_addr), .in(set_data), .out(test_reg_0), .changed());

  wire [31:0] test_reg_1;
  setting_reg #(
    .my_addr(SR_TEST_REG_1), .awidth(8), .width(32))
  sr_test_reg_1 (
    .clk(ce_clk), .rst(ce_rst),
    .strobe(set_stb), .addr(set_addr), .in(set_data), .out(test_reg_1), .changed());

  // Readback registers
  // rb_stb set to 1'b1 on NoC Shell
  always @(posedge ce_clk) begin
    case(rb_addr)
      8'd0 : rb_data <= {32'd0, test_reg_0};
      8'd1 : rb_data <= {32'd0, test_reg_1};
      default : rb_data <= 64'h0BADC0DE0BADC0DE;
    endcase
  end

  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [63:0] pipe_in_tdata;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [1:0]  pipe_in_tvalid;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [1:0]  pipe_in_tlast;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [1:0]  pipe_in_tready;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire        pipe_in_join_tvalid;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire        pipe_in_join_tlast;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire        pipe_in_join_tready;

  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire [31:0] pipe_out_tdata;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire        pipe_out_tvalid;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire        pipe_out_tlast;
  (* DONT_TOUCH = "true", MARK_DEBUG = "TRUE"*) wire        pipe_out_tready;

  // Adding FIFO to ensure Pipeline
  axi_fifo_flop #(.WIDTH(32+1))
  pipeline0_axi_fifo_flop_0 (
    .clk(ce_clk),
    .reset(ce_rst),
    .clear(clear_tx_seqnum),
    .i_tdata({m_axis_data_tlast[0],m_axis_data_tdata[31:0]}),
    .i_tvalid(m_axis_data_tvalid[0]),
    .i_tready(m_axis_data_tready[0]),
    .o_tdata({pipe_in_tlast[0],pipe_in_tdata[31:0]}),
    .o_tvalid(pipe_in_tvalid[0]),
    .o_tready(pipe_in_tready[0]));

  // Adding FIFO to ensure Pipeline
  axi_fifo_flop #(.WIDTH(32+1))
  pipeline0_axi_fifo_flop_1 (
    .clk(ce_clk),
    .reset(ce_rst),
    .clear(clear_tx_seqnum),
    .i_tdata({m_axis_data_tlast[1],m_axis_data_tdata[63:32]}),
    .i_tvalid(m_axis_data_tvalid[1]),
    .i_tready(m_axis_data_tready[1]),
    .o_tdata({pipe_in_tlast[1],pipe_in_tdata[63:32]}),
    .o_tvalid(pipe_in_tvalid[1]),
    .o_tready(pipe_in_tready[1]));

  // AXI join
  axi_join #(.INPUTS(2))
  pipeline0_axi_join (
    .i_tlast(pipe_in_tlast),
    .i_tvalid(pipe_in_tvalid),
    .i_tready(pipe_in_tready),
    .o_tlast(pipe_in_join_tlast),
    .o_tvalid(pipe_in_join_tvalid),
    .o_tready(pipe_in_join_tready));

  // Combine streams
  wire [16:0] sum_i = {pipe_in_tdata[31],pipe_in_tdata[31:16]} + {pipe_in_tdata[63],pipe_in_tdata[63:48]};
  wire [16:0] sum_q = {pipe_in_tdata[15],pipe_in_tdata[15:0]} + {pipe_in_tdata[47],pipe_in_tdata[47:32]};

  // Right shift and pack
  wire [31:0] sum_iq = {sum_i[16:1],sum_q[16:1]};
  
  axi_fifo_flop #(.WIDTH(32+1))
  pipeline1_axi_fifo_flop (
    .clk(ce_clk),
    .reset(ce_rst),
    .clear(clear_tx_seqnum),
    .i_tdata({pipe_in_join_tlast,sum_iq}),
    .i_tvalid(pipe_in_join_tvalid),
    .i_tready(pipe_in_join_tready),
    .o_tdata({pipe_out_tlast,pipe_out_tdata}),
    .o_tvalid(pipe_out_tvalid),
    .o_tready(pipe_out_tready));

  /* Output Signals */
  assign pipe_out_tready = s_axis_data_tready;
  assign s_axis_data_tvalid = pipe_out_tvalid;
  assign s_axis_data_tlast  = pipe_out_tlast;
  assign s_axis_data_tdata  = pipe_out_tdata;

  /* Simple Loopback */
  /*assign m_axis_data_tready[0] = s_axis_data_tready;
  assign s_axis_data_tvalid = m_axis_data_tvalid[1];
  assign s_axis_data_tlast  = m_axis_data_tlast[1];
  assign s_axis_data_tdata  = m_axis_data_tdata[63:32];
  assign m_axis_data_tready[1] = s_axis_data_tready;*/


endmodule
/* 
 * Copyright 2019 <+YOU OR YOUR COMPANY+>.
 * 
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3, or (at your option)
 * any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this software; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

`timescale 1ns/1ps
`define NS_PER_TICK 1
`define NUM_TEST_CASES 4

`include "sim_exec_report.vh"
`include "sim_clks_rsts.vh"
`include "sim_rfnoc_lib.svh"

module noc_block_multiplycomplex_tb();
  `TEST_BENCH_INIT("noc_block_multiplycomplex",`NUM_TEST_CASES,`NS_PER_TICK);
  localparam BUS_CLK_PERIOD = $ceil(1e9/166.67e6);
  localparam CE_CLK_PERIOD  = $ceil(1e9/200e6);
  localparam NUM_CE         = 1;  // Number of Computation Engines / User RFNoC blocks to simulate
  localparam NUM_STREAMS    = 2;  // Number of test bench streams
  `RFNOC_SIM_INIT(NUM_CE, NUM_STREAMS, BUS_CLK_PERIOD, CE_CLK_PERIOD);
  `RFNOC_ADD_BLOCK(noc_block_multiplycomplex, 0);

  localparam SPP = 64; // Samples per packet

  /********************************************************
  ** Verification
  ********************************************************/
  initial begin : tb_main
    string s;
    logic [63:0] readback;
    logic [31:0] random_word;
    logic [15:0] real_val;
    logic [15:0] cplx_val;
    logic last;

    /********************************************************
    ** Test 1 -- Reset
    ********************************************************/
    `TEST_CASE_START("Wait for Reset");
    while (bus_rst) @(posedge bus_clk);
    while (ce_rst) @(posedge ce_clk);
    `TEST_CASE_DONE(~bus_rst & ~ce_rst);

    /********************************************************
    ** Test 2 -- Check for correct NoC IDs
    ********************************************************/
    `TEST_CASE_START("Check NoC ID");
    // Read NOC IDs
    tb_streamer.read_reg(sid_noc_block_multiplycomplex, RB_NOC_ID, readback);
    $display("Read multiplycomplex NOC ID: %16x", readback);
    `ASSERT_ERROR(readback == noc_block_multiplycomplex.NOC_ID, "Incorrect NOC ID");
    `TEST_CASE_DONE(1);

    /********************************************************
    ** Test 3 -- Connect RFNoC blocks
    ********************************************************/
    `TEST_CASE_START("Connect RFNoC blocks");
    //`RFNOC_CONNECT_BLOCK_PORT(noc_block_multiplycomplex,1,noc_block_tb,1,SC16,SPP);
    `RFNOC_CONNECT_BLOCK_PORT(noc_block_multiplycomplex,0,noc_block_tb,0,SC16,SPP);
    `RFNOC_CONNECT_BLOCK_PORT(noc_block_tb,0,noc_block_multiplycomplex,0,SC16,SPP);
    `RFNOC_CONNECT_BLOCK_PORT(noc_block_tb,1,noc_block_multiplycomplex,1,SC16,SPP);
    
    `TEST_CASE_DONE(1);

    /********************************************************
    ** Test 4 -- Test sequence
    ********************************************************/
    `TEST_CASE_START("Test sequence");
    fork
      begin
	    cvita_payload_t send_payload_0;
	    cvita_payload_t send_payload_1;
	    cvita_metadata_t tx_md;
        for (int i = 0; i < (SPP/2); i++) begin
          real_val = (i%4 + 1)*100;
          cplx_val = -1*(i%4 + 1)*100;
          
          $display("Send Stream %0d: %0d + %0dj",0, $signed(real_val), $signed(cplx_val));
          send_payload_0.push_back({real_val, cplx_val});
          //tb_streamer.push_word( {real_val, cplx_val}, 0, 0 );
          
          cplx_val = 1*(i%4 + 1)*100;
          $display("Send Stream %0d: %0d + %0dj",1, $signed(real_val), $signed(cplx_val));
          send_payload_1.push_back({real_val, cplx_val});
          //tb_streamer.push_word( {real_val, cplx_val}, 0, 1 );
          
        end
        $display("Last Payload Stream 0");
        tx_md.eob = 1'b1;
        tb_streamer.send(send_payload_0,tx_md,0);
        //tb_streamer.push_word( {16'd0,16'd0}, 1, 0 );
        
        $display("Last Payload Stream 1");
        tx_md.eob = 1'b1;
        tb_streamer.send(send_payload_1,tx_md,1);
        //tb_streamer.push_word( {16'd0,16'd0}, 1, 1 );
        
      end
      begin
        cvita_payload_t recv_payload;
        cvita_metadata_t rx_md;
        logic [63:0] recv_value;
        tb_streamer.recv(recv_payload,rx_md,0);
        for (int i = 0; i < SPP/2; i=i++) begin
          $display("Pull Word %0d",i);
          {real_val,cplx_val} = recv_payload[i];
          //tb_streamer.pull_word( {real_val,cplx_val}, last );
          $display("%0d + %0dj", $signed(real_val), $signed(cplx_val));
          //if(last == 1) begin
          //     break;
          //end
        end
      end
    join
    `TEST_CASE_DONE(1);
    `TEST_BENCH_DONE;

  end
endmodule
_______________________________________________
USRP-users mailing list
USRP-users@lists.ettus.com
http://lists.ettus.com/mailman/listinfo/usrp-users_lists.ettus.com

Reply via email to