use alloc::{collections::VecDeque, string::String, sync::Arc};
use core::{
sync::atomic::{AtomicBool, AtomicPtr, Ordering},
time::Duration,
};
use ax_lazyinit::LazyInit;
use ax_memory_addr::PhysAddr;
use ax_std::os::arceos::{
task as scheduler,
task::{
sync::{
WaitQueue,
irq::{IrqRegisterResult, IrqWaitCell, IrqWaitRegistration},
},
thread::ThreadId,
},
};
use sg2002_tpu::{
ion::IonBuffer,
tpu::{
Sg2002Tpu,
error::TpuError,
types::{
CVITPU_DMABUF_FLUSH, CVITPU_DMABUF_FLUSH_FD, CVITPU_DMABUF_INVLD,
CVITPU_DMABUF_INVLD_FD, CVITPU_LOAD_TEE, CVITPU_PIO_MODE, CVITPU_SUBMIT_DMABUF,
CVITPU_SUBMIT_TEE, CVITPU_UNLOAD_TEE, CVITPU_WAIT_DMABUF, CviCacheOpArg,
CviSubmitDmaArg, CviWaitDmaArg,
},
},
};
use crate::{
file::{get_file_like, ion::IonBufferFile},
mm::{UserConstPtr, UserPtr},
pseudofs::{
DeviceOps,
dev::{IrqRegistration, request_shared_disabled},
},
sync::IrqMutex,
};
struct TpuTask {
task_id: ThreadId,
seq_no: u32,
vaddr: usize,
paddr: u64,
_buffer: Arc<IonBuffer>,
ret: i32,
}
static TASK_LIST: IrqMutex<VecDeque<TpuTask>> = IrqMutex::new(VecDeque::new());
static DONE_LIST: IrqMutex<VecDeque<TpuTask>> = IrqMutex::new(VecDeque::new());
const DONE_LIST_MAX: usize = 64;
static TASK_WQ: WaitQueue = WaitQueue::new();
static DONE_WQ: WaitQueue = WaitQueue::new();
static TPU_IRQ_PARK: WaitQueue = WaitQueue::new();
static TPU_IRQ_NOTIFY: IrqWaitCell = IrqWaitCell::new();
static TPU_IRQ_WAITER: LazyInit<TpuIrqWaiter> = LazyInit::new();
static WORKER_SPAWNED: AtomicBool = AtomicBool::new(false);
static HW_PTR: AtomicPtr<Sg2002Tpu> = AtomicPtr::new(core::ptr::null_mut());
pub struct TpuDevice {
hw: Arc<Sg2002Tpu>,
resource: TpuResource,
irq_registration: Option<IrqRegistration>,
}
const TPU_COMPATIBLES: &[&str] = &["cvitek,tpu"];
const TPU_TDMA_IRQ_NAME: &str = "tdma_irq";
const TPU_DEFAULT_MMIO_SIZE: usize = 0x1000;
const TPU_WAIT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone, Copy)]
struct TpuResource {
tdma_paddr: usize,
tdma_size: usize,
tiu_paddr: usize,
tiu_size: usize,
irq: Option<ax_runtime::hal::irq::IrqId>,
}
impl TpuResource {
fn probe() -> Option<Self> {
let resource = Self::from_fdt();
if resource.is_none() {
warn!("[TPU] cvitek,tpu node not found or invalid in FDT");
}
resource
}
fn from_fdt() -> Option<Self> {
rdrive::with_fdt(|fdt| {
fdt.find_compatible(TPU_COMPATIBLES)
.into_iter()
.find_map(Self::from_fdt_node)
})
.flatten()
}
fn from_fdt_node(node: rdrive::probe::fdt::NodeType<'_>) -> Option<Self> {
if matches!(
node.as_node().status(),
Some(rdrive::probe::fdt::Status::Disabled)
) {
return None;
}
let mut regs = node.regs().into_iter();
let tdma = regs.next()?;
let tiu = regs.next()?;
let irq = match resolve_named_fdt_irq(&node, TPU_TDMA_IRQ_NAME) {
Ok(irq) => irq,
Err(err) => {
warn!("[TPU] failed to resolve {TPU_TDMA_IRQ_NAME}: {err:?}");
return None;
}
};
Some(Self {
tdma_paddr: tdma.address as usize,
tdma_size: tdma.size.unwrap_or(TPU_DEFAULT_MMIO_SIZE as u64) as usize,
tiu_paddr: tiu.address as usize,
tiu_size: tiu.size.unwrap_or(TPU_DEFAULT_MMIO_SIZE as u64) as usize,
irq,
})
}
}
fn resolve_named_fdt_irq(
node: &rdrive::probe::fdt::NodeType<'_>,
name: &str,
) -> Result<Option<ax_runtime::hal::irq::IrqId>, ax_runtime::hal::irq::IrqError> {
let Some(irq) = ax_driver::binding_irq_from_named_fdt_interrupt(node, name)
.map_err(|_| ax_runtime::hal::irq::IrqError::Unsupported)?
else {
return Ok(None);
};
ax_runtime::irq::resolve_binding_irq(irq).map(Some)
}
fn map_tpu_mmio(resource: TpuResource) -> Option<(*mut u8, *mut u8)> {
let tdma = match axklib::mem::iomap(PhysAddr::from(resource.tdma_paddr), resource.tdma_size) {
Ok(vaddr) => vaddr.as_mut_ptr(),
Err(err) => {
warn!(
"[TPU] failed to map TDMA MMIO at {:#x}+{:#x}: {err:?}",
resource.tdma_paddr, resource.tdma_size
);
return None;
}
};
let tiu = match axklib::mem::iomap(PhysAddr::from(resource.tiu_paddr), resource.tiu_size) {
Ok(vaddr) => vaddr.as_mut_ptr(),
Err(err) => {
warn!(
"[TPU] failed to map TIU MMIO at {:#x}+{:#x}: {err:?}",
resource.tiu_paddr, resource.tiu_size
);
return None;
}
};
Some((tdma, tiu))
}
fn register_tpu_irq(
irq: Option<ax_runtime::hal::irq::IrqId>,
hw: &Arc<Sg2002Tpu>,
) -> Option<IrqRegistration> {
let Some(irq) = irq else {
warn!("[TPU] TDMA IRQ not available; execution will use MMIO poll fallback");
return None;
};
let hw = Arc::clone(hw);
let registration = match request_shared_disabled(irq, move |_| {
if hw.handle_irq() {
warn!("[TPU] TDMA IRQ {irq:?} reports error status");
}
let _result = TPU_IRQ_NOTIFY.notify();
ax_runtime::hal::irq::IrqReturn::Handled
}) {
Ok(registration) => registration,
Err(err) => {
warn!("[TPU] failed to register TDMA IRQ {irq:?}: {err:?}");
return None;
}
};
if let Err(err) = registration.enable() {
warn!("[TPU] failed to enable TDMA IRQ {irq:?}: {err:?}");
return None;
}
info!("[TPU] TDMA IRQ {irq:?} registered and enabled");
Some(registration)
}
fn tpu_wait_irq(timeout_us: u64) -> bool {
let hw = HW_PTR.load(Ordering::Acquire);
if hw.is_null() {
return false;
}
let hw = unsafe { &*hw };
let current = scheduler::thread::current::current_thread_handle()
.unwrap_or_else(|error| panic!("TPU worker has no scheduler thread: {error}"));
let waiter = TPU_IRQ_WAITER.get_or_init(|| create_tpu_irq_waiter(¤t));
assert_eq!(
waiter.owner,
current.id(),
"TPU IRQ notifications must target the fixed worker thread"
);
match TPU_IRQ_NOTIFY.register(&waiter.registration) {
IrqRegisterResult::ConsumedPending => hw.irq_pending(),
IrqRegisterResult::Registered(token) | IrqRegisterResult::NotificationInFlight(token) => {
let _timed_out = TPU_IRQ_PARK
.wait_timeout_until(Duration::from_micros(timeout_us), || {
!token.is_attached() || hw.irq_pending()
});
scheduler::sync::irq::quiesce_irq_wait(token)
.unwrap_or_else(|error| panic!("TPU IRQ waiter could not quiesce: {error}"));
hw.irq_pending()
}
IrqRegisterResult::Occupied => false,
}
}
struct TpuIrqWaiter {
owner: ThreadId,
registration: IrqWaitRegistration,
}
fn create_tpu_irq_waiter(current: &scheduler::thread::ThreadHandle) -> TpuIrqWaiter {
TpuIrqWaiter {
owner: current.id(),
registration: IrqWaitRegistration::new(current.wake_handle()),
}
}
fn tpu_worker(hw: Arc<Sg2002Tpu>) {
info!("[TPU] worker thread started");
loop {
let mut task = loop {
if let Some(task) = TASK_LIST.lock().pop_front() {
break task;
}
TASK_WQ.wait_until(|| !TASK_LIST.lock().is_empty());
};
task.ret = hw
.run_one(task.seq_no, task.vaddr, task.paddr)
.map_or(-1, |_| 0);
{
let mut done = DONE_LIST.lock();
done.push_back(task);
while done.len() > DONE_LIST_MAX {
let dropped = done.pop_front();
if let Some(t) = dropped {
warn!(
"[TPU] done list full, dropping orphaned result (task_id={:?}, seq_no={})",
t.task_id, t.seq_no
);
}
}
}
DONE_WQ.notify_all();
}
}
impl TpuDevice {
pub fn probe() -> Option<Self> {
let resource = TpuResource::probe()?;
let hw = {
let (tdma, tiu) = map_tpu_mmio(resource)?;
Arc::new(unsafe { Sg2002Tpu::from_vaddr(tdma, tiu) })
};
Some(Self::setup(hw, resource))
}
fn setup(hw: Arc<Sg2002Tpu>, resource: TpuResource) -> Self {
hw.set_wait_irq_fn(tpu_wait_irq);
if let Err(err) = hw.init() {
warn!("[TPU] init failed: {:?}", err);
}
let irq_registration = register_tpu_irq(resource.irq, &hw);
info!(
"[TPU] resource tdma=[{:#x}, +{:#x}) tiu=[{:#x}, +{:#x}) irq={:?} irq_wait={} \
source=fdt",
resource.tdma_paddr,
resource.tdma_size,
resource.tiu_paddr,
resource.tiu_size,
resource.irq,
irq_registration.is_some(),
);
if WORKER_SPAWNED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
HW_PTR.store(Arc::as_ptr(&hw) as *mut Sg2002Tpu, Ordering::Release);
let worker_hw = hw.clone();
crate::task::kernel_thread_builder(String::from("tpu-worker"))
.spawn(move || tpu_worker(worker_hw))
.expect("failed to spawn kernel thread");
}
Self {
hw,
resource,
irq_registration,
}
}
fn submit_dmabuf(
&self,
current: &crate::task::UserTaskRef,
arg: usize,
) -> Result<usize, TpuError> {
let submit_arg = unsafe { UserConstPtr::<CviSubmitDmaArg>::from(arg).read_abi(current) }
.map_err(|_| TpuError::InvalidDmabuf)?;
debug!(
"[TPU] submit dmabuf: fd={}, seq_no={}",
submit_arg.fd, submit_arg.seq_no
);
if self.irq_registration.is_none() {
warn!("[TPU] TDMA IRQ {:?} not registered", self.resource.irq);
}
let fd = submit_arg.fd;
let file = get_file_like(fd).map_err(|_| {
error!("[TPU] Failed to get file for fd={}", fd);
TpuError::InvalidDmabuf
})?;
let ion_file: Arc<IonBufferFile> = file.downcast_arc::<IonBufferFile>().map_err(|_| {
error!("[TPU] fd={} is not an IonBufferFile", fd);
TpuError::InvalidDmabuf
})?;
let buffer = ion_file.buffer().clone();
debug!(
"[TPU] dmabuf info: handle={}, size={}, paddr=0x{:x}",
buffer.handle.as_u32(),
buffer.size,
buffer.dma_addr().as_u64()
);
let task = TpuTask {
task_id: current.id(),
seq_no: submit_arg.seq_no,
vaddr: buffer.cpu_ptr().as_ptr() as usize,
paddr: buffer.dma_addr().as_u64(),
_buffer: buffer,
ret: 0,
};
TASK_LIST.lock().push_back(task);
TASK_WQ.notify_one();
Ok(0)
}
fn wait_dmabuf(
&self,
current: &crate::task::UserTaskRef,
arg: usize,
) -> Result<usize, TpuError> {
let wait_arg = unsafe { UserConstPtr::<CviWaitDmaArg>::from(arg).read_abi(current) }
.map_err(|_| TpuError::InvalidDmabuf)?;
let seq_no = wait_arg.seq_no;
let task_id = current.id();
let timed_out = DONE_WQ.wait_timeout_until(TPU_WAIT_TIMEOUT, || {
DONE_LIST
.lock()
.iter()
.any(|t| t.task_id == task_id && t.seq_no == seq_no)
});
let found = {
let mut done = DONE_LIST.lock();
done.iter()
.position(|t| t.task_id == task_id && t.seq_no == seq_no)
.map(|idx| done.remove(idx).unwrap())
};
let (ret, result) = match found {
Some(task) => {
if task.ret != 0 {
(task.ret, Err(TpuError::Timeout))
} else {
(task.ret, Ok(0))
}
}
None => {
warn!(
"[TPU] wait dmabuf: (task_id={task_id:?}, seq_no={seq_no}) not found \
(timed_out={timed_out})"
);
(-1, Err(TpuError::Timeout))
}
};
UserPtr::<i32>::from(arg + core::mem::offset_of!(CviWaitDmaArg, ret))
.write(current, ret)
.map_err(|_| TpuError::InvalidDmabuf)?;
result
}
fn cache_flush(
&self,
current: &crate::task::UserTaskRef,
arg: usize,
) -> Result<usize, TpuError> {
let flush_arg = unsafe { UserConstPtr::<CviCacheOpArg>::from(arg).read_abi(current) }
.map_err(|_| TpuError::InvalidDmabuf)?;
self.hw.cache_flush_paddr(flush_arg.paddr, flush_arg.size)?;
Ok(0)
}
fn cache_invalidate(
&self,
current: &crate::task::UserTaskRef,
arg: usize,
) -> Result<usize, TpuError> {
let invalidate_arg = unsafe { UserConstPtr::<CviCacheOpArg>::from(arg).read_abi(current) }
.map_err(|_| TpuError::InvalidDmabuf)?;
self.hw
.cache_invalidate_paddr(invalidate_arg.paddr, invalidate_arg.size)?;
Ok(0)
}
fn dmabuf_flush_fd(&self, arg: usize) -> Result<usize, TpuError> {
let fd = arg as i32;
debug!("TPU dmabuf flush fd: {}", fd);
let buffer = self.lookup_ion_buffer(fd)?;
let paddr = buffer.dma_addr().as_u64();
let size = buffer.size as u64;
self.hw.cache_flush_paddr(paddr, size)?;
debug!("Flushed buffer: paddr=0x{:x}, size={}", paddr, size);
Ok(0)
}
fn dmabuf_invld_fd(&self, arg: usize) -> Result<usize, TpuError> {
let fd = arg as i32;
debug!("TPU dmabuf invalidate fd: {}", fd);
let buffer = self.lookup_ion_buffer(fd)?;
let paddr = buffer.dma_addr().as_u64();
let size = buffer.size as u64;
self.hw.cache_invalidate_paddr(paddr, size)?;
Ok(0)
}
fn lookup_ion_buffer(&self, fd: i32) -> Result<Arc<IonBuffer>, TpuError> {
let file = get_file_like(fd).map_err(|err| {
error!("[TPU] failed to get file for fd={}: {:?}", fd, err);
TpuError::InvalidDmabuf
})?;
let ion_file: Arc<IonBufferFile> = file.downcast_arc::<IonBufferFile>().map_err(|_| {
error!("[TPU] fd={} is not an IonBufferFile", fd);
TpuError::InvalidDmabuf
})?;
Ok(ion_file.buffer().clone())
}
}
impl DeviceOps for TpuDevice {
fn read_at(&self, _buf: &mut [u8], _offset: u64) -> axfs_ng_vfs::VfsResult<usize> {
Ok(0)
}
fn write_at(&self, _buf: &[u8], _offset: u64) -> axfs_ng_vfs::VfsResult<usize> {
Ok(0)
}
fn ioctl(
&self,
current: &crate::task::UserTaskRef,
cmd: u32,
arg: usize,
) -> axfs_ng_vfs::VfsResult<usize> {
debug!("TPU ioctl: cmd=0x{:x}, arg=0x{:x}", cmd, arg);
let result = match cmd {
CVITPU_SUBMIT_DMABUF => self.submit_dmabuf(current, arg),
CVITPU_DMABUF_FLUSH_FD => self.dmabuf_flush_fd(arg),
CVITPU_DMABUF_INVLD_FD => self.dmabuf_invld_fd(arg),
CVITPU_DMABUF_FLUSH => self.cache_flush(current, arg),
CVITPU_DMABUF_INVLD => self.cache_invalidate(current, arg),
CVITPU_WAIT_DMABUF => self.wait_dmabuf(current, arg),
CVITPU_PIO_MODE => {
warn!("TPU PIO mode not implemented");
Ok(0)
}
CVITPU_LOAD_TEE | CVITPU_SUBMIT_TEE | CVITPU_UNLOAD_TEE => {
warn!("TPU TEE operations not supported");
Err(TpuError::NotInitialized)
}
_ => {
warn!("Unknown TPU ioctl command: 0x{:x}", cmd);
Err(TpuError::NotInitialized)
}
};
match result {
Ok(v) => Ok(v),
Err(e) => {
error!("TPU ioctl error: {:?}", e);
Err(crate::StarryError::from(e).into())
}
}
}
fn as_any(&self) -> &dyn core::any::Any {
self
}
}