Compare commits
11
Commits
4e3f1f52ca
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e87561b37
|
||
|
|
34df570dda
|
||
|
|
bdd946ad11
|
||
|
|
31edf91933
|
||
|
|
ab09c98084
|
||
|
|
5e8d6e2a34
|
||
|
|
e1d7be5e76 | ||
|
|
74e37192fa
|
||
|
|
c8d3d0409a
|
||
|
|
4c2384fba5
|
||
|
|
69511f4061
|
@@ -0,0 +1,66 @@
|
||||
name: Build and Release Firmware
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
TARGET: thumbv8m.main-none-eabihf
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo-binutils
|
||||
run: cargo install cargo-binutils
|
||||
- name: Build Firmware Binaries
|
||||
run: |
|
||||
cd firmware
|
||||
|
||||
# Array of "variant_name:features"
|
||||
VARIANTS=(
|
||||
"cs4398:cs4398,hid"
|
||||
"ak4490:ak4490,hid"
|
||||
"evk:evk,hid"
|
||||
)
|
||||
|
||||
mkdir -p ../dist
|
||||
|
||||
for entry in "${VARIANTS[@]}"; do
|
||||
IFS=":" read -r name features <<< "$entry"
|
||||
echo "Building variant: $name with features: $features"
|
||||
|
||||
# 1. Compile release binary
|
||||
cargo build --release --target ${{ env.TARGET }} --no-default-features --features "$features"
|
||||
|
||||
# 2. Paths setup
|
||||
ELF_PATH="../target/${{ env.TARGET }}/release/guac"
|
||||
HEX_PATH="../target/${{ env.TARGET }}/release/guac-${name}.hex"
|
||||
RENAMED_ELF="../target/${{ env.TARGET }}/release/guac-${name}.elf"
|
||||
|
||||
# 3. Create .hex file via rust-objcopy
|
||||
cargo objcopy --release --target ${{ env.TARGET }} --no-default-features --features "$features" -- -O ihex "$HEX_PATH"
|
||||
cp "$ELF_PATH" "$RENAMED_ELF"
|
||||
|
||||
# 4. Package ELF and HEX into target-specific ZIP
|
||||
ZIP_NAME="guac-firmware-${name}.zip"
|
||||
zip -j "../dist/${ZIP_NAME}" "$RENAMED_ELF" "$HEX_PATH"
|
||||
done
|
||||
|
||||
- name: Create Gitea Release
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
files: dist/*.zip
|
||||
draft: false
|
||||
prerelease: false
|
||||
@@ -1,9 +1,15 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-check
|
||||
name: Cargo check
|
||||
entry: cargo check
|
||||
- id: cargo-check-host
|
||||
name: Cargo check (host)
|
||||
entry: cargo check -p cli -p shared
|
||||
pass_filenames: false
|
||||
types: [file, rust]
|
||||
language: system
|
||||
- id: cargo-check-firmware
|
||||
name: Cargo check (firmware)
|
||||
entry: cargo check -p guac --target thumbv8m.main-none-eabihf
|
||||
pass_filenames: false
|
||||
types: [file, rust]
|
||||
language: system
|
||||
|
||||
Generated
+895
-43
File diff suppressed because it is too large
Load Diff
+8
-31
@@ -1,35 +1,12 @@
|
||||
[package]
|
||||
name = "guac"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
[workspace]
|
||||
members = [
|
||||
"firmware",
|
||||
"cli",
|
||||
"shared"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
default = ["nodac", "hid"]
|
||||
ak4490 = []
|
||||
cs4398 = []
|
||||
nodac = []
|
||||
hid = [ "dep:usbd-hid"]
|
||||
|
||||
[dependencies]
|
||||
atomic = "0.6.1"
|
||||
bbqueue = "0.7.0"
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
|
||||
cortex-m-rt = "0.7.5"
|
||||
defmt = "1.0.1"
|
||||
defmt-rtt = "1.1.0"
|
||||
embedded-hal = "0.2.7"
|
||||
embedded-io = "0.7.1"
|
||||
log-to-defmt = "0.1.0"
|
||||
# Includes update to usb-device 0.3, fix for isochronous and smaller critical sections
|
||||
lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" }
|
||||
nb = "1.1.0"
|
||||
panic-halt = "1.0.0"
|
||||
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
|
||||
static_cell = "2.1.1"
|
||||
usb-device = { version = "0.3", features = ["control-buffer-256"] }
|
||||
usbd-hid = { version = "0.10.0", optional = true }
|
||||
usbd-uac2 = { version = "0.1.0", features = ["defmt"]}
|
||||
default-members = ["firmware"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-hid = "0.5.1"
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
colog = "1.4.0"
|
||||
csv = "1.4.0"
|
||||
deku = "0.20.3"
|
||||
futures-lite = "2.6.1"
|
||||
log = { version = "0.4.29", features = ["std"] }
|
||||
pollster = { version = "0.4.0", features = ["macro"] }
|
||||
shared = { path = "../shared", features = ["serde"] }
|
||||
@@ -0,0 +1,89 @@
|
||||
use std::io;
|
||||
|
||||
use async_hid::{AsyncHidRead, HidBackend, HidResult};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use deku::DekuContainerRead;
|
||||
use futures_lite::StreamExt;
|
||||
use shared::{AudioTelemetrySnapshot, hid::AudioTelemetryReport};
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
enum Format {
|
||||
Debug,
|
||||
Csv,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
#[arg(short, long, default_value = "debug")]
|
||||
format: Format,
|
||||
}
|
||||
|
||||
trait StateEmitter<W: io::Write> {
|
||||
fn from_writer(writer: W) -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
fn emit(&mut self, r: &AudioTelemetrySnapshot);
|
||||
}
|
||||
|
||||
struct DebugEmitter<T: io::Write> {
|
||||
writer: T,
|
||||
}
|
||||
|
||||
impl<W: io::Write> StateEmitter<W> for DebugEmitter<W> {
|
||||
fn from_writer(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
fn emit(&mut self, r: &AudioTelemetrySnapshot) {
|
||||
writeln!(self.writer, "{r:?}");
|
||||
}
|
||||
}
|
||||
|
||||
struct CsvEmitter<W: io::Write> {
|
||||
csv: csv::Writer<W>,
|
||||
}
|
||||
|
||||
impl<W: io::Write> StateEmitter<W> for CsvEmitter<W> {
|
||||
fn from_writer(writer: W) -> Self {
|
||||
Self {
|
||||
csv: csv::Writer::from_writer(writer),
|
||||
}
|
||||
}
|
||||
fn emit(&mut self, r: &AudioTelemetrySnapshot) {
|
||||
if let Err(e) = self.csv.serialize(r) {
|
||||
eprintln!("Serialization error: {e:?}");
|
||||
} else {
|
||||
self.csv.flush().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pollster::main]
|
||||
async fn main() -> HidResult<()> {
|
||||
colog::init();
|
||||
let args = Args::parse();
|
||||
let usbhid = HidBackend::default();
|
||||
let dev = usbhid
|
||||
.enumerate()
|
||||
.await?
|
||||
.find(|d| d.product_id == 0xcc1d && d.vendor_id == 0x1209)
|
||||
.await
|
||||
.expect("GUAC device not found or not accessible (try as root?)");
|
||||
let mut reader = dev.open_readable().await?;
|
||||
let mut writer: Box<dyn StateEmitter<_>> = match args.format {
|
||||
Format::Debug => Box::new(DebugEmitter::from_writer(io::stdout())),
|
||||
Format::Csv => Box::new(CsvEmitter::from_writer(io::stdout())),
|
||||
};
|
||||
|
||||
let mut buf = [0u8; core::mem::size_of::<AudioTelemetryReport>()];
|
||||
while let Ok(r) = reader.read_input_report(&mut buf).await {
|
||||
log::debug!("read {}: {:?}", r, &buf[..r]);
|
||||
let buf = &buf[..r];
|
||||
match AudioTelemetryReport::from_bytes((buf, 0)) {
|
||||
Ok((_, r)) => writer.emit(&r.into()),
|
||||
Err(e) => eprintln!("Unable to parse report: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Generated
+1168
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "guac"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["nodac", "hid"]
|
||||
ak4490 = []
|
||||
cs4398 = []
|
||||
nodac = []
|
||||
wm8904 = []
|
||||
hid = [ "dep:usbd-hid" ]
|
||||
evk = [ "wm8904" ]
|
||||
|
||||
[dependencies]
|
||||
shared = { path="../shared" }
|
||||
atomic = "0.6.1"
|
||||
bbqueue = "0.7.0"
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
|
||||
cortex-m-rt = "0.7.5"
|
||||
defmt = "1.0.1"
|
||||
defmt-rtt = "1.1.0"
|
||||
embedded-hal = "0.2.7"
|
||||
embedded-io = "0.7.1"
|
||||
log-to-defmt = "0.1.0"
|
||||
# Includes update to usb-device 0.3, fix for isochronous and smaller critical sections
|
||||
lpc55-hal = { git = "https://github.com/ktims/lpc55-hal", branch = "main" }
|
||||
nb = "1.1.0"
|
||||
panic-halt = "1.0.0"
|
||||
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
|
||||
static_cell = "2.1.1"
|
||||
usb-device = { version = "0.3", features = ["control-buffer-256"] }
|
||||
usbd-hid = { version = "0.10.0", optional = true }
|
||||
usbd-uac2 = { version = "0.1.2", features = ["defmt"]}
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
debug = true
|
||||
codegen-units = 1
|
||||
@@ -0,0 +1,5 @@
|
||||
// Find the actual path of memory.x and add it to link search, required for building in workspace
|
||||
fn main() {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
println!("cargo:rustc-link-search={}", manifest_dir);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ enum RegisterAddress {
|
||||
pub struct Ak4490Dac<T> {
|
||||
i2c: T,
|
||||
pins: CodecPins, // this dependency is unfortunate, but non trivial to generalize
|
||||
volume: (u8, u8),
|
||||
}
|
||||
|
||||
impl<T> Ak4490Dac<T>
|
||||
@@ -43,6 +44,10 @@ where
|
||||
_ => 5,
|
||||
}
|
||||
}
|
||||
fn set_volume_impl(&mut self, left: u8, right: u8) {
|
||||
self.write_reg(RegisterAddress::LeftAtt, left);
|
||||
self.write_reg(RegisterAddress::RightAtt, right);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dac<T> for Ak4490Dac<T>
|
||||
@@ -50,7 +55,11 @@ where
|
||||
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
|
||||
{
|
||||
fn new(i2c: T, pins: CodecPins) -> Self {
|
||||
Self { i2c, pins }
|
||||
Self {
|
||||
i2c,
|
||||
pins,
|
||||
volume: (0xff, 0xff),
|
||||
}
|
||||
}
|
||||
fn init(&mut self) {
|
||||
// bring out of reset
|
||||
@@ -66,7 +75,13 @@ where
|
||||
self.write_reg(RegisterAddress::Control4, (dfs & 0x4) >> 1);
|
||||
}
|
||||
fn set_volume(&mut self, left: u8, right: u8) {
|
||||
self.write_reg(RegisterAddress::LeftAtt, left);
|
||||
self.write_reg(RegisterAddress::RightAtt, right);
|
||||
self.set_volume_impl(left, right);
|
||||
self.volume = (left, right);
|
||||
}
|
||||
fn mute(&mut self) {
|
||||
self.set_volume_impl(0, 0);
|
||||
}
|
||||
fn unmute(&mut self) {
|
||||
self.set_volume_impl(self.volume.0, self.volume.1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
use crate::{CodecPins, traits::Dac};
|
||||
use embedded_hal::prelude::{
|
||||
_embedded_hal_blocking_i2c_Write, _embedded_hal_blocking_i2c_WriteRead,
|
||||
};
|
||||
const WM8904_I2C_ADDRESS: u8 = 0b0011010;
|
||||
const MCLK: u32 = 24576000 / 2; // assume EVK TODO: better
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
enum RegisterAddress {
|
||||
SwResetId = 0x00,
|
||||
BiasControl0 = 0x04,
|
||||
VmidControl = 0x05,
|
||||
MicBiasControl0 = 0x06,
|
||||
MicBiasControl1 = 0x07,
|
||||
AnaAdc0 = 0x0a,
|
||||
PowerMgmt0 = 0x0c,
|
||||
PowerMgmt2 = 0x0e,
|
||||
PowerMgmt3 = 0x0f,
|
||||
PowerMgmt6 = 0x12,
|
||||
ClockRates0 = 0x14,
|
||||
ClockRates1 = 0x15,
|
||||
ClockRates2 = 0x16,
|
||||
AudioInterface0 = 0x18,
|
||||
AudioInterface1 = 0x19,
|
||||
AudioInterface2 = 0x1a,
|
||||
AudioInterface3 = 0x1b,
|
||||
DacDigiVolLeft = 0x1e,
|
||||
DacDigiVolRight = 0x1f,
|
||||
DacDigital0 = 0x20,
|
||||
DacDigi1 = 0x21,
|
||||
AdcDigiVolLeft = 0x24,
|
||||
AdcDigiVolRight = 0x25,
|
||||
AdcDigital0 = 0x26,
|
||||
DigiMic0 = 0x27,
|
||||
Drc0 = 0x28,
|
||||
Drc1 = 0x29,
|
||||
Drc2 = 0x2a,
|
||||
Drc3 = 0x2b,
|
||||
AnaLeftIn0 = 0x2c,
|
||||
AnaRightIn0 = 0x2d,
|
||||
AnaLeftIn1 = 0x2e,
|
||||
AnaRightIn1 = 0x2f,
|
||||
AnaOut1Left = 0x39,
|
||||
AnaOut1Right = 0x3a,
|
||||
AnaOut2Left = 0x3b,
|
||||
AnaOut2Right = 0x3c,
|
||||
AnaOut12Zc = 0x3d,
|
||||
DcServo0 = 0x43,
|
||||
DcServo1 = 0x44,
|
||||
DcServo2 = 0x45,
|
||||
DcServo4 = 0x47,
|
||||
DcServo5 = 0x48,
|
||||
DcServo6 = 0x49,
|
||||
DcServo7 = 0x4a,
|
||||
DcServo8 = 0x4b,
|
||||
DcServo9 = 0x4c,
|
||||
DcServoRb0 = 0x4d,
|
||||
AnaHp0 = 0x5a,
|
||||
AnaLineOut0 = 0x5e,
|
||||
ChargePump0 = 0x62,
|
||||
ClassW = 0x68,
|
||||
WriteSeq0 = 0x6c,
|
||||
WriteSeq1 = 0x6d,
|
||||
WriteSeq2 = 0x6e,
|
||||
WriteSeq3 = 0x6f,
|
||||
WriteSeq4 = 0x70,
|
||||
FllControl1 = 0x74,
|
||||
FllControl2 = 0x75,
|
||||
FllControl3 = 0x76,
|
||||
FllControl4 = 0x77,
|
||||
FllControl5 = 0x78,
|
||||
GpioControl1 = 0x79,
|
||||
GpioControl2 = 0x7a,
|
||||
GpioControl3 = 0x7b,
|
||||
GpioControl4 = 0x7c,
|
||||
DigiPulls = 0x7e,
|
||||
IntStatus = 0x7f,
|
||||
IntStatusMask = 0x80,
|
||||
IntPriority = 0x81,
|
||||
IntDebounce = 0x82,
|
||||
Eq1 = 0x86,
|
||||
Eq2 = 0x87,
|
||||
Eq3 = 0x88,
|
||||
Eq4 = 0x89,
|
||||
Eq5 = 0x8a,
|
||||
Eq6 = 0x8b,
|
||||
Eq7 = 0x8c,
|
||||
Eq8 = 0x8d,
|
||||
Eq9 = 0x8e,
|
||||
Eq10 = 0x8f,
|
||||
Eq11 = 0x90,
|
||||
Eq12 = 0x91,
|
||||
Eq13 = 0x92,
|
||||
Eq14 = 0x93,
|
||||
Eq15 = 0x94,
|
||||
Eq16 = 0x95,
|
||||
Eq17 = 0x96,
|
||||
Eq18 = 0x97,
|
||||
Eq19 = 0x98,
|
||||
Eq20 = 0x99,
|
||||
Eq21 = 0x9a,
|
||||
Eq22 = 0x9b,
|
||||
Eq23 = 0x9c,
|
||||
Eq24 = 0x9d,
|
||||
AdcTest0 = 0xc6,
|
||||
FllNcoTest0 = 0xf7,
|
||||
FllNcoTest1 = 0xf8,
|
||||
}
|
||||
|
||||
pub struct Wm8904Dac<T> {
|
||||
i2c: T,
|
||||
pins: CodecPins,
|
||||
mclk: u32,
|
||||
}
|
||||
|
||||
impl<T> Wm8904Dac<T>
|
||||
where
|
||||
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
|
||||
{
|
||||
#[inline]
|
||||
fn write_reg(&mut self, reg: RegisterAddress, val: u16) {
|
||||
let b = val.to_be_bytes();
|
||||
defmt::info!("i2c w [{:?}]", &[reg as u8, b[0], b[1]]);
|
||||
self.i2c
|
||||
.write(WM8904_I2C_ADDRESS, &[reg as u8, b[0], b[1]])
|
||||
.ok();
|
||||
}
|
||||
fn cr1_for_rate(&self, rate: u32) -> u16 {
|
||||
let fs_ratio = self.mclk / rate;
|
||||
if !self.mclk.is_multiple_of(rate) {
|
||||
defmt::warn!("[wm8904] sample rate should be a multiple of mclk");
|
||||
}
|
||||
let clk_sys_rate: u16 = match fs_ratio {
|
||||
64 => 0,
|
||||
128 => 1,
|
||||
192 => 2,
|
||||
256 => 3,
|
||||
384 => 4,
|
||||
512 => 5,
|
||||
768 => 6,
|
||||
1024 => 7,
|
||||
1408 => 8,
|
||||
1536 => 9,
|
||||
_ => {
|
||||
defmt::warn!("[wm8904] unsupport ratio {}", fs_ratio);
|
||||
0
|
||||
}
|
||||
};
|
||||
let sample_rate: u16 = match rate {
|
||||
r if r < 11025 => 0, // 0-11024
|
||||
r if r < 16000 => 1, // 11025 - 15999
|
||||
r if r < 22050 => 2, // 16000 - 22049
|
||||
r if r < 32000 => 3, // 22050 - 31999
|
||||
r if r < 44100 => 4, // 32000 - 44099
|
||||
_ => 5, // 44100+
|
||||
};
|
||||
(clk_sys_rate << 10) | sample_rate
|
||||
}
|
||||
fn blck_div_for_rate(&self, rate: u32) -> u16 {
|
||||
let bits_per_frame = 64;
|
||||
let bits_per_second = bits_per_frame * rate;
|
||||
(self.mclk / bits_per_second) as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dac<T> for Wm8904Dac<T>
|
||||
where
|
||||
T: _embedded_hal_blocking_i2c_WriteRead + _embedded_hal_blocking_i2c_Write,
|
||||
{
|
||||
fn new(i2c: T, pins: CodecPins) -> Self {
|
||||
Self {
|
||||
i2c,
|
||||
pins,
|
||||
mclk: MCLK,
|
||||
}
|
||||
}
|
||||
fn init(&mut self) {
|
||||
let mut buf = [0u8; 2];
|
||||
match self.i2c.write_read(WM8904_I2C_ADDRESS, &[0], &mut buf) {
|
||||
Ok(_) => {
|
||||
let chip_id = ((buf[0] as u16) << 8) | buf[1] as u16;
|
||||
defmt::info!("[wm8904] Read chip ID: {:x}", chip_id)
|
||||
}
|
||||
Err(_) => defmt::error!("[wm8904] Error reading I2C"),
|
||||
}
|
||||
|
||||
self.write_reg(RegisterAddress::ClockRates2, 0x000f); // OPCLK_ENA | CLK_SYS_ENA | CLK_DSP_ENA | TOCLK_ENA
|
||||
self.write_reg(RegisterAddress::WriteSeq0, 0x0100); // write sequencer 0 ENA
|
||||
self.write_reg(RegisterAddress::WriteSeq3, 0x0100); // write sequencer 3 START, INDEX=0
|
||||
// wait on write sequencer
|
||||
defmt::info!("[wm8904] waiting on write seq");
|
||||
loop {
|
||||
let mut buf = [0; 2];
|
||||
self.i2c
|
||||
.write_read(
|
||||
WM8904_I2C_ADDRESS,
|
||||
&[RegisterAddress::WriteSeq4 as u8],
|
||||
&mut buf,
|
||||
)
|
||||
.ok();
|
||||
if buf[1] & 1 == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
defmt::debug!("[wm8904] write seq done");
|
||||
self.write_reg(RegisterAddress::ClockRates0, 0);
|
||||
self.write_reg(RegisterAddress::PowerMgmt0, 0); // IN PGAs disabled
|
||||
self.write_reg(RegisterAddress::PowerMgmt2, 0x0003); // HPL_PGA_ENA | HPR_PGA_ENA
|
||||
self.write_reg(RegisterAddress::PowerMgmt3, 0); //line outs disabled
|
||||
|
||||
self.write_reg(RegisterAddress::PowerMgmt6, 0x000c); // power management 6 = DACL_ENA | DACR_ENA
|
||||
self.write_reg(RegisterAddress::AudioInterface0, 0x0050); // audio if 0 = AIFADCR_SRC | AIFDACR_SRC
|
||||
self.write_reg(RegisterAddress::DacDigi1, 0x0040); // dac digital 1 = DAC_OSR128
|
||||
self.write_reg(RegisterAddress::AnaLeftIn0, 0x0005);
|
||||
self.write_reg(RegisterAddress::AnaRightIn0, 0x0005);
|
||||
self.write_reg(RegisterAddress::AnaOut1Left, 0x0039); // analog out1 left = vol=0dB
|
||||
self.write_reg(RegisterAddress::AnaOut1Right, 0x0039); // analog out1 right = vol=0dB
|
||||
self.write_reg(RegisterAddress::AnaOut2Left, 0x0039); // analog out2 left = vol=0dB
|
||||
self.write_reg(RegisterAddress::AnaOut2Right, 0x0039); // analog out2 right = vol=0dB
|
||||
self.write_reg(RegisterAddress::DcServo0, 0x0003); // dc servo 0 = HPOUTL_ENA | HPOUTR_ENA
|
||||
self.write_reg(RegisterAddress::AnaHp0, 0x00ff); // analog hp 0 = remove all shorts etc
|
||||
self.write_reg(RegisterAddress::AnaLineOut0, 0x00ff); // analog lineout 0 = remove all shorts etc
|
||||
self.write_reg(RegisterAddress::ClassW, 0x0001); // enable class w charge pump
|
||||
self.write_reg(RegisterAddress::ChargePump0, 0x0001); // enable charge pump
|
||||
self.write_reg(RegisterAddress::AudioInterface1, (3 << 2) | 2); // audio if 1 = i2s, 32 bit per sample
|
||||
|
||||
self.write_reg(RegisterAddress::ClockRates1, self.cr1_for_rate(96000)); // Set up for 48k, impl will change if needed
|
||||
self.write_reg(RegisterAddress::ClockRates2, 0x000f); // clock rates 2 = CLK_SYS_ENA
|
||||
|
||||
self.write_reg(
|
||||
RegisterAddress::AudioInterface2,
|
||||
self.blck_div_for_rate(96000),
|
||||
);
|
||||
|
||||
self.write_reg(RegisterAddress::AudioInterface3, 0); // audio interface 3 = input lrclock
|
||||
self.write_reg(RegisterAddress::AnaOut12Zc, 0); // analog out12 zc = play source = dac
|
||||
self.write_reg(RegisterAddress::DacDigiVolLeft, 0x01ff); // dac vol left = update left/right = 0dB
|
||||
}
|
||||
fn change_rate(&mut self, new_rate: u32) {
|
||||
// TODO: mute, stop clocks etc.
|
||||
defmt::info!("[wm8904] dac rate -> {}", new_rate);
|
||||
self.write_reg(RegisterAddress::ClockRates1, self.cr1_for_rate(new_rate));
|
||||
self.write_reg(
|
||||
RegisterAddress::AudioInterface2,
|
||||
self.blck_div_for_rate(new_rate),
|
||||
);
|
||||
}
|
||||
fn mute(&mut self) {
|
||||
// self.write_reg(RegisterAddress::DacDigiVolLeft, 0x0100);
|
||||
}
|
||||
fn unmute(&mut self) {
|
||||
// TODO: restore previous volume
|
||||
// self.write_reg(RegisterAddress::DacDigiVolLeft, 0x01ff);
|
||||
}
|
||||
}
|
||||
@@ -70,29 +70,22 @@ impl core::fmt::Display for DmaError {
|
||||
/// Slot-based DMA ring
|
||||
pub struct DmaRing<const N: usize, const MAX_SLOT_BYTES: usize> {
|
||||
dma: pac::DMA0,
|
||||
|
||||
/// Destination peripheral register (FIFO write register)
|
||||
dst_reg: *mut u32,
|
||||
|
||||
// SAFETY: only written by USB task (on start)
|
||||
pub(crate) channel_desc: UnsafeCell<DescriptorTable>,
|
||||
// SAFETY: only written by USB task (on start)
|
||||
pub(crate) desc: UnsafeCell<RingDescriptors<N>>,
|
||||
slots: UnsafeCell<[[u8; MAX_SLOT_BYTES]; N]>,
|
||||
|
||||
/// Effective bytes per slot. Maybe be smaller than MAX_SLOT_BYTES (e.g. at lower sample rates), as the setup is designed for constant rate not constant size.
|
||||
slot_bytes: usize,
|
||||
/// How many bytes to transfer to the FIFO
|
||||
/// Effective bytes per slot (atomic for interior mutability)
|
||||
slot_bytes: AtomicUsize,
|
||||
word_bytes: usize,
|
||||
|
||||
// SAFETY: producer only
|
||||
write_slot: UnsafeCell<usize>,
|
||||
write_off: UnsafeCell<usize>,
|
||||
|
||||
produced: AtomicUsize,
|
||||
consumed: AtomicUsize,
|
||||
|
||||
/// Leave at least one slot empty so producer never overwrites a slot DMA may still read.
|
||||
safety_gap: usize,
|
||||
pub produced_bytes: AtomicUsize,
|
||||
pub consumed_bytes: AtomicUsize,
|
||||
@@ -132,7 +125,7 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
}; N],
|
||||
}),
|
||||
slots: UnsafeCell::new([[0u8; MAX_SLOT_BYTES]; N]),
|
||||
slot_bytes: MAX_SLOT_BYTES,
|
||||
slot_bytes: AtomicUsize::new(MAX_SLOT_BYTES),
|
||||
word_bytes,
|
||||
write_slot: UnsafeCell::new(0),
|
||||
write_off: UnsafeCell::new(0),
|
||||
@@ -149,9 +142,10 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
self.safety_gap = gap_slots.min(N);
|
||||
}
|
||||
pub fn slot_size(&self) -> usize {
|
||||
self.slot_bytes
|
||||
self.slot_bytes.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn set_slot_size(&mut self, slot_bytes: usize) -> Result<(), ConfigError> {
|
||||
|
||||
pub fn set_slot_size(&self, slot_bytes: usize) -> Result<(), ConfigError> {
|
||||
if slot_bytes == 0 {
|
||||
return Err(ConfigError::SlotTooSmall);
|
||||
}
|
||||
@@ -161,8 +155,13 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
if slot_bytes % self.word_bytes != 0 {
|
||||
return Err(ConfigError::SlotNotAligned);
|
||||
}
|
||||
self.slot_bytes = slot_bytes;
|
||||
self.reset_producer();
|
||||
|
||||
// Update atomic size
|
||||
self.slot_bytes.store(slot_bytes, Ordering::Release);
|
||||
|
||||
// Re-initialize descriptors and reset producer state safely through internal mutability
|
||||
self.init_descriptors();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -185,7 +184,8 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
break;
|
||||
}
|
||||
|
||||
let cap = self.slot_bytes - *write_off;
|
||||
let slot_bytes = self.slot_bytes.load(Ordering::Acquire);
|
||||
let cap = slot_bytes - *write_off;
|
||||
let n = core::cmp::min(cap, data.len());
|
||||
|
||||
unsafe {
|
||||
@@ -197,7 +197,7 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
written += n;
|
||||
data = &data[n..];
|
||||
|
||||
if *write_off == self.slot_bytes {
|
||||
if *write_off == slot_bytes {
|
||||
// publish completed slot
|
||||
compiler_fence(Ordering::Release);
|
||||
self.produced.fetch_add(1, Ordering::Release);
|
||||
@@ -221,8 +221,9 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
let consumed = self.consumed.load(Ordering::Relaxed);
|
||||
if consumed < produced {
|
||||
self.consumed.fetch_add(slots, Ordering::Release);
|
||||
let slot_bytes = self.slot_bytes.load(Ordering::Acquire);
|
||||
self.consumed_bytes
|
||||
.fetch_add(slots * self.slot_bytes, Ordering::Relaxed);
|
||||
.fetch_add(slots * slot_bytes, Ordering::Relaxed);
|
||||
Ok(())
|
||||
} else {
|
||||
defmt::error!("DMA underrun!");
|
||||
@@ -243,25 +244,23 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
loop {
|
||||
let consumed_start = self.consumed.load(Ordering::Acquire);
|
||||
|
||||
let reg_1 = self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff;
|
||||
let reg_2 = self.dma.channel19.xfercfg.read().bits() as usize >> 16 & 0x3ff;
|
||||
let reg_1 = (self.dma.channel19.xfercfg.read().bits() >> 16) & 0x3FF;
|
||||
let reg_2 = (self.dma.channel19.xfercfg.read().bits() >> 16) & 0x3FF;
|
||||
|
||||
let consumed_end = self.consumed.load(Ordering::Acquire);
|
||||
|
||||
if consumed_start == consumed_end && reg_1 == reg_2 {
|
||||
// 1. Map the hardware remaining countdown into a clean byte count
|
||||
let remaining_bytes = if reg_1 == 0x3ff {
|
||||
0 // 0x3FF means all transfers completed, 0 bytes remaining
|
||||
let remaining_bytes = if reg_1 == 0x3FF {
|
||||
0
|
||||
} else {
|
||||
// Formula from NXP manual: (XFERCOUNT + 1) * Data Width
|
||||
(reg_1 + 1) * self.word_bytes
|
||||
(reg_1 as usize + 1) * self.word_bytes
|
||||
};
|
||||
|
||||
// 2. Total bytes consumed in this specific active slot
|
||||
let active_slot_consumed = self.slot_bytes - remaining_bytes;
|
||||
// Active slot consumed calculation accounts for dynamic slot size
|
||||
let slot_bytes = self.slot_bytes.load(Ordering::Acquire);
|
||||
let active_slot_consumed = slot_bytes.saturating_sub(remaining_bytes);
|
||||
|
||||
// 3. Combine with your software index history accumulator
|
||||
return consumed_start * self.slot_bytes + active_slot_consumed;
|
||||
return consumed_start * slot_bytes + active_slot_consumed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,8 +310,8 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
|
||||
fn reset_producer(&self) {
|
||||
unsafe {
|
||||
*(&mut *self.write_slot.get()) = 0;
|
||||
*(&mut *self.write_off.get()) = 0;
|
||||
*self.write_slot.get() = 0;
|
||||
*self.write_off.get() = 0;
|
||||
}
|
||||
self.produced.store(0, Ordering::Relaxed);
|
||||
self.produced_bytes.store(0, Ordering::Relaxed);
|
||||
@@ -324,45 +323,30 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
let fill = self.fill_slots();
|
||||
fill >= N.wrapping_sub(self.safety_gap)
|
||||
}
|
||||
fn reset_producer_init_only(&self) {
|
||||
unsafe {
|
||||
*self.write_slot.get() = 0;
|
||||
}
|
||||
unsafe {
|
||||
*self.write_off.get() = 0;
|
||||
}
|
||||
|
||||
self.produced.store(0, Ordering::Relaxed);
|
||||
self.consumed.store(0, Ordering::Relaxed);
|
||||
|
||||
self.produced_bytes.store(0, Ordering::Relaxed);
|
||||
self.consumed_bytes.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn init_descriptors(&self) {
|
||||
let slot_bytes = self.slot_bytes.load(Ordering::Acquire);
|
||||
let slots = unsafe { &mut *self.slots.get() };
|
||||
let desc = unsafe { &mut *self.desc.get() };
|
||||
let chan_desc = unsafe { &mut *self.channel_desc.get() };
|
||||
defmt::debug!("slots base: &{:x}", self.slots.get());
|
||||
|
||||
// Pre-fill with silence so underrun replays silence.
|
||||
// Pre-fill active slot regions with silence
|
||||
for i in 0..N {
|
||||
slots[i][..self.slot_bytes].fill(0);
|
||||
slots[i][..slot_bytes].fill(0);
|
||||
}
|
||||
|
||||
let transfers = (self.slot_bytes / self.word_bytes) as u32;
|
||||
let transfers = (slot_bytes / self.word_bytes) as u32;
|
||||
|
||||
for i in 0..N {
|
||||
let src_start = slots[i].as_ptr() as usize;
|
||||
let src_end = (src_start + self.slot_bytes - self.word_bytes) as *const u8;
|
||||
|
||||
let src_end = (src_start + slot_bytes - self.word_bytes) as *const u8;
|
||||
let next = &desc.d[(i + 1) % N] as *const DmaDescriptor;
|
||||
|
||||
desc.d[i] = DmaDescriptor {
|
||||
xfercfg: encode_xfercfg(
|
||||
true, // valid
|
||||
true, // reload
|
||||
false, // swtrig (we use XFERCFG SWTRIG kick)
|
||||
false, // swtrig
|
||||
false, // clrtrig
|
||||
true, // intA
|
||||
false, // intB
|
||||
@@ -376,11 +360,14 @@ impl<const N: usize, const MAX_SLOT_BYTES: usize> DmaRing<N, MAX_SLOT_BYTES> {
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure memory writes complete before reloading DMA hardware pointers
|
||||
compiler_fence(Ordering::Release);
|
||||
|
||||
chan_desc.d[19] = desc.d[0];
|
||||
chan_desc.d[19].xfercfg = 0;
|
||||
|
||||
// reset producer indices + counters (init-only action)
|
||||
self.reset_producer_init_only();
|
||||
self.reset_producer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
use core::cell::UnsafeCell;
|
||||
use core::mem::MaybeUninit;
|
||||
|
||||
use crate::hal;
|
||||
use crate::pac;
|
||||
|
||||
use defmt::{debug, info};
|
||||
use hal::{
|
||||
Enabled, Iocon, Pin,
|
||||
drivers::pins,
|
||||
traits::wg::digital::v2::{OutputPin, ToggleableOutputPin},
|
||||
typestates::pin::{gpio::direction::Output, state::Gpio},
|
||||
};
|
||||
|
||||
pub(crate) struct PllConstants {
|
||||
pub m: u16, // 1-65535
|
||||
pub n: u8, // 1-255
|
||||
pub p: u8, // 1-31
|
||||
pub selp: u8, // 5 bits
|
||||
pub seli: u8, // 6 bits
|
||||
}
|
||||
|
||||
impl PllConstants {
|
||||
pub(crate) const fn new(n: u8, m: u16, p: u8) -> Self {
|
||||
assert!(n != 0, "1 <= N <= 255");
|
||||
assert!(m != 0, "1 <= M <= 65535");
|
||||
assert!(p != 0 && p <= 31, "1 <= P <= 31");
|
||||
|
||||
// Following ripped from lpc55-hal and made const
|
||||
// UM 4.6.6.3.2
|
||||
let selp = {
|
||||
let v = (m >> 2) + 1;
|
||||
if v < 31 { v } else { 31 }
|
||||
} as u8;
|
||||
|
||||
let seli = {
|
||||
let v = match m {
|
||||
m if m >= 8000 => 1,
|
||||
m if m >= 122 => 8000 / m,
|
||||
_ => 2 * (m >> 2) + 3,
|
||||
};
|
||||
|
||||
if v < 63 { v } else { 63 }
|
||||
} as u8;
|
||||
// let seli = min(2*(m >> 2) + 3, 63);
|
||||
Self {
|
||||
n,
|
||||
m,
|
||||
p,
|
||||
selp,
|
||||
seli,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl defmt::Format for PllConstants {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
let factor = f32::from(self.m) / (f32::from(self.n) * 2.0 * f32::from(self.p));
|
||||
|
||||
defmt::write!(
|
||||
fmt,
|
||||
"m: {} n: {} p: {} selp: {} seli: {} fout: fin * {}",
|
||||
self.m,
|
||||
self.n,
|
||||
self.p,
|
||||
self.selp,
|
||||
self.seli,
|
||||
factor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SYS_PLL: PllConstants = PllConstants::new(4, 75, 1); // 150MHz
|
||||
|
||||
pub(crate) fn init_sys_pll1() {
|
||||
let syscon = unsafe { &*pac::SYSCON::ptr() };
|
||||
let pmc = unsafe { &*pac::PMC::ptr() };
|
||||
let anactrl = unsafe { &*pac::ANACTRL::ptr() };
|
||||
|
||||
debug!("start clk_in");
|
||||
pmc.pdruncfg0
|
||||
.modify(|_, w| w.pden_xtal32m().poweredon().pden_ldoxo32m().poweredon());
|
||||
syscon.clock_ctrl.modify(|_, w| w.clkin_ena().enable());
|
||||
anactrl
|
||||
.xo32m_ctrl
|
||||
.modify(|_, w| w.enable_system_clk_out().enable());
|
||||
|
||||
debug!("init pll1: {}", SYS_PLL);
|
||||
pmc.pdruncfg0.modify(|_, w| w.pden_pll1().poweredoff());
|
||||
syscon.pll1clksel.write(|w| w.sel().enum_0x1()); // clk_in
|
||||
syscon.pll1ctrl.write(|w| unsafe {
|
||||
w.clken()
|
||||
.enable()
|
||||
.seli()
|
||||
.bits(SYS_PLL.seli)
|
||||
.selp()
|
||||
.bits(SYS_PLL.selp)
|
||||
});
|
||||
|
||||
syscon
|
||||
.pll1ndec
|
||||
.write(|w| unsafe { w.ndiv().bits(SYS_PLL.n) });
|
||||
syscon.pll1ndec.write(|w| unsafe {
|
||||
w.ndiv().bits(SYS_PLL.n).nreq().set_bit() // latch
|
||||
});
|
||||
syscon
|
||||
.pll1mdec
|
||||
.write(|w| unsafe { w.mdiv().bits(SYS_PLL.m) });
|
||||
syscon
|
||||
.pll1pdec
|
||||
.write(|w| unsafe { w.pdiv().bits(SYS_PLL.p) });
|
||||
syscon.pll1pdec.write(|w| unsafe {
|
||||
w.pdiv().bits(SYS_PLL.p).preq().set_bit() // latch
|
||||
});
|
||||
|
||||
pmc.pdruncfg0.modify(|_, w| w.pden_pll1().poweredon());
|
||||
debug!("pll1 wait for lock");
|
||||
let mut i = 0usize;
|
||||
while syscon.pll1stat.read().lock().bit_is_clear() {
|
||||
i += 1;
|
||||
}
|
||||
debug!("pll1 locked after {} tries", i);
|
||||
// switch system clock to pll1
|
||||
syscon.fmccr.modify(|_, w| w.flashtim().flashtim11());
|
||||
syscon.mainclkselb.modify(|_, w| w.sel().enum_0x2()); // pll1
|
||||
}
|
||||
|
||||
// Fo = M/(N*2*P) * Fin
|
||||
// Fo = 3072/(125*2*8) * 16MHz = 24.576MHz
|
||||
const AUDIO_PLL: PllConstants = PllConstants::new(125, 3072, 8);
|
||||
|
||||
// Set PLL0 to 24.576MHz, start, and wait for lock
|
||||
// This is not exposed by lpc55-hal, unfortunately. Copy their implementation here.
|
||||
pub(crate) fn init_audio_pll() {
|
||||
let syscon = unsafe { &*pac::SYSCON::ptr() };
|
||||
let pmc = unsafe { &*pac::PMC::ptr() };
|
||||
let anactrl = unsafe { &*pac::ANACTRL::ptr() };
|
||||
|
||||
debug!("start clk_in");
|
||||
pmc.pdruncfg0
|
||||
.modify(|_, w| w.pden_xtal32m().poweredon().pden_ldoxo32m().poweredon());
|
||||
syscon.clock_ctrl.modify(|_, w| w.clkin_ena().enable());
|
||||
anactrl
|
||||
.xo32m_ctrl
|
||||
.modify(|_, w| w.enable_system_clk_out().enable());
|
||||
|
||||
debug!("init pll0: {}", AUDIO_PLL);
|
||||
pmc.pdruncfg0
|
||||
.modify(|_, w| w.pden_pll0().poweredoff().pden_pll0_sscg().poweredoff());
|
||||
syscon.pll0clksel.write(|w| w.sel().enum_0x1()); // clk_in
|
||||
syscon.pll0ctrl.write(|w| unsafe {
|
||||
w.clken()
|
||||
.enable()
|
||||
.seli()
|
||||
.bits(AUDIO_PLL.seli)
|
||||
.selp()
|
||||
.bits(AUDIO_PLL.selp)
|
||||
});
|
||||
|
||||
syscon
|
||||
.pll0ndec
|
||||
.write(|w| unsafe { w.ndiv().bits(AUDIO_PLL.n) });
|
||||
syscon.pll0ndec.write(|w| unsafe {
|
||||
w.ndiv().bits(AUDIO_PLL.n).nreq().set_bit() // latch
|
||||
});
|
||||
|
||||
syscon
|
||||
.pll0pdec
|
||||
.write(|w| unsafe { w.pdiv().bits(AUDIO_PLL.p) });
|
||||
syscon.pll0pdec.write(|w| unsafe {
|
||||
w.pdiv().bits(AUDIO_PLL.p).preq().set_bit() // latch
|
||||
});
|
||||
|
||||
syscon.pll0sscg0.write(|w| unsafe { w.md_lbs().bits(0) });
|
||||
|
||||
syscon
|
||||
.pll0sscg1
|
||||
.write(|w| unsafe { w.mdiv_ext().bits(AUDIO_PLL.m).sel_ext().set_bit() });
|
||||
syscon.pll0sscg1.write(|w| unsafe {
|
||||
w.mdiv_ext()
|
||||
.bits(AUDIO_PLL.m)
|
||||
.sel_ext()
|
||||
.set_bit()
|
||||
.mreq()
|
||||
.set_bit() // latch
|
||||
.md_req()
|
||||
.set_bit() // latch
|
||||
});
|
||||
|
||||
pmc.pdruncfg0
|
||||
.modify(|_, w| w.pden_pll0().poweredon().pden_pll0_sscg().poweredon());
|
||||
info!("pll0 wait for lock");
|
||||
let mut i = 0usize;
|
||||
while syscon.pll0stat.read().lock().bit_is_clear() {
|
||||
i += 1;
|
||||
}
|
||||
info!("pll0 locked after {} loops", i);
|
||||
}
|
||||
|
||||
pub struct SharedLed<T: OutputPin> {
|
||||
inner: UnsafeCell<T>,
|
||||
}
|
||||
unsafe impl<T: OutputPin> Sync for SharedLed<T> {}
|
||||
impl<T: OutputPin> SharedLed<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner: UnsafeCell::new(inner),
|
||||
}
|
||||
}
|
||||
pub fn on(&self) {
|
||||
unsafe {
|
||||
(*self.inner.get()).set_low().ok();
|
||||
}
|
||||
}
|
||||
pub fn off(&self) {
|
||||
unsafe {
|
||||
(*self.inner.get()).set_high().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: OutputPin + ToggleableOutputPin> SharedLed<T> {
|
||||
pub fn toggle(&self) {
|
||||
unsafe {
|
||||
(*self.inner.get()).toggle().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Led1 = Pin<pins::Pio0_13, Gpio<Output>>;
|
||||
type Led2 = Pin<pins::Pio0_14, Gpio<Output>>;
|
||||
pub static LED1: MaybeUninit<SharedLed<Led1>> = MaybeUninit::uninit();
|
||||
pub static LED2: MaybeUninit<SharedLed<Led2>> = MaybeUninit::uninit();
|
||||
|
||||
pub fn init_leds(iocon: &mut Iocon<Enabled>, gpio: &mut hal::Gpio<Enabled>) {
|
||||
let led1 = SharedLed::new(
|
||||
pins::Pio0_13::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(iocon, gpio)
|
||||
.into_output_low(),
|
||||
);
|
||||
let led2 = SharedLed::new(
|
||||
pins::Pio0_14::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(iocon, gpio)
|
||||
.into_output_low(),
|
||||
);
|
||||
unsafe {
|
||||
core::ptr::write(LED1.as_ptr() as *mut SharedLed<Led1>, led1);
|
||||
core::ptr::write(LED2.as_ptr() as *mut SharedLed<Led2>, led2);
|
||||
}
|
||||
}
|
||||
pub fn led1() -> &'static SharedLed<Led1> {
|
||||
unsafe { &*LED1.as_ptr() }
|
||||
}
|
||||
pub fn led2() -> &'static SharedLed<Led2> {
|
||||
unsafe { &*LED2.as_ptr() }
|
||||
}
|
||||
+269
-135
@@ -8,7 +8,6 @@ fn panic() -> ! {
|
||||
}
|
||||
|
||||
use atomic::Atomic;
|
||||
use bytemuck::NoUninit;
|
||||
use core::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
|
||||
use cortex_m_rt::entry;
|
||||
use defmt;
|
||||
@@ -39,8 +38,11 @@ use usbd_uac2::{
|
||||
|
||||
use crate::dac::DacImpl;
|
||||
use crate::dma::DmaRing;
|
||||
use crate::hid::AudioTelemetryReport;
|
||||
use crate::hw::led1;
|
||||
use crate::hw::led2;
|
||||
use crate::traits::Dac;
|
||||
use shared::AudioState;
|
||||
use shared::hid::AudioTelemetryReport;
|
||||
|
||||
#[cfg(feature = "ak4490")]
|
||||
pub mod dac {
|
||||
@@ -57,28 +59,52 @@ pub mod dac {
|
||||
mod noop;
|
||||
pub use self::noop::NoopDac as DacImpl;
|
||||
}
|
||||
#[cfg(feature = "wm8904")]
|
||||
pub mod dac {
|
||||
mod wm8904;
|
||||
pub use self::wm8904::Wm8904Dac as DacImpl;
|
||||
}
|
||||
|
||||
mod dma;
|
||||
#[cfg(feature = "hid")]
|
||||
mod hid;
|
||||
mod hw;
|
||||
mod traits;
|
||||
|
||||
#[cfg(not(feature = "evk"))]
|
||||
const MAX_SAMPLE_RATE: u32 = 192000;
|
||||
#[cfg(feature = "evk")]
|
||||
const MAX_SAMPLE_RATE: u32 = 96000;
|
||||
|
||||
#[cfg(not(feature = "evk"))]
|
||||
const SAMPLE_RATES: [RangeEntry<u32>; 6] = [
|
||||
RangeEntry::new_fixed(44100),
|
||||
RangeEntry::new_fixed(48000),
|
||||
RangeEntry::new_fixed(44100 * 2),
|
||||
RangeEntry::new_fixed(48000 * 2),
|
||||
RangeEntry::new_fixed(44100 * 4),
|
||||
RangeEntry::new_fixed(48000 * 4),
|
||||
];
|
||||
|
||||
#[cfg(feature = "evk")]
|
||||
const SAMPLE_RATES: [RangeEntry<u32>; 2] = [
|
||||
RangeEntry::new_fixed(48000),
|
||||
RangeEntry::new_fixed(48000 * 2),
|
||||
];
|
||||
|
||||
const DMA_RATE: usize = 4000;
|
||||
const BYTES_PER_SAMPLE: usize = 4; // 32 bit samples
|
||||
const BYTES_PER_FRAME: usize = BYTES_PER_SAMPLE * 2; // 2 channels
|
||||
const FRAMES_PER_SLOT: usize = SAMPLE_RATE as usize / 2000; // run the DMA at 2khz
|
||||
const BYTES_PER_SLOT: usize = FRAMES_PER_SLOT * BYTES_PER_FRAME;
|
||||
const MAX_FRAMES_PER_SLOT: usize = MAX_SAMPLE_RATE as usize / 4000; // run the DMA at 4khz
|
||||
const MAX_BYTES_PER_SLOT: usize = MAX_FRAMES_PER_SLOT * BYTES_PER_FRAME;
|
||||
const N_SLOTS: usize = 8;
|
||||
const FILL_TARGET_BYTES: i32 = (BYTES_PER_SLOT * N_SLOTS) as i32 / 2;
|
||||
// const FILL_TARGET_BYTES: i32 = (BYTES_PER_SLOT * N_SLOTS) as i32 / 2;
|
||||
const USB_FRAME_RATE: u32 = 8000; // microframe rate: 8000 for HS, 1000 for FS
|
||||
|
||||
// In frames
|
||||
const QUEUE_RUNNING_UP: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 4) / 10; // 40%
|
||||
const QUEUE_RUNNING_DOWN: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 2) / 10; // 20%
|
||||
const NODATA_TIMEOUT_FRAMES: usize = SAMPLE_RATE as usize / 100; // ~100ms
|
||||
const MCLK_FREQ: u32 = 24576000;
|
||||
const SAMPLE_RATE: u32 = 192000;
|
||||
const HID_INTERVAL_MS: u8 = 100;
|
||||
// const QUEUE_RUNNING_UP: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 5) / 10; // 50%
|
||||
// const QUEUE_RUNNING_DOWN: usize = ((FRAMES_PER_SLOT * N_SLOTS) * 2) / 10; // 20%
|
||||
// const NODATA_TIMEOUT_FRAMES: usize = SAMPLE_RATE as usize / 100; // ~100ms
|
||||
|
||||
const HID_INTERVAL_MS: u8 = 10;
|
||||
|
||||
struct CodecPins {
|
||||
reset: Pin<pins::Pio0_3, Gpio<Output>>,
|
||||
@@ -91,6 +117,7 @@ struct ClockSelPins {
|
||||
|
||||
#[derive(Default)]
|
||||
struct PerfCounters {
|
||||
state: Atomic<AudioState>,
|
||||
received_frames: AtomicUsize,
|
||||
played_frames: AtomicUsize,
|
||||
min_fill: AtomicUsize,
|
||||
@@ -98,6 +125,10 @@ struct PerfCounters {
|
||||
queue_underflows: AtomicUsize,
|
||||
queue_overflows: AtomicUsize,
|
||||
audio_underflows: AtomicUsize,
|
||||
integrator: AtomicI32,
|
||||
p: AtomicI32,
|
||||
i: AtomicI32,
|
||||
fb: AtomicI32,
|
||||
}
|
||||
|
||||
impl PerfCounters {
|
||||
@@ -105,20 +136,27 @@ impl PerfCounters {
|
||||
self.received_frames.store(0, Ordering::Relaxed);
|
||||
self.played_frames.store(0, Ordering::Relaxed);
|
||||
self.min_fill
|
||||
.store(N_SLOTS * BYTES_PER_SLOT, Ordering::Relaxed);
|
||||
self.avg_fill
|
||||
.store(FILL_TARGET_BYTES as usize, Ordering::Relaxed);
|
||||
.store(N_SLOTS * MAX_BYTES_PER_SLOT, Ordering::Relaxed);
|
||||
self.avg_fill.store(0 as usize, Ordering::Relaxed);
|
||||
self.queue_underflows.store(0, Ordering::Relaxed);
|
||||
self.queue_overflows.store(0, Ordering::Relaxed);
|
||||
self.audio_underflows.store(0, Ordering::Relaxed);
|
||||
self.p.store(0, Ordering::Relaxed);
|
||||
self.i.store(0, Ordering::Relaxed);
|
||||
// FB loop will have to take care of the fb value
|
||||
}
|
||||
fn build_report(&self) -> AudioTelemetryReport {
|
||||
AudioTelemetryReport {
|
||||
average_buffer_fill: self.avg_fill.load(Ordering::Relaxed) as i32,
|
||||
state: self.state.load(Ordering::Relaxed) as u8,
|
||||
average_buffer_fill: self.avg_fill.load(Ordering::Relaxed) as u16,
|
||||
frame_count: self.played_frames.load(Ordering::Relaxed) as i32,
|
||||
dac_underflow_count: self.audio_underflows.load(Ordering::Relaxed) as i32,
|
||||
usb_underflow_count: self.queue_underflows.load(Ordering::Relaxed) as i32,
|
||||
dac_overflow_count: self.queue_overflows.load(Ordering::Relaxed) as i32,
|
||||
dac_underflow_count: self.audio_underflows.load(Ordering::Relaxed) as u16,
|
||||
usb_underflow_count: self.queue_underflows.load(Ordering::Relaxed) as u16,
|
||||
dac_overflow_count: self.queue_overflows.load(Ordering::Relaxed) as u16,
|
||||
integrator: self.integrator.load(Ordering::Relaxed),
|
||||
p: self.p.load(Ordering::Relaxed),
|
||||
i: self.i.load(Ordering::Relaxed),
|
||||
fb: u32::cast_signed(self.fb.load(Ordering::Relaxed) as u32),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,19 +178,26 @@ impl defmt::Format for PerfCounters {
|
||||
}
|
||||
|
||||
static PERF: PerfCounters = PerfCounters {
|
||||
state: Atomic::new(AudioState::Stopped),
|
||||
received_frames: AtomicUsize::new(0), // received from USB
|
||||
played_frames: AtomicUsize::new(0), // played audio frames
|
||||
min_fill: AtomicUsize::new(0), // not recording this for now, need to figure out how to make it meaningful, since the queue starts empty
|
||||
avg_fill: AtomicUsize::new(FILL_TARGET_BYTES as usize),
|
||||
avg_fill: AtomicUsize::new(0),
|
||||
queue_underflows: AtomicUsize::new(0), // ditto here, since we underflow at startup, but we record this one as it can be trended
|
||||
queue_overflows: AtomicUsize::new(0),
|
||||
audio_underflows: AtomicUsize::new(0),
|
||||
integrator: AtomicI32::new(0),
|
||||
p: AtomicI32::new(0),
|
||||
i: AtomicI32::new(0),
|
||||
fb: AtomicI32::new(0),
|
||||
};
|
||||
|
||||
static DMA_RING: StaticCell<DmaRing<N_SLOTS, BYTES_PER_SLOT>> = StaticCell::new();
|
||||
static mut DMA_RING_REF: Option<&'static DmaRing<N_SLOTS, BYTES_PER_SLOT>> = None;
|
||||
static NODATA_FLAG: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
static DMA_RING: StaticCell<DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>> = StaticCell::new();
|
||||
static mut DMA_RING_REF: Option<&'static DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>> = None;
|
||||
#[inline]
|
||||
fn dma_ring() -> &'static DmaRing<N_SLOTS, BYTES_PER_SLOT> {
|
||||
fn dma_ring() -> &'static DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT> {
|
||||
unsafe { DMA_RING_REF.unwrap() }
|
||||
}
|
||||
|
||||
@@ -164,6 +209,24 @@ fn cur_fill() -> usize {
|
||||
produced_bytes.wrapping_sub(consumed_bytes) as usize
|
||||
}
|
||||
|
||||
fn cur_fill_target() -> i32 {
|
||||
(dma_ring().slot_size() * N_SLOTS) as i32 / 2
|
||||
}
|
||||
|
||||
fn frames_per_slot() -> usize {
|
||||
dma_ring().slot_size() / BYTES_PER_FRAME
|
||||
}
|
||||
|
||||
// 50%
|
||||
fn queue_running_up_threshold() -> usize {
|
||||
(frames_per_slot() * N_SLOTS) / 2
|
||||
}
|
||||
|
||||
// 20%
|
||||
fn queue_running_down_threshold() -> usize {
|
||||
(frames_per_slot() * N_SLOTS) / 5
|
||||
}
|
||||
|
||||
#[interrupt]
|
||||
fn DMA0() {
|
||||
defmt::debug!("dma0");
|
||||
@@ -185,73 +248,32 @@ fn DMA0() {
|
||||
err,
|
||||
mem
|
||||
);
|
||||
// red_led().on();
|
||||
dma.errint0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
}
|
||||
|
||||
if (inta & (1 << 19)) != 0 {
|
||||
dma.inta0.write(|w| unsafe { w.bits(1 << 19) });
|
||||
if dma_ring().advance_consumed(1).is_err() {
|
||||
// red_led().on();
|
||||
PERF.audio_underflows.fetch_add(1, Ordering::Relaxed);
|
||||
} else {
|
||||
led1().toggle();
|
||||
PERF.played_frames
|
||||
.fetch_add(FRAMES_PER_SLOT, Ordering::Relaxed);
|
||||
.fetch_add(frames_per_slot(), Ordering::Relaxed);
|
||||
}
|
||||
if cur_fill() <= dma_ring().slot_size() {
|
||||
led2().on();
|
||||
NODATA_FLAG.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, NoUninit, Eq, PartialEq)]
|
||||
enum AudioState {
|
||||
/// Knowingly stopped, ie. AltSetting=0. DAC muted, I2S disabled.
|
||||
///
|
||||
/// AltSetting = 1 -> ARMED
|
||||
Stopped,
|
||||
/// Waiting for data. DAC muted, I2S running sending 0s (FIFO not serviced).
|
||||
///
|
||||
/// USB OUT data packet -> ARMED
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
Armed,
|
||||
/// Filling the buffer before playback starts. Feedback does not run,
|
||||
/// playout does not start draining the queue. Gets us better feedback
|
||||
/// behaviour and a full buffer without a feedback rate spike at startup.
|
||||
///
|
||||
/// queue reaches <QUEUE_RUNNING_UP> -> RUNNING
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
///
|
||||
Prefill,
|
||||
/// Normal running state. Start servicing FIFO and begin playing out from the buffer.
|
||||
///
|
||||
/// queue reaches <QUEUE_RUNNING_DOWN> -> DRAINING
|
||||
/// AltSetting = 0 -> DRAINING
|
||||
Running,
|
||||
/// The queue is low. We will continue playout.
|
||||
///
|
||||
/// queue is empty && altSetting 1 -> NODATA
|
||||
/// queue is empty && altSetting 0 -> STOPPED
|
||||
/// queue reaches <QUEUE_RUNNING_UP> && altSetting 1 -> RUNNING
|
||||
LowData,
|
||||
/// There is no data in the queue. We will count underflows for a while, send 0s, and hope the host comes back, but maybe playback is done, which we should notice and shut down.
|
||||
///
|
||||
/// countdown reaches DATA_TIMEOUT -> STOPPED
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
NoData,
|
||||
}
|
||||
impl defmt::Format for AudioState {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(
|
||||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Stopped => "Stopped",
|
||||
Self::Armed => "Armed",
|
||||
Self::Prefill => "Prefill",
|
||||
Self::Running => "Running",
|
||||
Self::LowData => "Draining",
|
||||
Self::NoData => "NoData",
|
||||
}
|
||||
)
|
||||
}
|
||||
#[interrupt]
|
||||
fn FLEXCOMM7() {
|
||||
// Count I2S TX FIFO error (should be underrun, assuming we set up our DMA trigger correctly)
|
||||
PERF.audio_underflows.fetch_add(1, Ordering::Relaxed);
|
||||
unsafe { &*pac::I2S7::ptr() }
|
||||
.fifostat
|
||||
.modify(|_, w| w.txerr().set_bit())
|
||||
}
|
||||
|
||||
struct FeedbackState {
|
||||
@@ -267,7 +289,7 @@ impl FeedbackState {
|
||||
self.correction_enabled.store(false, Ordering::Relaxed);
|
||||
self.integrator.store(0, Ordering::Relaxed);
|
||||
self.filtered_fill
|
||||
.store(FILL_TARGET_BYTES, Ordering::Relaxed);
|
||||
.store(cur_fill_target(), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
impl Default for FeedbackState {
|
||||
@@ -275,7 +297,7 @@ impl Default for FeedbackState {
|
||||
Self {
|
||||
correction_enabled: AtomicBool::new(false),
|
||||
integrator: AtomicI32::new(0),
|
||||
filtered_fill: AtomicI32::new(FILL_TARGET_BYTES),
|
||||
filtered_fill: AtomicI32::new(cur_fill_target()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,7 +307,7 @@ struct Audio<'a, D: Dac<I>, I> {
|
||||
alt_setting: u8,
|
||||
i2s: I2sTx,
|
||||
dac: D,
|
||||
dma: &'a DmaRing<N_SLOTS, BYTES_PER_SLOT>,
|
||||
dma: &'a DmaRing<N_SLOTS, MAX_BYTES_PER_SLOT>,
|
||||
fb: FeedbackState,
|
||||
nodata_timeout_frame: AtomicUsize,
|
||||
cur_rate: u32,
|
||||
@@ -293,7 +315,7 @@ struct Audio<'a, D: Dac<I>, I> {
|
||||
_marker: core::marker::PhantomData<I>,
|
||||
}
|
||||
impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||
const RATES: [RangeEntry<u32>; 1] = [RangeEntry::new_fixed(SAMPLE_RATE)];
|
||||
const RATES: &'static [RangeEntry<u32>] = &SAMPLE_RATES;
|
||||
/// Perform a state transition to `state`
|
||||
fn transition(&mut self, state: AudioState) {
|
||||
defmt::info!(
|
||||
@@ -308,14 +330,15 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||
AudioState::Running => self.run(),
|
||||
AudioState::LowData => {}
|
||||
AudioState::NoData => self.nodata(),
|
||||
AudioState::Stopping => self.stopping(),
|
||||
}
|
||||
self.state.store(state, Ordering::SeqCst);
|
||||
PERF.state.store(state, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
let regs = &self.i2s.i2s;
|
||||
// Enable TX FIFO only
|
||||
regs.fifocfg.modify(|_, w| {
|
||||
self.i2s.i2s.fifocfg.modify(|_, w| {
|
||||
w.enabletx()
|
||||
.enabled()
|
||||
.enablerx()
|
||||
@@ -327,17 +350,17 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||
});
|
||||
|
||||
// Flush
|
||||
regs.fifocfg.modify(|_, w| w.emptytx().set_bit());
|
||||
self.i2s.i2s.fifocfg.modify(|_, w| w.emptytx().set_bit());
|
||||
|
||||
regs.cfg2
|
||||
self.i2s
|
||||
.i2s
|
||||
.cfg2
|
||||
.modify(|_, w| unsafe { w.position().bits(0).framelen().bits(63) }); // framelen = 64
|
||||
|
||||
let bclk_div = (MCLK_FREQ / SAMPLE_RATE / 64) as u16;
|
||||
regs.div
|
||||
.modify(|_, w| unsafe { w.div().bits(bclk_div - 1) }); // Clock source is MCLK (12.288MHz) / 4 = 3MHz
|
||||
self.update_bclk();
|
||||
|
||||
// Config
|
||||
regs.cfg1.modify(|_, w| unsafe {
|
||||
self.i2s.i2s.cfg1.modify(|_, w| unsafe {
|
||||
w.mstslvcfg()
|
||||
.normal_master()
|
||||
.onechannel()
|
||||
@@ -351,11 +374,15 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||
.datapause()
|
||||
.normal()
|
||||
});
|
||||
|
||||
unsafe { pac::NVIC::unmask(pac::Interrupt::FLEXCOMM7) };
|
||||
self.dac.init();
|
||||
}
|
||||
///Transition -> Stopped:
|
||||
///clear queue, mute DAC, mask I2S ISR, stop I2S peripheral, disable & reset feedback and performance queues
|
||||
fn stop(&mut self) {
|
||||
// Disable FIFO error interrupt
|
||||
self.i2s.i2s.fifointenclr.write(|w| w.txerr().set_bit());
|
||||
dma_ring().stop();
|
||||
pac::NVIC::mask(pac::Interrupt::DMA0);
|
||||
self.dac.mute();
|
||||
@@ -368,8 +395,8 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||
// reset performance counters
|
||||
PERF.reset();
|
||||
// Stop the clocks
|
||||
self.clock_pins.sel_22m.set_low().ok();
|
||||
self.clock_pins.sel_24m.set_low().ok();
|
||||
// self.clock_pins.sel_22m.set_low().ok();
|
||||
// self.clock_pins.sel_24m.set_low().ok();
|
||||
}
|
||||
///Transition -> Armed
|
||||
/// Start I2S peripheral and MCLK. Since we assume we have interrupts disabled at
|
||||
@@ -399,6 +426,10 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||
.fifocfg
|
||||
.modify(|_, w| w.enabletx().enabled().dmatx().enabled());
|
||||
dma_ring().run();
|
||||
// clear tx error status
|
||||
self.i2s.i2s.fifostat.write(|w| w.txerr().set_bit());
|
||||
// enable tx error interrupt
|
||||
self.i2s.i2s.fifointenset.write(|w| w.txerr().enabled());
|
||||
unsafe {
|
||||
pac::NVIC::unmask(pac::Interrupt::DMA0);
|
||||
}
|
||||
@@ -406,14 +437,31 @@ impl<D: Dac<I>, I> Audio<'_, D, I> {
|
||||
///Transition->NoData
|
||||
///store framecount at transition so we can time out recovery
|
||||
fn nodata(&mut self) {
|
||||
self.nodata_timeout_frame.store(
|
||||
PERF.queue_underflows.load(Ordering::Relaxed) + NODATA_TIMEOUT_FRAMES, // we underflow every frame, use it as a timeout counter
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
// TODO: Actually handle this
|
||||
// self.nodata_timeout_frame.store(
|
||||
// PERF.queue_underflows.load(Ordering::Relaxed) + NODATA_TIMEOUT_FRAMES, // we underflow every frame, use it as a timeout counter
|
||||
// Ordering::Relaxed,
|
||||
// );
|
||||
}
|
||||
/// Transition -> Stopping
|
||||
/// just a marker that upcoming nodata is expected, do nothing
|
||||
fn stopping(&mut self) {}
|
||||
|
||||
fn update_bclk(&mut self) {
|
||||
let mclk_freq = if 24_576_000u32.is_multiple_of(self.cur_rate) {
|
||||
24576000
|
||||
} else {
|
||||
22579200
|
||||
};
|
||||
let bclk_div = (mclk_freq / self.cur_rate / 64) as u16;
|
||||
self.i2s
|
||||
.i2s
|
||||
.div
|
||||
.modify(|_, w| unsafe { w.div().bits(bclk_div - 1) });
|
||||
}
|
||||
}
|
||||
impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
|
||||
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalFixed;
|
||||
const CLOCK_TYPE: usbd_uac2::descriptors::ClockType = ClockType::InternalProgrammable;
|
||||
const SOF_SYNC: bool = false;
|
||||
|
||||
fn sample_rate(&self) -> u32 {
|
||||
@@ -423,25 +471,34 @@ impl<D: Dac<I>, I> ClockSource for Audio<'_, D, I> {
|
||||
&mut self,
|
||||
sample_rate: u32,
|
||||
) -> core::result::Result<(), usbd_uac2::UsbAudioClassError> {
|
||||
defmt::info!("[clock] changing rate to {}", sample_rate);
|
||||
if self.state.load(Ordering::SeqCst) != AudioState::Stopped {
|
||||
defmt::warn!("[clock] changing rate when not stopped, stopping first");
|
||||
self.stop();
|
||||
}
|
||||
let slot_bytes = (self.cur_rate as usize / DMA_RATE) * BYTES_PER_FRAME;
|
||||
dma_ring().set_slot_size(slot_bytes);
|
||||
self.cur_rate = sample_rate;
|
||||
if 24_576_000u32.is_multiple_of(sample_rate) {
|
||||
defmt::info!("[clock] 24M clock selected");
|
||||
defmt::info!("[clock] 24M osc selected");
|
||||
self.clock_pins.sel_22m.set_low().ok();
|
||||
// hal::wait_at_least(1);
|
||||
self.clock_pins.sel_24m.set_high().ok();
|
||||
} else {
|
||||
defmt::info!("[clock] 22M clock selected");
|
||||
defmt::info!("[clock] 22M osc selected");
|
||||
self.clock_pins.sel_24m.set_low().ok();
|
||||
// hal::wait_at_least(1);
|
||||
self.clock_pins.sel_22m.set_high().ok();
|
||||
};
|
||||
self.dac.change_rate(sample_rate);
|
||||
self.cur_rate = sample_rate;
|
||||
self.update_bclk();
|
||||
Ok(())
|
||||
}
|
||||
fn sample_rates(
|
||||
&self,
|
||||
) -> core::result::Result<&[usbd_uac2::RangeEntry<u32>], usbd_uac2::UsbAudioClassError> {
|
||||
Ok(&Self::RATES)
|
||||
defmt::debug!("[clock] sample_rates will return {:?}", &Self::RATES.len());
|
||||
Ok(Self::RATES)
|
||||
}
|
||||
fn clock_validity(&self) -> Result<bool, UsbAudioClassError> {
|
||||
Ok(true)
|
||||
@@ -454,7 +511,8 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
||||
(0, AudioState::Armed | AudioState::Prefill | AudioState::NoData) => {
|
||||
self.transition(AudioState::Stopped)
|
||||
}
|
||||
(0, AudioState::Running | AudioState::Stopped) => {} // noop, we naturally transition through LowData to Stopped
|
||||
(0, AudioState::Running) => self.transition(AudioState::Stopping),
|
||||
(0, AudioState::Stopped | AudioState::Stopping) => {} // already stopped/ing
|
||||
(1, AudioState::Stopped) => self.transition(AudioState::Armed),
|
||||
(1, _) => {} // altSetting 1 in any other state is a no-op
|
||||
(_, _) => {
|
||||
@@ -468,7 +526,8 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
||||
ep: &usb_device::endpoint::Endpoint<'_, B, usb_device::endpoint::Out>,
|
||||
) {
|
||||
let state = self.state.load(Ordering::Relaxed);
|
||||
let mut buf = [0; (SAMPLE_RATE.div_ceil(USB_FRAME_RATE) + 1) as usize * BYTES_PER_FRAME];
|
||||
let mut buf =
|
||||
[0; (MAX_SAMPLE_RATE.div_ceil(USB_FRAME_RATE) + 1) as usize * BYTES_PER_FRAME];
|
||||
let len = match ep.read(&mut buf) {
|
||||
Ok(len) => len,
|
||||
Err(_) => {
|
||||
@@ -481,7 +540,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
||||
|
||||
if res.dropped != 0 {
|
||||
// Overflow: some or all bytes couldn't be queued.
|
||||
defmt::error!(
|
||||
defmt::warn!(
|
||||
"overflowed dma ring, asked {}, wrote {}, dropped {}",
|
||||
buf.len(),
|
||||
res.written,
|
||||
@@ -496,14 +555,16 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
||||
|
||||
// Valid states here are Armed, Prefill, Running, Draining and NoData
|
||||
match state {
|
||||
AudioState::Stopped => {
|
||||
defmt::error!("Received audio data when stopped")
|
||||
AudioState::Stopped | AudioState::Stopping => {
|
||||
defmt::error!("Received audio data when stopped/stopping")
|
||||
}
|
||||
// When armed, data rx goes to prefill
|
||||
AudioState::Armed => self.transition(AudioState::Prefill),
|
||||
// When prefilling, if we have received frames over the up threshold, move to running
|
||||
AudioState::Prefill => {
|
||||
if PERF.received_frames.load(Ordering::Relaxed) >= QUEUE_RUNNING_UP {
|
||||
if PERF.received_frames.load(Ordering::Relaxed) >= queue_running_up_threshold()
|
||||
// 50%
|
||||
{
|
||||
self.transition(AudioState::Running);
|
||||
}
|
||||
}
|
||||
@@ -513,7 +574,7 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
||||
AudioState::LowData => {
|
||||
let fill = cur_fill() as usize;
|
||||
// Do we check alt setting here? We shouldn't be receiving data at all if we are not in altSetting 1
|
||||
if fill >= QUEUE_RUNNING_UP {
|
||||
if fill >= queue_running_up_threshold() {
|
||||
self.transition(AudioState::Running);
|
||||
} else if fill == 0 && self.alt_setting == 0 {
|
||||
self.transition(AudioState::Stopped);
|
||||
@@ -542,28 +603,43 @@ impl<D: Dac<I>, I, B: bus::UsbBus> AudioHandler<'_, B> for Audio<'_, D, I> {
|
||||
PERF.queue_underflows.fetch_add(1, Ordering::Relaxed);
|
||||
return Some(nominal_rate);
|
||||
}
|
||||
|
||||
PERF.avg_fill
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| {
|
||||
Some(((v << 6) - v + current_bytes as usize) >> 6)
|
||||
})
|
||||
.ok();
|
||||
|
||||
// normalize error wrt. frame size etc.
|
||||
let error_permille = ((current_bytes - FILL_TARGET_BYTES) * 1000) / FILL_TARGET_BYTES;
|
||||
let raw_error = current_bytes - cur_fill_target();
|
||||
let i_error = if raw_error.abs() <= 4 { 0 } else { raw_error }; // deadband
|
||||
let current_i = self.fb.integrator.load(Ordering::Relaxed);
|
||||
let leak = current_i >> 7;
|
||||
let new_i = current_i
|
||||
.saturating_sub(leak)
|
||||
.saturating_add(i_error)
|
||||
.clamp(-5000, 5000);
|
||||
self.fb.integrator.store(new_i, Ordering::Relaxed);
|
||||
PERF.integrator.store(new_i, Ordering::Relaxed);
|
||||
|
||||
let nominal_v = nominal_rate.to_u32_12_13() as i32;
|
||||
let max_allowed_deviation = nominal_v / 500; // 0.2%
|
||||
|
||||
// 0.2% which is a huge clock error
|
||||
let max_allowed_deviation = nominal_v / 500;
|
||||
// 3. SEPARATE GAINS FOR P AND I
|
||||
// For P: Keep your working math (converting raw error to a permille equivalent scale)
|
||||
let error_permille = (raw_error * 1000) / cur_fill_target();
|
||||
let p_term = (-((error_permille as i64) * (nominal_v as i64)) / (10 * 256000)) as i32;
|
||||
let i_term = (-((new_i as i64) * (nominal_v as i64)) / (256000 * 1000)) as i32;
|
||||
let i_term = 0;
|
||||
|
||||
let p_term = -(error_permille * nominal_v) / 256000; // this works reasonably well to keep the buffer
|
||||
let i_term = 0; // placeholder
|
||||
PERF.p.store(p_term, Ordering::Relaxed);
|
||||
PERF.i.store(i_term, Ordering::Relaxed);
|
||||
|
||||
let mut v = nominal_v + p_term + i_term;
|
||||
v = v.clamp(
|
||||
nominal_v - max_allowed_deviation,
|
||||
nominal_v + max_allowed_deviation,
|
||||
);
|
||||
PERF.fb.store(v, Ordering::Relaxed);
|
||||
|
||||
Some(UsbIsochronousFeedback::new(v as u32))
|
||||
}
|
||||
@@ -579,6 +655,14 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
|
||||
syscon.reset(&mut fc7);
|
||||
syscon.enable_clock(&mut fc7);
|
||||
|
||||
unsafe {
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fcclksel7()
|
||||
.modify(|_, w| w.sel().enum_0x5()); // MCLK
|
||||
}
|
||||
#[cfg(not(feature = "evk"))]
|
||||
unsafe {
|
||||
pac::IOCON::ptr().as_ref().unwrap().pio0_23.modify(|_, w| {
|
||||
w.func()
|
||||
@@ -599,12 +683,40 @@ pub fn init_i2s(mut fc7: pac::FLEXCOMM7, i2s7: pac::I2S7, syscon: &mut Syscon) -
|
||||
.unwrap()
|
||||
.mclkio
|
||||
.modify(|_, w| w.mclkio().input());
|
||||
};
|
||||
|
||||
#[cfg(feature = "evk")]
|
||||
unsafe {
|
||||
pac::IOCON::ptr().as_ref().unwrap().pio1_31.modify(|_, w| {
|
||||
w.func()
|
||||
.alt1()
|
||||
.mode()
|
||||
.inactive()
|
||||
.slew()
|
||||
.fast()
|
||||
.invert()
|
||||
.disabled()
|
||||
.digimode()
|
||||
.digital()
|
||||
.od()
|
||||
.normal()
|
||||
});
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fcclksel7()
|
||||
.modify(|_, w| w.sel().enum_0x5()); // MCLK
|
||||
};
|
||||
.mclkclksel
|
||||
.modify(|_, w| w.sel().enum_0x1()); // PLL0
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mclkdiv
|
||||
.modify(|_, w| w.div().bits(1).halt().run().reset().released()); // div by 2 = PLL0 fout / 2 = 12.288MHz, max for WM8904 @ 96k
|
||||
pac::SYSCON::ptr()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mclkio
|
||||
.modify(|_, w| w.mclkio().output());
|
||||
}
|
||||
|
||||
// Select I2S TX function
|
||||
fc7.pselid.write(|w| w.persel().i2s_transmit());
|
||||
@@ -629,10 +741,17 @@ fn main() -> ! {
|
||||
let usb0_vbus_pin = pins::Pio0_22::take()
|
||||
.unwrap()
|
||||
.into_usb0_vbus_pin(&mut iocon);
|
||||
|
||||
#[cfg(not(feature = "evk"))]
|
||||
let codec_i2c_pins = (
|
||||
pins::Pio0_16::take().unwrap().into_i2c4_scl_pin(&mut iocon),
|
||||
pins::Pio0_5::take().unwrap().into_i2c4_sda_pin(&mut iocon),
|
||||
);
|
||||
#[cfg(feature = "evk")]
|
||||
let codec_i2c_pins = (
|
||||
pins::Pio1_20::take().unwrap().into_i2c4_scl_pin(&mut iocon),
|
||||
pins::Pio1_21::take().unwrap().into_i2c4_sda_pin(&mut iocon),
|
||||
);
|
||||
let codec_i2s_pins = (
|
||||
pins::Pio0_21::take().unwrap().into_spi7_sck_pin(&mut iocon),
|
||||
pins::Pio0_20::take().unwrap().into_i2s7_sda_pin(&mut iocon),
|
||||
@@ -655,16 +774,8 @@ fn main() -> ! {
|
||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||
.into_output_low(),
|
||||
};
|
||||
let leds = (
|
||||
pins::Pio0_13::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||
.into_output_low(),
|
||||
pins::Pio0_14::take()
|
||||
.unwrap()
|
||||
.into_gpio_pin(&mut iocon, &mut gpio)
|
||||
.into_output_low(),
|
||||
);
|
||||
|
||||
hw::init_leds(&mut iocon, &mut gpio);
|
||||
|
||||
// iocon.disabled(&mut syscon).release(); // save the environment :)
|
||||
|
||||
@@ -674,6 +785,9 @@ fn main() -> ! {
|
||||
.configure(&mut anactrl, &mut pmc, &mut syscon)
|
||||
.unwrap();
|
||||
hw::init_sys_pll1();
|
||||
#[cfg(feature = "evk")]
|
||||
hw::init_audio_pll();
|
||||
|
||||
let mut delay_timer = Timer::new(
|
||||
hal.ctimer
|
||||
.0
|
||||
@@ -709,8 +823,12 @@ fn main() -> ! {
|
||||
|
||||
defmt::info!("dma init");
|
||||
let i2s_dma_addr = &i2s_peripheral.i2s.fifowr as *const _ as *mut u32;
|
||||
let dma =
|
||||
DmaRing::<N_SLOTS, BYTES_PER_SLOT>::new(hal.dma.release(), &mut syscon, i2s_dma_addr, 4)
|
||||
let dma = DmaRing::<N_SLOTS, MAX_BYTES_PER_SLOT>::new(
|
||||
hal.dma.release(),
|
||||
&mut syscon,
|
||||
i2s_dma_addr,
|
||||
4,
|
||||
)
|
||||
.unwrap();
|
||||
let dma_ref = DMA_RING.init(dma);
|
||||
unsafe { DMA_RING_REF = Some(dma_ref) };
|
||||
@@ -724,7 +842,7 @@ fn main() -> ! {
|
||||
fb: FeedbackState::default(),
|
||||
alt_setting: 0,
|
||||
nodata_timeout_frame: AtomicUsize::new(0),
|
||||
cur_rate: SAMPLE_RATE,
|
||||
cur_rate: SAMPLE_RATES[0].min,
|
||||
clock_pins: clock_sel_pins,
|
||||
_marker: core::marker::PhantomData,
|
||||
};
|
||||
@@ -762,22 +880,38 @@ fn main() -> ! {
|
||||
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
|
||||
|
||||
move || {
|
||||
let active = usb_dev.poll(&mut [&mut uac2, &mut hid]);
|
||||
if active && hid_update_timer.wait().is_ok() {
|
||||
usb_dev.poll(&mut [&mut uac2, &mut hid]);
|
||||
// DMA ring is empty; if altsetting = 0 then -> stopped else NoData
|
||||
// TODO: handle NoData state
|
||||
if NODATA_FLAG.swap(false, Ordering::Acquire) {
|
||||
match uac2.handler().state.load(Ordering::Acquire) {
|
||||
AudioState::Stopping => uac2.handler().transition(AudioState::Stopped),
|
||||
_ => uac2.handler().transition(AudioState::Stopped),
|
||||
}
|
||||
}
|
||||
if hid_update_timer.wait().is_ok() {
|
||||
let report = PERF.build_report();
|
||||
match hid.push_input(&report) {
|
||||
Ok(_) => {}
|
||||
Err(UsbError::WouldBlock) => {}
|
||||
Err(e) => defmt::error!("Failed to send HID report: {:?}", e),
|
||||
}
|
||||
// lpc55 timer is not Periodic, so restart it
|
||||
// lpc55 ctimer is not Periodic, so restart it
|
||||
hid_update_timer.start(Microseconds::new(HID_INTERVAL_MS as u32 * 1000));
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(not(feature = "hid"))]
|
||||
let poll_all = || {
|
||||
let mut poll_all = {
|
||||
move || {
|
||||
usb_dev.poll(&mut [&mut uac2]);
|
||||
if NODATA_FLAG.swap(false, Ordering::Acquire) {
|
||||
match uac2.handler().state.load(Ordering::Acquire) {
|
||||
AudioState::Stopping => uac2.handler().transition(AudioState::Stopped),
|
||||
_ => uac2.handler().transition(AudioState::Stopped),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
defmt::info!("main loop");
|
||||
@@ -0,0 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.95.0"
|
||||
targets = ["thumbv8m.main-none-eabihf"]
|
||||
components = ["llvm-tools-preview"]
|
||||
@@ -1,39 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
from time import sleep
|
||||
from typing import Self
|
||||
import hid
|
||||
|
||||
VID = 0x1209
|
||||
PID = 0xCC1D
|
||||
INTERVAL = 0.1
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioTelemetry:
|
||||
STRUCT = "<LLLLL"
|
||||
LEN = struct.calcsize(STRUCT)
|
||||
|
||||
average_buffer_fill: int
|
||||
frame_count: int
|
||||
dac_underflow_count: int
|
||||
usb_underflow_count: int
|
||||
dac_overflow_count: int
|
||||
|
||||
def from_bytes(b: bytes) -> Self:
|
||||
if len(b) != AudioTelemetry.LEN:
|
||||
raise ValueError(f"wrong size report ({len(b)} != {AudioTelemetry.LEN})")
|
||||
fields = struct.unpack(AudioTelemetry.STRUCT, b)
|
||||
return AudioTelemetry(*fields)
|
||||
|
||||
|
||||
def main():
|
||||
with hid.Device(VID, PID) as h:
|
||||
while True:
|
||||
report = AudioTelemetry.from_bytes(h.read(AudioTelemetry.LEN))
|
||||
print(f"{report}")
|
||||
sleep(INTERVAL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,10 +0,0 @@
|
||||
[project]
|
||||
name = "guac-scripts"
|
||||
version = "0.1.0"
|
||||
description = "Scripts to work with GUAC devices"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"hid>=1.0.9",
|
||||
"rich-click>=1.9.7",
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "shared"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
std = []
|
||||
serde = ["dep:serde"]
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
defmt = "1.1.1"
|
||||
deku = { version = "0.20.3", default-features = false }
|
||||
num_enum = { version = "0.7.6", default-features = false }
|
||||
serde = { version = "1.0.228", optional = true, features = ["derive"] }
|
||||
usbd-hid = { version = "0.10.0" }
|
||||
@@ -0,0 +1,162 @@
|
||||
#![no_std]
|
||||
|
||||
use bytemuck::NoUninit;
|
||||
use num_enum::TryFromPrimitive;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, NoUninit, Eq, PartialEq, TryFromPrimitive)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum AudioState {
|
||||
/// Knowingly stopped, ie. AltSetting=0. DAC muted, I2S disabled.
|
||||
///
|
||||
/// AltSetting = 1 -> ARMED
|
||||
Stopped = 0,
|
||||
/// Waiting for data. DAC muted, I2S running sending 0s (FIFO not serviced).
|
||||
///
|
||||
/// USB OUT data packet -> ARMED
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
Armed = 1,
|
||||
/// Filling the buffer before playback starts. Feedback does not run,
|
||||
/// playout does not start draining the queue. Gets us better feedback
|
||||
/// behaviour and a full buffer without a feedback rate spike at startup.
|
||||
///
|
||||
/// queue reaches <QUEUE_RUNNING_UP> -> RUNNING
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
///
|
||||
Prefill = 2,
|
||||
/// Normal running state. Start servicing FIFO and begin playing out from the buffer.
|
||||
///
|
||||
/// queue reaches <QUEUE_RUNNING_DOWN> -> DRAINING
|
||||
/// AltSetting = 0 -> DRAINING
|
||||
Running = 3,
|
||||
/// The queue is low. We will continue playout.
|
||||
///
|
||||
/// queue is empty && altSetting 1 -> NODATA
|
||||
/// queue is empty && altSetting 0 -> STOPPED
|
||||
/// queue reaches <QUEUE_RUNNING_UP> && altSetting 1 -> RUNNING
|
||||
LowData = 4,
|
||||
/// There is no data in the queue. We will count underflows for a while, send 0s, and hope the host comes back.
|
||||
///
|
||||
/// countdown reaches DATA_TIMEOUT -> STOPPED
|
||||
/// AltSetting = 0 -> STOPPED
|
||||
NoData = 5,
|
||||
/// The host has asked us to stop (altSetting 0), but we need to play out the remaining buffer
|
||||
///
|
||||
/// queue is empty -> STOPPED
|
||||
Stopping = 6,
|
||||
}
|
||||
|
||||
impl Default for AudioState {
|
||||
fn default() -> Self {
|
||||
AudioState::Stopped
|
||||
}
|
||||
}
|
||||
impl defmt::Format for AudioState {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
defmt::write!(
|
||||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Stopped => "Stopped",
|
||||
Self::Armed => "Armed",
|
||||
Self::Prefill => "Prefill",
|
||||
Self::Running => "Running",
|
||||
Self::LowData => "Draining",
|
||||
Self::NoData => "NoData",
|
||||
Self::Stopping => "Stopping",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for AudioState {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Stopped => "Stopped",
|
||||
Self::Armed => "Armed",
|
||||
Self::Prefill => "Prefill",
|
||||
Self::Running => "Running",
|
||||
Self::LowData => "Draining",
|
||||
Self::NoData => "NoData",
|
||||
Self::Stopping => "Stopping",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct AudioTelemetrySnapshot {
|
||||
pub state: AudioState,
|
||||
pub average_buffer_fill: u16,
|
||||
pub frame_count: u32,
|
||||
pub dac_underflow_count: u16,
|
||||
pub usb_underflow_count: u16,
|
||||
pub dac_overflow_count: u16,
|
||||
pub p: i32,
|
||||
pub i: i32,
|
||||
pub fb: i32,
|
||||
}
|
||||
|
||||
pub mod hid {
|
||||
use deku::DekuRead;
|
||||
use usbd_hid::descriptor::generator_prelude::*;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{AudioState, AudioTelemetrySnapshot};
|
||||
|
||||
#[derive(DekuRead)]
|
||||
#[deku(endian = "little")]
|
||||
#[gen_hid_descriptor(
|
||||
(collection = APPLICATION, usage_page = VENDOR_DEFINED_START, usage = 0x01, ) = {
|
||||
state=input;
|
||||
average_buffer_fill=input;
|
||||
frame_count=input;
|
||||
dac_underflow_count=input;
|
||||
usb_underflow_count=input;
|
||||
dac_overflow_count=input;
|
||||
p=input;
|
||||
i=input;
|
||||
fb=input;
|
||||
integrator=input;
|
||||
}
|
||||
)]
|
||||
#[repr(C, packed)]
|
||||
// TODO: Fix that most of these values are actually u32, but usbd_hid macro doesn't work properly on u32
|
||||
pub struct AudioTelemetryReport {
|
||||
pub state: u8,
|
||||
pub average_buffer_fill: u16,
|
||||
pub frame_count: i32,
|
||||
pub dac_underflow_count: u16,
|
||||
pub usb_underflow_count: u16,
|
||||
pub dac_overflow_count: u16,
|
||||
pub p: i32,
|
||||
pub i: i32,
|
||||
pub fb: i32,
|
||||
pub integrator: i32,
|
||||
}
|
||||
|
||||
impl From<AudioTelemetryReport> for AudioTelemetrySnapshot {
|
||||
fn from(value: AudioTelemetryReport) -> Self {
|
||||
AudioTelemetrySnapshot {
|
||||
state: AudioState::try_from(value.state).expect("Invalid AudioState"),
|
||||
average_buffer_fill: value.average_buffer_fill,
|
||||
frame_count: i32::cast_unsigned(value.frame_count), // on firmware side is usize == u32
|
||||
dac_underflow_count: value.dac_underflow_count,
|
||||
usb_underflow_count: value.usb_underflow_count,
|
||||
dac_overflow_count: value.dac_overflow_count,
|
||||
p: value.p,
|
||||
i: value.i,
|
||||
fb: value.fb,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
use usbd_hid::descriptor::generator_prelude::*;
|
||||
|
||||
#[gen_hid_descriptor(
|
||||
(collection = APPLICATION, usage_page = VENDOR_DEFINED_START, usage = 0x01, ) = {
|
||||
average_buffer_fill=input;
|
||||
frame_count=input;
|
||||
dac_underflow_count=input;
|
||||
usb_underflow_count=input;
|
||||
dac_overflow_count=input;
|
||||
}
|
||||
)]
|
||||
#[repr(C)]
|
||||
// Note these are all actually u32
|
||||
pub struct AudioTelemetryReport {
|
||||
pub average_buffer_fill: i32,
|
||||
pub frame_count: i32,
|
||||
pub dac_underflow_count: i32,
|
||||
pub usb_underflow_count: i32,
|
||||
pub dac_overflow_count: i32,
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
use crate::pac;
|
||||
use defmt::debug;
|
||||
|
||||
pub(crate) struct PllConstants {
|
||||
pub m: u16, // 1-65535
|
||||
pub n: u8, // 1-255
|
||||
pub p: u8, // 1-31
|
||||
pub selp: u8, // 5 bits
|
||||
pub seli: u8, // 6 bits
|
||||
}
|
||||
|
||||
impl PllConstants {
|
||||
pub(crate) const fn new(n: u8, m: u16, p: u8) -> Self {
|
||||
assert!(n != 0, "1 <= N <= 255");
|
||||
assert!(m != 0, "1 <= M <= 65535");
|
||||
assert!(p != 0 && p <= 31, "1 <= P <= 31");
|
||||
|
||||
// Following ripped from lpc55-hal and made const
|
||||
// UM 4.6.6.3.2
|
||||
let selp = {
|
||||
let v = (m >> 2) + 1;
|
||||
if v < 31 { v } else { 31 }
|
||||
} as u8;
|
||||
|
||||
let seli = {
|
||||
let v = match m {
|
||||
m if m >= 8000 => 1,
|
||||
m if m >= 122 => 8000 / m,
|
||||
_ => 2 * (m >> 2) + 3,
|
||||
};
|
||||
|
||||
if v < 63 { v } else { 63 }
|
||||
} as u8;
|
||||
// let seli = min(2*(m >> 2) + 3, 63);
|
||||
Self {
|
||||
n,
|
||||
m,
|
||||
p,
|
||||
selp,
|
||||
seli,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl defmt::Format for PllConstants {
|
||||
fn format(&self, fmt: defmt::Formatter) {
|
||||
let factor = f32::from(self.m) / (f32::from(self.n) * 2.0 * f32::from(self.p));
|
||||
|
||||
defmt::write!(
|
||||
fmt,
|
||||
"m: {} n: {} p: {} selp: {} seli: {} fout: fin * {}",
|
||||
self.m,
|
||||
self.n,
|
||||
self.p,
|
||||
self.selp,
|
||||
self.seli,
|
||||
factor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SYS_PLL: PllConstants = PllConstants::new(4, 75, 1); // 150MHz
|
||||
|
||||
pub(crate) fn init_sys_pll1() {
|
||||
let syscon = unsafe { &*pac::SYSCON::ptr() };
|
||||
let pmc = unsafe { &*pac::PMC::ptr() };
|
||||
let anactrl = unsafe { &*pac::ANACTRL::ptr() };
|
||||
|
||||
debug!("start clk_in");
|
||||
pmc.pdruncfg0
|
||||
.modify(|_, w| w.pden_xtal32m().poweredon().pden_ldoxo32m().poweredon());
|
||||
syscon.clock_ctrl.modify(|_, w| w.clkin_ena().enable());
|
||||
anactrl
|
||||
.xo32m_ctrl
|
||||
.modify(|_, w| w.enable_system_clk_out().enable());
|
||||
|
||||
debug!("init pll1: {}", SYS_PLL);
|
||||
pmc.pdruncfg0.modify(|_, w| w.pden_pll1().poweredoff());
|
||||
syscon.pll1clksel.write(|w| w.sel().enum_0x1()); // clk_in
|
||||
syscon.pll1ctrl.write(|w| unsafe {
|
||||
w.clken()
|
||||
.enable()
|
||||
.seli()
|
||||
.bits(SYS_PLL.seli)
|
||||
.selp()
|
||||
.bits(SYS_PLL.selp)
|
||||
});
|
||||
|
||||
syscon
|
||||
.pll1ndec
|
||||
.write(|w| unsafe { w.ndiv().bits(SYS_PLL.n) });
|
||||
syscon.pll1ndec.write(|w| unsafe {
|
||||
w.ndiv().bits(SYS_PLL.n).nreq().set_bit() // latch
|
||||
});
|
||||
syscon
|
||||
.pll1mdec
|
||||
.write(|w| unsafe { w.mdiv().bits(SYS_PLL.m) });
|
||||
syscon
|
||||
.pll1pdec
|
||||
.write(|w| unsafe { w.pdiv().bits(SYS_PLL.p) });
|
||||
syscon.pll1pdec.write(|w| unsafe {
|
||||
w.pdiv().bits(SYS_PLL.p).preq().set_bit() // latch
|
||||
});
|
||||
|
||||
pmc.pdruncfg0.modify(|_, w| w.pden_pll1().poweredon());
|
||||
debug!("pll1 wait for lock");
|
||||
let mut i = 0usize;
|
||||
while syscon.pll1stat.read().lock().bit_is_clear() {
|
||||
i += 1;
|
||||
}
|
||||
debug!("pll1 locked after {} tries", i);
|
||||
// switch system clock to pll1
|
||||
syscon.fmccr.modify(|_, w| w.flashtim().flashtim11());
|
||||
syscon.mainclkselb.modify(|_, w| w.sel().enum_0x2()); // pll1
|
||||
}
|
||||
Reference in New Issue
Block a user