Merge branch 'main' of https://github.com/embassy-rs/embassy into mac
This commit is contained in:
commit
2cdd593290
41
.gitattributes
vendored
Normal file
41
.gitattributes
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.adoc text
|
||||||
|
*.html text
|
||||||
|
*.in text
|
||||||
|
*.json text
|
||||||
|
*.md text
|
||||||
|
*.proto text
|
||||||
|
*.py text
|
||||||
|
*.rs text
|
||||||
|
*.service text
|
||||||
|
*.sh text
|
||||||
|
*.toml text
|
||||||
|
*.txt text
|
||||||
|
*.x text
|
||||||
|
*.yml text
|
||||||
|
|
||||||
|
*.raw binary
|
||||||
|
*.bin binary
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.mov binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.flv binary
|
||||||
|
*.fla binary
|
||||||
|
*.swf binary
|
||||||
|
*.gz binary
|
||||||
|
*.zip binary
|
||||||
|
*.7z binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.woff binary
|
||||||
|
*.pyc binary
|
||||||
|
*.pdf binary
|
||||||
|
*.ez binary
|
||||||
|
*.bz2 binary
|
||||||
|
*.swp binary
|
17
.github/ci/crlf.sh
vendored
Executable file
17
.github/ci/crlf.sh
vendored
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
## on push branch~=gh-readonly-queue/main/.*
|
||||||
|
## on pull_request
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
FILES_WITH_CRLF=$(find ! -path "./.git/*" -not -type d | xargs file -N | (grep " CRLF " || true))
|
||||||
|
|
||||||
|
if [ -z "$FILES_WITH_CRLF" ]; then
|
||||||
|
echo -e "No files with CRLF endings found."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
NR_FILES=$(echo "$FILES_WITH_CRLF" | wc -l)
|
||||||
|
echo -e "ERROR: Found ${NR_FILES} files with CRLF endings."
|
||||||
|
echo "$FILES_WITH_CRLF"
|
||||||
|
exit "$NR_FILES"
|
||||||
|
fi
|
1
.github/ci/doc.sh
vendored
1
.github/ci/doc.sh
vendored
@ -37,6 +37,7 @@ docserver-builder -i ./embassy-usb-logger -o webroot/crates/embassy-usb-logger/g
|
|||||||
docserver-builder -i ./cyw43 -o webroot/crates/cyw43/git.zup
|
docserver-builder -i ./cyw43 -o webroot/crates/cyw43/git.zup
|
||||||
docserver-builder -i ./cyw43-pio -o webroot/crates/cyw43-pio/git.zup
|
docserver-builder -i ./cyw43-pio -o webroot/crates/cyw43-pio/git.zup
|
||||||
docserver-builder -i ./embassy-net-w5500 -o webroot/crates/embassy-net-w5500/git.zup
|
docserver-builder -i ./embassy-net-w5500 -o webroot/crates/embassy-net-w5500/git.zup
|
||||||
|
docserver-builder -i ./embassy-net-esp-hosted -o webroot/crates/embassy-net-esp-hosted/git.zup
|
||||||
docserver-builder -i ./embassy-stm32-wpan -o webroot/crates/embassy-stm32-wpan/git.zup --output-static webroot/static
|
docserver-builder -i ./embassy-stm32-wpan -o webroot/crates/embassy-stm32-wpan/git.zup --output-static webroot/static
|
||||||
|
|
||||||
export KUBECONFIG=/ci/secrets/kubeconfig.yml
|
export KUBECONFIG=/ci/secrets/kubeconfig.yml
|
||||||
|
@ -18,3 +18,9 @@ embedded-hal-async = { version = "=0.2.0-alpha.2" }
|
|||||||
noproto = { git="https://github.com/embassy-rs/noproto", default-features = false, features = ["derive"] }
|
noproto = { git="https://github.com/embassy-rs/noproto", default-features = false, features = ["derive"] }
|
||||||
#noproto = { version = "0.1", path = "/home/dirbaio/noproto", default-features = false, features = ["derive"] }
|
#noproto = { version = "0.1", path = "/home/dirbaio/noproto", default-features = false, features = ["derive"] }
|
||||||
heapless = "0.7.16"
|
heapless = "0.7.16"
|
||||||
|
|
||||||
|
[package.metadata.embassy_docs]
|
||||||
|
src_base = "https://github.com/embassy-rs/embassy/blob/embassy-net-esp-hosted-v$VERSION/embassy-net-esp-hosted/src/"
|
||||||
|
src_base_git = "https://github.com/embassy-rs/embassy/blob/$COMMIT/embassy-net-esp-hosted/src/"
|
||||||
|
target = "thumbv7em-none-eabi"
|
||||||
|
features = ["defmt"]
|
@ -490,30 +490,78 @@ impl<D: Driver + 'static> Stack<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "igmp")]
|
#[cfg(feature = "igmp")]
|
||||||
impl<D: Driver + smoltcp::phy::Device + 'static> Stack<D> {
|
impl<D: Driver + 'static> Stack<D> {
|
||||||
/// Join a multicast group.
|
/// Join a multicast group.
|
||||||
pub fn join_multicast_group<T>(&self, addr: T) -> Result<bool, MulticastError>
|
pub async fn join_multicast_group<T>(&self, addr: T) -> Result<bool, MulticastError>
|
||||||
|
where
|
||||||
|
T: Into<IpAddress>,
|
||||||
|
{
|
||||||
|
let addr = addr.into();
|
||||||
|
|
||||||
|
poll_fn(move |cx| self.poll_join_multicast_group(addr, cx)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join a multicast group.
|
||||||
|
///
|
||||||
|
/// When the send queue is full, this method will return `Poll::Pending`
|
||||||
|
/// and register the current task to be notified when the queue has space available.
|
||||||
|
pub fn poll_join_multicast_group<T>(&self, addr: T, cx: &mut Context<'_>) -> Poll<Result<bool, MulticastError>>
|
||||||
where
|
where
|
||||||
T: Into<IpAddress>,
|
T: Into<IpAddress>,
|
||||||
{
|
{
|
||||||
let addr = addr.into();
|
let addr = addr.into();
|
||||||
|
|
||||||
self.with_mut(|s, i| {
|
self.with_mut(|s, i| {
|
||||||
s.iface
|
let mut smoldev = DriverAdapter {
|
||||||
.join_multicast_group(&mut i.device, addr, instant_to_smoltcp(Instant::now()))
|
cx: Some(cx),
|
||||||
|
inner: &mut i.device,
|
||||||
|
};
|
||||||
|
|
||||||
|
match s
|
||||||
|
.iface
|
||||||
|
.join_multicast_group(&mut smoldev, addr, instant_to_smoltcp(Instant::now()))
|
||||||
|
{
|
||||||
|
Ok(announce_sent) => Poll::Ready(Ok(announce_sent)),
|
||||||
|
Err(MulticastError::Exhausted) => Poll::Pending,
|
||||||
|
Err(other) => Poll::Ready(Err(other)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Leave a multicast group.
|
/// Leave a multicast group.
|
||||||
pub fn leave_multicast_group<T>(&self, addr: T) -> Result<bool, MulticastError>
|
pub async fn leave_multicast_group<T>(&self, addr: T) -> Result<bool, MulticastError>
|
||||||
|
where
|
||||||
|
T: Into<IpAddress>,
|
||||||
|
{
|
||||||
|
let addr = addr.into();
|
||||||
|
|
||||||
|
poll_fn(move |cx| self.poll_leave_multicast_group(addr, cx)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Leave a multicast group.
|
||||||
|
///
|
||||||
|
/// When the send queue is full, this method will return `Poll::Pending`
|
||||||
|
/// and register the current task to be notified when the queue has space available.
|
||||||
|
pub fn poll_leave_multicast_group<T>(&self, addr: T, cx: &mut Context<'_>) -> Poll<Result<bool, MulticastError>>
|
||||||
where
|
where
|
||||||
T: Into<IpAddress>,
|
T: Into<IpAddress>,
|
||||||
{
|
{
|
||||||
let addr = addr.into();
|
let addr = addr.into();
|
||||||
|
|
||||||
self.with_mut(|s, i| {
|
self.with_mut(|s, i| {
|
||||||
s.iface
|
let mut smoldev = DriverAdapter {
|
||||||
.leave_multicast_group(&mut i.device, addr, instant_to_smoltcp(Instant::now()))
|
cx: Some(cx),
|
||||||
|
inner: &mut i.device,
|
||||||
|
};
|
||||||
|
|
||||||
|
match s
|
||||||
|
.iface
|
||||||
|
.leave_multicast_group(&mut smoldev, addr, instant_to_smoltcp(Instant::now()))
|
||||||
|
{
|
||||||
|
Ok(leave_sent) => Poll::Ready(Ok(leave_sent)),
|
||||||
|
Err(MulticastError::Exhausted) => Poll::Pending,
|
||||||
|
Err(other) => Poll::Ready(Err(other)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,11 +590,14 @@ impl<D: Driver + 'static> Inner<D> {
|
|||||||
|
|
||||||
debug!(" IP address: {}", config.address);
|
debug!(" IP address: {}", config.address);
|
||||||
s.iface.update_ip_addrs(|addrs| {
|
s.iface.update_ip_addrs(|addrs| {
|
||||||
if addrs.is_empty() {
|
if let Some((index, _)) = addrs
|
||||||
addrs.push(IpCidr::Ipv4(config.address)).unwrap();
|
.iter()
|
||||||
} else {
|
.enumerate()
|
||||||
addrs[0] = IpCidr::Ipv4(config.address);
|
.find(|(_, &addr)| matches!(addr, IpCidr::Ipv4(_)))
|
||||||
|
{
|
||||||
|
addrs.remove(index);
|
||||||
}
|
}
|
||||||
|
addrs.push(IpCidr::Ipv4(config.address)).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(feature = "medium-ethernet")]
|
#[cfg(feature = "medium-ethernet")]
|
||||||
@ -581,11 +632,14 @@ impl<D: Driver + 'static> Inner<D> {
|
|||||||
|
|
||||||
debug!(" IP address: {}", config.address);
|
debug!(" IP address: {}", config.address);
|
||||||
s.iface.update_ip_addrs(|addrs| {
|
s.iface.update_ip_addrs(|addrs| {
|
||||||
if addrs.is_empty() {
|
if let Some((index, _)) = addrs
|
||||||
addrs.push(IpCidr::Ipv6(config.address)).unwrap();
|
.iter()
|
||||||
} else {
|
.enumerate()
|
||||||
addrs[0] = IpCidr::Ipv6(config.address);
|
.find(|(_, &addr)| matches!(addr, IpCidr::Ipv6(_)))
|
||||||
|
{
|
||||||
|
addrs.remove(index);
|
||||||
}
|
}
|
||||||
|
addrs.push(IpCidr::Ipv6(config.address)).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(feature = "medium-ethernet")]
|
#[cfg(feature = "medium-ethernet")]
|
||||||
@ -653,13 +707,21 @@ impl<D: Driver + 'static> Inner<D> {
|
|||||||
socket.set_retry_config(config.retry_config);
|
socket.set_retry_config(config.retry_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)] // used only with dhcp
|
#[cfg(feature = "dhcpv4")]
|
||||||
fn unapply_config(&mut self, s: &mut SocketStack) {
|
fn unapply_config_v4(&mut self, s: &mut SocketStack) {
|
||||||
#[cfg(feature = "medium-ethernet")]
|
#[cfg(feature = "medium-ethernet")]
|
||||||
let medium = self.device.capabilities().medium;
|
let medium = self.device.capabilities().medium;
|
||||||
|
|
||||||
debug!("Lost IP configuration");
|
debug!("Lost IP configuration");
|
||||||
s.iface.update_ip_addrs(|ip_addrs| ip_addrs.clear());
|
s.iface.update_ip_addrs(|ip_addrs| {
|
||||||
|
#[cfg(feature = "proto-ipv4")]
|
||||||
|
if let Some((index, _)) = ip_addrs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, &addr)| matches!(addr, IpCidr::Ipv4(_)))
|
||||||
|
{
|
||||||
|
ip_addrs.remove(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
#[cfg(feature = "medium-ethernet")]
|
#[cfg(feature = "medium-ethernet")]
|
||||||
if medium == Medium::Ethernet {
|
if medium == Medium::Ethernet {
|
||||||
#[cfg(feature = "proto-ipv4")]
|
#[cfg(feature = "proto-ipv4")]
|
||||||
@ -706,7 +768,7 @@ impl<D: Driver + 'static> Inner<D> {
|
|||||||
if self.link_up {
|
if self.link_up {
|
||||||
match socket.poll() {
|
match socket.poll() {
|
||||||
None => {}
|
None => {}
|
||||||
Some(dhcpv4::Event::Deconfigured) => self.unapply_config(s),
|
Some(dhcpv4::Event::Deconfigured) => self.unapply_config_v4(s),
|
||||||
Some(dhcpv4::Event::Configured(config)) => {
|
Some(dhcpv4::Event::Configured(config)) => {
|
||||||
let config = StaticConfigV4 {
|
let config = StaticConfigV4 {
|
||||||
address: config.address,
|
address: config.address,
|
||||||
@ -718,7 +780,7 @@ impl<D: Driver + 'static> Inner<D> {
|
|||||||
}
|
}
|
||||||
} else if old_link_up {
|
} else if old_link_up {
|
||||||
socket.reset();
|
socket.reset();
|
||||||
self.unapply_config(s);
|
self.unapply_config_v4(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//if old_link_up || self.link_up {
|
//if old_link_up || self.link_up {
|
||||||
|
@ -8,12 +8,22 @@ use core::task::Poll;
|
|||||||
|
|
||||||
use embassy_hal_common::drop::OnDrop;
|
use embassy_hal_common::drop::OnDrop;
|
||||||
use embassy_hal_common::{into_ref, PeripheralRef};
|
use embassy_hal_common::{into_ref, PeripheralRef};
|
||||||
|
use fixed::types::I7F1;
|
||||||
use futures::future::poll_fn;
|
use futures::future::poll_fn;
|
||||||
|
|
||||||
use crate::chip::EASY_DMA_SIZE;
|
use crate::chip::EASY_DMA_SIZE;
|
||||||
use crate::gpio::sealed::Pin;
|
use crate::gpio::sealed::Pin;
|
||||||
use crate::gpio::{AnyPin, Pin as GpioPin};
|
use crate::gpio::{AnyPin, Pin as GpioPin};
|
||||||
use crate::interrupt::typelevel::Interrupt;
|
use crate::interrupt::typelevel::Interrupt;
|
||||||
|
use crate::pac::pdm::mode::{EDGE_A, OPERATION_A};
|
||||||
|
pub use crate::pac::pdm::pdmclkctrl::FREQ_A as Frequency;
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "nrf52840",
|
||||||
|
feature = "nrf52833",
|
||||||
|
feature = "_nrf5340-app",
|
||||||
|
feature = "_nrf9160",
|
||||||
|
))]
|
||||||
|
pub use crate::pac::pdm::ratio::RATIO_A as Ratio;
|
||||||
use crate::{interrupt, Peripheral};
|
use crate::{interrupt, Peripheral};
|
||||||
|
|
||||||
/// Interrupt handler.
|
/// Interrupt handler.
|
||||||
@ -23,7 +33,20 @@ pub struct InterruptHandler<T: Instance> {
|
|||||||
|
|
||||||
impl<T: Instance> interrupt::typelevel::Handler<T::Interrupt> for InterruptHandler<T> {
|
impl<T: Instance> interrupt::typelevel::Handler<T::Interrupt> for InterruptHandler<T> {
|
||||||
unsafe fn on_interrupt() {
|
unsafe fn on_interrupt() {
|
||||||
T::regs().intenclr.write(|w| w.end().clear());
|
let r = T::regs();
|
||||||
|
|
||||||
|
if r.events_end.read().bits() != 0 {
|
||||||
|
r.intenclr.write(|w| w.end().clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.events_started.read().bits() != 0 {
|
||||||
|
r.intenclr.write(|w| w.started().clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.events_stopped.read().bits() != 0 {
|
||||||
|
r.intenclr.write(|w| w.stopped().clear());
|
||||||
|
}
|
||||||
|
|
||||||
T::state().waker.wake();
|
T::state().waker.wake();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,10 +67,24 @@ pub enum Error {
|
|||||||
BufferZeroLength,
|
BufferZeroLength,
|
||||||
/// PDM is not running
|
/// PDM is not running
|
||||||
NotRunning,
|
NotRunning,
|
||||||
|
/// PDM is already running
|
||||||
|
AlreadyRunning,
|
||||||
}
|
}
|
||||||
|
|
||||||
static DUMMY_BUFFER: [i16; 1] = [0; 1];
|
static DUMMY_BUFFER: [i16; 1] = [0; 1];
|
||||||
|
|
||||||
|
/// The state of a continuously running sampler. While it reflects
|
||||||
|
/// the progress of a sampler, it also signals what should be done
|
||||||
|
/// next. For example, if the sampler has stopped then the Pdm implementation
|
||||||
|
/// can then tear down its infrastructure.
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum SamplerState {
|
||||||
|
/// The sampler processed the samples and is ready for more.
|
||||||
|
Sampled,
|
||||||
|
/// The sampler is done processing samples.
|
||||||
|
Stopped,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'d, T: Instance> Pdm<'d, T> {
|
impl<'d, T: Instance> Pdm<'d, T> {
|
||||||
/// Create PDM driver
|
/// Create PDM driver
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@ -79,18 +116,24 @@ impl<'d, T: Instance> Pdm<'d, T> {
|
|||||||
r.psel.clk.write(|w| unsafe { w.bits(clk.psel_bits()) });
|
r.psel.clk.write(|w| unsafe { w.bits(clk.psel_bits()) });
|
||||||
|
|
||||||
// configure
|
// configure
|
||||||
// use default for
|
r.pdmclkctrl.write(|w| w.freq().variant(config.frequency));
|
||||||
// - gain right
|
#[cfg(any(
|
||||||
// - gain left
|
feature = "nrf52840",
|
||||||
// - clk
|
feature = "nrf52833",
|
||||||
// - ratio
|
feature = "_nrf5340-app",
|
||||||
|
feature = "_nrf9160",
|
||||||
|
))]
|
||||||
|
r.ratio.write(|w| w.ratio().variant(config.ratio));
|
||||||
r.mode.write(|w| {
|
r.mode.write(|w| {
|
||||||
w.edge().bit(config.edge == Edge::LeftRising);
|
w.operation().variant(config.operation_mode.into());
|
||||||
w.operation().bit(config.operation_mode == OperationMode::Mono);
|
w.edge().variant(config.edge.into());
|
||||||
w
|
w
|
||||||
});
|
});
|
||||||
r.gainl.write(|w| w.gainl().default_gain());
|
|
||||||
r.gainr.write(|w| w.gainr().default_gain());
|
Self::_set_gain(r, config.gain_left, config.gain_right);
|
||||||
|
|
||||||
|
// Disable all events interrupts
|
||||||
|
r.intenclr.write(|w| unsafe { w.bits(0x003F_FFFF) });
|
||||||
|
|
||||||
// IRQ
|
// IRQ
|
||||||
T::Interrupt::unpend();
|
T::Interrupt::unpend();
|
||||||
@ -101,6 +144,25 @@ impl<'d, T: Instance> Pdm<'d, T> {
|
|||||||
Self { _peri: pdm }
|
Self { _peri: pdm }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _set_gain(r: &crate::pac::pdm::RegisterBlock, gain_left: I7F1, gain_right: I7F1) {
|
||||||
|
let gain_left = gain_left
|
||||||
|
.saturating_add(I7F1::from_bits(40))
|
||||||
|
.saturating_to_num::<u8>()
|
||||||
|
.clamp(0, 0x50);
|
||||||
|
let gain_right = gain_right
|
||||||
|
.saturating_add(I7F1::from_bits(40))
|
||||||
|
.saturating_to_num::<u8>()
|
||||||
|
.clamp(0, 0x50);
|
||||||
|
|
||||||
|
r.gainl.write(|w| unsafe { w.gainl().bits(gain_left) });
|
||||||
|
r.gainr.write(|w| unsafe { w.gainr().bits(gain_right) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adjust the gain of the PDM microphone on the fly
|
||||||
|
pub fn set_gain(&mut self, gain_left: I7F1, gain_right: I7F1) {
|
||||||
|
Self::_set_gain(T::regs(), gain_left, gain_right)
|
||||||
|
}
|
||||||
|
|
||||||
/// Start sampling microphon data into a dummy buffer
|
/// Start sampling microphon data into a dummy buffer
|
||||||
/// Usefull to start the microphon and keep it active between recording samples
|
/// Usefull to start the microphon and keep it active between recording samples
|
||||||
pub async fn start(&mut self) {
|
pub async fn start(&mut self) {
|
||||||
@ -198,6 +260,108 @@ impl<'d, T: Instance> Pdm<'d, T> {
|
|||||||
|
|
||||||
compiler_fence(Ordering::SeqCst);
|
compiler_fence(Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Continuous sampling with double buffers.
|
||||||
|
///
|
||||||
|
/// A sampler closure is provided that receives the buffer of samples, noting
|
||||||
|
/// that the size of this buffer can be less than the original buffer's size.
|
||||||
|
/// A command is return from the closure that indicates whether the sampling
|
||||||
|
/// should continue or stop.
|
||||||
|
///
|
||||||
|
/// NOTE: The time spent within the callback supplied should not exceed the time
|
||||||
|
/// taken to acquire the samples into a single buffer. You should measure the
|
||||||
|
/// time taken by the callback and set the sample buffer size accordingly.
|
||||||
|
/// Exceeding this time can lead to samples becoming dropped.
|
||||||
|
pub async fn run_task_sampler<S, const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
bufs: &mut [[i16; N]; 2],
|
||||||
|
mut sampler: S,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
S: FnMut(&[i16; N]) -> SamplerState,
|
||||||
|
{
|
||||||
|
let r = T::regs();
|
||||||
|
|
||||||
|
if r.events_started.read().bits() != 0 {
|
||||||
|
return Err(Error::AlreadyRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.sample
|
||||||
|
.ptr
|
||||||
|
.write(|w| unsafe { w.sampleptr().bits(bufs[0].as_mut_ptr() as u32) });
|
||||||
|
r.sample.maxcnt.write(|w| unsafe { w.buffsize().bits(N as _) });
|
||||||
|
|
||||||
|
// Reset and enable the events
|
||||||
|
r.events_end.reset();
|
||||||
|
r.events_started.reset();
|
||||||
|
r.events_stopped.reset();
|
||||||
|
r.intenset.write(|w| {
|
||||||
|
w.end().set();
|
||||||
|
w.started().set();
|
||||||
|
w.stopped().set();
|
||||||
|
w
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't reorder the start event before the previous writes. Hopefully self
|
||||||
|
// wouldn't happen anyway.
|
||||||
|
compiler_fence(Ordering::SeqCst);
|
||||||
|
|
||||||
|
r.tasks_start.write(|w| unsafe { w.bits(1) });
|
||||||
|
|
||||||
|
let mut current_buffer = 0;
|
||||||
|
|
||||||
|
let mut done = false;
|
||||||
|
|
||||||
|
let drop = OnDrop::new(|| {
|
||||||
|
r.tasks_stop.write(|w| unsafe { w.bits(1) });
|
||||||
|
// N.B. It would be better if this were async, but Drop only support sync code.
|
||||||
|
while r.events_stopped.read().bits() != 0 {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for events and complete when the sampler indicates it has had enough.
|
||||||
|
poll_fn(|cx| {
|
||||||
|
let r = T::regs();
|
||||||
|
|
||||||
|
T::state().waker.register(cx.waker());
|
||||||
|
|
||||||
|
if r.events_end.read().bits() != 0 {
|
||||||
|
compiler_fence(Ordering::SeqCst);
|
||||||
|
|
||||||
|
r.events_end.reset();
|
||||||
|
r.intenset.write(|w| w.end().set());
|
||||||
|
|
||||||
|
if !done {
|
||||||
|
// Discard the last buffer after the user requested a stop.
|
||||||
|
if sampler(&bufs[current_buffer]) == SamplerState::Sampled {
|
||||||
|
let next_buffer = 1 - current_buffer;
|
||||||
|
current_buffer = next_buffer;
|
||||||
|
} else {
|
||||||
|
r.tasks_stop.write(|w| unsafe { w.bits(1) });
|
||||||
|
done = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.events_started.read().bits() != 0 {
|
||||||
|
r.events_started.reset();
|
||||||
|
r.intenset.write(|w| w.started().set());
|
||||||
|
|
||||||
|
let next_buffer = 1 - current_buffer;
|
||||||
|
r.sample
|
||||||
|
.ptr
|
||||||
|
.write(|w| unsafe { w.sampleptr().bits(bufs[next_buffer].as_mut_ptr() as u32) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.events_stopped.read().bits() != 0 {
|
||||||
|
return Poll::Ready(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
drop.defuse();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PDM microphone driver Config
|
/// PDM microphone driver Config
|
||||||
@ -206,6 +370,20 @@ pub struct Config {
|
|||||||
pub operation_mode: OperationMode,
|
pub operation_mode: OperationMode,
|
||||||
/// On which edge the left channel should be samples
|
/// On which edge the left channel should be samples
|
||||||
pub edge: Edge,
|
pub edge: Edge,
|
||||||
|
/// Clock frequency
|
||||||
|
pub frequency: Frequency,
|
||||||
|
/// Clock ratio
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "nrf52840",
|
||||||
|
feature = "nrf52833",
|
||||||
|
feature = "_nrf5340-app",
|
||||||
|
feature = "_nrf9160",
|
||||||
|
))]
|
||||||
|
pub ratio: Ratio,
|
||||||
|
/// Gain left in dB
|
||||||
|
pub gain_left: I7F1,
|
||||||
|
/// Gain right in dB
|
||||||
|
pub gain_right: I7F1,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@ -213,6 +391,16 @@ impl Default for Config {
|
|||||||
Self {
|
Self {
|
||||||
operation_mode: OperationMode::Mono,
|
operation_mode: OperationMode::Mono,
|
||||||
edge: Edge::LeftFalling,
|
edge: Edge::LeftFalling,
|
||||||
|
frequency: Frequency::DEFAULT,
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "nrf52840",
|
||||||
|
feature = "nrf52833",
|
||||||
|
feature = "_nrf5340-app",
|
||||||
|
feature = "_nrf9160",
|
||||||
|
))]
|
||||||
|
ratio: Ratio::RATIO80,
|
||||||
|
gain_left: I7F1::ZERO,
|
||||||
|
gain_right: I7F1::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,6 +414,15 @@ pub enum OperationMode {
|
|||||||
Stereo,
|
Stereo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<OperationMode> for OPERATION_A {
|
||||||
|
fn from(mode: OperationMode) -> Self {
|
||||||
|
match mode {
|
||||||
|
OperationMode::Mono => OPERATION_A::MONO,
|
||||||
|
OperationMode::Stereo => OPERATION_A::STEREO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// PDM edge polarity
|
/// PDM edge polarity
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum Edge {
|
pub enum Edge {
|
||||||
@ -235,6 +432,15 @@ pub enum Edge {
|
|||||||
LeftFalling,
|
LeftFalling,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Edge> for EDGE_A {
|
||||||
|
fn from(edge: Edge) -> Self {
|
||||||
|
match edge {
|
||||||
|
Edge::LeftRising => EDGE_A::LEFT_RISING,
|
||||||
|
Edge::LeftFalling => EDGE_A::LEFT_FALLING,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'d, T: Instance> Drop for Pdm<'d, T> {
|
impl<'d, T: Instance> Drop for Pdm<'d, T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let r = T::regs();
|
let r = T::regs();
|
||||||
|
@ -81,6 +81,16 @@ pub struct Adc<'d, M: Mode> {
|
|||||||
phantom: PhantomData<(&'d ADC, M)>,
|
phantom: PhantomData<(&'d ADC, M)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'d, M: Mode> Drop for Adc<'d, M> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let r = Self::regs();
|
||||||
|
// disable ADC. leaving it enabled comes with a ~150µA static
|
||||||
|
// current draw. the temperature sensor has already been disabled
|
||||||
|
// by the temperature-reading methods, so we don't need to touch that.
|
||||||
|
r.cs().write(|w| w.set_en(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'d, M: Mode> Adc<'d, M> {
|
impl<'d, M: Mode> Adc<'d, M> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn regs() -> pac::adc::Adc {
|
fn regs() -> pac::adc::Adc {
|
||||||
|
@ -716,6 +716,9 @@ mod nightly {
|
|||||||
async fn transaction(&mut self, address: A, operations: &mut [Operation<'_>]) -> Result<(), Self::Error> {
|
async fn transaction(&mut self, address: A, operations: &mut [Operation<'_>]) -> Result<(), Self::Error> {
|
||||||
let addr: u16 = address.into();
|
let addr: u16 = address.into();
|
||||||
|
|
||||||
|
if operations.len() > 0 {
|
||||||
|
Self::setup(addr)?;
|
||||||
|
}
|
||||||
let mut iterator = operations.iter_mut();
|
let mut iterator = operations.iter_mut();
|
||||||
|
|
||||||
while let Some(op) = iterator.next() {
|
while let Some(op) = iterator.next() {
|
||||||
@ -723,11 +726,9 @@ mod nightly {
|
|||||||
|
|
||||||
match op {
|
match op {
|
||||||
Operation::Read(buffer) => {
|
Operation::Read(buffer) => {
|
||||||
Self::setup(addr)?;
|
|
||||||
self.read_async_internal(buffer, false, last).await?;
|
self.read_async_internal(buffer, false, last).await?;
|
||||||
}
|
}
|
||||||
Operation::Write(buffer) => {
|
Operation::Write(buffer) => {
|
||||||
Self::setup(addr)?;
|
|
||||||
self.write_async_internal(buffer.into_iter().cloned(), last).await?;
|
self.write_async_internal(buffer.into_iter().cloned(), last).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,19 +45,18 @@ use self::phy_consts::*;
|
|||||||
pub struct GenericSMI {
|
pub struct GenericSMI {
|
||||||
#[cfg(feature = "time")]
|
#[cfg(feature = "time")]
|
||||||
poll_interval: Duration,
|
poll_interval: Duration,
|
||||||
|
#[cfg(not(feature = "time"))]
|
||||||
|
_private: (),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GenericSMI {
|
impl GenericSMI {
|
||||||
#[cfg(feature = "time")]
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
#[cfg(feature = "time")]
|
||||||
poll_interval: Duration::from_millis(500),
|
poll_interval: Duration::from_millis(500),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "time"))]
|
#[cfg(not(feature = "time"))]
|
||||||
pub fn new() -> Self {
|
_private: (),
|
||||||
Self {}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +101,7 @@ unsafe impl PHY for GenericSMI {
|
|||||||
|
|
||||||
/// Public functions for the PHY
|
/// Public functions for the PHY
|
||||||
impl GenericSMI {
|
impl GenericSMI {
|
||||||
|
#[cfg(feature = "time")]
|
||||||
pub fn set_poll_interval(&mut self, poll_interval: Duration) {
|
pub fn set_poll_interval(&mut self, poll_interval: Duration) {
|
||||||
self.poll_interval = poll_interval
|
self.poll_interval = poll_interval
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,10 @@ pub struct Config {
|
|||||||
/// but will effectively disable noise detection.
|
/// but will effectively disable noise detection.
|
||||||
#[cfg(not(usart_v1))]
|
#[cfg(not(usart_v1))]
|
||||||
pub assume_noise_free: bool,
|
pub assume_noise_free: bool,
|
||||||
|
|
||||||
|
/// Set this to true to swap the RX and TX pins.
|
||||||
|
#[cfg(any(usart_v3, usart_v4))]
|
||||||
|
pub swap_rx_tx: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@ -129,6 +133,8 @@ impl Default for Config {
|
|||||||
detect_previous_overrun: false,
|
detect_previous_overrun: false,
|
||||||
#[cfg(not(usart_v1))]
|
#[cfg(not(usart_v1))]
|
||||||
assume_noise_free: false,
|
assume_noise_free: false,
|
||||||
|
#[cfg(any(usart_v3, usart_v4))]
|
||||||
|
swap_rx_tx: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -688,8 +694,22 @@ impl<'d, T: BasicInstance, TxDma, RxDma> Uart<'d, T, TxDma, RxDma> {
|
|||||||
|
|
||||||
let r = T::regs();
|
let r = T::regs();
|
||||||
|
|
||||||
|
// Some chips do not have swap_rx_tx bit
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(any(usart_v3, usart_v4))] {
|
||||||
|
if config.swap_rx_tx {
|
||||||
|
let (rx, tx) = (tx, rx);
|
||||||
rx.set_as_af(rx.af_num(), AFType::Input);
|
rx.set_as_af(rx.af_num(), AFType::Input);
|
||||||
tx.set_as_af(tx.af_num(), AFType::OutputPushPull);
|
tx.set_as_af(tx.af_num(), AFType::OutputPushPull);
|
||||||
|
} else {
|
||||||
|
rx.set_as_af(rx.af_num(), AFType::Input);
|
||||||
|
tx.set_as_af(tx.af_num(), AFType::OutputPushPull);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rx.set_as_af(rx.af_num(), AFType::Input);
|
||||||
|
tx.set_as_af(tx.af_num(), AFType::OutputPushPull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configure(r, &config, T::frequency(), T::KIND, true, true);
|
configure(r, &config, T::frequency(), T::KIND, true, true);
|
||||||
|
|
||||||
@ -847,6 +867,9 @@ fn configure(r: Regs, config: &Config, pclk_freq: Hertz, kind: Kind, enable_rx:
|
|||||||
StopBits::STOP1P5 => vals::Stop::STOP1P5,
|
StopBits::STOP1P5 => vals::Stop::STOP1P5,
|
||||||
StopBits::STOP2 => vals::Stop::STOP2,
|
StopBits::STOP2 => vals::Stop::STOP2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(any(usart_v3, usart_v4))]
|
||||||
|
w.set_swap(config.swap_rx_tx);
|
||||||
});
|
});
|
||||||
r.cr1().write(|w| {
|
r.cr1().write(|w| {
|
||||||
// enable uart
|
// enable uart
|
||||||
|
@ -43,6 +43,7 @@ embassy-net-esp-hosted = { version = "0.1.0", path = "../../embassy-net-esp-host
|
|||||||
defmt = "0.3"
|
defmt = "0.3"
|
||||||
defmt-rtt = "0.4"
|
defmt-rtt = "0.4"
|
||||||
|
|
||||||
|
fixed = "1.10.0"
|
||||||
static_cell = "1.1"
|
static_cell = "1.1"
|
||||||
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
|
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
|
||||||
cortex-m-rt = "0.7.0"
|
cortex-m-rt = "0.7.0"
|
||||||
@ -53,6 +54,8 @@ embedded-storage = "0.3.0"
|
|||||||
usbd-hid = "0.6.0"
|
usbd-hid = "0.6.0"
|
||||||
serde = { version = "1.0.136", default-features = false }
|
serde = { version = "1.0.136", default-features = false }
|
||||||
embedded-hal-async = { version = "0.2.0-alpha.2", optional = true }
|
embedded-hal-async = { version = "0.2.0-alpha.2", optional = true }
|
||||||
|
num-integer = { version = "0.1.45", default-features = false }
|
||||||
|
microfft = "0.5.0"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
lora-phy = { git = "https://github.com/embassy-rs/lora-phy", rev = "ad289428fd44b02788e2fa2116445cc8f640a265" }
|
lora-phy = { git = "https://github.com/embassy-rs/lora-phy", rev = "ad289428fd44b02788e2fa2116445cc8f640a265" }
|
||||||
|
@ -7,6 +7,8 @@ use embassy_executor::Spawner;
|
|||||||
use embassy_nrf::pdm::{self, Config, Pdm};
|
use embassy_nrf::pdm::{self, Config, Pdm};
|
||||||
use embassy_nrf::{bind_interrupts, peripherals};
|
use embassy_nrf::{bind_interrupts, peripherals};
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
|
use fixed::types::I7F1;
|
||||||
|
use num_integer::Roots;
|
||||||
use {defmt_rtt as _, panic_probe as _};
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
bind_interrupts!(struct Irqs {
|
bind_interrupts!(struct Irqs {
|
||||||
@ -20,6 +22,9 @@ async fn main(_p: Spawner) {
|
|||||||
let mut pdm = Pdm::new(p.PDM, Irqs, p.P0_01, p.P0_00, config);
|
let mut pdm = Pdm::new(p.PDM, Irqs, p.P0_01, p.P0_00, config);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
for gain in [I7F1::from_num(-20), I7F1::from_num(0), I7F1::from_num(20)] {
|
||||||
|
pdm.set_gain(gain, gain);
|
||||||
|
info!("Gain = {} dB", defmt::Debug2Format(&gain));
|
||||||
pdm.start().await;
|
pdm.start().await;
|
||||||
|
|
||||||
// wait some time till the microphon settled
|
// wait some time till the microphon settled
|
||||||
@ -29,9 +34,24 @@ async fn main(_p: Spawner) {
|
|||||||
let mut buf = [0i16; SAMPLES];
|
let mut buf = [0i16; SAMPLES];
|
||||||
pdm.sample(&mut buf).await.unwrap();
|
pdm.sample(&mut buf).await.unwrap();
|
||||||
|
|
||||||
|
let mean = (buf.iter().map(|v| i32::from(*v)).sum::<i32>() / buf.len() as i32) as i16;
|
||||||
|
info!(
|
||||||
|
"{} samples, min {=i16}, max {=i16}, mean {=i16}, AC RMS {=i16}",
|
||||||
|
buf.len(),
|
||||||
|
buf.iter().min().unwrap(),
|
||||||
|
buf.iter().max().unwrap(),
|
||||||
|
mean,
|
||||||
|
(buf.iter()
|
||||||
|
.map(|v| i32::from(*v - mean).pow(2))
|
||||||
|
.fold(0i32, |a, b| a.saturating_add(b))
|
||||||
|
/ buf.len() as i32)
|
||||||
|
.sqrt() as i16,
|
||||||
|
);
|
||||||
|
|
||||||
info!("samples: {:?}", &buf);
|
info!("samples: {:?}", &buf);
|
||||||
|
|
||||||
pdm.stop().await;
|
pdm.stop().await;
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
81
examples/nrf52840/src/bin/pdm_continuous.rs
Normal file
81
examples/nrf52840/src/bin/pdm_continuous.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
#![feature(type_alias_impl_trait)]
|
||||||
|
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
|
||||||
|
use defmt::info;
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_nrf::pdm::{self, Config, Frequency, OperationMode, Pdm, Ratio, SamplerState};
|
||||||
|
use embassy_nrf::{bind_interrupts, peripherals};
|
||||||
|
use fixed::types::I7F1;
|
||||||
|
use microfft::real::rfft_1024;
|
||||||
|
use num_integer::Roots;
|
||||||
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
|
// Demonstrates both continuous sampling and scanning multiple channels driven by a PPI linked timer
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
PDM => pdm::InterruptHandler<peripherals::PDM>;
|
||||||
|
});
|
||||||
|
|
||||||
|
#[embassy_executor::main]
|
||||||
|
async fn main(_p: Spawner) {
|
||||||
|
let mut p = embassy_nrf::init(Default::default());
|
||||||
|
let mut config = Config::default();
|
||||||
|
// Pins are correct for the onboard microphone on the Feather nRF52840 Sense.
|
||||||
|
config.frequency = Frequency::_1280K; // 16 kHz sample rate
|
||||||
|
config.ratio = Ratio::RATIO80;
|
||||||
|
config.operation_mode = OperationMode::Mono;
|
||||||
|
config.gain_left = I7F1::from_bits(5); // 2.5 dB
|
||||||
|
let mut pdm = Pdm::new(p.PDM, Irqs, &mut p.P0_00, &mut p.P0_01, config);
|
||||||
|
|
||||||
|
let mut bufs = [[0; 1024]; 2];
|
||||||
|
|
||||||
|
pdm.run_task_sampler(&mut bufs, move |buf| {
|
||||||
|
// NOTE: It is important that the time spent within this callback
|
||||||
|
// does not exceed the time taken to acquire the 1500 samples we
|
||||||
|
// have in this example, which would be 10us + 2us per
|
||||||
|
// sample * 1500 = 18ms. You need to measure the time taken here
|
||||||
|
// and set the sample buffer size accordingly. Exceeding this
|
||||||
|
// time can lead to the peripheral re-writing the other buffer.
|
||||||
|
let mean = (buf.iter().map(|v| i32::from(*v)).sum::<i32>() / buf.len() as i32) as i16;
|
||||||
|
let (peak_freq_index, peak_mag) = fft_peak_freq(&buf);
|
||||||
|
let peak_freq = peak_freq_index * 16000 / buf.len();
|
||||||
|
info!(
|
||||||
|
"{} samples, min {=i16}, max {=i16}, mean {=i16}, AC RMS {=i16}, peak {} @ {} Hz",
|
||||||
|
buf.len(),
|
||||||
|
buf.iter().min().unwrap(),
|
||||||
|
buf.iter().max().unwrap(),
|
||||||
|
mean,
|
||||||
|
(buf.iter()
|
||||||
|
.map(|v| i32::from(*v - mean).pow(2))
|
||||||
|
.fold(0i32, |a, b| a.saturating_add(b))
|
||||||
|
/ buf.len() as i32)
|
||||||
|
.sqrt() as i16,
|
||||||
|
peak_mag,
|
||||||
|
peak_freq,
|
||||||
|
);
|
||||||
|
SamplerState::Sampled
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fft_peak_freq(input: &[i16; 1024]) -> (usize, u32) {
|
||||||
|
let mut f = [0f32; 1024];
|
||||||
|
for i in 0..input.len() {
|
||||||
|
f[i] = (input[i] as f32) / 32768.0;
|
||||||
|
}
|
||||||
|
// N.B. rfft_1024 does the FFT in-place so result is actually also a reference to f.
|
||||||
|
let result = rfft_1024(&mut f);
|
||||||
|
result[0].im = 0.0;
|
||||||
|
|
||||||
|
result
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.norm_sqr())
|
||||||
|
.enumerate()
|
||||||
|
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Equal))
|
||||||
|
.map(|(i, v)| (i, ((v * 32768.0) as u32).sqrt()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user