forked from M-Labs/artiq
gateware,runtime: optimize RTIO kernel interface further
* now pinning (TODO: atomicity) * for inputs, merge request and timeout registers
This commit is contained in:
parent
aadf5112b7
commit
8caea0e6d3
|
@ -1946,8 +1946,7 @@ class ARTIQIRGenerator(algorithm.Visitor):
|
||||||
def printf(format_string, *args):
|
def printf(format_string, *args):
|
||||||
format = ir.Constant(format_string, builtins.TStr())
|
format = ir.Constant(format_string, builtins.TStr())
|
||||||
if as_rtio:
|
if as_rtio:
|
||||||
now_mu = self.append(ir.Builtin("now_mu", [], builtins.TInt64()))
|
self.append(ir.Builtin("rtio_log", [format, *args], builtins.TNone()))
|
||||||
self.append(ir.Builtin("rtio_log", [now_mu, format, *args], builtins.TNone()))
|
|
||||||
else:
|
else:
|
||||||
self.append(ir.Builtin("printf", [format, *args], builtins.TNone()))
|
self.append(ir.Builtin("printf", [format, *args], builtins.TNone()))
|
||||||
|
|
||||||
|
|
|
@ -336,7 +336,7 @@ class LLVMIRGenerator:
|
||||||
elif name == self.target.print_function:
|
elif name == self.target.print_function:
|
||||||
llty = ll.FunctionType(llvoid, [llptr], var_arg=True)
|
llty = ll.FunctionType(llvoid, [llptr], var_arg=True)
|
||||||
elif name == "rtio_log":
|
elif name == "rtio_log":
|
||||||
llty = ll.FunctionType(llvoid, [lli64, llptr], var_arg=True)
|
llty = ll.FunctionType(llvoid, [llptr], var_arg=True)
|
||||||
elif name == "__artiq_personality":
|
elif name == "__artiq_personality":
|
||||||
llty = ll.FunctionType(lli32, [], var_arg=True)
|
llty = ll.FunctionType(lli32, [], var_arg=True)
|
||||||
elif name == "__artiq_raise":
|
elif name == "__artiq_raise":
|
||||||
|
@ -1137,7 +1137,7 @@ class LLVMIRGenerator:
|
||||||
lloperands = []
|
lloperands = []
|
||||||
for i, operand in enumerate(insn.operands):
|
for i, operand in enumerate(insn.operands):
|
||||||
lloperand = self.map(operand)
|
lloperand = self.map(operand)
|
||||||
if i == 0 and insn.op == "printf" or i == 1 and insn.op == "rtio_log":
|
if i == 0 and (insn.op == "printf" or insn.op == "rtio_log"):
|
||||||
lloperands.append(self.llbuilder.extract_value(lloperand, 0))
|
lloperands.append(self.llbuilder.extract_value(lloperand, 0))
|
||||||
elif builtins.is_str(operand.type) or builtins.is_bytes(operand.type):
|
elif builtins.is_str(operand.type) or builtins.is_bytes(operand.type):
|
||||||
lloperands.append(self.llbuilder.extract_value(lloperand, 1))
|
lloperands.append(self.llbuilder.extract_value(lloperand, 1))
|
||||||
|
|
|
@ -3,13 +3,12 @@ from artiq.language.types import TInt64, TInt32, TNone, TList
|
||||||
|
|
||||||
|
|
||||||
@syscall(flags={"nowrite"})
|
@syscall(flags={"nowrite"})
|
||||||
def rtio_output(time_mu: TInt64, target: TInt32, data: TInt32) -> TNone:
|
def rtio_output(target: TInt32, data: TInt32) -> TNone:
|
||||||
raise NotImplementedError("syscall not simulated")
|
raise NotImplementedError("syscall not simulated")
|
||||||
|
|
||||||
|
|
||||||
@syscall(flags={"nowrite"})
|
@syscall(flags={"nowrite"})
|
||||||
def rtio_output_wide(time_mu: TInt64, target: TInt32,
|
def rtio_output_wide(target: TInt32, data: TList(TInt32)) -> TNone:
|
||||||
data: TList(TInt32)) -> TNone:
|
|
||||||
raise NotImplementedError("syscall not simulated")
|
raise NotImplementedError("syscall not simulated")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ class TTLOut:
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def set_o(self, o):
|
def set_o(self, o):
|
||||||
rtio_output(now_mu(), self.target_o, 1 if o else 0)
|
rtio_output(self.target_o, 1 if o else 0)
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def on(self):
|
def on(self):
|
||||||
|
@ -120,7 +120,7 @@ class TTLInOut:
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def set_oe(self, oe):
|
def set_oe(self, oe):
|
||||||
rtio_output(now_mu(), self.target_oe, 1 if oe else 0)
|
rtio_output(self.target_oe, 1 if oe else 0)
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def output(self):
|
def output(self):
|
||||||
|
@ -142,7 +142,7 @@ class TTLInOut:
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def set_o(self, o):
|
def set_o(self, o):
|
||||||
rtio_output(now_mu(), self.target_o, 1 if o else 0)
|
rtio_output(self.target_o, 1 if o else 0)
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def on(self):
|
def on(self):
|
||||||
|
@ -187,7 +187,7 @@ class TTLInOut:
|
||||||
# Input API: gating
|
# Input API: gating
|
||||||
@kernel
|
@kernel
|
||||||
def _set_sensitivity(self, value):
|
def _set_sensitivity(self, value):
|
||||||
rtio_output(now_mu(), self.target_sens, value)
|
rtio_output(self.target_sens, value)
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def gate_rising_mu(self, duration):
|
def gate_rising_mu(self, duration):
|
||||||
|
@ -361,7 +361,7 @@ class TTLInOut:
|
||||||
position of the time cursor.
|
position of the time cursor.
|
||||||
|
|
||||||
The time cursor is not modified by this function."""
|
The time cursor is not modified by this function."""
|
||||||
rtio_output(now_mu(), self.target_sample, 0)
|
rtio_output(self.target_sample, 0)
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def sample_get(self):
|
def sample_get(self):
|
||||||
|
@ -398,13 +398,13 @@ class TTLInOut:
|
||||||
|
|
||||||
The time cursor is not modified by this function.
|
The time cursor is not modified by this function.
|
||||||
"""
|
"""
|
||||||
rtio_output(now_mu(), self.target_sample, 2) # gate falling
|
rtio_output(self.target_sample, 2) # gate falling
|
||||||
return rtio_input_data(self.channel) == 1
|
return rtio_input_data(self.channel) == 1
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def watch_stay_off(self):
|
def watch_stay_off(self):
|
||||||
"""Like :meth:`watch_stay_on`, but for low levels."""
|
"""Like :meth:`watch_stay_on`, but for low levels."""
|
||||||
rtio_output(now_mu(), self.target_sample, 1) # gate rising
|
rtio_output(self.target_sample, 1) # gate rising
|
||||||
return rtio_input_data(self.channel) == 0
|
return rtio_input_data(self.channel) == 0
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
|
@ -417,7 +417,7 @@ class TTLInOut:
|
||||||
The time cursor is not modified by this function. This function
|
The time cursor is not modified by this function. This function
|
||||||
always makes the slack negative.
|
always makes the slack negative.
|
||||||
"""
|
"""
|
||||||
rtio_output(now_mu(), self.target_sens, 0)
|
rtio_output(self.target_sens, 0)
|
||||||
success = True
|
success = True
|
||||||
try:
|
try:
|
||||||
while rtio_input_timestamp(now_mu(), self.channel) != -1:
|
while rtio_input_timestamp(now_mu(), self.channel) != -1:
|
||||||
|
@ -479,7 +479,7 @@ class TTLClockGen:
|
||||||
Due to the way the clock generator operates, frequency tuning words
|
Due to the way the clock generator operates, frequency tuning words
|
||||||
that are not powers of two cause jitter of one RTIO clock cycle at the
|
that are not powers of two cause jitter of one RTIO clock cycle at the
|
||||||
output."""
|
output."""
|
||||||
rtio_output(now_mu(), self.target, frequency)
|
rtio_output(self.target, frequency)
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def set(self, frequency):
|
def set(self, frequency):
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use board_misoc::csr;
|
||||||
|
|
||||||
macro_rules! api {
|
macro_rules! api {
|
||||||
($i:ident) => ({
|
($i:ident) => ({
|
||||||
extern { static $i: u8; }
|
extern { static $i: u8; }
|
||||||
|
@ -78,7 +80,7 @@ static mut API: &'static [(&'static str, *const ())] = &[
|
||||||
/* proxified syscalls */
|
/* proxified syscalls */
|
||||||
api!(core_log),
|
api!(core_log),
|
||||||
|
|
||||||
api!(now = &::NOW as *const _),
|
api!(now = csr::rtio::NOW_HI_ADDR as *const _),
|
||||||
|
|
||||||
api!(watchdog_set = ::watchdog_set),
|
api!(watchdog_set = ::watchdog_set),
|
||||||
api!(watchdog_clear = ::watchdog_clear),
|
api!(watchdog_clear = ::watchdog_clear),
|
||||||
|
|
|
@ -12,7 +12,7 @@ struct slice {
|
||||||
};
|
};
|
||||||
|
|
||||||
void send_to_core_log(struct slice str);
|
void send_to_core_log(struct slice str);
|
||||||
void send_to_rtio_log(long long int timestamp, struct slice data);
|
void send_to_rtio_log(struct slice data);
|
||||||
|
|
||||||
#define KERNELCPU_EXEC_ADDRESS 0x40800000
|
#define KERNELCPU_EXEC_ADDRESS 0x40800000
|
||||||
#define KERNELCPU_PAYLOAD_ADDRESS 0x40840000
|
#define KERNELCPU_PAYLOAD_ADDRESS 0x40840000
|
||||||
|
@ -139,8 +139,8 @@ int core_log(const char *fmt, ...)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* called by kernel */
|
/* called by kernel */
|
||||||
void rtio_log(long long int timestamp, const char *fmt, ...);
|
void rtio_log(const char *fmt, ...);
|
||||||
void rtio_log(long long int timestamp, const char *fmt, ...)
|
void rtio_log(const char *fmt, ...)
|
||||||
{
|
{
|
||||||
va_list args;
|
va_list args;
|
||||||
|
|
||||||
|
@ -154,5 +154,5 @@ void rtio_log(long long int timestamp, const char *fmt, ...)
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
|
||||||
struct slice str = { buf, size };
|
struct slice str = { buf, size };
|
||||||
send_to_rtio_log(timestamp, str);
|
send_to_rtio_log(str);
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,6 @@ mod api;
|
||||||
mod rtio;
|
mod rtio;
|
||||||
mod nrt_bus;
|
mod nrt_bus;
|
||||||
|
|
||||||
static mut NOW: u64 = 0;
|
|
||||||
static mut LIBRARY: Option<Library<'static>> = None;
|
static mut LIBRARY: Option<Library<'static>> = None;
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -114,8 +113,8 @@ pub extern fn send_to_core_log(text: CSlice<u8>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern fn send_to_rtio_log(timestamp: i64, text: CSlice<u8>) {
|
pub extern fn send_to_rtio_log(text: CSlice<u8>) {
|
||||||
rtio::log(timestamp, text.as_ref())
|
rtio::log(text.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unwind(aborts)]
|
#[unwind(aborts)]
|
||||||
|
@ -184,7 +183,6 @@ fn terminate(exception: &eh_artiq::Exception, backtrace: &mut [usize]) -> ! {
|
||||||
}
|
}
|
||||||
let backtrace = &mut backtrace.as_mut()[0..cursor];
|
let backtrace = &mut backtrace.as_mut()[0..cursor];
|
||||||
|
|
||||||
send(&NowSave(unsafe { NOW }));
|
|
||||||
send(&RunException {
|
send(&RunException {
|
||||||
exception: kernel_proto::Exception {
|
exception: kernel_proto::Exception {
|
||||||
name: str::from_utf8(exception.name.as_ref()).unwrap(),
|
name: str::from_utf8(exception.name.as_ref()).unwrap(),
|
||||||
|
@ -508,10 +506,7 @@ pub unsafe fn main() {
|
||||||
|
|
||||||
ptr::write_bytes(__bss_start as *mut u8, 0, (_end - __bss_start) as usize);
|
ptr::write_bytes(__bss_start as *mut u8, 0, (_end - __bss_start) as usize);
|
||||||
|
|
||||||
send(&NowInitRequest);
|
|
||||||
recv!(&NowInitReply(now) => NOW = now);
|
|
||||||
(mem::transmute::<u32, fn()>(__modinit__))();
|
(mem::transmute::<u32, fn()>(__modinit__))();
|
||||||
send(&NowSave(NOW));
|
|
||||||
|
|
||||||
if let Some(typeinfo) = typeinfo {
|
if let Some(typeinfo) = typeinfo {
|
||||||
attribute_writeback(typeinfo as *const ());
|
attribute_writeback(typeinfo as *const ());
|
||||||
|
|
|
@ -51,7 +51,8 @@ mod imp {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
unsafe fn process_exceptional_status(timestamp: i64, channel: i32, status: u8) {
|
unsafe fn process_exceptional_status(channel: i32, status: u8) {
|
||||||
|
let timestamp = *(csr::rtio::NOW_HI_ADDR as *const i64);
|
||||||
if status & RTIO_O_STATUS_WAIT != 0 {
|
if status & RTIO_O_STATUS_WAIT != 0 {
|
||||||
while csr::rtio::o_status_read() & RTIO_O_STATUS_WAIT != 0 {}
|
while csr::rtio::o_status_read() & RTIO_O_STATUS_WAIT != 0 {}
|
||||||
}
|
}
|
||||||
|
@ -67,30 +68,28 @@ mod imp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub extern fn output(timestamp: i64, target: i32, data: i32) {
|
pub extern fn output(target: i32, data: i32) {
|
||||||
unsafe {
|
unsafe {
|
||||||
csr::rtio::target_write(target as u32);
|
csr::rtio::target_write(target as u32);
|
||||||
// writing timestamp clears o_data
|
// writing target clears o_data
|
||||||
csr::rtio::timestamp_write(timestamp as u64);
|
|
||||||
rtio_o_data_write(0, data as _);
|
rtio_o_data_write(0, data as _);
|
||||||
let status = csr::rtio::o_status_read();
|
let status = csr::rtio::o_status_read();
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
process_exceptional_status(timestamp, target >> 8, status);
|
process_exceptional_status(target >> 8, status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub extern fn output_wide(timestamp: i64, target: i32, data: CSlice<i32>) {
|
pub extern fn output_wide(target: i32, data: CSlice<i32>) {
|
||||||
unsafe {
|
unsafe {
|
||||||
csr::rtio::target_write(target as u32);
|
csr::rtio::target_write(target as u32);
|
||||||
// writing timestamp clears o_data
|
// writing target clears o_data
|
||||||
csr::rtio::timestamp_write(timestamp as u64);
|
|
||||||
for i in 0..data.len() {
|
for i in 0..data.len() {
|
||||||
rtio_o_data_write(i, data[i] as _)
|
rtio_o_data_write(i, data[i] as _)
|
||||||
}
|
}
|
||||||
let status = csr::rtio::o_status_read();
|
let status = csr::rtio::o_status_read();
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
process_exceptional_status(timestamp, target >> 8, status);
|
process_exceptional_status(target >> 8, status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,8 +97,7 @@ mod imp {
|
||||||
pub extern fn input_timestamp(timeout: i64, channel: i32) -> u64 {
|
pub extern fn input_timestamp(timeout: i64, channel: i32) -> u64 {
|
||||||
unsafe {
|
unsafe {
|
||||||
csr::rtio::target_write((channel as u32) << 8);
|
csr::rtio::target_write((channel as u32) << 8);
|
||||||
csr::rtio::timestamp_write(timeout as u64);
|
csr::rtio::i_timeout_write(timeout as u64);
|
||||||
csr::rtio::i_request_write(1);
|
|
||||||
|
|
||||||
let mut status = RTIO_I_STATUS_WAIT_STATUS;
|
let mut status = RTIO_I_STATUS_WAIT_STATUS;
|
||||||
while status & RTIO_I_STATUS_WAIT_STATUS != 0 {
|
while status & RTIO_I_STATUS_WAIT_STATUS != 0 {
|
||||||
|
@ -128,8 +126,7 @@ mod imp {
|
||||||
pub extern fn input_data(channel: i32) -> i32 {
|
pub extern fn input_data(channel: i32) -> i32 {
|
||||||
unsafe {
|
unsafe {
|
||||||
csr::rtio::target_write((channel as u32) << 8);
|
csr::rtio::target_write((channel as u32) << 8);
|
||||||
csr::rtio::timestamp_write(0xffffffff_ffffffff);
|
csr::rtio::i_timeout_write(0xffffffff_ffffffff);
|
||||||
csr::rtio::i_request_write(1);
|
|
||||||
|
|
||||||
let mut status = RTIO_I_STATUS_WAIT_STATUS;
|
let mut status = RTIO_I_STATUS_WAIT_STATUS;
|
||||||
while status & RTIO_I_STATUS_WAIT_STATUS != 0 {
|
while status & RTIO_I_STATUS_WAIT_STATUS != 0 {
|
||||||
|
@ -153,10 +150,9 @@ mod imp {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(has_rtio_log)]
|
#[cfg(has_rtio_log)]
|
||||||
pub fn log(timestamp: i64, data: &[u8]) {
|
pub fn log(data: &[u8]) {
|
||||||
unsafe {
|
unsafe {
|
||||||
csr::rtio::target_write(csr::CONFIG_RTIO_LOG_CHANNEL << 8);
|
csr::rtio::target_write(csr::CONFIG_RTIO_LOG_CHANNEL << 8);
|
||||||
csr::rtio::timestamp_write(timestamp as u64);
|
|
||||||
|
|
||||||
let mut word: u32 = 0;
|
let mut word: u32 = 0;
|
||||||
for i in 0..data.len() {
|
for i in 0..data.len() {
|
||||||
|
@ -175,7 +171,7 @@ mod imp {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(has_rtio_log))]
|
#[cfg(not(has_rtio_log))]
|
||||||
pub fn log(_timestamp: i64, _data: &[u8]) {
|
pub fn log(_data: &[u8]) {
|
||||||
unimplemented!("not(has_rtio_log)")
|
unimplemented!("not(has_rtio_log)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,6 @@ pub enum Message<'a> {
|
||||||
LoadRequest(&'a [u8]),
|
LoadRequest(&'a [u8]),
|
||||||
LoadReply(Result<(), dyld::Error<'a>>),
|
LoadReply(Result<(), dyld::Error<'a>>),
|
||||||
|
|
||||||
NowInitRequest,
|
|
||||||
NowInitReply(u64),
|
|
||||||
NowSave(u64),
|
|
||||||
|
|
||||||
RtioInitRequest,
|
RtioInitRequest,
|
||||||
|
|
||||||
RtioDestinationStatusRequest { destination: u8 },
|
RtioDestinationStatusRequest { destination: u8 },
|
||||||
|
|
|
@ -63,7 +63,6 @@ macro_rules! unexpected {
|
||||||
// Persistent state
|
// Persistent state
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Congress {
|
struct Congress {
|
||||||
now: u64,
|
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
dma_manager: DmaManager,
|
dma_manager: DmaManager,
|
||||||
finished_cleanly: Cell<bool>
|
finished_cleanly: Cell<bool>
|
||||||
|
@ -72,7 +71,6 @@ struct Congress {
|
||||||
impl Congress {
|
impl Congress {
|
||||||
fn new() -> Congress {
|
fn new() -> Congress {
|
||||||
Congress {
|
Congress {
|
||||||
now: 0,
|
|
||||||
cache: Cache::new(),
|
cache: Cache::new(),
|
||||||
dma_manager: DmaManager::new(),
|
dma_manager: DmaManager::new(),
|
||||||
finished_cleanly: Cell::new(true)
|
finished_cleanly: Cell::new(true)
|
||||||
|
@ -365,14 +363,6 @@ fn process_kern_message(io: &Io, aux_mutex: &Mutex,
|
||||||
kern_acknowledge()
|
kern_acknowledge()
|
||||||
}
|
}
|
||||||
|
|
||||||
&kern::NowInitRequest =>
|
|
||||||
kern_send(io, &kern::NowInitReply(session.congress.now)),
|
|
||||||
|
|
||||||
&kern::NowSave(now) => {
|
|
||||||
session.congress.now = now;
|
|
||||||
kern_acknowledge()
|
|
||||||
}
|
|
||||||
|
|
||||||
&kern::DmaRecordStart(name) => {
|
&kern::DmaRecordStart(name) => {
|
||||||
session.congress.dma_manager.record_start(name);
|
session.congress.dma_manager.record_start(name);
|
||||||
kern_acknowledge()
|
kern_acknowledge()
|
||||||
|
|
|
@ -73,7 +73,11 @@ class RTController(Module):
|
||||||
rt_packet.sr_chan_sel.eq(chan_sel),
|
rt_packet.sr_chan_sel.eq(chan_sel),
|
||||||
rt_packet.sr_address.eq(self.cri.o_address),
|
rt_packet.sr_address.eq(self.cri.o_address),
|
||||||
rt_packet.sr_data.eq(self.cri.o_data),
|
rt_packet.sr_data.eq(self.cri.o_data),
|
||||||
rt_packet.sr_timestamp.eq(self.cri.timestamp),
|
If(rt_packet_read_request,
|
||||||
|
rt_packet.sr_timestamp.eq(self.cri.i_timeout)
|
||||||
|
).Else(
|
||||||
|
rt_packet.sr_timestamp.eq(self.cri.o_timestamp)
|
||||||
|
),
|
||||||
If(rt_packet_buffer_request,
|
If(rt_packet_buffer_request,
|
||||||
rt_packet.sr_notwrite.eq(1),
|
rt_packet.sr_notwrite.eq(1),
|
||||||
rt_packet.sr_address.eq(0)
|
rt_packet.sr_address.eq(0)
|
||||||
|
@ -103,7 +107,7 @@ class RTController(Module):
|
||||||
self.submodules += timeout_counter
|
self.submodules += timeout_counter
|
||||||
|
|
||||||
cond_underflow = Signal()
|
cond_underflow = Signal()
|
||||||
self.comb += cond_underflow.eq((self.cri.timestamp[tsc.glbl_fine_ts_width:]
|
self.comb += cond_underflow.eq((self.cri.o_timestamp[tsc.glbl_fine_ts_width:]
|
||||||
- self.csrs.underflow_margin.storage[tsc.glbl_fine_ts_width:]) < tsc.coarse_ts_sys)
|
- self.csrs.underflow_margin.storage[tsc.glbl_fine_ts_width:]) < tsc.coarse_ts_sys)
|
||||||
|
|
||||||
# buffer space
|
# buffer space
|
||||||
|
|
|
@ -61,7 +61,7 @@ class RTErrorsSatellite(Module, AutoCSR):
|
||||||
underflow.eq(cri.o_status[1]),
|
underflow.eq(cri.o_status[1]),
|
||||||
overflow.eq(cri.o_status[0]),
|
overflow.eq(cri.o_status[0]),
|
||||||
underflow_error_cri.eq(Cat(cri.chan_sel[:16],
|
underflow_error_cri.eq(Cat(cri.chan_sel[:16],
|
||||||
cri.timestamp,
|
cri.o_timestamp,
|
||||||
tsc.full_ts_cri)),
|
tsc.full_ts_cri)),
|
||||||
Cat(self.underflow_channel.status,
|
Cat(self.underflow_channel.status,
|
||||||
self.underflow_timestamp_event.status,
|
self.underflow_timestamp_event.status,
|
||||||
|
|
|
@ -68,7 +68,11 @@ class RTPacketRepeater(Module):
|
||||||
If(~self.reset & ~cb0_loaded & (self.cri.cmd != cri.commands["nop"]),
|
If(~self.reset & ~cb0_loaded & (self.cri.cmd != cri.commands["nop"]),
|
||||||
cb0_loaded.eq(1),
|
cb0_loaded.eq(1),
|
||||||
cb0_cmd.eq(self.cri.cmd),
|
cb0_cmd.eq(self.cri.cmd),
|
||||||
cb0_timestamp.eq(self.cri.timestamp),
|
If(self.cri.cmd == cri.commands["read"],
|
||||||
|
cb0_timestamp.eq(self.cri.i_timeout)
|
||||||
|
).Else(
|
||||||
|
cb0_timestamp.eq(self.cri.o_timestamp)
|
||||||
|
),
|
||||||
cb0_chan_sel.eq(self.cri.chan_sel),
|
cb0_chan_sel.eq(self.cri.chan_sel),
|
||||||
cb0_o_address.eq(self.cri.o_address),
|
cb0_o_address.eq(self.cri.o_address),
|
||||||
cb0_o_data.eq(self.cri.o_data)
|
cb0_o_data.eq(self.cri.o_data)
|
||||||
|
|
|
@ -94,13 +94,10 @@ class RTPacketSatellite(Module):
|
||||||
self.cri.chan_sel.eq(
|
self.cri.chan_sel.eq(
|
||||||
rx_dp.packet_as["write"].chan_sel),
|
rx_dp.packet_as["write"].chan_sel),
|
||||||
),
|
),
|
||||||
If(cri_read | read_request_pending,
|
self.cri.i_timeout.eq(
|
||||||
self.cri.timestamp.eq(
|
rx_dp.packet_as["read_request"].timeout),
|
||||||
rx_dp.packet_as["read_request"].timeout)
|
self.cri.o_timestamp.eq(
|
||||||
).Else(
|
rx_dp.packet_as["write"].timestamp),
|
||||||
self.cri.timestamp.eq(
|
|
||||||
rx_dp.packet_as["write"].timestamp)
|
|
||||||
),
|
|
||||||
self.cri.o_address.eq(
|
self.cri.o_address.eq(
|
||||||
rx_dp.packet_as["write"].address),
|
rx_dp.packet_as["write"].address),
|
||||||
self.cri.o_data.eq(
|
self.cri.o_data.eq(
|
||||||
|
|
|
@ -70,7 +70,7 @@ class MessageEncoder(Module, AutoCSR):
|
||||||
input_output.rtio_counter.eq(tsc.full_ts_cri),
|
input_output.rtio_counter.eq(tsc.full_ts_cri),
|
||||||
If(cri.cmd == cri_commands["write"],
|
If(cri.cmd == cri_commands["write"],
|
||||||
input_output.message_type.eq(MessageType.output.value),
|
input_output.message_type.eq(MessageType.output.value),
|
||||||
input_output.timestamp.eq(cri.timestamp),
|
input_output.timestamp.eq(cri.o_timestamp),
|
||||||
input_output.data.eq(cri.o_data)
|
input_output.data.eq(cri.o_data)
|
||||||
).Else(
|
).Else(
|
||||||
input_output.message_type.eq(MessageType.input.value),
|
input_output.message_type.eq(MessageType.input.value),
|
||||||
|
|
|
@ -29,8 +29,8 @@ layout = [
|
||||||
# 8 MSBs of chan_sel = routing destination
|
# 8 MSBs of chan_sel = routing destination
|
||||||
# 16 LSBs of chan_sel = channel within the destination
|
# 16 LSBs of chan_sel = channel within the destination
|
||||||
("chan_sel", 24, DIR_M_TO_S),
|
("chan_sel", 24, DIR_M_TO_S),
|
||||||
("timestamp", 64, DIR_M_TO_S),
|
|
||||||
|
|
||||||
|
("o_timestamp", 64, DIR_M_TO_S),
|
||||||
("o_data", 512, DIR_M_TO_S),
|
("o_data", 512, DIR_M_TO_S),
|
||||||
("o_address", 8, DIR_M_TO_S),
|
("o_address", 8, DIR_M_TO_S),
|
||||||
# o_status bits:
|
# o_status bits:
|
||||||
|
@ -43,6 +43,7 @@ layout = [
|
||||||
("o_buffer_space_valid", 1, DIR_S_TO_M),
|
("o_buffer_space_valid", 1, DIR_S_TO_M),
|
||||||
("o_buffer_space", 16, DIR_S_TO_M),
|
("o_buffer_space", 16, DIR_S_TO_M),
|
||||||
|
|
||||||
|
("i_timeout", 64, DIR_M_TO_S),
|
||||||
("i_data", 32, DIR_S_TO_M),
|
("i_data", 32, DIR_S_TO_M),
|
||||||
("i_timestamp", 64, DIR_S_TO_M),
|
("i_timestamp", 64, DIR_S_TO_M),
|
||||||
# i_status bits:
|
# i_status bits:
|
||||||
|
@ -61,17 +62,19 @@ class Interface(Record):
|
||||||
class KernelInitiator(Module, AutoCSR):
|
class KernelInitiator(Module, AutoCSR):
|
||||||
def __init__(self, tsc, cri=None):
|
def __init__(self, tsc, cri=None):
|
||||||
self.target = CSRStorage(32)
|
self.target = CSRStorage(32)
|
||||||
self.timestamp = CSRStorage(64)
|
# not using CSRStorage atomic_write feature here to make storage reset_less
|
||||||
|
self.now_hi = CSR(32)
|
||||||
|
self.now_lo = CSR(32)
|
||||||
|
|
||||||
# Writing timestamp clears o_data. This implements automatic
|
# Writing target clears o_data. This implements automatic
|
||||||
# zero-extension of output event data by the gateware. When staging an
|
# zero-extension of output event data by the gateware. When staging an
|
||||||
# output event, always write timestamp before o_data.
|
# output event, always write target before o_data.
|
||||||
self.o_data = CSRStorage(512, write_from_dev=True)
|
self.o_data = CSRStorage(512, write_from_dev=True)
|
||||||
self.o_status = CSRStatus(3)
|
self.o_status = CSRStatus(3)
|
||||||
|
|
||||||
|
self.i_timeout = CSRStorage(64)
|
||||||
self.i_data = CSRStatus(32)
|
self.i_data = CSRStatus(32)
|
||||||
self.i_timestamp = CSRStatus(64)
|
self.i_timestamp = CSRStatus(64)
|
||||||
self.i_request = CSR()
|
|
||||||
self.i_status = CSRStatus(4)
|
self.i_status = CSRStatus(4)
|
||||||
self.i_overflow_reset = CSR()
|
self.i_overflow_reset = CSR()
|
||||||
|
|
||||||
|
@ -84,24 +87,39 @@ class KernelInitiator(Module, AutoCSR):
|
||||||
|
|
||||||
# # #
|
# # #
|
||||||
|
|
||||||
|
now_lo_backing = Signal(32)
|
||||||
|
now = Signal(64, reset_less=True)
|
||||||
|
self.sync += [
|
||||||
|
# TODO: fix compiler and make atomic
|
||||||
|
#If(self.now_lo.re, now_lo_backing.eq(self.now_lo.r)),
|
||||||
|
#If(self.now_hi.re, now.eq(Cat(now_lo_backing, self.now_hi.r)))
|
||||||
|
If(self.now_lo.re, now[:32].eq(self.now_lo.r)),
|
||||||
|
If(self.now_hi.re, now[32:].eq(self.now_hi.r))
|
||||||
|
]
|
||||||
|
self.comb += [
|
||||||
|
self.now_lo.w.eq(now[:32]),
|
||||||
|
self.now_hi.w.eq(now[32:])
|
||||||
|
]
|
||||||
|
|
||||||
self.comb += [
|
self.comb += [
|
||||||
self.cri.cmd.eq(commands["nop"]),
|
self.cri.cmd.eq(commands["nop"]),
|
||||||
If(self.o_data.re, self.cri.cmd.eq(commands["write"])),
|
If(self.o_data.re, self.cri.cmd.eq(commands["write"])),
|
||||||
If(self.i_request.re, self.cri.cmd.eq(commands["read"])),
|
If(self.i_timeout.re, self.cri.cmd.eq(commands["read"])),
|
||||||
|
|
||||||
self.cri.chan_sel.eq(self.target.storage[8:]),
|
self.cri.chan_sel.eq(self.target.storage[8:]),
|
||||||
self.cri.timestamp.eq(self.timestamp.storage),
|
|
||||||
|
|
||||||
|
self.cri.o_timestamp.eq(now),
|
||||||
self.cri.o_data.eq(self.o_data.storage),
|
self.cri.o_data.eq(self.o_data.storage),
|
||||||
self.cri.o_address.eq(self.target.storage[:8]),
|
self.cri.o_address.eq(self.target.storage[:8]),
|
||||||
self.o_status.status.eq(self.cri.o_status),
|
self.o_status.status.eq(self.cri.o_status),
|
||||||
|
|
||||||
|
self.cri.i_timeout.eq(self.i_timeout.storage),
|
||||||
self.i_data.status.eq(self.cri.i_data),
|
self.i_data.status.eq(self.cri.i_data),
|
||||||
self.i_timestamp.status.eq(self.cri.i_timestamp),
|
self.i_timestamp.status.eq(self.cri.i_timestamp),
|
||||||
self.i_status.status.eq(self.cri.i_status),
|
self.i_status.status.eq(self.cri.i_status),
|
||||||
|
|
||||||
self.o_data.dat_w.eq(0),
|
self.o_data.dat_w.eq(0),
|
||||||
self.o_data.we.eq(self.timestamp.re),
|
self.o_data.we.eq(self.target.re),
|
||||||
]
|
]
|
||||||
self.sync += If(self.counter_update.re, self.counter.status.eq(tsc.full_ts_cri))
|
self.sync += If(self.counter_update.re, self.counter.status.eq(tsc.full_ts_cri))
|
||||||
|
|
||||||
|
|
|
@ -274,7 +274,7 @@ class CRIMaster(Module, AutoCSR):
|
||||||
|
|
||||||
self.comb += [
|
self.comb += [
|
||||||
self.cri.chan_sel.eq(self.sink.channel),
|
self.cri.chan_sel.eq(self.sink.channel),
|
||||||
self.cri.timestamp.eq(self.sink.timestamp),
|
self.cri.o_timestamp.eq(self.sink.timestamp),
|
||||||
self.cri.o_address.eq(self.sink.address),
|
self.cri.o_address.eq(self.sink.address),
|
||||||
self.cri.o_data.eq(self.sink.data)
|
self.cri.o_data.eq(self.sink.data)
|
||||||
]
|
]
|
||||||
|
|
|
@ -118,7 +118,7 @@ class InputCollector(Module):
|
||||||
|
|
||||||
i_status_raw = Signal(2)
|
i_status_raw = Signal(2)
|
||||||
self.comb += i_status_raw.eq(Array(i_statuses)[sel])
|
self.comb += i_status_raw.eq(Array(i_statuses)[sel])
|
||||||
input_timeout = Signal.like(self.cri.timestamp, reset_less=True)
|
input_timeout = Signal.like(self.cri.i_timeout, reset_less=True)
|
||||||
input_pending = Signal()
|
input_pending = Signal()
|
||||||
self.cri.i_data.reset_less = True
|
self.cri.i_data.reset_less = True
|
||||||
self.cri.i_timestamp.reset_less = True
|
self.cri.i_timestamp.reset_less = True
|
||||||
|
@ -134,7 +134,7 @@ class InputCollector(Module):
|
||||||
input_pending.eq(0)
|
input_pending.eq(0)
|
||||||
),
|
),
|
||||||
If(self.cri.cmd == cri.commands["read"],
|
If(self.cri.cmd == cri.commands["read"],
|
||||||
input_timeout.eq(self.cri.timestamp),
|
input_timeout.eq(self.cri.i_timeout),
|
||||||
input_pending.eq(1),
|
input_pending.eq(1),
|
||||||
self.cri.i_status.eq(0b100)
|
self.cri.i_status.eq(0b100)
|
||||||
)
|
)
|
||||||
|
|
|
@ -60,7 +60,7 @@ class LaneDistributor(Module):
|
||||||
self.comb += [
|
self.comb += [
|
||||||
lio.seqn.eq(seqn),
|
lio.seqn.eq(seqn),
|
||||||
lio.payload.channel.eq(self.cri.chan_sel[:16]),
|
lio.payload.channel.eq(self.cri.chan_sel[:16]),
|
||||||
lio.payload.timestamp.eq(self.cri.timestamp),
|
lio.payload.timestamp.eq(self.cri.o_timestamp),
|
||||||
]
|
]
|
||||||
if hasattr(lio.payload, "address"):
|
if hasattr(lio.payload, "address"):
|
||||||
self.comb += lio.payload.address.eq(self.cri.o_address)
|
self.comb += lio.payload.address.eq(self.cri.o_address)
|
||||||
|
@ -69,7 +69,7 @@ class LaneDistributor(Module):
|
||||||
|
|
||||||
# when timestamp and channel arrive in cycle #1, prepare computations
|
# when timestamp and channel arrive in cycle #1, prepare computations
|
||||||
coarse_timestamp = Signal(us_timestamp_width)
|
coarse_timestamp = Signal(us_timestamp_width)
|
||||||
self.comb += coarse_timestamp.eq(self.cri.timestamp[glbl_fine_ts_width:])
|
self.comb += coarse_timestamp.eq(self.cri.o_timestamp[glbl_fine_ts_width:])
|
||||||
min_minus_timestamp = Signal((us_timestamp_width + 1, True),
|
min_minus_timestamp = Signal((us_timestamp_width + 1, True),
|
||||||
reset_less=True)
|
reset_less=True)
|
||||||
laneAmin_minus_timestamp = Signal.like(min_minus_timestamp)
|
laneAmin_minus_timestamp = Signal.like(min_minus_timestamp)
|
||||||
|
@ -141,7 +141,7 @@ class LaneDistributor(Module):
|
||||||
Array(lio.we for lio in self.output)[use_lanen].eq(do_write)
|
Array(lio.we for lio in self.output)[use_lanen].eq(do_write)
|
||||||
]
|
]
|
||||||
compensated_timestamp = Signal(64)
|
compensated_timestamp = Signal(64)
|
||||||
self.comb += compensated_timestamp.eq(self.cri.timestamp + (compensation << glbl_fine_ts_width))
|
self.comb += compensated_timestamp.eq(self.cri.o_timestamp + (compensation << glbl_fine_ts_width))
|
||||||
self.sync += [
|
self.sync += [
|
||||||
If(do_write,
|
If(do_write,
|
||||||
current_lane.eq(use_lanen),
|
current_lane.eq(use_lanen),
|
||||||
|
|
|
@ -112,10 +112,10 @@ class OutputsTestbench:
|
||||||
|
|
||||||
def write(self, channel, data):
|
def write(self, channel, data):
|
||||||
kcsrs = self.dut.master_ki
|
kcsrs = self.dut.master_ki
|
||||||
yield from kcsrs.chan_sel.write(channel)
|
yield from kcsrs.target.write(channel << 8)
|
||||||
yield from kcsrs.timestamp.write(self.now)
|
yield from kcsrs.now_hi.write(self.now >> 32)
|
||||||
|
yield from kcsrs.now_lo.write(self.now & 0xffffffff)
|
||||||
yield from kcsrs.o_data.write(data)
|
yield from kcsrs.o_data.write(data)
|
||||||
yield from kcsrs.o_we.write(1)
|
|
||||||
yield
|
yield
|
||||||
status = 1
|
status = 1
|
||||||
wlen = 0
|
wlen = 0
|
||||||
|
@ -249,9 +249,8 @@ class TestFullStack(unittest.TestCase):
|
||||||
kcsrs = dut.master_ki
|
kcsrs = dut.master_ki
|
||||||
|
|
||||||
def get_input(timeout):
|
def get_input(timeout):
|
||||||
yield from kcsrs.chan_sel.write(2)
|
yield from kcsrs.target.write(2 << 8)
|
||||||
yield from kcsrs.timestamp.write(10)
|
yield from kcsrs.i_timeout.write(10)
|
||||||
yield from kcsrs.i_request.write(1)
|
|
||||||
yield
|
yield
|
||||||
status = yield from kcsrs.i_status.read()
|
status = yield from kcsrs.i_status.read()
|
||||||
while status & 0x4:
|
while status & 0x4:
|
||||||
|
|
|
@ -65,7 +65,7 @@ class TestRepeater(unittest.TestCase):
|
||||||
yield
|
yield
|
||||||
for channel, timestamp, address, data in test_writes:
|
for channel, timestamp, address, data in test_writes:
|
||||||
yield dut.cri.chan_sel.eq(channel)
|
yield dut.cri.chan_sel.eq(channel)
|
||||||
yield dut.cri.timestamp.eq(timestamp)
|
yield dut.cri.o_timestamp.eq(timestamp)
|
||||||
yield dut.cri.o_address.eq(address)
|
yield dut.cri.o_address.eq(address)
|
||||||
yield dut.cri.o_data.eq(data)
|
yield dut.cri.o_data.eq(data)
|
||||||
yield dut.cri.cmd.eq(cri.commands["write"])
|
yield dut.cri.cmd.eq(cri.commands["write"])
|
||||||
|
@ -135,7 +135,7 @@ class TestRepeater(unittest.TestCase):
|
||||||
|
|
||||||
def read(chan_sel, timeout):
|
def read(chan_sel, timeout):
|
||||||
yield dut.cri.chan_sel.eq(chan_sel)
|
yield dut.cri.chan_sel.eq(chan_sel)
|
||||||
yield dut.cri.timestamp.eq(timeout)
|
yield dut.cri.i_timeout.eq(timeout)
|
||||||
yield dut.cri.cmd.eq(cri.commands["read"])
|
yield dut.cri.cmd.eq(cri.commands["read"])
|
||||||
yield
|
yield
|
||||||
yield dut.cri.cmd.eq(cri.commands["nop"])
|
yield dut.cri.cmd.eq(cri.commands["nop"])
|
||||||
|
|
|
@ -90,7 +90,7 @@ class Testbench:
|
||||||
def write(self, channel, data):
|
def write(self, channel, data):
|
||||||
mcri = self.dut.master.cri
|
mcri = self.dut.master.cri
|
||||||
yield mcri.chan_sel.eq(channel)
|
yield mcri.chan_sel.eq(channel)
|
||||||
yield mcri.timestamp.eq(self.now)
|
yield mcri.o_timestamp.eq(self.now)
|
||||||
yield mcri.o_data.eq(data)
|
yield mcri.o_data.eq(data)
|
||||||
yield
|
yield
|
||||||
yield mcri.cmd.eq(cri.commands["write"])
|
yield mcri.cmd.eq(cri.commands["write"])
|
||||||
|
@ -109,7 +109,7 @@ class Testbench:
|
||||||
def read(self, channel, timeout):
|
def read(self, channel, timeout):
|
||||||
mcri = self.dut.master.cri
|
mcri = self.dut.master.cri
|
||||||
yield mcri.chan_sel.eq(channel)
|
yield mcri.chan_sel.eq(channel)
|
||||||
yield mcri.timestamp.eq(timeout)
|
yield mcri.i_timeout.eq(timeout)
|
||||||
yield
|
yield
|
||||||
yield mcri.cmd.eq(cri.commands["read"])
|
yield mcri.cmd.eq(cri.commands["read"])
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -26,7 +26,7 @@ def encode_record(channel, timestamp, address, data):
|
||||||
r = []
|
r = []
|
||||||
r += encode_n(channel, 3, 3)
|
r += encode_n(channel, 3, 3)
|
||||||
r += encode_n(timestamp, 8, 8)
|
r += encode_n(timestamp, 8, 8)
|
||||||
r += encode_n(address, 2, 2)
|
r += encode_n(address, 1, 1)
|
||||||
r += encode_n(data, 1, 64)
|
r += encode_n(data, 1, 64)
|
||||||
return encode_n(len(r)+1, 1, 1) + r
|
return encode_n(len(r)+1, 1, 1) + r
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ def do_dma(dut, address):
|
||||||
|
|
||||||
test_writes1 = [
|
test_writes1 = [
|
||||||
(0x01, 0x23, 0x12, 0x33),
|
(0x01, 0x23, 0x12, 0x33),
|
||||||
(0x901, 0x902, 0x911, 0xeeeeeeeeeeeeeefffffffffffffffffffffffffffffff28888177772736646717738388488),
|
(0x901, 0x902, 0x11, 0xeeeeeeeeeeeeeefffffffffffffffffffffffffffffff28888177772736646717738388488),
|
||||||
(0x81, 0x288, 0x88, 0x8888)
|
(0x81, 0x288, 0x88, 0x8888)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ class TestDMA(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
elif cmd == cri.commands["write"]:
|
elif cmd == cri.commands["write"]:
|
||||||
channel = yield dut_cri.chan_sel
|
channel = yield dut_cri.chan_sel
|
||||||
timestamp = yield dut_cri.timestamp
|
timestamp = yield dut_cri.o_timestamp
|
||||||
address = yield dut_cri.o_address
|
address = yield dut_cri.o_address
|
||||||
data = yield dut_cri.o_data
|
data = yield dut_cri.o_data
|
||||||
received.append((channel, timestamp, address, data))
|
received.append((channel, timestamp, address, data))
|
||||||
|
|
|
@ -54,7 +54,7 @@ def simulate(wait_cycles, ts_timeouts):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
for ts_timeout in ts_timeouts:
|
for ts_timeout in ts_timeouts:
|
||||||
yield dut.cri.timestamp.eq(ts_timeout)
|
yield dut.cri.i_timeout.eq(ts_timeout)
|
||||||
yield dut.cri.cmd.eq(cri.commands["read"])
|
yield dut.cri.cmd.eq(cri.commands["read"])
|
||||||
yield
|
yield
|
||||||
yield dut.cri.cmd.eq(cri.commands["nop"])
|
yield dut.cri.cmd.eq(cri.commands["nop"])
|
||||||
|
|
|
@ -21,7 +21,7 @@ def simulate(input_events, compensation=None, wait=True):
|
||||||
def gen():
|
def gen():
|
||||||
for channel, timestamp in input_events:
|
for channel, timestamp in input_events:
|
||||||
yield dut.cri.chan_sel.eq(channel)
|
yield dut.cri.chan_sel.eq(channel)
|
||||||
yield dut.cri.timestamp.eq(timestamp)
|
yield dut.cri.o_timestamp.eq(timestamp)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
yield dut.cri.cmd.eq(cri.commands["write"])
|
yield dut.cri.cmd.eq(cri.commands["write"])
|
||||||
|
|
|
@ -38,7 +38,7 @@ def simulate(input_events, **kwargs):
|
||||||
def gen():
|
def gen():
|
||||||
yield dut.sed.cri.chan_sel.eq(0)
|
yield dut.sed.cri.chan_sel.eq(0)
|
||||||
for timestamp, data in input_events:
|
for timestamp, data in input_events:
|
||||||
yield dut.sed.cri.timestamp.eq(timestamp)
|
yield dut.sed.cri.o_timestamp.eq(timestamp)
|
||||||
yield dut.sed.cri.o_data.eq(data)
|
yield dut.sed.cri.o_data.eq(data)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue