Compare commits
27 commits
app-cleanu
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3265325d28 | |||
| d8477e3d2d | |||
| 472e43056d | |||
| 4c80907c0f | |||
| 1d2e4a5482 | |||
| d053f8d080 | |||
| f26920a099 | |||
| 60d6aaecd6 | |||
| 10a38f9bbc | |||
| 5934336984 | |||
| f3ac43614c | |||
| b6fa0e3b2d | |||
| cd91b3f540 | |||
| 45db5e8af8 | |||
| 6ba94f1cbb | |||
| c71ace5063 | |||
| 7e187680f5 | |||
| 64aa1808d2 | |||
| 2f92805c1a | |||
| c43cc5599e | |||
| 8ea4b4401e | |||
| 1c2823eb1b | |||
| 17d6f156db | |||
| b0b77a1538 | |||
| 43790abbc5 | |||
| 5288cba869 | |||
| f4759a0c71 |
6 changed files with 340 additions and 133 deletions
6
.gitmodules
vendored
6
.gitmodules
vendored
|
|
@ -1,12 +1,12 @@
|
||||||
[submodule "ch32v-insert-coin/ext/ch32-hal"]
|
[submodule "ch32v-insert-coin/ext/ch32-hal"]
|
||||||
path = ch32v-insert-coin/ext/ch32-hal
|
path = ch32v-insert-coin/ext/ch32-hal
|
||||||
url = git@github.com:sigil-03/ch32-hal.git
|
url = https://github.com/sigil-03/ch32-hal.git
|
||||||
[submodule "ch32v-insert-coin/ext/qingke"]
|
[submodule "ch32v-insert-coin/ext/qingke"]
|
||||||
path = ch32v-insert-coin/ext/qingke
|
path = ch32v-insert-coin/ext/qingke
|
||||||
url = git@github.com:ch32-rs/qingke.git
|
url = https://github.com/ch32-rs/qingke.git
|
||||||
[submodule "ch32v-insert-coin/ext/adpcm-pwm-dac"]
|
[submodule "ch32v-insert-coin/ext/adpcm-pwm-dac"]
|
||||||
path = ch32v-insert-coin/ext/adpcm-pwm-dac
|
path = ch32v-insert-coin/ext/adpcm-pwm-dac
|
||||||
url = ssh://git@git.glyphs.tech:222/sigil-03/adpcm-pwm-dac.git
|
url = https://git.glyphs.tech/sigil-03/adpcm-pwm-dac.git
|
||||||
[submodule "ch32v-insert-coin/ext/wavetable-synth"]
|
[submodule "ch32v-insert-coin/ext/wavetable-synth"]
|
||||||
path = ch32v-insert-coin/ext/wavetable-synth
|
path = ch32v-insert-coin/ext/wavetable-synth
|
||||||
url = https://git.glyphs.tech/sigil-03/wavetable-synth.git
|
url = https://git.glyphs.tech/sigil-03/wavetable-synth.git
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit f41336744c4e2548c8f6ba9b323ae4aa39959f1d
|
Subproject commit 4f11d68e62dcb0e7098eecf357168724a8322d80
|
||||||
|
|
@ -9,7 +9,7 @@ pub enum State {
|
||||||
Active,
|
Active,
|
||||||
}
|
}
|
||||||
|
|
||||||
mod settings {
|
pub mod settings {
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub enum Level {
|
pub enum Level {
|
||||||
|
|
@ -183,7 +183,7 @@ use crate::synthesizer::SynthesizerService;
|
||||||
|
|
||||||
pub use settings::Settings;
|
pub use settings::Settings;
|
||||||
|
|
||||||
#[cfg(feature = "enable_print")]
|
// #[cfg(feature = "enable_print")]
|
||||||
use ch32_hal::println;
|
use ch32_hal::println;
|
||||||
|
|
||||||
pub struct TimerConfig {
|
pub struct TimerConfig {
|
||||||
|
|
@ -193,7 +193,8 @@ pub struct TimerConfig {
|
||||||
pub usb_adc_timer_ms: usize,
|
pub usb_adc_timer_ms: usize,
|
||||||
pub led0_timer_ms: usize,
|
pub led0_timer_ms: usize,
|
||||||
pub led1_timer_ms: usize,
|
pub led1_timer_ms: usize,
|
||||||
pub led2_timer_ms: usize,
|
pub shutdown_timer_s: usize,
|
||||||
|
// pub led2_timer_ms: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Timers {
|
pub struct Timers {
|
||||||
|
|
@ -203,7 +204,9 @@ pub struct Timers {
|
||||||
usb_adc_timer: TickTimerService,
|
usb_adc_timer: TickTimerService,
|
||||||
led0_timer: TickTimerService,
|
led0_timer: TickTimerService,
|
||||||
led1_timer: TickTimerService,
|
led1_timer: TickTimerService,
|
||||||
led2_timer: TickTimerService,
|
shutdown_timer: TickTimerService,
|
||||||
|
pps_timer: TickTimerService,
|
||||||
|
// led2_timer: TickTimerService,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timers {
|
impl Timers {
|
||||||
|
|
@ -233,10 +236,15 @@ impl Timers {
|
||||||
TickServiceData::new(config.led1_timer_ms * system_tick_rate_hz / 1000),
|
TickServiceData::new(config.led1_timer_ms * system_tick_rate_hz / 1000),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
led2_timer: TickTimerService::new(
|
shutdown_timer: TickTimerService::new(
|
||||||
TickServiceData::new(config.led2_timer_ms * system_tick_rate_hz / 1000),
|
TickServiceData::new(config.shutdown_timer_s),
|
||||||
true,
|
false,
|
||||||
),
|
),
|
||||||
|
pps_timer: TickTimerService::new(TickServiceData::new(system_tick_rate_hz), true),
|
||||||
|
// led2_timer: TickTimerService::new(
|
||||||
|
// TickServiceData::new(config.led2_timer_ms * system_tick_rate_hz / 1000),
|
||||||
|
// true,
|
||||||
|
// ),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn tick(&mut self) {
|
pub fn tick(&mut self) {
|
||||||
|
|
@ -246,7 +254,8 @@ impl Timers {
|
||||||
self.usb_adc_timer.tick();
|
self.usb_adc_timer.tick();
|
||||||
self.led0_timer.tick();
|
self.led0_timer.tick();
|
||||||
self.led1_timer.tick();
|
self.led1_timer.tick();
|
||||||
self.led2_timer.tick();
|
self.pps_timer.tick();
|
||||||
|
// self.led2_timer.tick();
|
||||||
}
|
}
|
||||||
pub fn need_service(&self) -> bool {
|
pub fn need_service(&self) -> bool {
|
||||||
self.sp_timer.need_service()
|
self.sp_timer.need_service()
|
||||||
|
|
@ -255,7 +264,9 @@ impl Timers {
|
||||||
| self.usb_adc_timer.need_service()
|
| self.usb_adc_timer.need_service()
|
||||||
| self.led0_timer.need_service()
|
| self.led0_timer.need_service()
|
||||||
| self.led1_timer.need_service()
|
| self.led1_timer.need_service()
|
||||||
| self.led2_timer.need_service()
|
| self.shutdown_timer.need_service()
|
||||||
|
| self.pps_timer.need_service()
|
||||||
|
// | self.led2_timer.need_service()
|
||||||
}
|
}
|
||||||
pub fn init(&mut self) {
|
pub fn init(&mut self) {
|
||||||
self.led0_timer.reset();
|
self.led0_timer.reset();
|
||||||
|
|
@ -272,7 +283,7 @@ impl Timers {
|
||||||
pub struct Services {
|
pub struct Services {
|
||||||
pub led0: LedService,
|
pub led0: LedService,
|
||||||
pub led1: LedService,
|
pub led1: LedService,
|
||||||
pub led2: LedService,
|
// pub led2: LedService,
|
||||||
pub synth0: SynthesizerService,
|
pub synth0: SynthesizerService,
|
||||||
pub sample_player: DacService<'static>,
|
pub sample_player: DacService<'static>,
|
||||||
pub sequencer: sequencer::DynamicSequence<'static>,
|
pub sequencer: sequencer::DynamicSequence<'static>,
|
||||||
|
|
@ -294,13 +305,16 @@ pub struct Config {
|
||||||
pub struct Sequences {
|
pub struct Sequences {
|
||||||
pub led0: sequencer::BasicSequence<'static>,
|
pub led0: sequencer::BasicSequence<'static>,
|
||||||
pub led1: sequencer::BasicSequence<'static>,
|
pub led1: sequencer::BasicSequence<'static>,
|
||||||
pub led2: sequencer::BasicSequence<'static>,
|
// pub led2: sequencer::BasicSequence<'static>,
|
||||||
pub audio: &'static [(&'static [sequencer::SequenceEntry], usize)],
|
pub audio: &'static [(&'static [sequencer::SequenceEntry], usize)],
|
||||||
}
|
}
|
||||||
|
|
||||||
// things that touch hardware
|
// things that touch hardware
|
||||||
pub struct Interfaces {
|
pub struct Interfaces {
|
||||||
pub pwm_core: SimplePwmCore<'static, ch32_hal::peripherals::TIM1>,
|
pub pwm_core: SimplePwmCore<'static, ch32_hal::peripherals::TIM1>,
|
||||||
|
pub adc_core: crate::AdcCore,
|
||||||
|
pub amp: crate::Amplifier,
|
||||||
|
pub usb: crate::Usb,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
|
@ -319,10 +333,11 @@ impl App {
|
||||||
services: Services,
|
services: Services,
|
||||||
sequences: Sequences,
|
sequences: Sequences,
|
||||||
interfaces: Interfaces,
|
interfaces: Interfaces,
|
||||||
|
settings: Settings,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: State::default(),
|
state: State::default(),
|
||||||
settings: Settings::default(),
|
settings,
|
||||||
timers: Timers::new(config.timers, config.system_tick_rate_hz),
|
timers: Timers::new(config.timers, config.system_tick_rate_hz),
|
||||||
services,
|
services,
|
||||||
sequences,
|
sequences,
|
||||||
|
|
@ -332,6 +347,8 @@ impl App {
|
||||||
|
|
||||||
pub fn init(&mut self) {
|
pub fn init(&mut self) {
|
||||||
// self.timers.init();
|
// self.timers.init();
|
||||||
|
self.interfaces.amp.enable();
|
||||||
|
|
||||||
self.timers.batt_adc_timer.reset();
|
self.timers.batt_adc_timer.reset();
|
||||||
self.timers.batt_adc_timer.enable(true);
|
self.timers.batt_adc_timer.enable(true);
|
||||||
|
|
||||||
|
|
@ -344,13 +361,20 @@ impl App {
|
||||||
self.timers.led1_timer.reset();
|
self.timers.led1_timer.reset();
|
||||||
self.timers.led1_timer.enable(true);
|
self.timers.led1_timer.enable(true);
|
||||||
|
|
||||||
self.timers.led2_timer.reset();
|
self.timers.shutdown_timer.reset();
|
||||||
self.timers.led2_timer.enable(true);
|
self.timers.shutdown_timer.enable(true);
|
||||||
|
|
||||||
self.services.synth0.set_freq(1);
|
self.timers.pps_timer.reset();
|
||||||
|
self.timers.pps_timer.enable(true);
|
||||||
|
|
||||||
|
// self.timers.led2_timer.reset();
|
||||||
|
// self.timers.led2_timer.enable(true);
|
||||||
|
|
||||||
|
// self.services.synth0.set_freq(1);
|
||||||
self.services.synth0.disable();
|
self.services.synth0.disable();
|
||||||
|
|
||||||
self.services.sequencer.disable();
|
self.services.sequencer.disable();
|
||||||
|
|
||||||
|
crate::riscv::asm::delay(2_500_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_state(&mut self, state: State) {
|
pub fn set_state(&mut self, state: State) {
|
||||||
|
|
@ -388,8 +412,16 @@ impl App {
|
||||||
}
|
}
|
||||||
if self.timers.batt_adc_timer.need_service() {
|
if self.timers.batt_adc_timer.need_service() {
|
||||||
self.timers.batt_adc_timer.service();
|
self.timers.batt_adc_timer.service();
|
||||||
#[cfg(feature = "enable_print")]
|
if !self.interfaces.usb.powered() {
|
||||||
println!("batt adc service");
|
let bv = self.interfaces.adc_core.get_battery_voltage();
|
||||||
|
let avg = self.interfaces.adc_core.get_average();
|
||||||
|
// #[cfg(feature = "enable_print")]
|
||||||
|
// println!("batt adc service: {bv}, {avg}");
|
||||||
|
// println!("none USB");
|
||||||
|
if avg < 421 {
|
||||||
|
self.set_state(State::DeepSleep);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if self.timers.usb_adc_timer.need_service() {
|
if self.timers.usb_adc_timer.need_service() {
|
||||||
self.timers.usb_adc_timer.service();
|
self.timers.usb_adc_timer.service();
|
||||||
|
|
@ -430,23 +462,33 @@ impl App {
|
||||||
// #[cfg(feature = "enable_print")]
|
// #[cfg(feature = "enable_print")]
|
||||||
// println!("led1 service");
|
// println!("led1 service");
|
||||||
}
|
}
|
||||||
if self.timers.led2_timer.need_service() {
|
if self.timers.pps_timer.need_service() {
|
||||||
let out = match self.settings.brightness {
|
self.timers.pps_timer.service();
|
||||||
Level::Off => 0,
|
self.timers.shutdown_timer.tick();
|
||||||
Level::Low => 5,
|
|
||||||
Level::Medium => 25,
|
|
||||||
Level::High => 75,
|
|
||||||
Level::Maximum => {
|
|
||||||
self.sequences.led2.next();
|
|
||||||
self.sequences.led2.get_value() / 6
|
|
||||||
}
|
}
|
||||||
};
|
if self.timers.shutdown_timer.need_service() {
|
||||||
self.timers.led2_timer.service();
|
self.timers.shutdown_timer.service();
|
||||||
self.services.led2.set_amplitude(out);
|
self.timers.shutdown_timer.reset();
|
||||||
|
// println!("eepy");
|
||||||
|
self.set_state(State::DeepSleep);
|
||||||
|
}
|
||||||
|
// if self.timers.led2_timer.need_service() {
|
||||||
|
// let out = match self.settings.brightness {
|
||||||
|
// Level::Off => 0,
|
||||||
|
// Level::Low => 5,
|
||||||
|
// Level::Medium => 25,
|
||||||
|
// Level::High => 75,
|
||||||
|
// Level::Maximum => {
|
||||||
|
// self.sequences.led2.next();
|
||||||
|
// self.sequences.led2.get_value() / 6
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// self.timers.led2_timer.service();
|
||||||
|
// self.services.led2.set_amplitude(out);
|
||||||
|
|
||||||
// #[cfg(feature = "enable_print")]
|
// // #[cfg(feature = "enable_print")]
|
||||||
// println!("led2 service");
|
// // println!("led2 service");
|
||||||
}
|
// }
|
||||||
|
|
||||||
// services
|
// services
|
||||||
if self.services.led0.need_service() {
|
if self.services.led0.need_service() {
|
||||||
|
|
@ -461,12 +503,12 @@ impl App {
|
||||||
.write_amplitude(self.services.led1.channel, self.services.led1.amplitude);
|
.write_amplitude(self.services.led1.channel, self.services.led1.amplitude);
|
||||||
self.services.led1.service();
|
self.services.led1.service();
|
||||||
}
|
}
|
||||||
if self.services.led2.need_service() {
|
// if self.services.led2.need_service() {
|
||||||
self.interfaces
|
// self.interfaces
|
||||||
.pwm_core
|
// .pwm_core
|
||||||
.write_amplitude(self.services.led2.channel, self.services.led2.amplitude);
|
// .write_amplitude(self.services.led2.channel, self.services.led2.amplitude);
|
||||||
self.services.led2.service();
|
// self.services.led2.service();
|
||||||
}
|
// }
|
||||||
|
|
||||||
// TODO: disable when you get to the end automatically
|
// TODO: disable when you get to the end automatically
|
||||||
// in the sequencer, not here
|
// in the sequencer, not here
|
||||||
|
|
@ -513,22 +555,29 @@ impl App {
|
||||||
self.interfaces
|
self.interfaces
|
||||||
.pwm_core
|
.pwm_core
|
||||||
.write_amplitude(self.services.led1.channel, 0);
|
.write_amplitude(self.services.led1.channel, 0);
|
||||||
self.interfaces
|
crate::riscv::asm::delay(10_000_000);
|
||||||
.pwm_core
|
|
||||||
.write_amplitude(self.services.led2.channel, 0);
|
self.interfaces.pwm_core.pwm.borrow().shutdown();
|
||||||
self.interfaces
|
self.interfaces.adc_core.shutdown();
|
||||||
.pwm_core
|
|
||||||
.disable(ch32_hal::timer::Channel::Ch4);
|
self.interfaces.amp.disable();
|
||||||
}
|
}
|
||||||
pub fn volume_button(&mut self) {
|
pub fn volume_button(&mut self) {
|
||||||
self.settings.volume.next();
|
self.settings.volume.next();
|
||||||
#[cfg(feature = "enable_print")]
|
#[cfg(feature = "enable_print")]
|
||||||
println!("new volume: {:?}", self.settings.volume);
|
println!("new volume: {:?}", self.settings.volume);
|
||||||
|
self.services
|
||||||
|
.sequencer
|
||||||
|
.play_sequence(&crate::sequences::COIN_CHIRP, 0);
|
||||||
|
self.timers.shutdown_timer.reset();
|
||||||
|
self.timers.shutdown_timer.enable(true);
|
||||||
}
|
}
|
||||||
pub fn brightness_button(&mut self) {
|
pub fn brightness_button(&mut self) {
|
||||||
self.settings.brightness.next();
|
self.settings.brightness.next();
|
||||||
#[cfg(feature = "enable_print")]
|
#[cfg(feature = "enable_print")]
|
||||||
println!("new brightness: {:?}", self.settings.brightness);
|
println!("new brightness: {:?}", self.settings.brightness);
|
||||||
|
self.timers.shutdown_timer.reset();
|
||||||
|
self.timers.shutdown_timer.enable(true);
|
||||||
}
|
}
|
||||||
pub fn main_button_press(&mut self) {
|
pub fn main_button_press(&mut self) {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
@ -536,8 +585,10 @@ impl App {
|
||||||
println!("main button press");
|
println!("main button press");
|
||||||
self.timers.sp_timer.reset();
|
self.timers.sp_timer.reset();
|
||||||
self.timers.lp_timer.reset();
|
self.timers.lp_timer.reset();
|
||||||
|
self.timers.shutdown_timer.reset();
|
||||||
self.timers.sp_timer.enable(true);
|
self.timers.sp_timer.enable(true);
|
||||||
self.timers.lp_timer.enable(true);
|
self.timers.lp_timer.enable(true);
|
||||||
|
self.timers.shutdown_timer.enable(true);
|
||||||
self.main_button_click();
|
self.main_button_click();
|
||||||
}
|
}
|
||||||
pub fn main_button_release(&mut self) {
|
pub fn main_button_release(&mut self) {
|
||||||
|
|
@ -569,12 +620,14 @@ impl App {
|
||||||
self.services
|
self.services
|
||||||
.sequencer
|
.sequencer
|
||||||
.play_sequence(&crate::sequences::COIN_CHIRP, 0);
|
.play_sequence(&crate::sequences::COIN_CHIRP, 0);
|
||||||
|
self.timers.shutdown_timer.reset();
|
||||||
|
self.timers.shutdown_timer.enable(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
impl App {
|
impl App {
|
||||||
fn main_button_click(&mut self) {
|
pub fn main_button_click(&mut self) {
|
||||||
// TODO
|
// TODO
|
||||||
#[cfg(feature = "enable_print")]
|
#[cfg(feature = "enable_print")]
|
||||||
println!("click");
|
println!("click");
|
||||||
|
|
@ -610,6 +663,19 @@ impl App {
|
||||||
pub fn get_state(&self) -> State {
|
pub fn get_state(&self) -> State {
|
||||||
self.state
|
self.state
|
||||||
}
|
}
|
||||||
|
pub fn get_settings(&self) -> Settings {
|
||||||
|
self.settings
|
||||||
|
}
|
||||||
|
pub fn should_wake(&self) -> bool {
|
||||||
|
if self.interfaces.usb.powered() {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if self.interfaces.adc_core.get_average() >= 421 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO LIST
|
// TODO LIST
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ impl<'a> DebouncedGPIO<'a> {
|
||||||
pub fn new(pin: AnyPin, system_tick_rate_hz: usize, debounce_time_ms: usize) -> Self {
|
pub fn new(pin: AnyPin, system_tick_rate_hz: usize, debounce_time_ms: usize) -> Self {
|
||||||
// coin debounce timer (100ms)
|
// coin debounce timer (100ms)
|
||||||
Self {
|
Self {
|
||||||
input: Input::new(pin, Pull::Up),
|
input: Input::new(pin, Pull::None),
|
||||||
value: false,
|
value: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
active: false,
|
active: false,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use crate::insert_coin::services::{DacService, LedService, Service, TickService,
|
||||||
// static LED1_DCS: [u8; 5] = [0u8, 25u8, 50u8, 75u8, 100u8];
|
// static LED1_DCS: [u8; 5] = [0u8, 25u8, 50u8, 75u8, 100u8];
|
||||||
|
|
||||||
pub struct SimplePwmCore<'d, T: GeneralInstance16bit> {
|
pub struct SimplePwmCore<'d, T: GeneralInstance16bit> {
|
||||||
pwm: core::cell::RefCell<SimplePwm<'d, T>>,
|
pub pwm: core::cell::RefCell<SimplePwm<'d, T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'d, T: GeneralInstance16bit> SimplePwmCore<'d, T> {
|
impl<'d, T: GeneralInstance16bit> SimplePwmCore<'d, T> {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ use insert_coin::{CoreConfig, DacService, InsertCoin, LedService, SimplePwmCore}
|
||||||
use ch32_hal as hal;
|
use ch32_hal as hal;
|
||||||
use hal::bind_interrupts;
|
use hal::bind_interrupts;
|
||||||
use hal::delay::Delay;
|
use hal::delay::Delay;
|
||||||
use hal::gpio::{AnyPin, Input, Level, Output, Pin, Pull};
|
use hal::gpio::{AnyPin, Input, Level, Output, OutputOpenDrain, Pin, Pull};
|
||||||
use hal::time::Hertz;
|
use hal::time::Hertz;
|
||||||
use hal::timer::low_level::CountingMode;
|
use hal::timer::low_level::CountingMode;
|
||||||
use hal::timer::simple_pwm::{PwmPin, SimplePwm};
|
use hal::timer::simple_pwm::{PwmPin, SimplePwm};
|
||||||
|
|
@ -44,6 +44,94 @@ use crate::app::sequencer::{DynamicSequence, SequenceEntry};
|
||||||
|
|
||||||
static LED0_SEQ: [u8; 8] = [0u8, 25u8, 50u8, 75u8, 100u8, 75u8, 50u8, 25u8];
|
static LED0_SEQ: [u8; 8] = [0u8, 25u8, 50u8, 75u8, 100u8, 75u8, 50u8, 25u8];
|
||||||
|
|
||||||
|
pub struct Usb {
|
||||||
|
usb_pin: Input<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Usb {
|
||||||
|
pub fn new(usb_pin: Input<'static>) -> Self {
|
||||||
|
Self { usb_pin }
|
||||||
|
}
|
||||||
|
pub fn powered(&self) -> bool {
|
||||||
|
self.usb_pin.is_high()
|
||||||
|
}
|
||||||
|
// pub fn enable(&mut self) {
|
||||||
|
// self.usb_pin.set_as_input(Pull::Up);
|
||||||
|
// }
|
||||||
|
// pub fn disable(&mut self) {
|
||||||
|
// self.usb_pin.set_as_input(Pull::None);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Amplifier {
|
||||||
|
amp_en: OutputOpenDrain<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Amplifier {
|
||||||
|
pub fn new(amp_en: OutputOpenDrain<'static>) -> Self {
|
||||||
|
let mut amp = Self { amp_en };
|
||||||
|
amp.disable();
|
||||||
|
amp
|
||||||
|
}
|
||||||
|
pub fn enable(&mut self) {
|
||||||
|
self.amp_en.set_low();
|
||||||
|
}
|
||||||
|
pub fn disable(&mut self) {
|
||||||
|
self.amp_en.set_high();
|
||||||
|
}
|
||||||
|
pub fn enabled(&self) -> bool {
|
||||||
|
!self.amp_en.is_set_high()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use hal::adc::Adc;
|
||||||
|
use hal::peripherals::{ADC1, PD4};
|
||||||
|
|
||||||
|
pub struct AdcCore {
|
||||||
|
adc: Adc<'static, ADC1>,
|
||||||
|
battery_pin: PD4,
|
||||||
|
batt_values: [u16; 10],
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdcCore {
|
||||||
|
pub fn new(mut adc: Adc<'static, ADC1>, pin: PD4) -> Self {
|
||||||
|
riscv::asm::delay(20_000);
|
||||||
|
adc.calibrate();
|
||||||
|
Self {
|
||||||
|
adc,
|
||||||
|
battery_pin: pin,
|
||||||
|
batt_values: [1024; 10],
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO make this a float or something
|
||||||
|
pub fn get_battery_voltage(&mut self) -> u16 {
|
||||||
|
let val = self
|
||||||
|
.adc
|
||||||
|
.convert(&mut self.battery_pin, hal::adc::SampleTime::CYCLES241);
|
||||||
|
self.batt_values[self.index] = val;
|
||||||
|
self.index += 1;
|
||||||
|
if self.index > &self.batt_values.len() - 1 {
|
||||||
|
self.index = 0;
|
||||||
|
}
|
||||||
|
val
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_average(&self) -> u16 {
|
||||||
|
let mut sum = 0;
|
||||||
|
for value in &self.batt_values {
|
||||||
|
sum += value;
|
||||||
|
}
|
||||||
|
sum / self.batt_values.len() as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(&mut self) {
|
||||||
|
self.adc.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Flag {
|
struct Flag {
|
||||||
value: bool,
|
value: bool,
|
||||||
|
|
@ -151,16 +239,40 @@ fn systick_init(tick_freq_hz: usize) {
|
||||||
w.set_stclk(ch32_hal::pac::systick::vals::Stclk::HCLK_DIV8); // HCLK/8 clock source
|
w.set_stclk(ch32_hal::pac::systick::vals::Stclk::HCLK_DIV8); // HCLK/8 clock source
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
fn systick_stop() {
|
||||||
|
let r = &ch32_hal::pac::SYSTICK;
|
||||||
|
// Reset SysTick
|
||||||
|
r.ctlr().write(|w| {
|
||||||
|
// Start with everything disabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bind_interrupts!(struct Irqs {
|
bind_interrupts!(struct Irqs {
|
||||||
EXTI7_0 => Test;
|
EXTI7_0 => Test;
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: remove
|
// TODO: remove
|
||||||
|
use app::settings::Settings;
|
||||||
use insert_coin::TickTimerService;
|
use insert_coin::TickTimerService;
|
||||||
use insert_coin::{TickService, TickServiceData};
|
use insert_coin::{TickService, TickServiceData};
|
||||||
|
|
||||||
fn app_main(mut p: hal::Peripherals) -> ! {
|
fn app_main(mut p: hal::Peripherals, app_settings: Settings) -> Settings {
|
||||||
|
// initialize ADC core first, and exit if battery is too low
|
||||||
|
let mut adc = hal::adc::Adc::new(p.ADC1, Default::default());
|
||||||
|
let mut batt_monitor_pin = p.PD4;
|
||||||
|
let mut adc_core = AdcCore::new(adc, batt_monitor_pin);
|
||||||
|
|
||||||
|
let mut usb_detect_pin = p.PD5;
|
||||||
|
let usb_detect_input = Input::new(usb_detect_pin, Pull::Up);
|
||||||
|
let usb = Usb::new(usb_detect_input);
|
||||||
|
|
||||||
|
let bv = adc_core.get_battery_voltage();
|
||||||
|
|
||||||
|
// if we don't have USB power, and the batt ADC reads under 421, don't wake
|
||||||
|
if !usb.powered() && bv < 421 {
|
||||||
|
adc_core.shutdown();
|
||||||
|
return app_settings;
|
||||||
|
}
|
||||||
// === output setup ===
|
// === output setup ===
|
||||||
|
|
||||||
// LED0 output setup
|
// LED0 output setup
|
||||||
|
|
@ -171,10 +283,6 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
let led1_pin = PwmPin::new_ch1::<0>(p.PD2);
|
let led1_pin = PwmPin::new_ch1::<0>(p.PD2);
|
||||||
let led1_ch = hal::timer::Channel::Ch1;
|
let led1_ch = hal::timer::Channel::Ch1;
|
||||||
|
|
||||||
// LED2 output setup
|
|
||||||
let led2_pin = PwmPin::new_ch2::<0>(p.PA1);
|
|
||||||
let led2_ch = hal::timer::Channel::Ch2;
|
|
||||||
|
|
||||||
// DAC output setup
|
// DAC output setup
|
||||||
let dac_pin = PwmPin::new_ch4::<0>(p.PC4);
|
let dac_pin = PwmPin::new_ch4::<0>(p.PC4);
|
||||||
// let dac_ch = hal::timer::Channel::Ch4;
|
// let dac_ch = hal::timer::Channel::Ch4;
|
||||||
|
|
@ -183,7 +291,8 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
let mut pwm = SimplePwm::new(
|
let mut pwm = SimplePwm::new(
|
||||||
p.TIM1,
|
p.TIM1,
|
||||||
Some(led1_pin),
|
Some(led1_pin),
|
||||||
Some(led2_pin),
|
// Some(led2_pin),
|
||||||
|
None,
|
||||||
Some(led0_pin),
|
Some(led0_pin),
|
||||||
Some(dac_pin),
|
Some(dac_pin),
|
||||||
Hertz::khz(200),
|
Hertz::khz(200),
|
||||||
|
|
@ -192,26 +301,27 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
|
|
||||||
pwm.set_polarity(led0_ch, OutputPolarity::ActiveHigh);
|
pwm.set_polarity(led0_ch, OutputPolarity::ActiveHigh);
|
||||||
pwm.set_polarity(led1_ch, OutputPolarity::ActiveLow);
|
pwm.set_polarity(led1_ch, OutputPolarity::ActiveLow);
|
||||||
pwm.set_polarity(led2_ch, OutputPolarity::ActiveLow);
|
let mut pwm_core = SimplePwmCore::new(pwm);
|
||||||
|
pwm_core.write_amplitude(led0_ch, 0);
|
||||||
|
pwm_core.write_amplitude(led1_ch, 0);
|
||||||
|
|
||||||
|
// pwm.set_polarity(led2_ch, OutputPolarity::ActiveLow);
|
||||||
|
|
||||||
let tick_rate_hz = 50000;
|
let tick_rate_hz = 50000;
|
||||||
|
|
||||||
let core_config = CoreConfig::new(tick_rate_hz);
|
let core_config = CoreConfig::new(tick_rate_hz);
|
||||||
|
|
||||||
let pwm_core = SimplePwmCore::new(pwm);
|
|
||||||
|
|
||||||
// === input setup ===
|
// === input setup ===
|
||||||
|
|
||||||
// adc
|
// adc
|
||||||
let mut adc = hal::adc::Adc::new(p.ADC1, Default::default());
|
// let mut adc = hal::adc::Adc::new(p.ADC1, Default::default());
|
||||||
let mut batt_monitor_pin = p.PD4;
|
// let mut batt_monitor_pin = p.PD4;
|
||||||
|
// let adc_core = AdcCore::new(adc, batt_monitor_pin);
|
||||||
|
|
||||||
// adc2
|
// adc2
|
||||||
// let mut usb_detect_dc = hal::adc::Adc::new(p.ADC1, Default::default());
|
// let mut usb_detect_dc = hal::adc::Adc::new(p.ADC1, Default::default());
|
||||||
let mut usb_detect_pin = p.PD5;
|
|
||||||
|
|
||||||
// println!("ADC_PIN CHANNEL: {}", adc_pin.channel().channel());
|
// println!("ADC_PIN CHANNEL: {}", adc_pin.channel().channel());
|
||||||
let adc_cal = adc.calibrate();
|
|
||||||
|
|
||||||
// #[cfg(feature = "enable_print")]
|
// #[cfg(feature = "enable_print")]
|
||||||
// println!("ADC calibration value: {}", adc_cal);
|
// println!("ADC calibration value: {}", adc_cal);
|
||||||
|
|
@ -222,11 +332,11 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
let volume_btn_pin = p.PC6;
|
let volume_btn_pin = p.PC6;
|
||||||
let light_ctrl_btn_pin = p.PC7;
|
let light_ctrl_btn_pin = p.PC7;
|
||||||
let amp_en = p.PC5;
|
let amp_en = p.PC5;
|
||||||
let extra_io_1 = p.PD0;
|
// let extra_io_1 = p.PD0;
|
||||||
let extra_io_2 = p.PD3;
|
// let extra_io_2 = p.PD3;
|
||||||
|
|
||||||
let mut amp_en_output = Output::new(amp_en, Level::Low, Default::default());
|
let mut amp_en_output = OutputOpenDrain::new(amp_en, Level::Low, Default::default());
|
||||||
amp_en_output.set_low();
|
let amp = Amplifier::new(amp_en_output);
|
||||||
|
|
||||||
// set up interrupts
|
// set up interrupts
|
||||||
unsafe { system::init_gpio_irq(sense_coin_pin.pin(), sense_coin_pin.port(), true, false) };
|
unsafe { system::init_gpio_irq(sense_coin_pin.pin(), sense_coin_pin.port(), true, false) };
|
||||||
|
|
@ -248,11 +358,14 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
let timer_config = TimerConfig {
|
let timer_config = TimerConfig {
|
||||||
sp_timer_ms: 1000,
|
sp_timer_ms: 1000,
|
||||||
lp_timer_ms: 3000,
|
lp_timer_ms: 3000,
|
||||||
batt_adc_timer_ms: 10000,
|
batt_adc_timer_ms: 1000,
|
||||||
usb_adc_timer_ms: 10000,
|
usb_adc_timer_ms: 10000,
|
||||||
led0_timer_ms: 100,
|
led0_timer_ms: 100,
|
||||||
led1_timer_ms: 100,
|
led1_timer_ms: 100,
|
||||||
led2_timer_ms: 100,
|
// 4 hours:
|
||||||
|
// shutdown_timer_s: 1,
|
||||||
|
shutdown_timer_s: 4 * 60 * 60 * 110 / 100,
|
||||||
|
// led2_timer_ms: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app_config = Config {
|
let app_config = Config {
|
||||||
|
|
@ -265,18 +378,18 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
let dac_tick_per_service = tick_rate_hz / dac_sample_rate_hz;
|
let dac_tick_per_service = tick_rate_hz / dac_sample_rate_hz;
|
||||||
let dac_service_data = TickServiceData::new(dac_tick_per_service);
|
let dac_service_data = TickServiceData::new(dac_tick_per_service);
|
||||||
|
|
||||||
let coin_sound = include_bytes!("../audio/coin5.raw");
|
// let coin_sound = include_bytes!("../audio/coin5.raw");
|
||||||
// let coin_sound = include_bytes!("../audio/coin2.raw");
|
// let coin_sound = include_bytes!("../audio/coin2.raw");
|
||||||
|
|
||||||
let sample_player = DacService::new(ch32_hal::timer::Channel::Ch4, dac_service_data);
|
let sample_player = DacService::new(ch32_hal::timer::Channel::Ch4, dac_service_data);
|
||||||
sample_player.load_data(coin_sound);
|
// sample_player.load_data(coin_sound);
|
||||||
|
|
||||||
let sequencer = app::sequencer::DynamicSequence::new(&SEQUENCE_LIST[0].0, tick_rate_hz);
|
let sequencer = app::sequencer::DynamicSequence::new(&SEQUENCE_LIST[0].0, tick_rate_hz);
|
||||||
|
|
||||||
let app_services = Services {
|
let app_services = Services {
|
||||||
led0: LedService::new(led0_ch),
|
led0: LedService::new(led0_ch),
|
||||||
led1: LedService::new(led1_ch),
|
led1: LedService::new(led1_ch),
|
||||||
led2: LedService::new(led2_ch),
|
// led2: LedService::new(led2_ch),
|
||||||
synth0: SynthesizerService::new(tick_rate_hz),
|
synth0: SynthesizerService::new(tick_rate_hz),
|
||||||
sample_player,
|
sample_player,
|
||||||
sequencer,
|
sequencer,
|
||||||
|
|
@ -285,13 +398,35 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
let app_sequences = Sequences {
|
let app_sequences = Sequences {
|
||||||
led0: BasicSequence::new(&LED0_SEQ),
|
led0: BasicSequence::new(&LED0_SEQ),
|
||||||
led1: BasicSequence::new(&LED0_SEQ),
|
led1: BasicSequence::new(&LED0_SEQ),
|
||||||
led2: BasicSequence::new(&LED0_SEQ),
|
// led2: BasicSequence::new(&LED0_SEQ),
|
||||||
audio: &SEQUENCE_LIST,
|
audio: &SEQUENCE_LIST,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app_interfaces = Interfaces { pwm_core };
|
let app_interfaces = Interfaces {
|
||||||
|
pwm_core,
|
||||||
|
adc_core,
|
||||||
|
amp,
|
||||||
|
usb,
|
||||||
|
};
|
||||||
|
|
||||||
let mut app = App::new(app_config, app_services, app_sequences, app_interfaces);
|
let mut app = App::new(
|
||||||
|
app_config,
|
||||||
|
app_services,
|
||||||
|
app_sequences,
|
||||||
|
app_interfaces,
|
||||||
|
app_settings,
|
||||||
|
);
|
||||||
|
|
||||||
|
let need_sound = unsafe {
|
||||||
|
#[allow(static_mut_refs)]
|
||||||
|
if INPUT_FLAGS.main_btn_flag.active() {
|
||||||
|
#[allow(static_mut_refs)]
|
||||||
|
INPUT_FLAGS.main_btn_flag.clear();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// init systick
|
// init systick
|
||||||
systick_init(tick_rate_hz);
|
systick_init(tick_rate_hz);
|
||||||
|
|
@ -324,6 +459,9 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
let mut light_ctrl_btn_prev = light_ctrl_btn_input.is_high_immediate();
|
let mut light_ctrl_btn_prev = light_ctrl_btn_input.is_high_immediate();
|
||||||
|
|
||||||
app.init();
|
app.init();
|
||||||
|
if need_sound {
|
||||||
|
app.main_button_click();
|
||||||
|
}
|
||||||
loop {
|
loop {
|
||||||
// system servicing
|
// system servicing
|
||||||
|
|
||||||
|
|
@ -422,65 +560,54 @@ fn app_main(mut p: hal::Peripherals) -> ! {
|
||||||
// enter standby
|
// enter standby
|
||||||
app::State::DeepSleep => {
|
app::State::DeepSleep => {
|
||||||
app.shut_down();
|
app.shut_down();
|
||||||
|
return app.get_settings();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use ch32_hal::timer::low_level::{OutputCompareMode, Timer};
|
||||||
|
use ch32_hal::timer::Channel;
|
||||||
|
|
||||||
|
// fn shutdown_main(p: Peripherals) {
|
||||||
|
fn shutdown_main(p: hal::Peripherals) {
|
||||||
|
systick_stop();
|
||||||
|
// LED0 output setup
|
||||||
|
let led0_pin = OutputOpenDrain::new(p.PC3, Level::Low, Default::default());
|
||||||
|
let led1_pin = OutputOpenDrain::new(p.PD2, Level::High, Default::default());
|
||||||
|
let led2_pin = OutputOpenDrain::new(p.PA1, Level::High, Default::default());
|
||||||
|
let dac_pin = OutputOpenDrain::new(p.PC4, Level::Low, Default::default());
|
||||||
|
let mut amp_pin = OutputOpenDrain::new(p.PC5, Level::Low, Default::default());
|
||||||
|
amp_pin.set_high();
|
||||||
|
let volume_btn_pin = OutputOpenDrain::new(p.PC6, Level::Low, Default::default());
|
||||||
|
let light_ctrl_btn_pin = OutputOpenDrain::new(p.PC7, Level::Low, Default::default());
|
||||||
|
let usb_detect_input = OutputOpenDrain::new(p.PD5, Level::Low, Default::default());
|
||||||
|
|
||||||
|
let sense_coin_pin = p.PC2;
|
||||||
|
let main_btn_pin = p.PD6;
|
||||||
|
|
||||||
|
unsafe { system::init_gpio_irq(sense_coin_pin.pin(), sense_coin_pin.port(), true, false) };
|
||||||
|
unsafe { system::init_gpio_irq(main_btn_pin.pin(), main_btn_pin.port(), true, false) };
|
||||||
|
|
||||||
|
let sense_coin_pin = Input::new(sense_coin_pin, Pull::None);
|
||||||
|
let main_btn_pin = Input::new(main_btn_pin, Pull::None);
|
||||||
|
|
||||||
|
riscv::asm::delay(1_000_000);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
unsafe { system::enter_standby() };
|
unsafe { system::enter_standby() };
|
||||||
unsafe {
|
|
||||||
#[allow(static_mut_refs)]
|
|
||||||
INPUT_FLAGS.sense_coin_flag.clear();
|
|
||||||
#[allow(static_mut_refs)]
|
|
||||||
INPUT_FLAGS.main_btn_flag.clear();
|
|
||||||
}
|
|
||||||
riscv::asm::wfi();
|
riscv::asm::wfi();
|
||||||
let mut config = hal::Config::default();
|
|
||||||
config.rcc = hal::rcc::Config::SYSCLK_FREQ_48MHZ_HSI;
|
|
||||||
unsafe {
|
|
||||||
hal::rcc::init(config.rcc);
|
|
||||||
}
|
|
||||||
unsafe {
|
unsafe {
|
||||||
#[allow(static_mut_refs)]
|
#[allow(static_mut_refs)]
|
||||||
if INPUT_FLAGS.sense_coin_flag.active()
|
if (INPUT_FLAGS.sense_coin_flag.active() || INPUT_FLAGS.main_btn_flag.active())
|
||||||
|| (INPUT_FLAGS.main_btn_flag.active()
|
// && app.should_wake()
|
||||||
&& main_btn_input.is_high_immediate())
|
|
||||||
{
|
{
|
||||||
unsafe {
|
|
||||||
use hal::pac::Interrupt;
|
|
||||||
use qingke::interrupt::Priority;
|
|
||||||
use qingke_rt::CoreInterrupt;
|
|
||||||
|
|
||||||
system::clear_interrupt(2, 6);
|
|
||||||
|
|
||||||
qingke::pfic::set_priority(
|
|
||||||
CoreInterrupt::SysTick as u8,
|
|
||||||
Priority::P15 as u8,
|
|
||||||
);
|
|
||||||
|
|
||||||
qingke::pfic::enable_interrupt(Interrupt::EXTI7_0 as u8);
|
|
||||||
qingke::pfic::enable_interrupt(CoreInterrupt::SysTick as u8);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.set_state(State::Active);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// for everything else, don't do anything
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// // if adc1_timer.need_service() {
|
|
||||||
// // let val = adc.convert(&mut batt_monitor_pin, hal::adc::SampleTime::CYCLES241);
|
|
||||||
// // let val = adc.convert(&mut usb_detect_pin, hal::adc::SampleTime::CYCLES241);
|
|
||||||
// // #[cfg(feature = "enable_print")]
|
|
||||||
// // println!("ADC value: {}", val);
|
|
||||||
|
|
||||||
// // adc1_timer.reset();
|
|
||||||
// // adc1_timer.enable(true);
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[qingke_rt::entry]
|
#[qingke_rt::entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
|
|
@ -497,7 +624,21 @@ fn main() -> ! {
|
||||||
// println!("post");
|
// println!("post");
|
||||||
// debug_main(p);
|
// debug_main(p);
|
||||||
|
|
||||||
app_main(p);
|
let mut app_settings = Settings::default();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
unsafe {
|
||||||
|
hal::rcc::init(hal::rcc::Config::SYSCLK_FREQ_48MHZ_HSI);
|
||||||
|
}
|
||||||
|
let mut p = unsafe { hal::Peripherals::steal() };
|
||||||
|
app_settings = app_main(p, app_settings);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
hal::rcc::init(hal::rcc::Config::SYSCLK_FREQ_48MHZ_HSI);
|
||||||
|
}
|
||||||
|
let mut p = unsafe { hal::Peripherals::steal() };
|
||||||
|
shutdown_main(p);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[panic_handler]
|
#[panic_handler]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue