mirror of https://github.com/m-labs/artiq.git
firmware: split out libboard_artiq from libboard.
This commit is contained in:
parent
6801921fc0
commit
ca419aa3c2
|
@ -26,11 +26,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
name = "board"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"build_artiq 0.0.0",
|
||||
"build_misoc 0.0.0",
|
||||
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "board_artiq"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"board 0.0.0",
|
||||
"build_artiq 0.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -40,6 +47,10 @@ dependencies = [
|
|||
"walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build_misoc"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.2.1"
|
||||
|
@ -169,6 +180,7 @@ dependencies = [
|
|||
"amp 0.0.0",
|
||||
"backtrace_artiq 0.0.0",
|
||||
"board 0.0.0",
|
||||
"board_artiq 0.0.0",
|
||||
"build_artiq 0.0.0",
|
||||
"byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"compiler_builtins 0.1.0 (git+https://github.com/m-labs/compiler-builtins?rev=97916b1)",
|
||||
|
|
|
@ -10,12 +10,10 @@ path = "lib.rs"
|
|||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
build_artiq = { path = "../libbuild_artiq" }
|
||||
build_misoc = { path = "../libbuild_misoc" }
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
byteorder = { version = "1.0", default-features = false }
|
||||
log = { version = "0.3", default-features = false }
|
||||
|
||||
[features]
|
||||
uart_console = []
|
||||
|
|
|
@ -1,38 +1,18 @@
|
|||
extern crate build_artiq;
|
||||
extern crate build_misoc;
|
||||
extern crate cc;
|
||||
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn build_vectors() {
|
||||
println!("cargo:rerun-if-changed=vectors.S");
|
||||
cc::Build::new()
|
||||
.file("vectors.S")
|
||||
.compile("vectors");
|
||||
}
|
||||
|
||||
fn gen_hmc7043_writes() {
|
||||
println!("cargo:rerun-if-changed=hmc7043_gen_writes.py");
|
||||
println!("cargo:rerun-if-changed=hmc7043_guiexport_6gbps.py");
|
||||
|
||||
let hmc7043_writes =
|
||||
Command::new("python3")
|
||||
.arg("hmc7043_gen_writes.py")
|
||||
.arg("hmc7043_guiexport_6gbps.py")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.unwrap();
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let mut f = File::create(out_dir.join("hmc7043_writes.rs")).unwrap();
|
||||
write!(f, "{}", hmc7043_writes).unwrap();
|
||||
}
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
build_artiq::misoc_cfg();
|
||||
build_vectors();
|
||||
gen_hmc7043_writes();
|
||||
build_misoc::cfg();
|
||||
|
||||
let triple = env::var("TARGET").unwrap();
|
||||
let arch = triple.split("-").next().unwrap();
|
||||
let vectors_path = Path::new(arch).join("vectors.S");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", vectors_path.to_str().unwrap());
|
||||
cc::Build::new()
|
||||
.file(vectors_path)
|
||||
.compile("vectors");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use core::i64;
|
||||
use csr;
|
||||
|
||||
const INIT: u64 = ::core::i64::MAX as u64;
|
||||
const INIT: u64 = i64::MAX as u64;
|
||||
const FREQ: u64 = csr::CONFIG_CLOCK_FREQUENCY as u64;
|
||||
|
||||
pub fn init() {
|
||||
|
@ -35,49 +36,3 @@ pub fn spin_us(interval: u64) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Watchdog {
|
||||
active: bool,
|
||||
threshold: u64
|
||||
}
|
||||
|
||||
pub const MAX_WATCHDOGS: usize = 16;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WatchdogSet {
|
||||
watchdogs: [Watchdog; MAX_WATCHDOGS]
|
||||
}
|
||||
|
||||
impl WatchdogSet {
|
||||
pub fn new() -> WatchdogSet {
|
||||
WatchdogSet {
|
||||
watchdogs: [Watchdog { active: false, threshold: 0 }; MAX_WATCHDOGS]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ms(&mut self, interval: u64) -> Result<usize, ()> {
|
||||
for (index, watchdog) in self.watchdogs.iter_mut().enumerate() {
|
||||
if !watchdog.active {
|
||||
watchdog.active = true;
|
||||
watchdog.threshold = get_ms() + interval;
|
||||
return Ok(index)
|
||||
}
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, index: usize) {
|
||||
if index < MAX_WATCHDOGS {
|
||||
self.watchdogs[index].active = false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expired(&self) -> bool {
|
||||
self.watchdogs.iter()
|
||||
.filter(|wd| wd.active)
|
||||
.min_by_key(|wd| wd.threshold)
|
||||
.map_or(false, |wd| get_ms() > wd.threshold)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,41 @@
|
|||
use core::{str, fmt};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
AlreadyLocked,
|
||||
SpaceExhausted,
|
||||
Truncated { offset: usize },
|
||||
InvalidSize { offset: usize, size: usize },
|
||||
MissingSeparator { offset: usize },
|
||||
Utf8Error(str::Utf8Error)
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
&Error::AlreadyLocked =>
|
||||
write!(f, "attempt at reentrant access"),
|
||||
&Error::SpaceExhausted =>
|
||||
write!(f, "space exhausted"),
|
||||
&Error::Truncated { offset }=>
|
||||
write!(f, "truncated record at offset {}", offset),
|
||||
&Error::InvalidSize { offset, size } =>
|
||||
write!(f, "invalid record size {} at offset {}", size, offset),
|
||||
&Error::MissingSeparator { offset } =>
|
||||
write!(f, "missing separator at offset {}", offset),
|
||||
&Error::Utf8Error(err) =>
|
||||
write!(f, "{}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(has_spiflash)]
|
||||
mod imp {
|
||||
use core::str;
|
||||
use byteorder::{ByteOrder, BigEndian};
|
||||
use cache;
|
||||
use spiflash;
|
||||
use super::Error;
|
||||
|
||||
// One flash sector immediately after the bootloader.
|
||||
const ADDR: usize = ::mem::FLASH_BOOT_ADDRESS - spiflash::PAGE_SIZE;
|
||||
|
@ -12,17 +44,18 @@ mod imp {
|
|||
mod lock {
|
||||
use core::slice;
|
||||
use core::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT};
|
||||
use super::Error;
|
||||
|
||||
static LOCKED: AtomicUsize = ATOMIC_USIZE_INIT;
|
||||
|
||||
pub struct Lock;
|
||||
|
||||
impl Lock {
|
||||
pub fn take() -> Result<Lock, ()> {
|
||||
pub fn take() -> Result<Lock, Error> {
|
||||
if LOCKED.swap(1, Ordering::SeqCst) != 0 {
|
||||
Err(()) // already locked
|
||||
Err(Error::AlreadyLocked)
|
||||
} else {
|
||||
Ok(Lock) // locked now
|
||||
Ok(Lock)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,29 +86,27 @@ mod imp {
|
|||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = Result<(&'a [u8], &'a [u8]), ()>;
|
||||
type Item = Result<(&'a [u8], &'a [u8]), Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let data = &self.data[self.offset..];
|
||||
|
||||
if data.len() < 4 {
|
||||
error!("offset {}: truncated record", self.offset);
|
||||
return Some(Err(()))
|
||||
// error!("offset {}: truncated record", self.offset);
|
||||
return Some(Err(Error::Truncated { offset: self.offset }))
|
||||
}
|
||||
|
||||
let record_size = BigEndian::read_u32(data) as usize;
|
||||
if record_size == !0 /* all ones; erased flash */ {
|
||||
return None
|
||||
} else if record_size < 4 || record_size > data.len() {
|
||||
error!("offset {}: invalid record size {}", self.offset, record_size);
|
||||
return Some(Err(()))
|
||||
return Some(Err(Error::InvalidSize { offset: self.offset, size: record_size }))
|
||||
}
|
||||
|
||||
let record_body = &data[4..record_size];
|
||||
match record_body.iter().position(|&x| x == 0) {
|
||||
None => {
|
||||
error!("offset {}: missing separator", self.offset);
|
||||
Some(Err(()))
|
||||
return Some(Err(Error::MissingSeparator { offset: self.offset }))
|
||||
}
|
||||
Some(pos) => {
|
||||
self.offset += record_size;
|
||||
|
@ -87,7 +118,7 @@ mod imp {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read<F: FnOnce(Result<&[u8], ()>) -> R, R>(key: &str, f: F) -> R {
|
||||
pub fn read<F: FnOnce(Result<&[u8], Error>) -> R, R>(key: &str, f: F) -> R {
|
||||
f(Lock::take().and_then(|lock| {
|
||||
let mut iter = Iter::new(lock.data());
|
||||
let mut value = &[][..];
|
||||
|
@ -102,52 +133,53 @@ mod imp {
|
|||
}))
|
||||
}
|
||||
|
||||
pub fn read_str<F: FnOnce(Result<&str, ()>) -> R, R>(key: &str, f: F) -> R {
|
||||
pub fn read_str<F: FnOnce(Result<&str, Error>) -> R, R>(key: &str, f: F) -> R {
|
||||
read(key, |result| {
|
||||
f(result.and_then(|value| str::from_utf8(value).map_err(|_| ())))
|
||||
f(result.and_then(|value| str::from_utf8(value).map_err(Error::Utf8Error)))
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn append_at<'a>(mut data: &'a [u8], key: &[u8], value: &[u8]) -> Result<&'a [u8], ()> {
|
||||
unsafe fn append_at(data: &[u8], mut offset: usize,
|
||||
key: &[u8], value: &[u8]) -> Result<usize, Error> {
|
||||
let record_size = 4 + key.len() + 1 + value.len();
|
||||
if data.len() < record_size {
|
||||
return Err(())
|
||||
if offset + record_size > data.len() {
|
||||
return Err(Error::SpaceExhausted)
|
||||
}
|
||||
|
||||
let mut record_size_bytes = [0u8; 4];
|
||||
BigEndian::write_u32(&mut record_size_bytes[..], record_size as u32);
|
||||
|
||||
spiflash::write(data.as_ptr() as usize, &record_size_bytes[..]);
|
||||
data = &data[record_size_bytes.len()..];
|
||||
{
|
||||
let mut write = |payload| {
|
||||
spiflash::write(data.as_ptr().offset(offset as isize) as usize, payload);
|
||||
offset += payload.len();
|
||||
};
|
||||
|
||||
spiflash::write(data.as_ptr() as usize, key);
|
||||
data = &data[key.len()..];
|
||||
write(&record_size_bytes[..]);
|
||||
write(key);
|
||||
write(&[0]);
|
||||
write(value);
|
||||
cache::flush_l2_cache();
|
||||
}
|
||||
|
||||
spiflash::write(data.as_ptr() as usize, &[0]);
|
||||
data = &data[1..];
|
||||
|
||||
spiflash::write(data.as_ptr() as usize, value);
|
||||
data = &data[value.len()..];
|
||||
|
||||
cache::flush_l2_cache();
|
||||
|
||||
Ok(data)
|
||||
Ok(offset)
|
||||
}
|
||||
|
||||
fn compact() -> Result<(), ()> {
|
||||
fn compact() -> Result<(), Error> {
|
||||
let lock = Lock::take()?;
|
||||
let data = lock.data();
|
||||
|
||||
static mut OLD_DATA: [u8; SIZE] = [0; SIZE];
|
||||
let old_data = unsafe {
|
||||
OLD_DATA.copy_from_slice(lock.data());
|
||||
OLD_DATA.copy_from_slice(data);
|
||||
&OLD_DATA[..]
|
||||
};
|
||||
|
||||
let mut data = lock.data();
|
||||
unsafe { spiflash::erase_sector(data.as_ptr() as usize) };
|
||||
|
||||
// This is worst-case quadratic, but we're limited by a small SPI flash sector size,
|
||||
// so it does not really matter.
|
||||
let mut offset = 0;
|
||||
let mut iter = Iter::new(old_data);
|
||||
while let Some(result) = iter.next() {
|
||||
let (key, mut value) = result?;
|
||||
|
@ -159,47 +191,48 @@ mod imp {
|
|||
value = next_value
|
||||
}
|
||||
}
|
||||
data = unsafe { append_at(data, key, value)? };
|
||||
offset = unsafe { append_at(data, offset, key, value)? };
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append(key: &str, value: &[u8]) -> Result<(), ()> {
|
||||
fn append(key: &str, value: &[u8]) -> Result<(), Error> {
|
||||
let lock = Lock::take()?;
|
||||
let data = lock.data();
|
||||
|
||||
let free = {
|
||||
let mut iter = Iter::new(lock.data());
|
||||
let free_offset = {
|
||||
let mut iter = Iter::new(data);
|
||||
while let Some(result) = iter.next() {
|
||||
let _ = result?;
|
||||
}
|
||||
&iter.data[iter.offset..]
|
||||
iter.offset
|
||||
};
|
||||
|
||||
unsafe { append_at(free, key.as_bytes(), value)? };
|
||||
unsafe { append_at(data, free_offset, key.as_bytes(), value)? };
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write(key: &str, value: &[u8]) -> Result<(), ()> {
|
||||
pub fn write(key: &str, value: &[u8]) -> Result<(), Error> {
|
||||
match append(key, value) {
|
||||
Ok(()) => (),
|
||||
Err(()) => {
|
||||
Err(Error::SpaceExhausted) => {
|
||||
compact()?;
|
||||
append(key, value)?;
|
||||
append(key, value)
|
||||
}
|
||||
res => res
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove(key: &str) -> Result<(), ()> {
|
||||
pub fn remove(key: &str) -> Result<(), Error> {
|
||||
write(key, &[])
|
||||
}
|
||||
|
||||
pub fn erase() -> Result<(), ()> {
|
||||
pub fn erase() -> Result<(), Error> {
|
||||
let lock = Lock::take()?;
|
||||
let data = lock.data();
|
||||
|
||||
unsafe { spiflash::erase_sector(lock.data().as_ptr() as usize) };
|
||||
unsafe { spiflash::erase_sector(data.as_ptr() as usize) };
|
||||
cache::flush_l2_cache();
|
||||
|
||||
Ok(())
|
||||
|
@ -208,23 +241,23 @@ mod imp {
|
|||
|
||||
#[cfg(not(has_spiflash))]
|
||||
mod imp {
|
||||
pub fn read<F: FnOnce(Result<&[u8], ()>) -> R, R>(_key: &str, f: F) -> R {
|
||||
pub fn read<F: FnOnce(Result<&[u8], Error>) -> R, R>(_key: &str, f: F) -> R {
|
||||
f(Err(()))
|
||||
}
|
||||
|
||||
pub fn read_str<F: FnOnce(Result<&str, ()>) -> R, R>(_key: &str, f: F) -> R {
|
||||
pub fn read_str<F: FnOnce(Result<&str, Error>) -> R, R>(_key: &str, f: F) -> R {
|
||||
f(Err(()))
|
||||
}
|
||||
|
||||
pub fn write(_key: &str, _value: &[u8]) -> Result<(), ()> {
|
||||
pub fn write(_key: &str, _value: &[u8]) -> Result<(), Error> {
|
||||
Err(())
|
||||
}
|
||||
|
||||
pub fn remove(_key: &str) -> Result<(), ()> {
|
||||
pub fn remove(_key: &str) -> Result<(), Error> {
|
||||
Err(())
|
||||
}
|
||||
|
||||
pub fn erase() -> Result<(), ()> {
|
||||
pub fn erase() -> Result<(), Error> {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
use core::{ptr, cmp, str};
|
||||
use csr;
|
||||
|
||||
pub fn read(buf: &mut [u8]) -> &str {
|
||||
unsafe {
|
||||
let len = ptr::read_volatile(csr::IDENTIFIER_MEM_BASE);
|
||||
let len = cmp::min(len as usize, buf.len());
|
||||
for i in 0..len {
|
||||
buf[i] = ptr::read_volatile(csr::IDENTIFIER_MEM_BASE.offset(1 + i as isize)) as u8
|
||||
}
|
||||
str::from_utf8_unchecked(&buf[..len])
|
||||
}
|
||||
}
|
|
@ -1,58 +1,22 @@
|
|||
#![feature(asm, lang_items)]
|
||||
#![no_std]
|
||||
#![feature(asm)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
extern crate byteorder;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use core::{cmp, ptr, str};
|
||||
#[cfg(target_arch = "or1k")]
|
||||
#[path = "or1k/mod.rs"]
|
||||
mod arch;
|
||||
|
||||
pub use arch::*;
|
||||
|
||||
include!(concat!(env!("BUILDINC_DIRECTORY"), "/generated/mem.rs"));
|
||||
include!(concat!(env!("BUILDINC_DIRECTORY"), "/generated/csr.rs"));
|
||||
include!(concat!(env!("BUILDINC_DIRECTORY"), "/generated/sdram_phy.rs"));
|
||||
pub mod spr;
|
||||
pub mod irq;
|
||||
pub mod cache;
|
||||
pub mod pcr;
|
||||
pub mod ident;
|
||||
pub mod clock;
|
||||
pub mod uart;
|
||||
#[cfg(feature = "uart_console")]
|
||||
pub mod uart_console;
|
||||
|
||||
#[cfg(has_spiflash)]
|
||||
pub mod spiflash;
|
||||
pub mod config;
|
||||
|
||||
pub mod i2c;
|
||||
pub mod spi;
|
||||
|
||||
#[cfg(has_si5324)]
|
||||
pub mod si5324;
|
||||
|
||||
#[cfg(has_serwb_phy_amc)]
|
||||
pub mod serwb;
|
||||
#[cfg(has_hmc830_7043)]
|
||||
pub mod hmc830_7043;
|
||||
#[cfg(has_ad9154)]
|
||||
#[allow(dead_code)]
|
||||
mod ad9154_reg;
|
||||
#[cfg(has_ad9154)]
|
||||
pub mod ad9154;
|
||||
|
||||
pub mod boot;
|
||||
|
||||
#[cfg(feature = "uart_console")]
|
||||
pub use uart_console::Console;
|
||||
|
||||
pub fn ident(buf: &mut [u8]) -> &str {
|
||||
unsafe {
|
||||
let len = ptr::read_volatile(csr::IDENTIFIER_MEM_BASE);
|
||||
let len = cmp::min(len as usize, buf.len());
|
||||
for i in 0..len {
|
||||
buf[i] = ptr::read_volatile(csr::IDENTIFIER_MEM_BASE.offset(1 + i as isize)) as u8
|
||||
}
|
||||
str::from_utf8_unchecked(&buf[..len])
|
||||
}
|
||||
}
|
||||
pub mod uart_console;
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
use core::ptr;
|
||||
use spr::{self, mfspr, mtspr};
|
||||
use super::spr::*;
|
||||
use csr;
|
||||
use mem;
|
||||
|
||||
pub fn flush_cpu_icache() {
|
||||
unsafe {
|
||||
let iccfgr = mfspr(spr::SPR_ICCFGR);
|
||||
let ways = 1 << (iccfgr & spr::SPR_ICCFGR_NCW);
|
||||
let set_size = 1 << ((iccfgr & spr::SPR_ICCFGR_NCS) >> 3);
|
||||
let block_size = if iccfgr & spr::SPR_ICCFGR_CBS != 0 { 32 } else { 16 };
|
||||
let iccfgr = mfspr(SPR_ICCFGR);
|
||||
let ways = 1 << (iccfgr & SPR_ICCFGR_NCW);
|
||||
let set_size = 1 << ((iccfgr & SPR_ICCFGR_NCS) >> 3);
|
||||
let block_size = if iccfgr & SPR_ICCFGR_CBS != 0 { 32 } else { 16 };
|
||||
let size = set_size * ways * block_size;
|
||||
|
||||
let mut i = 0;
|
||||
while i < size {
|
||||
mtspr(spr::SPR_ICBIR, i);
|
||||
mtspr(SPR_ICBIR, i);
|
||||
i += block_size;
|
||||
}
|
||||
}
|
||||
|
@ -21,15 +21,15 @@ pub fn flush_cpu_icache() {
|
|||
|
||||
pub fn flush_cpu_dcache() {
|
||||
unsafe {
|
||||
let dccfgr = mfspr(spr::SPR_DCCFGR);
|
||||
let ways = 1 << (dccfgr & spr::SPR_ICCFGR_NCW);
|
||||
let set_size = 1 << ((dccfgr & spr::SPR_DCCFGR_NCS) >> 3);
|
||||
let block_size = if dccfgr & spr::SPR_DCCFGR_CBS != 0 { 32 } else { 16 };
|
||||
let dccfgr = mfspr(SPR_DCCFGR);
|
||||
let ways = 1 << (dccfgr & SPR_ICCFGR_NCW);
|
||||
let set_size = 1 << ((dccfgr & SPR_DCCFGR_NCS) >> 3);
|
||||
let block_size = if dccfgr & SPR_DCCFGR_CBS != 0 { 32 } else { 16 };
|
||||
let size = set_size * ways * block_size;
|
||||
|
||||
let mut i = 0;
|
||||
while i < size {
|
||||
mtspr(spr::SPR_DCBIR, i);
|
||||
mtspr(SPR_DCBIR, i);
|
||||
i += block_size;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub mod spr;
|
||||
pub mod irq;
|
||||
pub mod cache;
|
|
@ -1,5 +1,3 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use core::cmp;
|
||||
use csr;
|
||||
|
||||
|
@ -8,7 +6,7 @@ pub const PAGE_SIZE: usize = csr::CONFIG_SPIFLASH_PAGE_SIZE as usize;
|
|||
const PAGE_MASK: usize = PAGE_SIZE - 1;
|
||||
|
||||
const CMD_PP: u8 = 0x02;
|
||||
const CMD_WRDI: u8 = 0x04;
|
||||
// const CMD_WRDI: u8 = 0x04;
|
||||
const CMD_RDSR: u8 = 0x05;
|
||||
const CMD_WREN: u8 = 0x06;
|
||||
const CMD_SE: u8 = 0xd8;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
authors = ["M-Labs"]
|
||||
name = "board_artiq"
|
||||
version = "0.0.0"
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
name = "board_artiq"
|
||||
path = "lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
build_artiq = { path = "../libbuild_artiq" }
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
board = { path = "../libboard" }
|
||||
|
||||
[features]
|
||||
uart_console = []
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
pub const SPI_INTFCONFA : u16 = 0x000;
|
||||
pub const SOFTRESET : u8 = 1 << 0;
|
||||
pub const LSBFIRST : u8 = 1 << 1;
|
|
@ -1,4 +1,4 @@
|
|||
use irq;
|
||||
use board::irq;
|
||||
|
||||
pub unsafe fn reboot() -> ! {
|
||||
irq::set_ie(false);
|
|
@ -0,0 +1,28 @@
|
|||
extern crate build_artiq;
|
||||
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn gen_hmc7043_writes() {
|
||||
println!("cargo:rerun-if-changed=hmc7043_gen_writes.py");
|
||||
println!("cargo:rerun-if-changed=hmc7043_guiexport_6gbps.py");
|
||||
|
||||
let hmc7043_writes =
|
||||
Command::new("python3")
|
||||
.arg("hmc7043_gen_writes.py")
|
||||
.arg("hmc7043_guiexport_6gbps.py")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.unwrap();
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let mut f = File::create(out_dir.join("hmc7043_writes.rs")).unwrap();
|
||||
write!(f, "{}", hmc7043_writes).unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
gen_hmc7043_writes();
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
#![feature(asm, lang_items)]
|
||||
#![no_std]
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
extern crate board;
|
||||
|
||||
pub mod pcr;
|
||||
|
||||
pub mod i2c;
|
||||
pub mod spi;
|
||||
|
||||
#[cfg(has_si5324)]
|
||||
pub mod si5324;
|
||||
|
||||
#[cfg(has_serwb_phy_amc)]
|
||||
pub mod serwb;
|
||||
#[cfg(has_hmc830_7043)]
|
||||
pub mod hmc830_7043;
|
||||
#[cfg(has_ad9154)]
|
||||
mod ad9154_reg;
|
||||
#[cfg(has_ad9154)]
|
||||
pub mod ad9154;
|
||||
|
||||
pub mod boot;
|
|
@ -1,4 +1,4 @@
|
|||
use spr::*;
|
||||
use board::spr::*;
|
||||
|
||||
bitflags! {
|
||||
pub struct Counters: u32 {
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
authors = ["M-Labs"]
|
||||
name = "build_misoc"
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
name = "build_misoc"
|
||||
path = "lib.rs"
|
|
@ -0,0 +1,15 @@
|
|||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn cfg() {
|
||||
let out_dir = env::var("BUILDINC_DIRECTORY").unwrap();
|
||||
let cfg_path = Path::new(&out_dir).join("generated").join("rust-cfg");
|
||||
println!("cargo:rerun-if-changed={}", cfg_path.to_str().unwrap());
|
||||
|
||||
let f = BufReader::new(File::open(&cfg_path).unwrap());
|
||||
for line in f.lines() {
|
||||
println!("cargo:rustc-cfg={}", line.unwrap());
|
||||
}
|
||||
}
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
extern crate log;
|
||||
extern crate log_buffer;
|
||||
#[macro_use]
|
||||
extern crate board;
|
||||
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::fmt::Write;
|
||||
use log::{Log, LogMetadata, LogRecord, LogLevelFilter, MaxLogLevelFilter};
|
||||
use log_buffer::LogBuffer;
|
||||
use board::{Console, clock};
|
||||
use board::clock;
|
||||
|
||||
pub struct BufferLogger {
|
||||
buffer: RefCell<LogBuffer<&'static mut [u8]>>,
|
||||
|
@ -110,9 +111,8 @@ impl Log for BufferLogger {
|
|||
record.level(), record.target(), record.args()).unwrap();
|
||||
|
||||
if record.level() <= self.uart_filter.get() {
|
||||
writeln!(Console,
|
||||
"[{:6}.{:06}s] {:>5}({}): {}", seconds, micros,
|
||||
record.level(), record.target(), record.args()).unwrap();
|
||||
println!("[{:6}.{:06}s] {:>5}({}): {}", seconds, micros,
|
||||
record.level(), record.target(), record.args());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,12 @@ build_artiq = { path = "../libbuild_artiq" }
|
|||
byteorder = { version = "1.0", default-features = false }
|
||||
cslice = { version = "0.3" }
|
||||
log = { version = "0.3", default-features = false }
|
||||
board = { path = "../libboard", features = ["uart_console"] }
|
||||
alloc_list = { path = "../liballoc_list" }
|
||||
std_artiq = { path = "../libstd_artiq", features = ["alloc", "io_error_alloc"] }
|
||||
logger_artiq = { path = "../liblogger_artiq" }
|
||||
backtrace_artiq = { path = "../libbacktrace_artiq" }
|
||||
board = { path = "../libboard", features = ["uart_console"] }
|
||||
board_artiq = { path = "../libboard_artiq" }
|
||||
proto = { path = "../libproto", features = ["log"] }
|
||||
amp = { path = "../libamp" }
|
||||
drtioaux = { path = "../libdrtioaux" }
|
||||
|
|
|
@ -120,14 +120,14 @@ mod drtio_i2c {
|
|||
}
|
||||
|
||||
mod i2c {
|
||||
use board;
|
||||
use board_artiq::i2c as local_i2c;
|
||||
use super::drtio_i2c;
|
||||
|
||||
pub fn start(busno: u32) -> Result<(), ()> {
|
||||
let nodeno = (busno >> 16) as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::i2c::start(node_busno)
|
||||
local_i2c::start(node_busno)
|
||||
} else {
|
||||
drtio_i2c::start(nodeno, node_busno)
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ mod i2c {
|
|||
let nodeno = (busno >> 16) as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::i2c::restart(node_busno)
|
||||
local_i2c::restart(node_busno)
|
||||
} else {
|
||||
drtio_i2c::restart(nodeno, node_busno)
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ mod i2c {
|
|||
let nodeno = (busno >> 16) as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::i2c::stop(node_busno)
|
||||
local_i2c::stop(node_busno)
|
||||
} else {
|
||||
drtio_i2c::stop(nodeno, node_busno)
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ mod i2c {
|
|||
let nodeno = (busno >> 16 )as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::i2c::write(node_busno, data)
|
||||
local_i2c::write(node_busno, data)
|
||||
} else {
|
||||
drtio_i2c::write(nodeno, node_busno, data)
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ mod i2c {
|
|||
let nodeno = (busno >> 16) as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::i2c::read(node_busno, ack)
|
||||
local_i2c::read(node_busno, ack)
|
||||
} else {
|
||||
drtio_i2c::read(nodeno, node_busno, ack)
|
||||
}
|
||||
|
@ -254,11 +254,13 @@ mod drtio_spi {
|
|||
|
||||
#[cfg(not(has_drtio))]
|
||||
mod drtio_spi {
|
||||
pub fn set_config(_nodeno: u8, _busno: u8, _flags: u8, _write_div: u8, _read_div: u8) -> Result<(), ()> {
|
||||
pub fn set_config(_nodeno: u8, _busno: u8, _flags: u8,
|
||||
_write_div: u8, _read_div: u8) -> Result<(), ()> {
|
||||
Err(())
|
||||
}
|
||||
|
||||
pub fn set_xfer(_nodeno: u8, _busno: u8, _chip_select: u16, _write_length: u8, _read_length: u8) -> Result<(), ()> {
|
||||
pub fn set_xfer(_nodeno: u8, _busno: u8, _chip_select: u16,
|
||||
_write_length: u8, _read_length: u8) -> Result<(), ()> {
|
||||
Err(())
|
||||
}
|
||||
|
||||
|
@ -272,14 +274,14 @@ mod drtio_spi {
|
|||
}
|
||||
|
||||
mod spi {
|
||||
use board;
|
||||
use board_artiq::spi as local_spi;
|
||||
use super::drtio_spi;
|
||||
|
||||
pub fn set_config(busno: u32, flags: u8, write_div: u8, read_div: u8) -> Result<(), ()> {
|
||||
let nodeno = (busno >> 16) as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::spi::set_config(node_busno, flags, write_div, read_div)
|
||||
local_spi::set_config(node_busno, flags, write_div, read_div)
|
||||
} else {
|
||||
drtio_spi::set_config(nodeno, node_busno, flags, write_div, read_div)
|
||||
}
|
||||
|
@ -289,7 +291,7 @@ mod spi {
|
|||
let nodeno = (busno >> 16) as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::spi::set_xfer(node_busno, chip_select, write_length, read_length)
|
||||
local_spi::set_xfer(node_busno, chip_select, write_length, read_length)
|
||||
} else {
|
||||
drtio_spi::set_xfer(nodeno, node_busno, chip_select, write_length, read_length)
|
||||
}
|
||||
|
@ -299,7 +301,7 @@ mod spi {
|
|||
let nodeno = (busno >> 16) as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::spi::write(node_busno, data)
|
||||
local_spi::write(node_busno, data)
|
||||
} else {
|
||||
drtio_spi::write(nodeno, node_busno, data)
|
||||
}
|
||||
|
@ -309,7 +311,7 @@ mod spi {
|
|||
let nodeno = (busno >> 16) as u8;
|
||||
let node_busno = busno as u8;
|
||||
if nodeno == 0 {
|
||||
board::spi::read(node_busno)
|
||||
local_spi::read(node_busno)
|
||||
} else {
|
||||
drtio_spi::read(nodeno, node_busno)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ extern crate logger_artiq;
|
|||
extern crate backtrace_artiq;
|
||||
#[macro_use]
|
||||
extern crate board;
|
||||
extern crate board_artiq;
|
||||
extern crate proto;
|
||||
extern crate amp;
|
||||
#[cfg(has_drtio)]
|
||||
|
@ -41,6 +42,7 @@ mod rtio_dma;
|
|||
mod mgmt;
|
||||
mod kernel;
|
||||
mod kern_hwreq;
|
||||
mod watchdog;
|
||||
mod session;
|
||||
#[cfg(any(has_rtio_moninj, has_drtio))]
|
||||
mod moninj;
|
||||
|
@ -51,7 +53,7 @@ fn startup() {
|
|||
board::clock::init();
|
||||
info!("ARTIQ runtime starting...");
|
||||
info!("software version {}", include_str!(concat!(env!("OUT_DIR"), "/git-describe")));
|
||||
info!("gateware version {}", board::ident(&mut [0; 64]));
|
||||
info!("gateware version {}", board::ident::read(&mut [0; 64]));
|
||||
|
||||
#[cfg(has_serwb_phy_amc)]
|
||||
board::serwb::wait_init();
|
||||
|
@ -69,7 +71,7 @@ fn startup() {
|
|||
info!("continuing boot");
|
||||
|
||||
#[cfg(has_i2c)]
|
||||
board::i2c::init();
|
||||
board_artiq::i2c::init();
|
||||
#[cfg(si5324_free_running)]
|
||||
setup_si5324_free_running();
|
||||
#[cfg(has_hmc830_7043)]
|
||||
|
@ -106,27 +108,27 @@ fn setup_si5324_free_running()
|
|||
#[cfg(has_ethmac)]
|
||||
fn startup_ethernet() {
|
||||
let hardware_addr;
|
||||
match config::read_str("mac", |r| r?.parse()) {
|
||||
Err(()) => {
|
||||
hardware_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
warn!("using default MAC address {}; consider changing it", hardware_addr);
|
||||
}
|
||||
Ok(addr) => {
|
||||
match config::read_str("mac", |r| r.map(|s| s.parse())) {
|
||||
Ok(Ok(addr)) => {
|
||||
hardware_addr = addr;
|
||||
info!("using MAC address {}", hardware_addr);
|
||||
}
|
||||
_ => {
|
||||
hardware_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
warn!("using default MAC address {}; consider changing it", hardware_addr);
|
||||
}
|
||||
}
|
||||
|
||||
let protocol_addr;
|
||||
match config::read_str("ip", |r| r?.parse()) {
|
||||
Err(()) => {
|
||||
protocol_addr = IpAddress::v4(192, 168, 1, 50);
|
||||
info!("using default IP address {}", protocol_addr);
|
||||
}
|
||||
Ok(addr) => {
|
||||
match config::read_str("ip", |r| r.map(|s| s.parse())) {
|
||||
Ok(Ok(addr)) => {
|
||||
protocol_addr = addr;
|
||||
info!("using IP address {}", protocol_addr);
|
||||
}
|
||||
_ => {
|
||||
protocol_addr = IpAddress::v4(192, 168, 1, 50);
|
||||
info!("using default IP address {}", protocol_addr);
|
||||
}
|
||||
}
|
||||
|
||||
// fn _net_trace_writer<U>(timestamp: u64, printer: smoltcp::wire::PrettyPrinter<U>)
|
||||
|
@ -159,26 +161,24 @@ fn startup_ethernet() {
|
|||
#[cfg(has_rtio_analyzer)]
|
||||
io.spawn(4096, analyzer::thread);
|
||||
|
||||
match config::read_str("log_level", |r| r?.parse()) {
|
||||
Err(()) => (),
|
||||
Ok(log_level_filter) => {
|
||||
match config::read_str("log_level", |r| r.map(|s| s.parse())) {
|
||||
Ok(Ok(log_level_filter)) => {
|
||||
info!("log level set to {} by `log_level` config key",
|
||||
log_level_filter);
|
||||
logger_artiq::BufferLogger::with(|logger|
|
||||
logger.set_max_log_level(log_level_filter));
|
||||
}
|
||||
_ => info!("log level set to INFO by default")
|
||||
}
|
||||
|
||||
match config::read_str("uart_log_level", |r| r?.parse()) {
|
||||
Err(()) => {
|
||||
info!("UART log level set to INFO by default");
|
||||
},
|
||||
Ok(uart_log_level_filter) => {
|
||||
match config::read_str("uart_log_level", |r| r.map(|s| s.parse())) {
|
||||
Ok(Ok(uart_log_level_filter)) => {
|
||||
info!("UART log level set to {} by `uart_log_level` config key",
|
||||
uart_log_level_filter);
|
||||
logger_artiq::BufferLogger::with(|logger|
|
||||
logger.set_uart_log_level(uart_log_level_filter));
|
||||
}
|
||||
_ => info!("UART log level set to INFO by default")
|
||||
}
|
||||
|
||||
let mut net_stats = ethmac::EthernetStatistics::new();
|
||||
|
@ -247,7 +247,7 @@ pub extern fn panic_fmt(args: core::fmt::Arguments, file: &'static str, line: u3
|
|||
|
||||
if config::read_str("panic_reboot", |r| r == Ok("1")) {
|
||||
println!("rebooting...");
|
||||
unsafe { board::boot::reboot() }
|
||||
unsafe { board_artiq::boot::reboot() }
|
||||
} else {
|
||||
println!("halting.");
|
||||
println!("use `artiq_coreconfig write -s panic_reboot 1` to reboot instead");
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use board_artiq::boot;
|
||||
use std::io::{self, Read, Write};
|
||||
use log::LogLevelFilter;
|
||||
use logger_artiq::BufferLogger;
|
||||
use sched::Io;
|
||||
use sched::{TcpListener, TcpStream};
|
||||
use board;
|
||||
use proto::WriteExt;
|
||||
use mgmt_proto::*;
|
||||
|
||||
|
@ -86,14 +86,14 @@ fn worker(io: &Io, stream: &mut TcpStream) -> io::Result<()> {
|
|||
Reply::RebootImminent.write_to(stream)?;
|
||||
stream.close()?;
|
||||
stream.flush()?;
|
||||
unsafe { board::boot::hotswap(&firmware) }
|
||||
unsafe { boot::hotswap(&firmware) }
|
||||
},
|
||||
|
||||
Request::Reboot => {
|
||||
Reply::RebootImminent.write_to(stream)?;
|
||||
stream.close()?;
|
||||
warn!("rebooting");
|
||||
unsafe { board::boot::reboot() }
|
||||
unsafe { boot::reboot() }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -226,12 +226,13 @@ pub fn startup(io: &Io) {
|
|||
info!("using external startup RTIO clock");
|
||||
RtioClock::External
|
||||
},
|
||||
Err(()) => {
|
||||
Err(_) => {
|
||||
info!("using internal startup RTIO clock (by default)");
|
||||
RtioClock::Internal
|
||||
},
|
||||
Ok(_) => {
|
||||
error!("unrecognized startup_clock configuration entry, using internal RTIO clock");
|
||||
error!("unrecognized startup_clock configuration entry, \
|
||||
using internal RTIO clock");
|
||||
RtioClock::Internal
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ use rtio_mgt;
|
|||
use rtio_dma::Manager as DmaManager;
|
||||
use cache::Cache;
|
||||
use kern_hwreq;
|
||||
use watchdog::WatchdogSet;
|
||||
|
||||
use rpc_proto as rpc;
|
||||
use session_proto as host;
|
||||
|
@ -66,7 +67,7 @@ enum KernelState {
|
|||
struct Session<'a> {
|
||||
congress: &'a mut Congress,
|
||||
kernel_state: KernelState,
|
||||
watchdog_set: board::clock::WatchdogSet,
|
||||
watchdog_set: WatchdogSet,
|
||||
log_buffer: String
|
||||
}
|
||||
|
||||
|
@ -75,7 +76,7 @@ impl<'a> Session<'a> {
|
|||
Session {
|
||||
congress: congress,
|
||||
kernel_state: KernelState::Absent,
|
||||
watchdog_set: board::clock::WatchdogSet::new(),
|
||||
watchdog_set: WatchdogSet::new(),
|
||||
log_buffer: String::new()
|
||||
}
|
||||
}
|
||||
|
@ -226,7 +227,7 @@ fn process_host_message(io: &Io,
|
|||
match host_read(stream)? {
|
||||
host::Request::SystemInfo => {
|
||||
host_write(stream, host::Reply::SystemInfo {
|
||||
ident: board::ident(&mut [0; 64]),
|
||||
ident: board::ident::read(&mut [0; 64]),
|
||||
finished_cleanly: session.congress.finished_cleanly.get()
|
||||
})?;
|
||||
session.congress.finished_cleanly.set(true);
|
||||
|
@ -238,7 +239,7 @@ fn process_host_message(io: &Io,
|
|||
config::read(key, |result| {
|
||||
match result {
|
||||
Ok(value) => host_write(stream, host::Reply::FlashRead(&value)),
|
||||
Err(()) => host_write(stream, host::Reply::FlashError)
|
||||
Err(_) => host_write(stream, host::Reply::FlashError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
use board::clock;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Watchdog {
|
||||
active: bool,
|
||||
threshold: u64
|
||||
}
|
||||
|
||||
pub const MAX_WATCHDOGS: usize = 16;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WatchdogSet {
|
||||
watchdogs: [Watchdog; MAX_WATCHDOGS]
|
||||
}
|
||||
|
||||
impl WatchdogSet {
|
||||
pub fn new() -> WatchdogSet {
|
||||
WatchdogSet {
|
||||
watchdogs: [Watchdog { active: false, threshold: 0 }; MAX_WATCHDOGS]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ms(&mut self, interval: u64) -> Result<usize, ()> {
|
||||
for (index, watchdog) in self.watchdogs.iter_mut().enumerate() {
|
||||
if !watchdog.active {
|
||||
watchdog.active = true;
|
||||
watchdog.threshold = clock::get_ms() + interval;
|
||||
return Ok(index)
|
||||
}
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, index: usize) {
|
||||
if index < MAX_WATCHDOGS {
|
||||
self.watchdogs[index].active = false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expired(&self) -> bool {
|
||||
self.watchdogs.iter()
|
||||
.filter(|wd| wd.active)
|
||||
.min_by_key(|wd| wd.threshold)
|
||||
.map_or(false, |wd| clock::get_ms() > wd.threshold)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue