mirror of
https://github.com/embassy-rs/embassy.git
synced 2024-11-25 08:12:30 +00:00
Merge pull request #3433 from 1-rafael-1/rp-pwm-embedded-hal-traits
embassy_rp: implement pwm traits from embedded_hal
This commit is contained in:
commit
0c22d4cccb
@ -118,18 +118,18 @@ embassy-usb-driver = {version = "0.1.0", path = "../embassy-usb-driver" }
|
|||||||
atomic-polyfill = "1.0.1"
|
atomic-polyfill = "1.0.1"
|
||||||
defmt = { version = "0.3", optional = true }
|
defmt = { version = "0.3", optional = true }
|
||||||
log = { version = "0.4.14", optional = true }
|
log = { version = "0.4.14", optional = true }
|
||||||
nb = "1.0.0"
|
nb = "1.1.0"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
cortex-m-rt = ">=0.6.15,<0.8"
|
cortex-m-rt = ">=0.6.15,<0.8"
|
||||||
cortex-m = "0.7.6"
|
cortex-m = "0.7.6"
|
||||||
critical-section = "1.1"
|
critical-section = "1.2.0"
|
||||||
chrono = { version = "0.4", default-features = false, optional = true }
|
chrono = { version = "0.4", default-features = false, optional = true }
|
||||||
embedded-io = { version = "0.6.1" }
|
embedded-io = { version = "0.6.1" }
|
||||||
embedded-io-async = { version = "0.6.1" }
|
embedded-io-async = { version = "0.6.1" }
|
||||||
embedded-storage = { version = "0.3" }
|
embedded-storage = { version = "0.3" }
|
||||||
embedded-storage-async = { version = "0.4.1" }
|
embedded-storage-async = { version = "0.4.1" }
|
||||||
rand_core = "0.6.4"
|
rand_core = "0.6.4"
|
||||||
fixed = "1.23.1"
|
fixed = "1.28.0"
|
||||||
|
|
||||||
rp-pac = { git = "https://github.com/embassy-rs/rp-pac.git", rev = "a7f42d25517f7124ad3b4ed492dec8b0f50a0e6c", feature = ["rt"] }
|
rp-pac = { git = "https://github.com/embassy-rs/rp-pac.git", rev = "a7f42d25517f7124ad3b4ed492dec8b0f50a0e6c", feature = ["rt"] }
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ embedded-hal-nb = { version = "1.0" }
|
|||||||
pio-proc = {version= "0.2" }
|
pio-proc = {version= "0.2" }
|
||||||
pio = {version= "0.2.1" }
|
pio = {version= "0.2.1" }
|
||||||
rp2040-boot2 = "0.3"
|
rp2040-boot2 = "0.3"
|
||||||
document-features = "0.2.7"
|
document-features = "0.2.10"
|
||||||
sha2-const-stable = "0.1"
|
sha2-const-stable = "0.1"
|
||||||
rp-binary-info = { version = "0.1.0", optional = true }
|
rp-binary-info = { version = "0.1.0", optional = true }
|
||||||
smart-leds = "0.4.0"
|
smart-leds = "0.4.0"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
//! Pulse Width Modulation (PWM)
|
//! Pulse Width Modulation (PWM)
|
||||||
|
|
||||||
use embassy_hal_internal::{into_ref, Peripheral, PeripheralRef};
|
use embassy_hal_internal::{into_ref, Peripheral, PeripheralRef};
|
||||||
|
pub use embedded_hal_1::pwm::SetDutyCycle;
|
||||||
|
use embedded_hal_1::pwm::{Error, ErrorKind, ErrorType};
|
||||||
use fixed::traits::ToFixed;
|
use fixed::traits::ToFixed;
|
||||||
use fixed::FixedU16;
|
use fixed::FixedU16;
|
||||||
use pac::pwm::regs::{ChDiv, Intr};
|
use pac::pwm::regs::{ChDiv, Intr};
|
||||||
@ -80,6 +82,21 @@ impl From<InputMode> for Divmode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PWM error.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PwmError {
|
||||||
|
/// Invalid Duty Cycle.
|
||||||
|
InvalidDutyCycle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for PwmError {
|
||||||
|
fn kind(&self) -> ErrorKind {
|
||||||
|
match self {
|
||||||
|
PwmError::InvalidDutyCycle => ErrorKind::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// PWM driver.
|
/// PWM driver.
|
||||||
pub struct Pwm<'d> {
|
pub struct Pwm<'d> {
|
||||||
pin_a: Option<PeripheralRef<'d, AnyPin>>,
|
pin_a: Option<PeripheralRef<'d, AnyPin>>,
|
||||||
@ -87,6 +104,30 @@ pub struct Pwm<'d> {
|
|||||||
slice: usize,
|
slice: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'d> ErrorType for Pwm<'d> {
|
||||||
|
type Error = PwmError;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d> SetDutyCycle for Pwm<'d> {
|
||||||
|
fn max_duty_cycle(&self) -> u16 {
|
||||||
|
pac::PWM.ch(self.slice).top().read().top()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_duty_cycle(&mut self, duty: u16) -> Result<(), Self::Error> {
|
||||||
|
let max_duty = self.max_duty_cycle();
|
||||||
|
if duty > max_duty {
|
||||||
|
return Err(PwmError::InvalidDutyCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = pac::PWM.ch(self.slice);
|
||||||
|
p.cc().modify(|w| {
|
||||||
|
w.set_a(duty);
|
||||||
|
w.set_b(duty);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'d> Pwm<'d> {
|
impl<'d> Pwm<'d> {
|
||||||
fn new_inner(
|
fn new_inner(
|
||||||
slice: usize,
|
slice: usize,
|
||||||
|
@ -1,24 +1,36 @@
|
|||||||
//! This example shows how to use PWM (Pulse Width Modulation) in the RP2040 chip.
|
//! This example shows how to use PWM (Pulse Width Modulation) in the RP2040 chip.
|
||||||
//!
|
//!
|
||||||
//! The LED on the RP Pico W board is connected differently. Add a LED and resistor to another pin.
|
//! We demonstrate two ways of using PWM:
|
||||||
|
//! 1. Via config
|
||||||
|
//! 2. Via setting a duty cycle
|
||||||
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use defmt::*;
|
use defmt::*;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_rp::pwm::{Config, Pwm};
|
use embassy_rp::peripherals::{PIN_25, PIN_4, PWM_SLICE2, PWM_SLICE4};
|
||||||
|
use embassy_rp::pwm::{Config, Pwm, SetDutyCycle};
|
||||||
use embassy_time::Timer;
|
use embassy_time::Timer;
|
||||||
use {defmt_rtt as _, panic_probe as _};
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
async fn main(_spawner: Spawner) {
|
async fn main(spawner: Spawner) {
|
||||||
let p = embassy_rp::init(Default::default());
|
let p = embassy_rp::init(Default::default());
|
||||||
|
spawner.spawn(pwm_set_config(p.PWM_SLICE4, p.PIN_25)).unwrap();
|
||||||
|
spawner.spawn(pwm_set_dutycycle(p.PWM_SLICE2, p.PIN_4)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let mut c: Config = Default::default();
|
/// Demonstrate PWM by modifying & applying the config
|
||||||
c.top = 0x8000;
|
///
|
||||||
|
/// Using the onboard led, if You are using a different Board than plain Pico2 (i.e. W variant)
|
||||||
|
/// you must use another slice & pin and an appropriate resistor.
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn pwm_set_config(slice4: PWM_SLICE4, pin25: PIN_25) {
|
||||||
|
let mut c = Config::default();
|
||||||
|
c.top = 32_768;
|
||||||
c.compare_b = 8;
|
c.compare_b = 8;
|
||||||
let mut pwm = Pwm::new_output_b(p.PWM_SLICE4, p.PIN_25, c.clone());
|
let mut pwm = Pwm::new_output_b(slice4, pin25, c.clone());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
info!("current LED duty cycle: {}/32768", c.compare_b);
|
info!("current LED duty cycle: {}/32768", c.compare_b);
|
||||||
@ -27,3 +39,37 @@ async fn main(_spawner: Spawner) {
|
|||||||
pwm.set_config(&c);
|
pwm.set_config(&c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Demonstrate PWM by setting duty cycle
|
||||||
|
///
|
||||||
|
/// Using GP4 in Slice2, make sure to use an appropriate resistor.
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn pwm_set_dutycycle(slice2: PWM_SLICE2, pin4: PIN_4) {
|
||||||
|
// If we aim for a specific frequency, here is how we can calculate the top value.
|
||||||
|
// The top value sets the period of the PWM cycle, so a counter goes from 0 to top and then wraps around to 0.
|
||||||
|
// Every such wraparound is one PWM cycle. So here is how we get 25KHz:
|
||||||
|
let mut c = Config::default();
|
||||||
|
let pwm_freq = 25_000; // Hz, our desired frequency
|
||||||
|
let clock_freq = embassy_rp::clocks::clk_sys_freq();
|
||||||
|
c.top = (clock_freq / pwm_freq) as u16 - 1;
|
||||||
|
|
||||||
|
let mut pwm = Pwm::new_output_a(slice2, pin4, c.clone());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// 100% duty cycle, fully on
|
||||||
|
pwm.set_duty_cycle_fully_on().unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
|
||||||
|
// 66% duty cycle. Expressed as simple percentage.
|
||||||
|
pwm.set_duty_cycle_percent(66).unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
|
||||||
|
// 25% duty cycle. Expressed as 32768/4 = 8192.
|
||||||
|
pwm.set_duty_cycle(c.top / 4).unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
|
||||||
|
// 0% duty cycle, fully off.
|
||||||
|
pwm.set_duty_cycle_fully_off().unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -30,6 +30,9 @@ serde-json-core = "0.5.1"
|
|||||||
# for assign resources example
|
# for assign resources example
|
||||||
assign-resources = { git = "https://github.com/adamgreig/assign-resources", rev = "94ad10e2729afdf0fd5a77cd12e68409a982f58a" }
|
assign-resources = { git = "https://github.com/adamgreig/assign-resources", rev = "94ad10e2729afdf0fd5a77cd12e68409a982f58a" }
|
||||||
|
|
||||||
|
# for TB6612FNG example
|
||||||
|
tb6612fng = "1.0.0"
|
||||||
|
|
||||||
#cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
|
#cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
|
||||||
cortex-m = { version = "0.7.6", features = ["inline-asm"] }
|
cortex-m = { version = "0.7.6", features = ["inline-asm"] }
|
||||||
cortex-m-rt = "0.7.0"
|
cortex-m-rt = "0.7.0"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
//! This example shows how to use PWM (Pulse Width Modulation) in the RP2040 chip.
|
//! This example shows how to use PWM (Pulse Width Modulation) in the RP235x chip.
|
||||||
//!
|
//!
|
||||||
//! The LED on the RP Pico W board is connected differently. Add a LED and resistor to another pin.
|
//! We demonstrate two ways of using PWM:
|
||||||
|
//! 1. Via config
|
||||||
|
//! 2. Via setting a duty cycle
|
||||||
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
@ -8,7 +10,8 @@
|
|||||||
use defmt::*;
|
use defmt::*;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_rp::block::ImageDef;
|
use embassy_rp::block::ImageDef;
|
||||||
use embassy_rp::pwm::{Config, Pwm};
|
use embassy_rp::peripherals::{PIN_25, PIN_4, PWM_SLICE2, PWM_SLICE4};
|
||||||
|
use embassy_rp::pwm::{Config, Pwm, SetDutyCycle};
|
||||||
use embassy_time::Timer;
|
use embassy_time::Timer;
|
||||||
use {defmt_rtt as _, panic_probe as _};
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
@ -17,13 +20,22 @@ use {defmt_rtt as _, panic_probe as _};
|
|||||||
pub static IMAGE_DEF: ImageDef = ImageDef::secure_exe();
|
pub static IMAGE_DEF: ImageDef = ImageDef::secure_exe();
|
||||||
|
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
async fn main(_spawner: Spawner) {
|
async fn main(spawner: Spawner) {
|
||||||
let p = embassy_rp::init(Default::default());
|
let p = embassy_rp::init(Default::default());
|
||||||
|
spawner.spawn(pwm_set_config(p.PWM_SLICE4, p.PIN_25)).unwrap();
|
||||||
|
spawner.spawn(pwm_set_dutycycle(p.PWM_SLICE2, p.PIN_4)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let mut c: Config = Default::default();
|
/// Demonstrate PWM by modifying & applying the config
|
||||||
c.top = 0x8000;
|
///
|
||||||
|
/// Using the onboard led, if You are using a different Board than plain Pico2 (i.e. W variant)
|
||||||
|
/// you must use another slice & pin and an appropriate resistor.
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn pwm_set_config(slice4: PWM_SLICE4, pin25: PIN_25) {
|
||||||
|
let mut c = Config::default();
|
||||||
|
c.top = 32_768;
|
||||||
c.compare_b = 8;
|
c.compare_b = 8;
|
||||||
let mut pwm = Pwm::new_output_b(p.PWM_SLICE4, p.PIN_25, c.clone());
|
let mut pwm = Pwm::new_output_b(slice4, pin25, c.clone());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
info!("current LED duty cycle: {}/32768", c.compare_b);
|
info!("current LED duty cycle: {}/32768", c.compare_b);
|
||||||
@ -32,3 +44,37 @@ async fn main(_spawner: Spawner) {
|
|||||||
pwm.set_config(&c);
|
pwm.set_config(&c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Demonstrate PWM by setting duty cycle
|
||||||
|
///
|
||||||
|
/// Using GP4 in Slice2, make sure to use an appropriate resistor.
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn pwm_set_dutycycle(slice2: PWM_SLICE2, pin4: PIN_4) {
|
||||||
|
// If we aim for a specific frequency, here is how we can calculate the top value.
|
||||||
|
// The top value sets the period of the PWM cycle, so a counter goes from 0 to top and then wraps around to 0.
|
||||||
|
// Every such wraparound is one PWM cycle. So here is how we get 25KHz:
|
||||||
|
let mut c = Config::default();
|
||||||
|
let pwm_freq = 25_000; // Hz, our desired frequency
|
||||||
|
let clock_freq = embassy_rp::clocks::clk_sys_freq();
|
||||||
|
c.top = (clock_freq / pwm_freq) as u16 - 1;
|
||||||
|
|
||||||
|
let mut pwm = Pwm::new_output_a(slice2, pin4, c.clone());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// 100% duty cycle, fully on
|
||||||
|
pwm.set_duty_cycle_fully_on().unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
|
||||||
|
// 66% duty cycle. Expressed as simple percentage.
|
||||||
|
pwm.set_duty_cycle_percent(66).unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
|
||||||
|
// 25% duty cycle. Expressed as 32768/4 = 8192.
|
||||||
|
pwm.set_duty_cycle(c.top / 4).unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
|
||||||
|
// 0% duty cycle, fully off.
|
||||||
|
pwm.set_duty_cycle_fully_off().unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
107
examples/rp23/src/bin/pwm_tb6612fng_motor_driver.rs
Normal file
107
examples/rp23/src/bin/pwm_tb6612fng_motor_driver.rs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
//! # PWM TB6612FNG motor driver
|
||||||
|
//!
|
||||||
|
//! This example shows the use of a TB6612FNG motor driver. The driver is built on top of embedded_hal and the example demonstrates how embassy_rp can be used to interact with ist.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use assign_resources::assign_resources;
|
||||||
|
use defmt::*;
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_rp::block::ImageDef;
|
||||||
|
use embassy_rp::config::Config;
|
||||||
|
use embassy_rp::gpio::Output;
|
||||||
|
use embassy_rp::{gpio, peripherals, pwm};
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use tb6612fng::{DriveCommand, Motor, Tb6612fng};
|
||||||
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
|
#[link_section = ".start_block"]
|
||||||
|
#[used]
|
||||||
|
pub static IMAGE_DEF: ImageDef = ImageDef::secure_exe();
|
||||||
|
|
||||||
|
assign_resources! {
|
||||||
|
motor: MotorResources {
|
||||||
|
standby_pin: PIN_22,
|
||||||
|
left_slice: PWM_SLICE6,
|
||||||
|
left_pwm_pin: PIN_28,
|
||||||
|
left_forward_pin: PIN_21,
|
||||||
|
left_backward_pin: PIN_20,
|
||||||
|
right_slice: PWM_SLICE5,
|
||||||
|
right_pwm_pin: PIN_27,
|
||||||
|
right_forward_pin: PIN_19,
|
||||||
|
right_backward_pin: PIN_18,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::main]
|
||||||
|
async fn main(_spawner: Spawner) {
|
||||||
|
let p = embassy_rp::init(Config::default());
|
||||||
|
let s = split_resources!(p);
|
||||||
|
let r = s.motor;
|
||||||
|
|
||||||
|
// we want a PWM frequency of 1KHz, especially cheaper motors do not respond well to higher frequencies
|
||||||
|
let pwm_freq = 1_000; // Hz, our desired frequency
|
||||||
|
let clock_freq = embassy_rp::clocks::clk_sys_freq();
|
||||||
|
let period = (clock_freq / pwm_freq) as u16 - 1;
|
||||||
|
|
||||||
|
// we need a standby output and two motors to construct a full TB6612FNG
|
||||||
|
|
||||||
|
// standby pin
|
||||||
|
let stby = Output::new(r.standby_pin, gpio::Level::Low);
|
||||||
|
|
||||||
|
// motor A, here defined to be the left motor
|
||||||
|
let left_fwd = gpio::Output::new(r.left_forward_pin, gpio::Level::Low);
|
||||||
|
let left_bckw = gpio::Output::new(r.left_backward_pin, gpio::Level::Low);
|
||||||
|
let mut left_speed = pwm::Config::default();
|
||||||
|
left_speed.top = period;
|
||||||
|
let left_pwm = pwm::Pwm::new_output_a(r.left_slice, r.left_pwm_pin, left_speed);
|
||||||
|
let left_motor = Motor::new(left_fwd, left_bckw, left_pwm).unwrap();
|
||||||
|
|
||||||
|
// motor B, here defined to be the right motor
|
||||||
|
let right_fwd = gpio::Output::new(r.right_forward_pin, gpio::Level::Low);
|
||||||
|
let right_bckw = gpio::Output::new(r.right_backward_pin, gpio::Level::Low);
|
||||||
|
let mut right_speed = pwm::Config::default();
|
||||||
|
right_speed.top = period;
|
||||||
|
let right_pwm = pwm::Pwm::new_output_b(r.right_slice, r.right_pwm_pin, right_speed);
|
||||||
|
let right_motor = Motor::new(right_fwd, right_bckw, right_pwm).unwrap();
|
||||||
|
|
||||||
|
// construct the motor driver
|
||||||
|
let mut control = Tb6612fng::new(left_motor, right_motor, stby).unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// wake up the motor driver
|
||||||
|
info!("end standby");
|
||||||
|
control.disable_standby().unwrap();
|
||||||
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
// drive a straight line forward at 20% speed for 5s
|
||||||
|
info!("drive straight");
|
||||||
|
control.motor_a.drive(DriveCommand::Forward(80)).unwrap();
|
||||||
|
control.motor_b.drive(DriveCommand::Forward(80)).unwrap();
|
||||||
|
Timer::after(Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
// coast for 2s
|
||||||
|
info!("coast");
|
||||||
|
control.motor_a.drive(DriveCommand::Stop).unwrap();
|
||||||
|
control.motor_b.drive(DriveCommand::Stop).unwrap();
|
||||||
|
Timer::after(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// actively brake
|
||||||
|
info!("brake");
|
||||||
|
control.motor_a.drive(DriveCommand::Brake).unwrap();
|
||||||
|
control.motor_b.drive(DriveCommand::Brake).unwrap();
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
// slowly turn for 3s
|
||||||
|
info!("turn");
|
||||||
|
control.motor_a.drive(DriveCommand::Backward(50)).unwrap();
|
||||||
|
control.motor_b.drive(DriveCommand::Forward(50)).unwrap();
|
||||||
|
Timer::after(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
// and put the driver in standby mode and wait for 5s
|
||||||
|
info!("standby");
|
||||||
|
control.enable_standby().unwrap();
|
||||||
|
Timer::after(Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user