diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/README.md b/firmware/firmware-makerpnpcontrolcore/fpga-pac/README.md index 57cb3b1..d0d6540 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/README.md +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/README.md @@ -63,11 +63,15 @@ OctoSPI transaction that writes to 2 adjacent registers. This is currently a manual process. * rebase on a commit before the patches were applied -* move patch comments to the end of commit history +* COPY patch commits to the end of commit history * add a break before the commits are applied * disable patches in rebuild.sh, commit * rebuild, commit unpatched svd. -* continue rebasing, updating patches. -* run git format patch HEAD~n for the patch commits -* revert the disable patches commit -* run rebuild, commit updated patches. \ No newline at end of file +* continue rebasing, updating patch commits, resolve merge conflicts if they don't apply cleanly to the generated code. do not modify the patch files themselves. +* run git format-patch HEAD~n for the patch commits +* move the updated patch files to the patches directory. +* run git add on the updated patch files. +* run git rm on any unused patch files. +* add the patches back to rebuild.sh, run rebuild.sh, when it applies cleanly commit updated patches and rebuild.sh as one commit. +* if any new patches are required, create commits that modify the generated code. then as a separate commit, use format-patch again and add them to the repo. +* rebase if required to clean up the history. diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0001-delete-interrupt-block.patch b/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0001-delete-interrupt-block.patch index 77e94a6..02d0b3e 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0001-delete-interrupt-block.patch +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0001-delete-interrupt-block.patch @@ -1,14 +1,14 @@ -From b4fa5bd97d767d5fc472c04f407012602f916eb4 Mon Sep 17 00:00:00 2001 +From cc69b3a804fdc73da51e8892d88a3b778e8bb4cd Mon Sep 17 00:00:00 2001 From: Dominic Clifton Date: Sun, 28 Jun 2026 15:50:00 +0200 -Subject: [PATCH 1/4] delete-interrupt-block +Subject: [PATCH 1/3] delete-interrupt-block --- .../fpga-pac/src/lib.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs -index 35f226f..a723787 100644 +index 93aa9cf..ae90b98 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs @@ -3,15 +3,6 @@ @@ -27,8 +27,8 @@ index 35f226f..a723787 100644 #[cfg(feature = "rt")] mod _vectors { unsafe extern "C" {} -@@ -39,10 +30,6 @@ pub const ENCODERS: encoders::encoders = - unsafe { encoders::encoders::from_ptr(0x9000_0c00usize as _) }; +@@ -45,10 +36,6 @@ pub const STEPPERS: steppers::steppers = + unsafe { steppers::steppers::from_ptr(0x9000_0d00usize as _) }; #[doc = "system block 1"] pub const SYSTEM1: system1::system1 = unsafe { system1::system1::from_ptr(0x9000_ff00usize as _) }; -#[cfg(feature = "rt")] diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0002-delete-rt-block.patch b/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0002-delete-rt-block.patch index a653d52..68dab9d 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0002-delete-rt-block.patch +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0002-delete-rt-block.patch @@ -1,14 +1,14 @@ -From e91fc7b6296553ffdca529a117dee27f396522cb Mon Sep 17 00:00:00 2001 +From 8dc618fc00d1e068053b50eaed89a3274e8ff0b9 Mon Sep 17 00:00:00 2001 From: Dominic Clifton Date: Thu, 18 Jun 2026 12:19:11 +0200 -Subject: [PATCH 2/4] delete-rt-block +Subject: [PATCH 2/3] delete-rt-block --- .../firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs -index a723787..f087c24 100644 +index ae90b98..c20d45f 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs @@ -3,17 +3,6 @@ diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0003-octospi-fifo-bypass.patch b/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0003-octospi-fifo-bypass.patch index 341284b..b37f778 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0003-octospi-fifo-bypass.patch +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0003-octospi-fifo-bypass.patch @@ -1,17 +1,17 @@ -From 161ac28ee2590e5d6a16f1cec31ef55f88bd2a83 Mon Sep 17 00:00:00 2001 +From 4bad8de36ab42e6fa5799e6598e75802843ca5cb Mon Sep 17 00:00:00 2001 From: Dominic Clifton Date: Thu, 18 Jun 2026 12:08:08 +0200 -Subject: [PATCH 3/4] octospi-fifo-bypass +Subject: [PATCH 3/3] octospi-fifo-bypass --- .../fpga-pac/src/lib.rs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs -index f087c24..8d2c6ac 100644 +index c20d45f..1a98bf6 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs -@@ -102,7 +102,47 @@ pub mod buzzer { +@@ -108,7 +108,47 @@ pub mod buzzer { } } pub mod common { @@ -59,7 +59,7 @@ index f087c24..8d2c6ac 100644 #[derive(Copy, Clone, PartialEq, Eq)] pub struct RW; #[derive(Copy, Clone, PartialEq, Eq)] -@@ -147,12 +187,28 @@ pub mod common { +@@ -153,12 +193,28 @@ pub mod common { self.ptr as _ } } diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0004-add-ws2812_1.patch b/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0004-add-ws2812_1.patch deleted file mode 100644 index bcd8d31..0000000 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/patches/0004-add-ws2812_1.patch +++ /dev/null @@ -1,26 +0,0 @@ -From d63527c7e2aa4b2292f1252c6b2e458a40856f6a Mon Sep 17 00:00:00 2001 -From: Dominic Clifton -Date: Sun, 28 Jun 2026 16:22:28 +0200 -Subject: [PATCH 4/4] add-ws2812_1 - ---- - firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs | 3 +++ - 1 file changed, 3 insertions(+) - -diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs -index 8d2c6ac..228ffb9 100644 ---- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs -+++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs -@@ -14,6 +14,9 @@ pub const IO: io::io = unsafe { io::io::from_ptr(0x9000_0400usize as _) }; - #[doc = "ws2812 RGB LED block"] - pub const WS2812_0: ws2812_0::ws2812_0 = - unsafe { ws2812_0::ws2812_0::from_ptr(0x9000_0800usize as _) }; -+#[doc = "ws2812 RGB LED block"] -+pub const WS2812_1: ws2812_0::ws2812_0 = -+ unsafe { ws2812_0::ws2812_0::from_ptr(0x9000_0900usize as _) }; - #[doc = "encoders control block"] - pub const ENCODERS: encoders::encoders = - unsafe { encoders::encoders::from_ptr(0x9000_0c00usize as _) }; --- -2.53.0.windows.2 - diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/rebuild.sh b/firmware/firmware-makerpnpcontrolcore/fpga-pac/rebuild.sh index 03fb842..ee866df 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/rebuild.sh +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/rebuild.sh @@ -5,5 +5,4 @@ rustfmt src/lib.rs git apply < patches/0001-delete-interrupt-block.patch git apply < patches/0002-delete-rt-block.patch git apply < patches/0003-octospi-fifo-bypass.patch -git apply < patches/0004-add-ws2812_1.patch rm src/device.x diff --git a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs index 228ffb9..f771e9b 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs +++ b/firmware/firmware-makerpnpcontrolcore/fpga-pac/src/lib.rs @@ -14,12 +14,15 @@ pub const IO: io::io = unsafe { io::io::from_ptr(0x9000_0400usize as _) }; #[doc = "ws2812 RGB LED block"] pub const WS2812_0: ws2812_0::ws2812_0 = unsafe { ws2812_0::ws2812_0::from_ptr(0x9000_0800usize as _) }; -#[doc = "ws2812 RGB LED block"] +#[doc = "ws2812 RGB LED block (second instance)"] pub const WS2812_1: ws2812_0::ws2812_0 = unsafe { ws2812_0::ws2812_0::from_ptr(0x9000_0900usize as _) }; #[doc = "encoders control block"] pub const ENCODERS: encoders::encoders = unsafe { encoders::encoders::from_ptr(0x9000_0c00usize as _) }; +#[doc = "8-channel stepper motor step/dir pulse generator (2 banks of 4)"] +pub const STEPPERS: steppers::steppers = + unsafe { steppers::steppers::from_ptr(0x9000_0d00usize as _) }; #[doc = "system block 1"] pub const SYSTEM1: system1::system1 = unsafe { system1::system1::from_ptr(0x9000_ff00usize as _) }; pub mod buzzer { @@ -1511,6 +1514,1532 @@ pub mod led { } } } +pub mod steppers { + #[doc = "8-channel stepper motor step/dir pulse generator (2 banks of 4)."] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct steppers { + ptr: *mut u8, + } + unsafe impl Send for steppers {} + unsafe impl Sync for steppers {} + impl steppers { + #[inline(always)] + pub const unsafe fn from_ptr(ptr: *mut ()) -> Self { + Self { ptr: ptr as _ } + } + #[inline(always)] + pub const fn as_ptr(&self) -> *mut () { + self.ptr as _ + } + #[doc = "start/stop control - WRITE: start/stop strobes per motor (bank 0 = motors 0-3, bank 1 = motors 4-7); READ: per-motor moving status."] + #[inline(always)] + pub const fn step_ctrl(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x0usize) as _) } + } + #[doc = "segment table streaming configuration - selects which motor subsequent step_seg_ctst/step_seg_spdm writes and reads target, and rewinds both the write and read pointers to segment 0."] + #[inline(always)] + pub const fn step_tx_config( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x04usize) as _) } + } + #[doc = "per-motor step pulse width preset (tick count - 1). Combined with that motor's bank prescaler in step_pls_prescaler: pulse width = (prescaler+1) * (preset+1) sys_clk cycles."] + #[inline(always)] + pub const fn step_pls_config( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x08usize) as _) } + } + #[doc = "per-bank step pulse width prescaler (sys_clk cycles per tick - 1). Bank 0 = motors 0-3 (low 16-bit half, bits\\[5:0\\]), bank 1 = motors 4-7 (high 16-bit half, bits\\[21:16\\]) - split this way so either field can widen, or other per-bank config bits can be added, without touching the other bank's half."] + #[inline(always)] + pub const fn step_pls_prescaler( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x0cusize) as _) } + } + #[doc = "segment table CTST word - command/direction/step-count for the segment addressed by step_tx_config's motor_instance and the current streaming pointer (advances on step_seg_spdm access, not this one)."] + #[inline(always)] + pub const fn step_seg_ctst( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x10usize) as _) } + } + #[doc = "segment table SPDM word - start period/delta magnitude for the segment addressed by step_tx_config's motor_instance and the current streaming pointer (advances the streaming pointer on access - write or read)."] + #[inline(always)] + pub const fn step_seg_spdm( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x14usize) as _) } + } + #[doc = "motor 0 status."] + #[inline(always)] + pub const fn step_status_0( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x20usize) as _) } + } + #[doc = "motor 1 status."] + #[inline(always)] + pub const fn step_status_1( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x24usize) as _) } + } + #[doc = "motor 2 status."] + #[inline(always)] + pub const fn step_status_2( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x28usize) as _) } + } + #[doc = "motor 3 status."] + #[inline(always)] + pub const fn step_status_3( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x2cusize) as _) } + } + #[doc = "motor 4 status."] + #[inline(always)] + pub const fn step_status_4( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x30usize) as _) } + } + #[doc = "motor 5 status."] + #[inline(always)] + pub const fn step_status_5( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x34usize) as _) } + } + #[doc = "motor 6 status."] + #[inline(always)] + pub const fn step_status_6( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x38usize) as _) } + } + #[doc = "motor 7 status."] + #[inline(always)] + pub const fn step_status_7( + self, + ) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x3cusize) as _) } + } + #[doc = "motor 0 current absolute position, in steps (signed, two's complement)."] + #[inline(always)] + pub const fn step_pos_0(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x40usize) as _) } + } + #[doc = "motor 1 current absolute position, in steps (signed, two's complement)."] + #[inline(always)] + pub const fn step_pos_1(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x44usize) as _) } + } + #[doc = "motor 2 current absolute position, in steps (signed, two's complement)."] + #[inline(always)] + pub const fn step_pos_2(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x48usize) as _) } + } + #[doc = "motor 3 current absolute position, in steps (signed, two's complement)."] + #[inline(always)] + pub const fn step_pos_3(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x4cusize) as _) } + } + #[doc = "motor 4 current absolute position, in steps (signed, two's complement)."] + #[inline(always)] + pub const fn step_pos_4(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x50usize) as _) } + } + #[doc = "motor 5 current absolute position, in steps (signed, two's complement)."] + #[inline(always)] + pub const fn step_pos_5(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x54usize) as _) } + } + #[doc = "motor 6 current absolute position, in steps (signed, two's complement)."] + #[inline(always)] + pub const fn step_pos_6(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x58usize) as _) } + } + #[doc = "motor 7 current absolute position, in steps (signed, two's complement)."] + #[inline(always)] + pub const fn step_pos_7(self) -> crate::common::Reg { + unsafe { crate::common::Reg::from_ptr(self.ptr.wrapping_add(0x5cusize) as _) } + } + } + pub mod regs { + #[doc = "start/stop control - WRITE: start/stop strobes per motor (bank 0 = motors 0-3, bank 1 = motors 4-7); READ: per-motor moving status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_ctrl(pub u32); + impl step_ctrl { + #[doc = "per-motor moving status (bit N = motor N is moving) - read-only, ignored on write."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> u8 { + let val = (self.0 >> 0usize) & 0xff; + val as u8 + } + #[doc = "per-motor moving status (bit N = motor N is moving) - read-only, ignored on write."] + #[inline(always)] + pub const fn set_moving(&mut self, val: u8) { + self.0 = (self.0 & !(0xff << 0usize)) | (((val as u32) & 0xff) << 0usize); + } + #[doc = "start strobe, bit N = motor N (bank 0, motors 0-3) - write-only, always reads as 0."] + #[must_use] + #[inline(always)] + pub const fn start_bank0(&self) -> u8 { + let val = (self.0 >> 8usize) & 0x0f; + val as u8 + } + #[doc = "start strobe, bit N = motor N (bank 0, motors 0-3) - write-only, always reads as 0."] + #[inline(always)] + pub const fn set_start_bank0(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 8usize)) | (((val as u32) & 0x0f) << 8usize); + } + #[doc = "stop strobe, bit N = motor N (bank 0, motors 0-3) - write-only, always reads as 0."] + #[must_use] + #[inline(always)] + pub const fn stop_bank0(&self) -> u8 { + let val = (self.0 >> 12usize) & 0x0f; + val as u8 + } + #[doc = "stop strobe, bit N = motor N (bank 0, motors 0-3) - write-only, always reads as 0."] + #[inline(always)] + pub const fn set_stop_bank0(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 12usize)) | (((val as u32) & 0x0f) << 12usize); + } + #[doc = "start strobe, bit N = motor (N+4) (bank 1, motors 4-7) - write-only, always reads as 0."] + #[must_use] + #[inline(always)] + pub const fn start_bank1(&self) -> u8 { + let val = (self.0 >> 16usize) & 0x0f; + val as u8 + } + #[doc = "start strobe, bit N = motor (N+4) (bank 1, motors 4-7) - write-only, always reads as 0."] + #[inline(always)] + pub const fn set_start_bank1(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 16usize)) | (((val as u32) & 0x0f) << 16usize); + } + #[doc = "stop strobe, bit N = motor (N+4) (bank 1, motors 4-7) - write-only, always reads as 0."] + #[must_use] + #[inline(always)] + pub const fn stop_bank1(&self) -> u8 { + let val = (self.0 >> 20usize) & 0x0f; + val as u8 + } + #[doc = "stop strobe, bit N = motor (N+4) (bank 1, motors 4-7) - write-only, always reads as 0."] + #[inline(always)] + pub const fn set_stop_bank1(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 20usize)) | (((val as u32) & 0x0f) << 20usize); + } + #[doc = "reserved, keep at reset value."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u8 { + let val = (self.0 >> 24usize) & 0xff; + val as u8 + } + #[doc = "reserved, keep at reset value."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u8) { + self.0 = (self.0 & !(0xff << 24usize)) | (((val as u32) & 0xff) << 24usize); + } + } + impl Default for step_ctrl { + #[inline(always)] + fn default() -> step_ctrl { + step_ctrl(0) + } + } + impl core::fmt::Debug for step_ctrl { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_ctrl") + .field("moving", &self.moving()) + .field("start_bank0", &self.start_bank0()) + .field("stop_bank0", &self.stop_bank0()) + .field("start_bank1", &self.start_bank1()) + .field("stop_bank1", &self.stop_bank1()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_ctrl { + fn format(&self, f: defmt::Formatter) { + defmt :: write ! (f , "step_ctrl {{ moving: {=u8:?}, start_bank0: {=u8:?}, stop_bank0: {=u8:?}, start_bank1: {=u8:?}, stop_bank1: {=u8:?}, reserved: {=u8:?} }}" , self . moving () , self . start_bank0 () , self . stop_bank0 () , self . start_bank1 () , self . stop_bank1 () , self . reserved ()) + } + } + #[doc = "per-motor step pulse width preset (tick count - 1). Combined with that motor's bank prescaler in step_pls_prescaler: pulse width = (prescaler+1) * (preset+1) sys_clk cycles."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pls_config(pub u32); + impl step_pls_config { + #[doc = "motor 0 pulse-width preset (tick count - 1)."] + #[must_use] + #[inline(always)] + pub const fn preset0(&self) -> u8 { + let val = (self.0 >> 0usize) & 0x0f; + val as u8 + } + #[doc = "motor 0 pulse-width preset (tick count - 1)."] + #[inline(always)] + pub const fn set_preset0(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 0usize)) | (((val as u32) & 0x0f) << 0usize); + } + #[doc = "motor 1 pulse-width preset (tick count - 1)."] + #[must_use] + #[inline(always)] + pub const fn preset1(&self) -> u8 { + let val = (self.0 >> 4usize) & 0x0f; + val as u8 + } + #[doc = "motor 1 pulse-width preset (tick count - 1)."] + #[inline(always)] + pub const fn set_preset1(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 4usize)) | (((val as u32) & 0x0f) << 4usize); + } + #[doc = "motor 2 pulse-width preset (tick count - 1)."] + #[must_use] + #[inline(always)] + pub const fn preset2(&self) -> u8 { + let val = (self.0 >> 8usize) & 0x0f; + val as u8 + } + #[doc = "motor 2 pulse-width preset (tick count - 1)."] + #[inline(always)] + pub const fn set_preset2(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 8usize)) | (((val as u32) & 0x0f) << 8usize); + } + #[doc = "motor 3 pulse-width preset (tick count - 1)."] + #[must_use] + #[inline(always)] + pub const fn preset3(&self) -> u8 { + let val = (self.0 >> 12usize) & 0x0f; + val as u8 + } + #[doc = "motor 3 pulse-width preset (tick count - 1)."] + #[inline(always)] + pub const fn set_preset3(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 12usize)) | (((val as u32) & 0x0f) << 12usize); + } + #[doc = "motor 4 pulse-width preset (tick count - 1)."] + #[must_use] + #[inline(always)] + pub const fn preset4(&self) -> u8 { + let val = (self.0 >> 16usize) & 0x0f; + val as u8 + } + #[doc = "motor 4 pulse-width preset (tick count - 1)."] + #[inline(always)] + pub const fn set_preset4(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 16usize)) | (((val as u32) & 0x0f) << 16usize); + } + #[doc = "motor 5 pulse-width preset (tick count - 1)."] + #[must_use] + #[inline(always)] + pub const fn preset5(&self) -> u8 { + let val = (self.0 >> 20usize) & 0x0f; + val as u8 + } + #[doc = "motor 5 pulse-width preset (tick count - 1)."] + #[inline(always)] + pub const fn set_preset5(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 20usize)) | (((val as u32) & 0x0f) << 20usize); + } + #[doc = "motor 6 pulse-width preset (tick count - 1)."] + #[must_use] + #[inline(always)] + pub const fn preset6(&self) -> u8 { + let val = (self.0 >> 24usize) & 0x0f; + val as u8 + } + #[doc = "motor 6 pulse-width preset (tick count - 1)."] + #[inline(always)] + pub const fn set_preset6(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 24usize)) | (((val as u32) & 0x0f) << 24usize); + } + #[doc = "motor 7 pulse-width preset (tick count - 1)."] + #[must_use] + #[inline(always)] + pub const fn preset7(&self) -> u8 { + let val = (self.0 >> 28usize) & 0x0f; + val as u8 + } + #[doc = "motor 7 pulse-width preset (tick count - 1)."] + #[inline(always)] + pub const fn set_preset7(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 28usize)) | (((val as u32) & 0x0f) << 28usize); + } + } + impl Default for step_pls_config { + #[inline(always)] + fn default() -> step_pls_config { + step_pls_config(0) + } + } + impl core::fmt::Debug for step_pls_config { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pls_config") + .field("preset0", &self.preset0()) + .field("preset1", &self.preset1()) + .field("preset2", &self.preset2()) + .field("preset3", &self.preset3()) + .field("preset4", &self.preset4()) + .field("preset5", &self.preset5()) + .field("preset6", &self.preset6()) + .field("preset7", &self.preset7()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pls_config { + fn format(&self, f: defmt::Formatter) { + defmt :: write ! (f , "step_pls_config {{ preset0: {=u8:?}, preset1: {=u8:?}, preset2: {=u8:?}, preset3: {=u8:?}, preset4: {=u8:?}, preset5: {=u8:?}, preset6: {=u8:?}, preset7: {=u8:?} }}" , self . preset0 () , self . preset1 () , self . preset2 () , self . preset3 () , self . preset4 () , self . preset5 () , self . preset6 () , self . preset7 ()) + } + } + #[doc = "per-bank step pulse width prescaler (sys_clk cycles per tick - 1). Bank 0 = motors 0-3 (low 16-bit half, bits\\[5:0\\]), bank 1 = motors 4-7 (high 16-bit half, bits\\[21:16\\]) - split this way so either field can widen, or other per-bank config bits can be added, without touching the other bank's half."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pls_prescaler(pub u32); + impl step_pls_prescaler { + #[doc = "bank 0 (motors 0-3) prescaler (sys_clk cycles per tick - 1), low 16-bit half."] + #[must_use] + #[inline(always)] + pub const fn prescaler0(&self) -> u8 { + let val = (self.0 >> 0usize) & 0x3f; + val as u8 + } + #[doc = "bank 0 (motors 0-3) prescaler (sys_clk cycles per tick - 1), low 16-bit half."] + #[inline(always)] + pub const fn set_prescaler0(&mut self, val: u8) { + self.0 = (self.0 & !(0x3f << 0usize)) | (((val as u32) & 0x3f) << 0usize); + } + #[doc = "reserved, keep at reset value (rest of bank 0's 16-bit half)."] + #[must_use] + #[inline(always)] + pub const fn reserved0(&self) -> u16 { + let val = (self.0 >> 6usize) & 0x03ff; + val as u16 + } + #[doc = "reserved, keep at reset value (rest of bank 0's 16-bit half)."] + #[inline(always)] + pub const fn set_reserved0(&mut self, val: u16) { + self.0 = (self.0 & !(0x03ff << 6usize)) | (((val as u32) & 0x03ff) << 6usize); + } + #[doc = "bank 1 (motors 4-7) prescaler (sys_clk cycles per tick - 1), high 16-bit half."] + #[must_use] + #[inline(always)] + pub const fn prescaler1(&self) -> u8 { + let val = (self.0 >> 16usize) & 0x3f; + val as u8 + } + #[doc = "bank 1 (motors 4-7) prescaler (sys_clk cycles per tick - 1), high 16-bit half."] + #[inline(always)] + pub const fn set_prescaler1(&mut self, val: u8) { + self.0 = (self.0 & !(0x3f << 16usize)) | (((val as u32) & 0x3f) << 16usize); + } + #[doc = "reserved, keep at reset value (rest of bank 1's 16-bit half)."] + #[must_use] + #[inline(always)] + pub const fn reserved1(&self) -> u16 { + let val = (self.0 >> 22usize) & 0x03ff; + val as u16 + } + #[doc = "reserved, keep at reset value (rest of bank 1's 16-bit half)."] + #[inline(always)] + pub const fn set_reserved1(&mut self, val: u16) { + self.0 = (self.0 & !(0x03ff << 22usize)) | (((val as u32) & 0x03ff) << 22usize); + } + } + impl Default for step_pls_prescaler { + #[inline(always)] + fn default() -> step_pls_prescaler { + step_pls_prescaler(0) + } + } + impl core::fmt::Debug for step_pls_prescaler { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pls_prescaler") + .field("prescaler0", &self.prescaler0()) + .field("reserved0", &self.reserved0()) + .field("prescaler1", &self.prescaler1()) + .field("reserved1", &self.reserved1()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pls_prescaler { + fn format(&self, f: defmt::Formatter) { + defmt :: write ! (f , "step_pls_prescaler {{ prescaler0: {=u8:?}, reserved0: {=u16:?}, prescaler1: {=u8:?}, reserved1: {=u16:?} }}" , self . prescaler0 () , self . reserved0 () , self . prescaler1 () , self . reserved1 ()) + } + } + #[doc = "motor 0 current absolute position, in steps (signed, two's complement)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pos_0(pub u32); + impl step_pos_0 { + #[doc = "current absolute position (signed, two's complement)."] + #[must_use] + #[inline(always)] + pub const fn position(&self) -> u32 { + let val = (self.0 >> 0usize) & 0xffff_ffff; + val as u32 + } + #[doc = "current absolute position (signed, two's complement)."] + #[inline(always)] + pub const fn set_position(&mut self, val: u32) { + self.0 = + (self.0 & !(0xffff_ffff << 0usize)) | (((val as u32) & 0xffff_ffff) << 0usize); + } + } + impl Default for step_pos_0 { + #[inline(always)] + fn default() -> step_pos_0 { + step_pos_0(0) + } + } + impl core::fmt::Debug for step_pos_0 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pos_0") + .field("position", &self.position()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pos_0 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "step_pos_0 {{ position: {=u32:?} }}", self.position()) + } + } + #[doc = "motor 1 current absolute position, in steps (signed, two's complement)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pos_1(pub u32); + impl step_pos_1 { + #[doc = "current absolute position (signed, two's complement)."] + #[must_use] + #[inline(always)] + pub const fn position(&self) -> u32 { + let val = (self.0 >> 0usize) & 0xffff_ffff; + val as u32 + } + #[doc = "current absolute position (signed, two's complement)."] + #[inline(always)] + pub const fn set_position(&mut self, val: u32) { + self.0 = + (self.0 & !(0xffff_ffff << 0usize)) | (((val as u32) & 0xffff_ffff) << 0usize); + } + } + impl Default for step_pos_1 { + #[inline(always)] + fn default() -> step_pos_1 { + step_pos_1(0) + } + } + impl core::fmt::Debug for step_pos_1 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pos_1") + .field("position", &self.position()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pos_1 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "step_pos_1 {{ position: {=u32:?} }}", self.position()) + } + } + #[doc = "motor 2 current absolute position, in steps (signed, two's complement)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pos_2(pub u32); + impl step_pos_2 { + #[doc = "current absolute position (signed, two's complement)."] + #[must_use] + #[inline(always)] + pub const fn position(&self) -> u32 { + let val = (self.0 >> 0usize) & 0xffff_ffff; + val as u32 + } + #[doc = "current absolute position (signed, two's complement)."] + #[inline(always)] + pub const fn set_position(&mut self, val: u32) { + self.0 = + (self.0 & !(0xffff_ffff << 0usize)) | (((val as u32) & 0xffff_ffff) << 0usize); + } + } + impl Default for step_pos_2 { + #[inline(always)] + fn default() -> step_pos_2 { + step_pos_2(0) + } + } + impl core::fmt::Debug for step_pos_2 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pos_2") + .field("position", &self.position()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pos_2 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "step_pos_2 {{ position: {=u32:?} }}", self.position()) + } + } + #[doc = "motor 3 current absolute position, in steps (signed, two's complement)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pos_3(pub u32); + impl step_pos_3 { + #[doc = "current absolute position (signed, two's complement)."] + #[must_use] + #[inline(always)] + pub const fn position(&self) -> u32 { + let val = (self.0 >> 0usize) & 0xffff_ffff; + val as u32 + } + #[doc = "current absolute position (signed, two's complement)."] + #[inline(always)] + pub const fn set_position(&mut self, val: u32) { + self.0 = + (self.0 & !(0xffff_ffff << 0usize)) | (((val as u32) & 0xffff_ffff) << 0usize); + } + } + impl Default for step_pos_3 { + #[inline(always)] + fn default() -> step_pos_3 { + step_pos_3(0) + } + } + impl core::fmt::Debug for step_pos_3 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pos_3") + .field("position", &self.position()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pos_3 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "step_pos_3 {{ position: {=u32:?} }}", self.position()) + } + } + #[doc = "motor 4 current absolute position, in steps (signed, two's complement)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pos_4(pub u32); + impl step_pos_4 { + #[doc = "current absolute position (signed, two's complement)."] + #[must_use] + #[inline(always)] + pub const fn position(&self) -> u32 { + let val = (self.0 >> 0usize) & 0xffff_ffff; + val as u32 + } + #[doc = "current absolute position (signed, two's complement)."] + #[inline(always)] + pub const fn set_position(&mut self, val: u32) { + self.0 = + (self.0 & !(0xffff_ffff << 0usize)) | (((val as u32) & 0xffff_ffff) << 0usize); + } + } + impl Default for step_pos_4 { + #[inline(always)] + fn default() -> step_pos_4 { + step_pos_4(0) + } + } + impl core::fmt::Debug for step_pos_4 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pos_4") + .field("position", &self.position()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pos_4 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "step_pos_4 {{ position: {=u32:?} }}", self.position()) + } + } + #[doc = "motor 5 current absolute position, in steps (signed, two's complement)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pos_5(pub u32); + impl step_pos_5 { + #[doc = "current absolute position (signed, two's complement)."] + #[must_use] + #[inline(always)] + pub const fn position(&self) -> u32 { + let val = (self.0 >> 0usize) & 0xffff_ffff; + val as u32 + } + #[doc = "current absolute position (signed, two's complement)."] + #[inline(always)] + pub const fn set_position(&mut self, val: u32) { + self.0 = + (self.0 & !(0xffff_ffff << 0usize)) | (((val as u32) & 0xffff_ffff) << 0usize); + } + } + impl Default for step_pos_5 { + #[inline(always)] + fn default() -> step_pos_5 { + step_pos_5(0) + } + } + impl core::fmt::Debug for step_pos_5 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pos_5") + .field("position", &self.position()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pos_5 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "step_pos_5 {{ position: {=u32:?} }}", self.position()) + } + } + #[doc = "motor 6 current absolute position, in steps (signed, two's complement)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pos_6(pub u32); + impl step_pos_6 { + #[doc = "current absolute position (signed, two's complement)."] + #[must_use] + #[inline(always)] + pub const fn position(&self) -> u32 { + let val = (self.0 >> 0usize) & 0xffff_ffff; + val as u32 + } + #[doc = "current absolute position (signed, two's complement)."] + #[inline(always)] + pub const fn set_position(&mut self, val: u32) { + self.0 = + (self.0 & !(0xffff_ffff << 0usize)) | (((val as u32) & 0xffff_ffff) << 0usize); + } + } + impl Default for step_pos_6 { + #[inline(always)] + fn default() -> step_pos_6 { + step_pos_6(0) + } + } + impl core::fmt::Debug for step_pos_6 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pos_6") + .field("position", &self.position()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pos_6 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "step_pos_6 {{ position: {=u32:?} }}", self.position()) + } + } + #[doc = "motor 7 current absolute position, in steps (signed, two's complement)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_pos_7(pub u32); + impl step_pos_7 { + #[doc = "current absolute position (signed, two's complement)."] + #[must_use] + #[inline(always)] + pub const fn position(&self) -> u32 { + let val = (self.0 >> 0usize) & 0xffff_ffff; + val as u32 + } + #[doc = "current absolute position (signed, two's complement)."] + #[inline(always)] + pub const fn set_position(&mut self, val: u32) { + self.0 = + (self.0 & !(0xffff_ffff << 0usize)) | (((val as u32) & 0xffff_ffff) << 0usize); + } + } + impl Default for step_pos_7 { + #[inline(always)] + fn default() -> step_pos_7 { + step_pos_7(0) + } + } + impl core::fmt::Debug for step_pos_7 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_pos_7") + .field("position", &self.position()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_pos_7 { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "step_pos_7 {{ position: {=u32:?} }}", self.position()) + } + } + #[doc = "segment table CTST word - command/direction/step-count for the segment addressed by step_tx_config's motor_instance and the current streaming pointer (advances on step_seg_spdm access, not this one)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_seg_ctst(pub u32); + impl step_seg_ctst { + #[doc = "number of steps in this segment."] + #[must_use] + #[inline(always)] + pub const fn n_steps(&self) -> u32 { + let val = (self.0 >> 0usize) & 0x00ff_ffff; + val as u32 + } + #[doc = "number of steps in this segment."] + #[inline(always)] + pub const fn set_n_steps(&mut self, val: u32) { + self.0 = + (self.0 & !(0x00ff_ffff << 0usize)) | (((val as u32) & 0x00ff_ffff) << 0usize); + } + #[doc = "segment command."] + #[must_use] + #[inline(always)] + pub const fn cmd(&self) -> super::vals::cmd { + let val = (self.0 >> 24usize) & 0x03; + super::vals::cmd::from_bits(val as u8) + } + #[doc = "segment command."] + #[inline(always)] + pub const fn set_cmd(&mut self, val: super::vals::cmd) { + self.0 = + (self.0 & !(0x03 << 24usize)) | (((val.to_bits() as u32) & 0x03) << 24usize); + } + #[doc = "step direction for this segment."] + #[must_use] + #[inline(always)] + pub const fn dir(&self) -> super::vals::dir { + let val = (self.0 >> 26usize) & 0x01; + super::vals::dir::from_bits(val as u8) + } + #[doc = "step direction for this segment."] + #[inline(always)] + pub const fn set_dir(&mut self, val: super::vals::dir) { + self.0 = + (self.0 & !(0x01 << 26usize)) | (((val.to_bits() as u32) & 0x01) << 26usize); + } + #[doc = "whether this segment's period ramps up or down progresses."] + #[must_use] + #[inline(always)] + pub const fn ramp(&self) -> super::vals::ramp { + let val = (self.0 >> 27usize) & 0x01; + super::vals::ramp::from_bits(val as u8) + } + #[doc = "whether this segment's period ramps up or down progresses."] + #[inline(always)] + pub const fn set_ramp(&mut self, val: super::vals::ramp) { + self.0 = + (self.0 & !(0x01 << 27usize)) | (((val.to_bits() as u32) & 0x01) << 27usize); + } + #[doc = "reserved, keep at reset value."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u8 { + let val = (self.0 >> 28usize) & 0x0f; + val as u8 + } + #[doc = "reserved, keep at reset value."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u8) { + self.0 = (self.0 & !(0x0f << 28usize)) | (((val as u32) & 0x0f) << 28usize); + } + } + impl Default for step_seg_ctst { + #[inline(always)] + fn default() -> step_seg_ctst { + step_seg_ctst(0) + } + } + impl core::fmt::Debug for step_seg_ctst { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_seg_ctst") + .field("n_steps", &self.n_steps()) + .field("cmd", &self.cmd()) + .field("dir", &self.dir()) + .field("ramp", &self.ramp()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_seg_ctst { + fn format(&self, f: defmt::Formatter) { + defmt :: write ! (f , "step_seg_ctst {{ n_steps: {=u32:?}, cmd: {:?}, dir: {:?}, ramp: {:?}, reserved: {=u8:?} }}" , self . n_steps () , self . cmd () , self . dir () , self . ramp () , self . reserved ()) + } + } + #[doc = "segment table SPDM word - start period/delta magnitude for the segment addressed by step_tx_config's motor_instance and the current streaming pointer (advances the streaming pointer on access - write or read)."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_seg_spdm(pub u32); + impl step_seg_spdm { + #[doc = "per-step period change magnitude, in stepper ticks."] + #[must_use] + #[inline(always)] + pub const fn delta_magnitude(&self) -> u16 { + let val = (self.0 >> 0usize) & 0xffff; + val as u16 + } + #[doc = "per-step period change magnitude, in stepper ticks."] + #[inline(always)] + pub const fn set_delta_magnitude(&mut self, val: u16) { + self.0 = (self.0 & !(0xffff << 0usize)) | (((val as u32) & 0xffff) << 0usize); + } + #[doc = "starting period for this segment, in stepper ticks."] + #[must_use] + #[inline(always)] + pub const fn start_period(&self) -> u16 { + let val = (self.0 >> 16usize) & 0xffff; + val as u16 + } + #[doc = "starting period for this segment, in stepper ticks."] + #[inline(always)] + pub const fn set_start_period(&mut self, val: u16) { + self.0 = (self.0 & !(0xffff << 16usize)) | (((val as u32) & 0xffff) << 16usize); + } + } + impl Default for step_seg_spdm { + #[inline(always)] + fn default() -> step_seg_spdm { + step_seg_spdm(0) + } + } + impl core::fmt::Debug for step_seg_spdm { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_seg_spdm") + .field("delta_magnitude", &self.delta_magnitude()) + .field("start_period", &self.start_period()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_seg_spdm { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_seg_spdm {{ delta_magnitude: {=u16:?}, start_period: {=u16:?} }}", + self.delta_magnitude(), + self.start_period() + ) + } + } + #[doc = "motor 0 status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_status_0(pub u32); + impl step_status_0 { + #[doc = "1 = motor is currently moving."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> bool { + let val = (self.0 >> 0usize) & 0x01; + val != 0 + } + #[doc = "1 = motor is currently moving."] + #[inline(always)] + pub const fn set_moving(&mut self, val: bool) { + self.0 = (self.0 & !(0x01 << 0usize)) | (((val as u32) & 0x01) << 0usize); + } + #[doc = "reserved."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 1usize) & 0x7fff_ffff; + val as u32 + } + #[doc = "reserved."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = + (self.0 & !(0x7fff_ffff << 1usize)) | (((val as u32) & 0x7fff_ffff) << 1usize); + } + } + impl Default for step_status_0 { + #[inline(always)] + fn default() -> step_status_0 { + step_status_0(0) + } + } + impl core::fmt::Debug for step_status_0 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_status_0") + .field("moving", &self.moving()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_status_0 { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_status_0 {{ moving: {=bool:?}, reserved: {=u32:?} }}", + self.moving(), + self.reserved() + ) + } + } + #[doc = "motor 1 status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_status_1(pub u32); + impl step_status_1 { + #[doc = "1 = motor is currently moving."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> bool { + let val = (self.0 >> 0usize) & 0x01; + val != 0 + } + #[doc = "1 = motor is currently moving."] + #[inline(always)] + pub const fn set_moving(&mut self, val: bool) { + self.0 = (self.0 & !(0x01 << 0usize)) | (((val as u32) & 0x01) << 0usize); + } + #[doc = "reserved."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 1usize) & 0x7fff_ffff; + val as u32 + } + #[doc = "reserved."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = + (self.0 & !(0x7fff_ffff << 1usize)) | (((val as u32) & 0x7fff_ffff) << 1usize); + } + } + impl Default for step_status_1 { + #[inline(always)] + fn default() -> step_status_1 { + step_status_1(0) + } + } + impl core::fmt::Debug for step_status_1 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_status_1") + .field("moving", &self.moving()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_status_1 { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_status_1 {{ moving: {=bool:?}, reserved: {=u32:?} }}", + self.moving(), + self.reserved() + ) + } + } + #[doc = "motor 2 status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_status_2(pub u32); + impl step_status_2 { + #[doc = "1 = motor is currently moving."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> bool { + let val = (self.0 >> 0usize) & 0x01; + val != 0 + } + #[doc = "1 = motor is currently moving."] + #[inline(always)] + pub const fn set_moving(&mut self, val: bool) { + self.0 = (self.0 & !(0x01 << 0usize)) | (((val as u32) & 0x01) << 0usize); + } + #[doc = "reserved."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 1usize) & 0x7fff_ffff; + val as u32 + } + #[doc = "reserved."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = + (self.0 & !(0x7fff_ffff << 1usize)) | (((val as u32) & 0x7fff_ffff) << 1usize); + } + } + impl Default for step_status_2 { + #[inline(always)] + fn default() -> step_status_2 { + step_status_2(0) + } + } + impl core::fmt::Debug for step_status_2 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_status_2") + .field("moving", &self.moving()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_status_2 { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_status_2 {{ moving: {=bool:?}, reserved: {=u32:?} }}", + self.moving(), + self.reserved() + ) + } + } + #[doc = "motor 3 status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_status_3(pub u32); + impl step_status_3 { + #[doc = "1 = motor is currently moving."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> bool { + let val = (self.0 >> 0usize) & 0x01; + val != 0 + } + #[doc = "1 = motor is currently moving."] + #[inline(always)] + pub const fn set_moving(&mut self, val: bool) { + self.0 = (self.0 & !(0x01 << 0usize)) | (((val as u32) & 0x01) << 0usize); + } + #[doc = "reserved."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 1usize) & 0x7fff_ffff; + val as u32 + } + #[doc = "reserved."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = + (self.0 & !(0x7fff_ffff << 1usize)) | (((val as u32) & 0x7fff_ffff) << 1usize); + } + } + impl Default for step_status_3 { + #[inline(always)] + fn default() -> step_status_3 { + step_status_3(0) + } + } + impl core::fmt::Debug for step_status_3 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_status_3") + .field("moving", &self.moving()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_status_3 { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_status_3 {{ moving: {=bool:?}, reserved: {=u32:?} }}", + self.moving(), + self.reserved() + ) + } + } + #[doc = "motor 4 status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_status_4(pub u32); + impl step_status_4 { + #[doc = "1 = motor is currently moving."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> bool { + let val = (self.0 >> 0usize) & 0x01; + val != 0 + } + #[doc = "1 = motor is currently moving."] + #[inline(always)] + pub const fn set_moving(&mut self, val: bool) { + self.0 = (self.0 & !(0x01 << 0usize)) | (((val as u32) & 0x01) << 0usize); + } + #[doc = "reserved."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 1usize) & 0x7fff_ffff; + val as u32 + } + #[doc = "reserved."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = + (self.0 & !(0x7fff_ffff << 1usize)) | (((val as u32) & 0x7fff_ffff) << 1usize); + } + } + impl Default for step_status_4 { + #[inline(always)] + fn default() -> step_status_4 { + step_status_4(0) + } + } + impl core::fmt::Debug for step_status_4 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_status_4") + .field("moving", &self.moving()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_status_4 { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_status_4 {{ moving: {=bool:?}, reserved: {=u32:?} }}", + self.moving(), + self.reserved() + ) + } + } + #[doc = "motor 5 status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_status_5(pub u32); + impl step_status_5 { + #[doc = "1 = motor is currently moving."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> bool { + let val = (self.0 >> 0usize) & 0x01; + val != 0 + } + #[doc = "1 = motor is currently moving."] + #[inline(always)] + pub const fn set_moving(&mut self, val: bool) { + self.0 = (self.0 & !(0x01 << 0usize)) | (((val as u32) & 0x01) << 0usize); + } + #[doc = "reserved."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 1usize) & 0x7fff_ffff; + val as u32 + } + #[doc = "reserved."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = + (self.0 & !(0x7fff_ffff << 1usize)) | (((val as u32) & 0x7fff_ffff) << 1usize); + } + } + impl Default for step_status_5 { + #[inline(always)] + fn default() -> step_status_5 { + step_status_5(0) + } + } + impl core::fmt::Debug for step_status_5 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_status_5") + .field("moving", &self.moving()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_status_5 { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_status_5 {{ moving: {=bool:?}, reserved: {=u32:?} }}", + self.moving(), + self.reserved() + ) + } + } + #[doc = "motor 6 status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_status_6(pub u32); + impl step_status_6 { + #[doc = "1 = motor is currently moving."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> bool { + let val = (self.0 >> 0usize) & 0x01; + val != 0 + } + #[doc = "1 = motor is currently moving."] + #[inline(always)] + pub const fn set_moving(&mut self, val: bool) { + self.0 = (self.0 & !(0x01 << 0usize)) | (((val as u32) & 0x01) << 0usize); + } + #[doc = "reserved."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 1usize) & 0x7fff_ffff; + val as u32 + } + #[doc = "reserved."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = + (self.0 & !(0x7fff_ffff << 1usize)) | (((val as u32) & 0x7fff_ffff) << 1usize); + } + } + impl Default for step_status_6 { + #[inline(always)] + fn default() -> step_status_6 { + step_status_6(0) + } + } + impl core::fmt::Debug for step_status_6 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_status_6") + .field("moving", &self.moving()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_status_6 { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_status_6 {{ moving: {=bool:?}, reserved: {=u32:?} }}", + self.moving(), + self.reserved() + ) + } + } + #[doc = "motor 7 status."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_status_7(pub u32); + impl step_status_7 { + #[doc = "1 = motor is currently moving."] + #[must_use] + #[inline(always)] + pub const fn moving(&self) -> bool { + let val = (self.0 >> 0usize) & 0x01; + val != 0 + } + #[doc = "1 = motor is currently moving."] + #[inline(always)] + pub const fn set_moving(&mut self, val: bool) { + self.0 = (self.0 & !(0x01 << 0usize)) | (((val as u32) & 0x01) << 0usize); + } + #[doc = "reserved."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 1usize) & 0x7fff_ffff; + val as u32 + } + #[doc = "reserved."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = + (self.0 & !(0x7fff_ffff << 1usize)) | (((val as u32) & 0x7fff_ffff) << 1usize); + } + } + impl Default for step_status_7 { + #[inline(always)] + fn default() -> step_status_7 { + step_status_7(0) + } + } + impl core::fmt::Debug for step_status_7 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_status_7") + .field("moving", &self.moving()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_status_7 { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "step_status_7 {{ moving: {=bool:?}, reserved: {=u32:?} }}", + self.moving(), + self.reserved() + ) + } + } + #[doc = "segment table streaming configuration - selects which motor subsequent step_seg_ctst/step_seg_spdm writes and reads target, and rewinds both the write and read pointers to segment 0."] + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq)] + pub struct step_tx_config(pub u32); + impl step_tx_config { + #[doc = "number of segments about to be streamed (informational - clamped internally to the segment table's actual capacity)."] + #[must_use] + #[inline(always)] + pub const fn num_points(&self) -> u8 { + let val = (self.0 >> 0usize) & 0xff; + val as u8 + } + #[doc = "number of segments about to be streamed (informational - clamped internally to the segment table's actual capacity)."] + #[inline(always)] + pub const fn set_num_points(&mut self, val: u8) { + self.0 = (self.0 & !(0xff << 0usize)) | (((val as u32) & 0xff) << 0usize); + } + #[doc = "which motor (0-7) subsequent step_seg_ctst/step_seg_spdm accesses target."] + #[must_use] + #[inline(always)] + pub const fn motor_instance(&self) -> u8 { + let val = (self.0 >> 8usize) & 0x07; + val as u8 + } + #[doc = "which motor (0-7) subsequent step_seg_ctst/step_seg_spdm accesses target."] + #[inline(always)] + pub const fn set_motor_instance(&mut self, val: u8) { + self.0 = (self.0 & !(0x07 << 8usize)) | (((val as u32) & 0x07) << 8usize); + } + #[doc = "reserved, keep at reset value."] + #[must_use] + #[inline(always)] + pub const fn reserved(&self) -> u32 { + let val = (self.0 >> 11usize) & 0x001f_ffff; + val as u32 + } + #[doc = "reserved, keep at reset value."] + #[inline(always)] + pub const fn set_reserved(&mut self, val: u32) { + self.0 = (self.0 & !(0x001f_ffff << 11usize)) + | (((val as u32) & 0x001f_ffff) << 11usize); + } + } + impl Default for step_tx_config { + #[inline(always)] + fn default() -> step_tx_config { + step_tx_config(0) + } + } + impl core::fmt::Debug for step_tx_config { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("step_tx_config") + .field("num_points", &self.num_points()) + .field("motor_instance", &self.motor_instance()) + .field("reserved", &self.reserved()) + .finish() + } + } + #[cfg(feature = "defmt")] + impl defmt::Format for step_tx_config { + fn format(&self, f: defmt::Formatter) { + defmt :: write ! (f , "step_tx_config {{ num_points: {=u8:?}, motor_instance: {=u8:?}, reserved: {=u32:?} }}" , self . num_points () , self . motor_instance () , self . reserved ()) + } + } + } + pub mod vals { + #[repr(u8)] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub enum cmd { + #[doc = "reserved, not a valid command."] + RESERVED = 0x0, + #[doc = "move this segment then seamlessly continue into the next segment."] + MOVE = 0x01, + #[doc = "move this segment then halt, requiring a fresh start strobe."] + MOVE_HALT = 0x02, + #[doc = "move this segment then halt, requiring a fresh start strobe (reserved alias of MOVE_HALT)."] + MOVE_HALT_WAIT = 0x03, + } + impl cmd { + #[inline(always)] + pub const fn from_bits(val: u8) -> cmd { + unsafe { core::mem::transmute(val & 0x03) } + } + #[inline(always)] + pub const fn to_bits(self) -> u8 { + unsafe { core::mem::transmute(self) } + } + } + impl From for cmd { + #[inline(always)] + fn from(val: u8) -> cmd { + cmd::from_bits(val) + } + } + impl From for u8 { + #[inline(always)] + fn from(val: cmd) -> u8 { + cmd::to_bits(val) + } + } + #[repr(u8)] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub enum dir { + #[doc = "forward/increasing position."] + NORMAL = 0x0, + #[doc = "reverse/decreasing position."] + REVERSE = 0x01, + } + impl dir { + #[inline(always)] + pub const fn from_bits(val: u8) -> dir { + unsafe { core::mem::transmute(val & 0x01) } + } + #[inline(always)] + pub const fn to_bits(self) -> u8 { + unsafe { core::mem::transmute(self) } + } + } + impl From for dir { + #[inline(always)] + fn from(val: u8) -> dir { + dir::from_bits(val) + } + } + impl From for u8 { + #[inline(always)] + fn from(val: dir) -> u8 { + dir::to_bits(val) + } + } + #[repr(u8)] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub enum ramp { + #[doc = "period decreases each step (accelerating)."] + UP = 0x0, + #[doc = "period increases each step (decelerating)."] + DOWN = 0x01, + } + impl ramp { + #[inline(always)] + pub const fn from_bits(val: u8) -> ramp { + unsafe { core::mem::transmute(val & 0x01) } + } + #[inline(always)] + pub const fn to_bits(self) -> u8 { + unsafe { core::mem::transmute(self) } + } + } + impl From for ramp { + #[inline(always)] + fn from(val: u8) -> ramp { + ramp::from_bits(val) + } + } + impl From for u8 { + #[inline(always)] + fn from(val: ramp) -> u8 { + ramp::to_bits(val) + } + } + } +} pub mod system0 { #[doc = "system block 0."] #[derive(Copy, Clone, Eq, PartialEq)] diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/Makefile b/firmware/firmware-makerpnpcontrolcore/fpga/Makefile index 32f4ec8..aaed143 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/Makefile +++ b/firmware/firmware-makerpnpcontrolcore/fpga/Makefile @@ -43,6 +43,8 @@ EXTRA_SRCS_timer_mux := EXTRA_SRCS_clock_out := EXTRA_SRCS_quadspi := $(SRC_MAIN)/registers/memory.v EXTRA_SRCS_encoders := $(SRC_MAIN)/io/encoder.v +EXTRA_SRCS_steppers := $(SRC_MAIN)/io/stepper_clk.v +EXTRA_SRCS_steppers_motion := $(SRC_MAIN)/io/stepper_clk.v $(SRC_MAIN)/io/steppers.v EXTRA_SRCS_leds := # Special / integration TBs @@ -78,7 +80,7 @@ $(BUILD): mkdir -p $(BUILD) $(BUILD)/$(PROJECT)-design.json: $(VERILOGS) $(BUILD) Makefile - yosys -q -l $(BUILD)/yosys.log -p 'synth_${FAMILY} -top $(TOP) -json $(BUILD)/$(PROJECT)-design.json' $(VERILOGS) + yosys -q -l $(BUILD)/yosys.log -p 'synth_${FAMILY} -top $(TOP) -json $(BUILD)/$(PROJECT)-design.json; share' $(VERILOGS) $(BUILD)/$(PROJECT).asc: $(BUILD)/$(PROJECT)-design.json pins.pcf nextpnr-${FAMILY} -q -l $(BUILD)/nextpnr.log --${TYPE} --package ${PACKAGE} --top $(TOP) --json $(BUILD)/$(PROJECT)-design.json --write $(BUILD)/$(PROJECT)-place.json --report $(BUILD)/$(PROJECT)-report.json --pcf pins.pcf --asc $(BUILD)/$(PROJECT).asc --placed-svg $(BUILD)/$(PROJECT)-placed.svg --freq $(FREQ) -v $(NEXTPNR_EXTRA_ARGS) diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/assets/svd/fpga.svd b/firmware/firmware-makerpnpcontrolcore/fpga/assets/svd/fpga.svd index 231e63d..58f1eb3 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/assets/svd/fpga.svd +++ b/firmware/firmware-makerpnpcontrolcore/fpga/assets/svd/fpga.svd @@ -572,6 +572,12 @@ + + ws2812_1 + ws2812 RGB LED block (second instance) + 0x90000900 + + encoders @@ -899,6 +905,690 @@ + + + steppers + 8-channel stepper motor step/dir pulse generator (2 banks of 4) + 0x90000d00 + + 0x0 + 0x80 + registers + + + + + + step_ctrl + start/stop control - WRITE: start/stop strobes per motor (bank 0 = motors 0-3, bank 1 = motors 4-7); READ: per-motor moving status + 0x0000 + 32 + read-write + 0x00000000 + + + + moving + per-motor moving status (bit N = motor N is moving) - read-only, ignored on write + 0 + 8 + read-only + + + + start_bank0 + start strobe, bit N = motor N (bank 0, motors 0-3) - write-only, always reads as 0 + 8 + 4 + write-only + + + + stop_bank0 + stop strobe, bit N = motor N (bank 0, motors 0-3) - write-only, always reads as 0 + 12 + 4 + write-only + + + + start_bank1 + start strobe, bit N = motor (N+4) (bank 1, motors 4-7) - write-only, always reads as 0 + 16 + 4 + write-only + + + + stop_bank1 + stop strobe, bit N = motor (N+4) (bank 1, motors 4-7) - write-only, always reads as 0 + 20 + 4 + write-only + + + + reserved + reserved, keep at reset value + 24 + 8 + + + + + + step_tx_config + segment table streaming configuration - selects which motor subsequent step_seg_ctst/step_seg_spdm writes and reads target, and rewinds both the write and read pointers to segment 0 + 0x0004 + 32 + read-write + 0x00000000 + + + + num_points + number of segments about to be streamed (informational - clamped internally to the segment table's actual capacity) + 0 + 8 + + + + motor_instance + which motor (0-7) subsequent step_seg_ctst/step_seg_spdm accesses target + 8 + 3 + + + + reserved + reserved, keep at reset value + 11 + 21 + + + + + + step_pls_config + per-motor step pulse width preset (tick count - 1). Combined with that motor's bank prescaler in step_pls_prescaler: pulse width = (prescaler+1) * (preset+1) sys_clk cycles + 0x0008 + 32 + read-write + 0x00000000 + + + + preset0 + motor 0 pulse-width preset (tick count - 1) + 0 + 4 + + + + preset1 + motor 1 pulse-width preset (tick count - 1) + 4 + 4 + + + + preset2 + motor 2 pulse-width preset (tick count - 1) + 8 + 4 + + + + preset3 + motor 3 pulse-width preset (tick count - 1) + 12 + 4 + + + + preset4 + motor 4 pulse-width preset (tick count - 1) + 16 + 4 + + + + preset5 + motor 5 pulse-width preset (tick count - 1) + 20 + 4 + + + + preset6 + motor 6 pulse-width preset (tick count - 1) + 24 + 4 + + + + preset7 + motor 7 pulse-width preset (tick count - 1) + 28 + 4 + + + + + + step_pls_prescaler + per-bank step pulse width prescaler (sys_clk cycles per tick - 1). Bank 0 = motors 0-3 (low 16-bit half, bits[5:0]), bank 1 = motors 4-7 (high 16-bit half, bits[21:16]) - split this way so either field can widen, or other per-bank config bits can be added, without touching the other bank's half + 0x000c + 32 + read-write + 0x00000000 + + + + prescaler0 + bank 0 (motors 0-3) prescaler (sys_clk cycles per tick - 1), low 16-bit half + 0 + 6 + + + + reserved0 + reserved, keep at reset value (rest of bank 0's 16-bit half) + 6 + 10 + + + + prescaler1 + bank 1 (motors 4-7) prescaler (sys_clk cycles per tick - 1), high 16-bit half + 16 + 6 + + + + reserved1 + reserved, keep at reset value (rest of bank 1's 16-bit half) + 22 + 10 + + + + + + step_seg_ctst + segment table CTST word - command/direction/step-count for the segment addressed by step_tx_config's motor_instance and the current streaming pointer (advances on step_seg_spdm access, not this one) + 0x0010 + 32 + read-write + 0x00000000 + + + + n_steps + number of steps in this segment + 0 + 24 + + + + cmd + segment command + 24 + 2 + + + + RESERVED + reserved, not a valid command + 0 + + + + MOVE + move this segment then seamlessly continue into the next segment + 1 + + + + MOVE_HALT + move this segment then halt, requiring a fresh start strobe + 2 + + + + MOVE_HALT_WAIT + move this segment then halt, requiring a fresh start strobe (reserved alias of MOVE_HALT) + 3 + + + + + + + dir + step direction for this segment + 26 + 1 + + + + NORMAL + forward/increasing position + 0 + + + + REVERSE + reverse/decreasing position + 1 + + + + + + + ramp + whether this segment's period ramps up or down progresses + 27 + 1 + + + + UP + period decreases each step (accelerating) + 0 + + + + DOWN + period increases each step (decelerating) + 1 + + + + + + + reserved + reserved, keep at reset value + 28 + 4 + + + + + + step_seg_spdm + segment table SPDM word - start period/delta magnitude for the segment addressed by step_tx_config's motor_instance and the current streaming pointer (advances the streaming pointer on access - write or read) + 0x0014 + 32 + read-write + 0x00000000 + + + + delta_magnitude + per-step period change magnitude, in stepper ticks + 0 + 16 + + + + start_period + starting period for this segment, in stepper ticks + 16 + 16 + + + + + + step_status_0 + motor 0 status + 0x0020 + 32 + read-only + 0x00000000 + + + + moving + 1 = motor is currently moving + 0 + 1 + + + + reserved + reserved + 1 + 31 + + + + + + step_status_1 + motor 1 status + 0x0024 + 32 + read-only + 0x00000000 + + + + moving + 1 = motor is currently moving + 0 + 1 + + + + reserved + reserved + 1 + 31 + + + + + + step_status_2 + motor 2 status + 0x0028 + 32 + read-only + 0x00000000 + + + + moving + 1 = motor is currently moving + 0 + 1 + + + + reserved + reserved + 1 + 31 + + + + + + step_status_3 + motor 3 status + 0x002c + 32 + read-only + 0x00000000 + + + + moving + 1 = motor is currently moving + 0 + 1 + + + + reserved + reserved + 1 + 31 + + + + + + step_status_4 + motor 4 status + 0x0030 + 32 + read-only + 0x00000000 + + + + moving + 1 = motor is currently moving + 0 + 1 + + + + reserved + reserved + 1 + 31 + + + + + + step_status_5 + motor 5 status + 0x0034 + 32 + read-only + 0x00000000 + + + + moving + 1 = motor is currently moving + 0 + 1 + + + + reserved + reserved + 1 + 31 + + + + + + step_status_6 + motor 6 status + 0x0038 + 32 + read-only + 0x00000000 + + + + moving + 1 = motor is currently moving + 0 + 1 + + + + reserved + reserved + 1 + 31 + + + + + + step_status_7 + motor 7 status + 0x003c + 32 + read-only + 0x00000000 + + + + moving + 1 = motor is currently moving + 0 + 1 + + + + reserved + reserved + 1 + 31 + + + + + + step_pos_0 + motor 0 current absolute position, in steps (signed, two's complement) + 0x0040 + 32 + read-only + 0x00000000 + + + + position + current absolute position (signed, two's complement) + 0 + 32 + + + + + + step_pos_1 + motor 1 current absolute position, in steps (signed, two's complement) + 0x0044 + 32 + read-only + 0x00000000 + + + + position + current absolute position (signed, two's complement) + 0 + 32 + + + + + + step_pos_2 + motor 2 current absolute position, in steps (signed, two's complement) + 0x0048 + 32 + read-only + 0x00000000 + + + + position + current absolute position (signed, two's complement) + 0 + 32 + + + + + + step_pos_3 + motor 3 current absolute position, in steps (signed, two's complement) + 0x004c + 32 + read-only + 0x00000000 + + + + position + current absolute position (signed, two's complement) + 0 + 32 + + + + + + step_pos_4 + motor 4 current absolute position, in steps (signed, two's complement) + 0x0050 + 32 + read-only + 0x00000000 + + + + position + current absolute position (signed, two's complement) + 0 + 32 + + + + + + step_pos_5 + motor 5 current absolute position, in steps (signed, two's complement) + 0x0054 + 32 + read-only + 0x00000000 + + + + position + current absolute position (signed, two's complement) + 0 + 32 + + + + + + step_pos_6 + motor 6 current absolute position, in steps (signed, two's complement) + 0x0058 + 32 + read-only + 0x00000000 + + + + position + current absolute position (signed, two's complement) + 0 + 32 + + + + + + step_pos_7 + motor 7 current absolute position, in steps (signed, two's complement) + 0x005c + 32 + read-only + 0x00000000 + + + + position + current absolute position (signed, two's complement) + 0 + 32 + + + + + + + system1 diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/pins.pcf b/firmware/firmware-makerpnpcontrolcore/fpga/pins.pcf index 6d87c7b..73e16c7 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/pins.pcf +++ b/firmware/firmware-makerpnpcontrolcore/fpga/pins.pcf @@ -120,3 +120,28 @@ set_io ENCODER_Z[0] F4 ### WS2812 outputs set_io RGB_PORTS M11 set_io RGB_UP_CAM D3 + +### ZYZF steppers +set_io XYZF_STEP[0] M4 +set_io XYZF_DIR[0] M5 +set_io XYZF_STEP[1] M6 +set_io XYZF_DIR[1] M7 +set_io XYZF_STEP[2] J4 +set_io XYZF_DIR[2] K3 +set_io XYZF_STEP[3] L3 +set_io XYZF_DIR[3] M3 + + +### BCDE steppers +set_io BCDE_STEP[0] G3 +set_io BCDE_DIR[0] G4 +set_io BCDE_STEP[1] H2 +set_io BCDE_DIR[1] J3 +set_io BCDE_STEP[2] H4 +set_io BCDE_DIR[2] H3 +set_io BCDE_STEP[3] E3 +set_io BCDE_DIR[3] E4 + +### BCDE_XYZF_EN pins +set_io BCDE_EN R2 +set_io XYZF_EN T1 \ No newline at end of file diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/core_top.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/core_top.v index 34c5914..35faa46 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/core_top.v +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/core_top.v @@ -16,7 +16,16 @@ module core_top ( output wire RGB_PORTS, output wire RGB_UP_CAM, - output wire [15:0] LA_IO, + output wire [15:0] LA_IO, + + output wire [3:0] XYZF_STEP, + output wire [3:0] XYZF_DIR, + + output wire [3:0] BCDE_STEP, + output wire [3:0] BCDE_DIR, + + output wire BCDE_EN, + output wire XYZF_EN, (* PULLUP = 1 *) input NWAKE_IN, @@ -46,7 +55,7 @@ module core_top ( input wire [2:0] ENCODER_Z ); - wire clk_100; + wire sys_clk; wire locked; wire reset; @@ -102,6 +111,13 @@ module core_top ( wire buzzer_stb; wire buzzer_ack; + wire [7:0] steppers_addr; + wire [31:0] steppers_din; + wire [31:0] steppers_dout; + wire steppers_we; + wire steppers_stb; + wire steppers_ack; + wire [15:0] led_debug; wire [15:0] buzzer_debug; wire [15:0] io_debug; @@ -111,6 +127,23 @@ module core_top ( //wire [15:0] la_in = buzzer_debug; wire [15:0] la_in = 16'h0F0F; + + wire global_motor_en; + reg emergency_stop_r = 1'b0; + assign global_motor_en = !emergency_stop_r; + + + always @(posedge sys_clk) begin + if (reset) begin + emergency_stop_r = 1'b1; + end else begin + // TODO add e-stop hardware support + // probably via some system configuration registers. + // and use an output from the IO module. + emergency_stop_r = 1'b0; + end + end + reg [7:0] reset_cnt = 0; reg reset_r = 1; @@ -118,7 +151,7 @@ module core_top ( // once the PLL is locked, release reset after a short delay // to allow subsystems to process the reset signal while a clock is present - always @(posedge clk_100 or negedge locked) begin + always @(posedge sys_clk or negedge locked) begin if (!locked) begin reset_cnt <= 0; reset_r <= 1; @@ -137,13 +170,13 @@ module core_top ( // ---------------------- pll u_pll ( .clock_in(TCXO), - .clock_out(clk_100), + .clock_out(sys_clk), .locked(locked) ); la la_inst ( .reset(reset), - .sys_clk(clk_100), + .sys_clk(sys_clk), .la_io(LA_IO), .la_src(la_src), .la_in(la_in) @@ -154,7 +187,7 @@ module core_top ( // ---------------------- leds led_inst ( .reset(reset), - .sys_clk(clk_100), + .sys_clk(sys_clk), .bus_stb(led_stb), .bus_we(led_we), @@ -173,7 +206,7 @@ module core_top ( // ---------------------- buzzer buzzer_inst ( .reset(reset), - .sys_clk(clk_100), + .sys_clk(sys_clk), .bus_stb(buzzer_stb), .bus_we(buzzer_we), @@ -191,7 +224,7 @@ module core_top ( // ---------------------- io io_inst ( .reset(reset), - .sys_clk(clk_100), + .sys_clk(sys_clk), .bus_stb(io_stb), .bus_we(io_we), @@ -216,7 +249,7 @@ module core_top ( // ---------------------- encoders encoder_inst ( .reset(reset), - .sys_clk(clk_100), + .sys_clk(sys_clk), .bus_stb(encoder_stb), .bus_we(encoder_we), @@ -239,7 +272,7 @@ module core_top ( // WS2812 - on-board LEDs // ---------------------- ws2812 ws2812_0_inst ( - .sys_clk(clk_100), + .sys_clk(sys_clk), .reset(reset), .bus_stb(ws0_stb), @@ -256,7 +289,7 @@ module core_top ( // WS2812 - Up-camera / Head / Work LEDs // ---------------------- ws2812 ws2812_1_inst ( - .sys_clk(clk_100), + .sys_clk(sys_clk), .reset(reset), .bus_stb(ws1_stb), @@ -269,12 +302,43 @@ module core_top ( .ws_out(RGB_UP_CAM) ); + // ---------------------- + // Steppers + // ---------------------- + + wire stepper_clk; + + stepper_clk stepper_clk_inst ( + .sys_clk(sys_clk), + .reset(reset), + .stepper_clk(stepper_clk) + ); + + steppers steppers_inst ( + .sys_clk(sys_clk), + .reset(reset), + + .stepper_clk(stepper_clk), + + .bus_stb(steppers_stb), + .bus_we(steppers_we), + .bus_addr(steppers_addr), + .bus_din(steppers_din), + .bus_dout(steppers_dout), + .bus_ack(steppers_ack), + + .step_pins({BCDE_STEP, XYZF_STEP}), + .dir_pins({BCDE_DIR, XYZF_DIR}), + .bank_enable_pins({BCDE_EN, XYZF_EN}), + .global_motor_en(global_motor_en) + ); + // ---------------------- // Instantiate Central Address Decoder // ---------------------- memory memory_map_inst ( .reset(reset), - .clk_a(clk_100), + .clk_a(sys_clk), .we_a(mem_we), .stb_a(mem_stb), .addr_a(mem_addr), @@ -317,6 +381,13 @@ module core_top ( .ws1_dout(ws1_dout), .ws1_ack(ws1_ack), + .steppers_stb(steppers_stb), + .steppers_we(steppers_we), + .steppers_addr(steppers_addr), + .steppers_din(steppers_din), + .steppers_dout(steppers_dout), + .steppers_ack(steppers_ack), + .buzzer_stb(buzzer_stb), .buzzer_we(buzzer_we), .buzzer_addr(buzzer_addr), @@ -329,7 +400,7 @@ module core_top ( // Connect QUADSPI1 interface engine to Memory Port A // ---------------------- quadspi qspi_inst ( - .sys_clk(clk_100), + .sys_clk(sys_clk), .sck(QUADSPI1_CLK), .cs_n(QUADSPI1_NCS), .io(QUADSPI1_IO), @@ -346,13 +417,13 @@ module core_top ( // ---------------------- // blink u_blink ( // .reset(reset), -// .clk(clk_100), +// .clk(sys_clk), // .led(FPGA_ACT) // ); wake u_wake ( .reset(reset), - .sys_clk(clk_100), + .sys_clk(sys_clk), .nwake_in(NWAKE_IN), .nwake_1(NWAKE_1), .nwake_2(NWAKE_2), @@ -362,7 +433,7 @@ module core_top ( timer_mux u_timer_mux ( .reset(reset), - .sys_clk(clk_100), + .sys_clk(sys_clk), .mux_sel1(MUX_SEL1), .mux_sel2(MUX_SEL2), .mux_sel3(MUX_SEL3), @@ -371,7 +442,7 @@ module core_top ( clock_out u_clock_out ( .reset(reset), - .sys_clk(clk_100), + .sys_clk(sys_clk), .clock_out1(FPGA_CLK_1), .clock_out2(FPGA_CLK_2), .clock_out3(FPGA_CLK_3), diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/stepper_clk.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/stepper_clk.v new file mode 100644 index 0000000..5903f33 --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/stepper_clk.v @@ -0,0 +1,22 @@ +module stepper_clk ( + input wire reset, + input wire sys_clk, + output wire stepper_clk +); + + reg [8:0] base_clk_div; + + assign stepper_clk = (base_clk_div == 9'd499); + + always @(posedge sys_clk) begin + if (reset) begin + base_clk_div <= 9'd0; + end else begin + if (stepper_clk) + base_clk_div <= 9'd0; + else + base_clk_div <= base_clk_div + 1'b1; + end + end + +endmodule diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers.v new file mode 100644 index 0000000..51ed7f7 --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers.v @@ -0,0 +1,1000 @@ +`include "src/main/logging.svh" + +// steppers.v +// +// 8-motor (2 banks x 4) step/dir pulse generator sharing one arithmetic +// datapath and one control FSM across all motors, to keep LC usage low on +// an iCE40HX8K. No multipliers/DSPs are used anywhere - only add/subtract/ +// compare. +// +// Each motor has its own independent segment table and live motion state. +// Motor 3 can be loaded and started while motor 0 is mid-profile; all 8 +// can be loaded with completely different sequences and started together +// (or staggered) with separate start strobes. Segment storage is +// addressed by {motor_instance, segment_index, word_select} - motor N's +// writes/reads only ever touch motor N's slice of the table. Nothing here +// shares live state (period/position/segment pointer/etc) across motors. +// +// ARCHITECTURE +// --------------------------------------------------------------------- +// - Per-motor segment table: up to MAX_SEG ramp segments per motor, held +// in explicit SB_RAM40_4K primitives (see the memory subsystem comment +// below for the full layout). +// - Per-motor live state (current period, delta, remaining step count, +// dir, cmd, segment pointer, position, moving flag, countdown) is a +// small 8-entry register file - unavoidable persistent state, but an +// order of magnitude smaller than replicating a full ALU+sequencer per +// motor. +// - One shared FSM (IDLE/RD0-RD4/COMMIT/FIRE_CALC) walks the 8 motors: +// * start/stop strobes are serviced by a combinational lowest-set-bit +// priority encoder, giving bounded response latency independent of +// where the round-robin currently is. +// * once per stepper_clk tick, the engine round-robins through all 8 +// motor slots (scan_idx), decrementing each active motor's +// countdown, firing a step pulse when it reaches zero, and +// reloading the next segment on segment/profile boundaries. +// +// SEGMENT READBACK: reading REG_STEP_SEG_CTST/SPDM back only returns +// correct data while the addressed motor has not yet been started. Once +// a motor is moving, the shared motion engine can be using the same +// physical memory read port to load that motor's own segment data at any +// time, and a concurrent host read is not synchronized against it - it +// will return whatever address happened to be on the port that cycle, +// which is only guaranteed to be the host's own address when the engine +// isn't using the port at all. There's no production need to read +// segment data back from a running motor; readback exists purely so a +// host can verify a just-uploaded profile before starting it. +// +// STEP PULSE WIDTH: REG_STEP_PLS_CONFIG holds an independent 4-bit +// preset per motor (bits [3:0]/[7:4]/.../[31:28] for motors 0-7). +// REG_STEP_PLS_PRESCALER holds an independent 6-bit prescaler per bank - +// bank 0 in bits [5:0] (the register's low 16-bit half) and bank 1 in +// bits [21:16] (the same position within the high 16-bit half), rather +// than packed back-to-back, so either field can widen later, or other +// per-bank config bits can be added, without touching the other bank's +// half. A motor's step pulse starts the instant that motor fires and +// stays high for exactly (prescaler+1)*(preset+1) sys_clk cycles - a +// per-bank divide value multiplied by a per-motor count value, the same +// shape as a UART baud-rate generator or an STM32 timer's PSC/ARR pair - +// reaching 474 distinct exact widths from 20ns up to 20.48us at 50 MHz. +// Both the pulse's start time and its width are exact, with no timing +// drift on either. See the "Step pulse width" comment further down for +// the exact mechanism (two small per-motor counters, no lookup table or +// adder needed at all). Reset default is preset 0, prescaler 0 on both +// banks - the shortest possible pulse (1 cycle, 20ns) - which is NOT +// safe for any real driver IC without explicit configuration first (see +// the NEMA17 driver minimums a few paragraphs down); this register pair +// works like many real MCU peripheral config registers (e.g. a UART's +// baud rate divisor) in that respect - the host is expected to program +// it before relying on it, not assume a production-safe value out of +// reset. +// +// STANDING DESIGN CHOICES +// --------------------------------------------------------------------- +// - bank_enable_pins is simply {2{global_motor_en}}. +// - CMD_MOVE_HALT and CMD_MOVE_HALT_WAIT are handled identically (both +// pause and wait for a fresh start strobe). +// - A stop strobe is a full abort: moving<=0, segment pointer reset to 0, +// remaining steps cleared. +// - REG_STEP_TX_CONFIG reconfigures a motor immediately, including +// aborting anything in flight for that motor. If it lands in the exact +// sys_clk cycle the shared engine's ST_COMMIT is finishing a load for +// that same motor (only reachable by reconfiguring a motor within a few +// cycles of having just started it), the commit's mot_moving<=1 can +// overwrite the reconfiguration's mot_moving<=0 for that one cycle, +// since the engine's case(state) block runs after the bus decode in +// program order. Not a concern for normal usage (TX_CONFIG, then load +// segments, then start), only for reconfiguring a motor within roughly +// 100ns of having just started that specific motor. +// --------------------------------------------------------------------- + + +module steppers ( + input wire reset, + input wire sys_clk, + + input wire stepper_clk, + + // Bus Slave Interface + input wire bus_stb, + input wire bus_we, + input wire [7:0] bus_addr, + input wire [31:0] bus_din, + output reg [31:0] bus_dout, + output reg bus_ack, + + // Physical Hardware Interfaces + output wire [7:0] step_pins, + output wire [7:0] dir_pins, + output wire [1:0] bank_enable_pins, + input wire global_motor_en +); + + `include "src/main/io/steppers_regs.svh" + `include "src/main/io/steppers_shared.svh" + + localparam NUM_MOTORS = 8; + + // Segments per motor. SEG_IDX_BITS is however many bits are needed to + // index MAX_SEG segments; the memory subsystem below scales its bank + // count directly from it, so raising MAX_SEG raises EBR usage + // (roughly doubling NUM_BANKS each time SEG_IDX_BITS grows by 1). + localparam [7:0] MAX_SEG = 8'd127; + localparam [7:0] MAX_SEG_IDX_LAST = 8'd126; // MAX_SEG-1 + + localparam SEG_IDX_BITS = 7; // clog2(MAX_SEG) + localparam WORD_ADDR_BITS = 3 + SEG_IDX_BITS + 1; // motor+seg_idx+word_sel = 11 + localparam BANK_SEL_BITS = WORD_ADDR_BITS - 8; // bits selecting among banks + localparam NUM_BANKS = 32'd1 << BANK_SEL_BITS; // each SB_RAM40_4K bank is 256 words deep + + // Bus-facing config latches. Declared here (ahead of the memory + // subsystem below, which references them in wire initializers) + // because Icarus requires declaration-before-use in that context. + reg [7:0] tx_num_points; + reg [2:0] tx_motor_instance; + reg [7:0] tx_write_ptr; // REG_STEP_SEG_CTST/SPDM write pointer + reg [7:0] tx_read_ptr; // and an independent read pointer - both + // reset to 0 by REG_STEP_TX_CONFIG, but + // each only advances on its own kind of + // access, so a write-then-read-back + // sequence with no TX_CONFIG re-issue in + // between still starts the read at index 0. + + // Bus-protocol sub-FSM state for REG_STEP_SEG_CTST/SPDM only - every + // other register acks in a single cycle. Writes ack at HI (3 cycles + // total); reads wait one further cycle before acking at ACK (4 cycles + // total). + localparam BUS_STATE_IDLE = 2'd0; + localparam BUS_STATE_LO = 2'd1; + localparam BUS_STATE_HI = 2'd2; + localparam BUS_STATE_ACK = 2'd3; + reg [1:0] bus_state; + + // Shared engine FSM state constants and the state/target registers - + // declared here (ahead of the memory subsystem below, which + // references `state` in engine_owns_port's wire initializer) for the + // same Icarus declaration-before-use reason as above. + localparam ST_IDLE = 3'd0; + localparam ST_RD0 = 3'd1; + localparam ST_RD1 = 3'd2; + localparam ST_RD2 = 3'd3; + localparam ST_RD3 = 3'd4; + localparam ST_RD4 = 3'd5; + localparam ST_COMMIT = 3'd6; + localparam ST_FIRE_CALC = 3'd7; + reg [2:0] state; + reg [2:0] target; // motor index currently being (re)loaded + + // ------------------------------------------------------------------ + // Segment table memory. One address space of NUM_MOTORS x MAX_SEG x 2 + // words, built from explicit SB_RAM40_4K primitives (256 x 16 each) + // rather than an inferred array, so it maps to block RAM + // unambiguously. A 32-bit word is split into two 16-bit halves (lo/ + // hi), each bank getting its own pair of RAM instances; the address + // space is wider than one bank's 256-deep range, so it's split across + // NUM_BANKS banks, selected by the top BANK_SEL_BITS address bits. + // + // There is exactly one physical read port, shared between the motion + // engine (loading a segment while a motor runs) and the bus (a host + // reading CTST/SPDM back). The engine takes priority whenever it's + // actively using the port (states ST_RD0-ST_RD4); otherwise the bus's + // own address is presented. See the "SEGMENT READBACK" note at the + // top of the file for what this means for host reads. + // + // Address layout: {motor[2:0], seg_idx[SEG_IDX_BITS-1:0], word_sel}. + // The top bit of motor selects which half of the bank space a motor's + // data lives in along with the high segment-index bits; in practice + // the low BANK_SEL_BITS bits of {motor, seg_idx} select the bank and + // the remaining bits address within it - the generate loop below and + // the address split (shared_raddr[WORD_ADDR_BITS-1:8] for bank, + // [7:0] for in-bank) handle this uniformly regardless of exactly + // where the bank/motor/segment boundaries fall. + // + // SB_RAM40_4K has a genuine 1-cycle registered-address read latency: + // the address must be stable for a full clock cycle before RDATA + // reflects it, not combinationally. ST_RD0-ST_RD4 below account for + // this explicitly (one cycle to present each address, one more to + // read the result). + // ------------------------------------------------------------------ + + // Write side (bus only - the engine never writes segment data). + // Purely combinational, asserted for exactly the one cycle + // (BUS_STATE_HI with bus_we) the write should commit. + wire word_sel_bus = (bus_addr == REG_STEP_SEG_SPDM); + wire [WORD_ADDR_BITS-1:0] wr_word_addr = {tx_motor_instance, tx_write_ptr[SEG_IDX_BITS-1:0], word_sel_bus}; + wire wr_en = bus_stb && (bus_state == BUS_STATE_HI) && bus_we && + (bus_addr == REG_STEP_SEG_CTST || bus_addr == REG_STEP_SEG_SPDM); + wire [7:0] wr_addr = wr_word_addr[7:0]; + wire [BANK_SEL_BITS-1:0] wr_bank = wr_word_addr[WORD_ADDR_BITS-1:8]; + wire [15:0] wr_data_lo = bus_din[15:0]; + wire [15:0] wr_data_hi = bus_din[31:16]; + + // Engine-side read address (set by ST_RD0/ST_RD2 below, held stable + // across the following state while the RAM captures it). + reg [WORD_ADDR_BITS-1:0] mem_addr; + + // Bus-side read address - fully determined by (tx_motor_instance, + // tx_read_ptr, which of CTST/SPDM is selected), stable for the whole + // multi-cycle bus transaction. + wire [WORD_ADDR_BITS-1:0] rd_word_addr = {tx_motor_instance, tx_read_ptr[SEG_IDX_BITS-1:0], word_sel_bus}; + + // Shared read port arbitration: the engine wins whenever it's + // actively presenting an address (ST_RD0-ST_RD4); the bus gets the + // port at every other time, including while a motor is moving but the + // engine isn't between reload cycles for it. + wire engine_owns_port = (state == ST_RD0) || (state == ST_RD1) || (state == ST_RD2) || + (state == ST_RD3) || (state == ST_RD4); + wire [WORD_ADDR_BITS-1:0] shared_raddr = engine_owns_port ? mem_addr : rd_word_addr; + wire [BANK_SEL_BITS-1:0] rd_bank_sel = shared_raddr[WORD_ADDR_BITS-1:8]; + + wire [NUM_BANKS-1:0] bank_we; + wire [15:0] bank_rdata_lo [0:NUM_BANKS-1]; + wire [15:0] bank_rdata_hi [0:NUM_BANKS-1]; + + genvar gb; + generate + for (gb = 0; gb < NUM_BANKS; gb = gb + 1) begin : SEG_BANK + assign bank_we[gb] = wr_en && (wr_bank == gb[BANK_SEL_BITS-1:0]); + + SB_RAM40_4K #(.WRITE_MODE(0), .READ_MODE(0)) ram_lo ( + .RCLK(sys_clk), .RCLKE(1'b1), .RE(1'b1), + .RADDR({3'b000, shared_raddr[7:0]}), .RDATA(bank_rdata_lo[gb]), + .WCLK(sys_clk), .WCLKE(1'b1), .WE(bank_we[gb]), + .WADDR({3'b000, wr_addr}), .WDATA(wr_data_lo), .MASK(16'h0000) + ); + SB_RAM40_4K #(.WRITE_MODE(0), .READ_MODE(0)) ram_hi ( + .RCLK(sys_clk), .RCLKE(1'b1), .RE(1'b1), + .RADDR({3'b000, shared_raddr[7:0]}), .RDATA(bank_rdata_hi[gb]), + .WCLK(sys_clk), .WCLKE(1'b1), .WE(bank_we[gb]), + .WADDR({3'b000, wr_addr}), .WDATA(wr_data_hi), .MASK(16'h0000) + ); + end + endgenerate + + wire [31:0] shared_rdata = {bank_rdata_hi[rd_bank_sel], bank_rdata_lo[rd_bank_sel]}; + + // CTST word layout (as written by the host): + // [31:28] reserved + // [27] ramp (RAMP_DOWN/RAMP_UP) + // [26] dir (DIR_NORMAL/DIR_REVERSE) + // [25:24] cmd (CMD_MOVE/CMD_MOVE_HALT/CMD_MOVE_HALT_WAIT) + // [23:0] n_steps (steps in this segment) + // SPDM word layout: + // [31:16] start_period (SP) + // [15:0] delta_magnitude (DM) + + // ------------------------------------------------------------------ + // Per-motor live context (small register file, 8 entries) - fully + // independent per motor. + // ------------------------------------------------------------------ + reg [15:0] mot_period [0:7]; + reg [15:0] mot_dm [0:7]; + reg [23:0] mot_remaining [0:7]; + reg [7:0] mot_segptr [0:7]; // 0..MAX_SEG, MAX_SEG means "exhausted" + reg [7:0] mot_totalseg [0:7]; + reg mot_moving [0:7]; + reg [31:0] mot_position [0:7]; + reg [15:0] mot_countdown [0:7]; + reg mot_dir [0:7]; + reg [1:0] mot_cmd [0:7]; + reg mot_incr [0:7]; + + // Start/stop strobe bitmaps (set by bus writes, cleared by the engine) + reg [7:0] pending_start; + reg [7:0] pending_stop; + + // ------------------------------------------------------------------ + // Step pulse width, UART-baud-rate-generator/STM32-timer style: a + // per-bank prescaler (divide) and a per-motor preset (count), which + // MULTIPLY rather than add. REG_STEP_PLS_CONFIG holds one 4-bit + // preset per motor; REG_STEP_PLS_PRESCALER holds one 6-bit prescaler + // per bank - bank 0 in bits[5:0], bank 1 in bits[21:16] (see the bus + // decode below), so each bank gets a full 16-bit half of the + // register to grow into later rather than the two fields packed + // back-to-back (motor[2] selects bank - motors 0-3 are bank 0, 4-7 + // are bank 1, same split as the memory subsystem's bank addressing). + // Register value V means "V+1" throughout (0 still means "1"), so: + // divide = pls_prescaler[bank_of(m)] + 1 (1-64 sys_clk cycles per tick) + // count = pls_preset[m] + 1 (1-16 ticks) + // width = divide * count sys_clk cycles (1-1024 cycles, exactly) + // + // Deliberately asymmetric: prescaler is wider (6 bits) than preset (4 + // bits) because prescaler is per-BANK (only 2 instances) while preset + // is per-MOTOR (8 instances) - widening prescaler buys more range for + // less LC cost than widening preset would, since only prescaler's + // config storage is cheap to widen (pulse_inner, the per-motor + // counter that actually holds it while counting down, still costs + // one bit per motor either way). + // + // An earlier version of this made preset and prescaler both act as + // exponents (K = preset+prescaler, width = 2^K), which turned out to + // be the wrong shape entirely: since the SUM saturated at a plain + // 4-bit K, the 256 possible (preset, prescaler) combinations only + // ever produced 16 distinct widths (the powers of 2 from 1 to + // 32768) - most combinations were redundant with each other, and + // plenty of practically useful widths (e.g. exactly 100 cycles, 2us + // at 50MHz) were simply unreachable no matter what was configured. + // Multiplying instead of adding fixes this: with the current 6-bit + // prescaler and 4-bit preset, the 1024 possible combinations reach + // 474 distinct widths, including awkward-looking targets like 100 + // (prescaler=9, preset=9) that no power of 2 is close to. + // + // Each motor has its own pair of small down-counters - pulse_inner + // (6 bits, counts sys_clk cycles within one tick) and pulse_outer (4 + // bits, counts ticks) - plus a pulse_active flag. The instant a + // motor fires: pulse_active is set, pulse_inner is loaded with that + // motor's bank's prescaler, pulse_outer is loaded with that motor's + // own preset, and step_pulse goes high immediately (exact start + // time, same as before). Every cycle after that: pulse_inner + // decrements; whenever it reaches zero, that's one tick - pulse_outer + // decrements and pulse_inner reloads from the prescaler for the next + // tick - until pulse_outer ALSO reaches zero on a tick boundary, at + // which point the pulse ends (exact width, same guarantee as + // before). Both counters are entirely private to each motor, freshly + // reloaded at that motor's own fire event - never a shared, + // continuously-running counter whose phase could drift relative to + // any particular motor's fire schedule the way an earlier version's + // shared prescale counter did. + // + // This is cheaper than the original additive version, not just more + // capable: two small counters plus two cheap zero-checks per motor, + // instead of one wide (15-bit) counter fed by a 16-way lookup table + // and a saturating adder. + // + // Reset default is preset 0, prescaler 0 on both banks (divide=1, + // count=1, width=1 cycle = 20ns at 50 MHz) - the shortest possible + // pulse, not a safe one. Common NEMA17 driver ICs need considerably + // more (A4988 ~1us, DRV8825 ~1.9us, TMC2209 ~100ns minimum/~1us + // recommended), so unlike the segment table or motion parameters, + // this is a register pair the host must explicitly configure before + // relying on it - there's no single default width that would be + // safe for every attached driver without knowing which one it is, + // and defaulting to the shortest possible pulse means an + // unconfigured system fails obviously (no movement, or a driver + // that visibly rejects too-short pulses) rather than silently + // working by chance on some drivers and not others. + // + // Configuring a pulse width wider than the interval between steps + // (i.e. wider than the segment's SP allows) is still a configuration + // error, same as it would be on real driver hardware - you cannot + // physically step faster than your own pulse width permits - but it + // no longer produces an ambiguous output. The previous pulse always + // gets its full configured width (never cut short), and the next + // one is deferred by exactly one cycle if it would otherwise start + // before the previous one finished - guaranteeing at least one low + // cycle between any two steps for the same motor no matter how the + // configured width compares to the step interval, so anything + // watching step_pins can always tell the steps apart. See + // pulse_force_reload below for the mechanism. + // ------------------------------------------------------------------ + reg [3:0] pls_preset [0:7]; // per motor: count-1 + reg [5:0] pls_prescaler [0:1]; // per bank (index 0 = bank 0, 1 = bank 1): divide-1 + reg [5:0] pulse_inner [0:7]; // per-motor: sys_clk cycles left in the current tick + reg [3:0] pulse_outer [0:7]; // per-motor: ticks left + reg pulse_active [0:7]; // this motor's pulse is currently running + reg pulse_force_reload [0:7]; // previous pulse was still active + // when this motor fired again - + // forcing one low cycle before the + // new pulse starts (see the FIRE + // branch and the per-cycle pulse + // output block further down) + + // A fire arriving while the previous pulse is still active (pulse + // width configured wider than the step interval) doesn't corrupt + // anything, but it used to mean the output never dropped low between + // those steps at all - no falling edge, so no way for anything + // watching step_pins to tell that two (or more) steps happened + // rather than one. Now: the previous pulse runs its FULL interval + // (never cut short), and the new one is deferred by exactly one + // cycle - forced low for that one cycle, then started - so there's + // always at least one low cycle between any two steps for the same + // motor, regardless of configured width vs. interval. Internal + // motion state (position, remaining count, segment advance) is + // completely unaffected either way - this only changes how the + // output pulse is shaped when misconfigured. + + // Step pulse outputs (1+ sys_clk cycles wide per fired step, + // stretched to the configured width by pulse_inner/pulse_outer above) + reg [7:0] step_pulse; + + // ------------------------------------------------------------------ + // New-request detection. A request counts as new whenever (address, + // we) differs from the last one this block started servicing, or + // bus_stb was low last cycle - this correctly recognizes a fresh + // request even if bus_stb toggles high/low/high entirely between two + // posedges (zero simulation-time gap between transactions), which + // gating on "!bus_ack" alone would miss. + // ------------------------------------------------------------------ + reg [7:0] last_bus_addr; + reg last_bus_we; + reg last_bus_valid; + wire is_new_request = bus_stb && (!last_bus_valid || (bus_addr != last_bus_addr) || (bus_we != last_bus_we)); + + // Round-robin / tick bookkeeping + reg [2:0] scan_idx; + reg tick_flag; + reg [7:0] tick_done; + reg stepper_clk_d; + wire tick_strobe = stepper_clk & ~stepper_clk_d; + + // ctst_word/spdm_word: captured segment data, used by ST_RD2/RD4/COMMIT. + reg [31:0] ctst_word; + reg [31:0] spdm_word; + + // Scratch combinational variables (blocking-assigned, used only + // within the same always-block evaluation, always fully computed + // before being written to any array). + reg [23:0] new_remaining; + reg [15:0] new_period; + reg [15:0] new_countdown; + reg [7:0] new_segptr; + reg [31:0] pos_delta; + reg [31:0] new_position; + reg [2:0] pidx; + reg [7:0] segptr_rd; // scratch: mot_segptr[target] read out before use + + // FIRE-completion snapshot registers. The step pulse and position + // update happen the same cycle the countdown reaches zero (ST_IDLE + // below); the remaining/period/segment-boundary bookkeeping is + // deferred one cycle (ST_FIRE_CALC) into these plain registers, + // keeping the scan_idx-indexed array read and the arithmetic on + // separate cycles for timing closure. + reg [23:0] snap_remaining; + reg [15:0] snap_period; + reg [15:0] snap_dm; + reg snap_incr; + reg [1:0] snap_cmd; + reg [7:0] snap_segptr; + reg [7:0] snap_totalseg; + + wire any_stop = |pending_stop; + wire any_start = |pending_start; + + integer i; + integer j; + + // ------------------------------------------------------------------ + // Lowest-set-bit priority encoder - gives start/stop strobes a + // bounded response latency independent of where scan_idx happens to + // be in its round-robin sweep. + // ------------------------------------------------------------------ + function [2:0] priority_idx; + input [7:0] bits; + begin + casez (bits) + 8'b???????1: priority_idx = 3'd0; + 8'b??????10: priority_idx = 3'd1; + 8'b?????100: priority_idx = 3'd2; + 8'b????1000: priority_idx = 3'd3; + 8'b???10000: priority_idx = 3'd4; + 8'b??100000: priority_idx = 3'd5; + 8'b?1000000: priority_idx = 3'd6; + 8'b10000000: priority_idx = 3'd7; + default: priority_idx = 3'd0; + endcase + end + endfunction + + // ------------------------------------------------------------------ + // Shared period ALU: saturating add (period increasing) / floor-at-1 + // subtract (period decreasing). + // ------------------------------------------------------------------ + function [15:0] calc_next_period; + input [15:0] period; + input [15:0] delta; + input incr; + reg [16:0] sum; + begin + if (incr) begin + sum = {1'b0, period} + {1'b0, delta}; + calc_next_period = (sum > 17'd65535) ? 16'd65535 : sum[15:0]; + end else begin + if (delta >= period) + calc_next_period = 16'd1; + else + calc_next_period = period - delta; + end + end + endfunction + + // ------------------------------------------------------------------ + // Output pin assigns + // ------------------------------------------------------------------ + assign step_pins = step_pulse; + assign dir_pins = {mot_dir[7], mot_dir[6], mot_dir[5], mot_dir[4], + mot_dir[3], mot_dir[2], mot_dir[1], mot_dir[0]}; + assign bank_enable_pins = {2{global_motor_en}}; + + always @(posedge sys_clk) begin + if (reset) begin + bus_dout <= 32'h00000000; + bus_ack <= 1'b0; + + tx_num_points <= 8'd0; + tx_motor_instance <= 3'd0; + tx_write_ptr <= 8'd0; + tx_read_ptr <= 8'd0; + bus_state <= BUS_STATE_IDLE; + last_bus_addr <= 8'd0; + last_bus_we <= 1'b0; + last_bus_valid <= 1'b0; + + pending_start <= 8'd0; + pending_stop <= 8'd0; + step_pulse <= 8'd0; + pls_prescaler[0] <= 6'd0; // default: shortest possible pulse (divide=1, count=1 -> 1 cycle = 20ns) - + pls_prescaler[1] <= 6'd0; // see the "Step pulse width" comment above for why + + scan_idx <= 3'd0; + tick_flag <= 1'b0; + tick_done <= 8'd0; + stepper_clk_d <= 1'b0; + + state <= ST_IDLE; + target <= 3'd0; + snap_remaining <= 24'd0; + snap_period <= 16'd0; + snap_dm <= 16'd0; + snap_incr <= 1'b0; + snap_cmd <= 2'd0; + snap_segptr <= 8'd0; + snap_totalseg <= 8'd0; + mem_addr <= {WORD_ADDR_BITS{1'b0}}; + + for (i = 0; i < 8; i = i + 1) begin + mot_period[i] <= 16'd0; + mot_dm[i] <= 16'd0; + mot_remaining[i] <= 24'd0; + mot_segptr[i] <= 8'd0; + mot_totalseg[i] <= 8'd0; + mot_moving[i] <= 1'b0; + mot_position[i] <= 32'd0; + mot_countdown[i] <= 16'd0; + mot_dir[i] <= 1'b0; + mot_cmd[i] <= 2'd0; + mot_incr[i] <= 1'b0; + pls_preset[i] <= 4'd0; // default: shortest possible pulse (paired with prescaler=0) + pulse_inner[i] <= 6'd0; + pulse_outer[i] <= 4'd0; + pulse_active[i] <= 1'b0; + pulse_force_reload[i] <= 1'b0; + end + end else begin + // Default: pulse outputs clear every cycle unless re-asserted + // below. + step_pulse <= 8'd0; + bus_ack <= 1'b0; + stepper_clk_d <= stepper_clk; + + // A new tick always (re)arms full service of all 8 motors, + // independent of bus activity or FSM state below. + if (tick_strobe) begin + tick_flag <= 1'b1; + tick_done <= 8'd0; + end + + // ============================================================ + // Bus slave interface + // ============================================================ + if (!bus_stb) begin + bus_state <= BUS_STATE_IDLE; + last_bus_valid <= 1'b0; + end else if (bus_addr == REG_STEP_SEG_CTST || bus_addr == REG_STEP_SEG_SPDM) begin + // Segment table access goes through a multi-cycle + // IDLE->LO->HI->(ACK on read) handshake. Writes commit + // combinationally into the SB_RAM40_4K instances above + // (bank_we/wr_addr/wr_data_lo/wr_data_hi are already + // asserted this exact cycle whenever bus_state==HI and + // bus_we) and ack at HI (3 cycles total); reads pull the + // already-assembled shared_rdata but still wait for the + // ACK cycle (4 cycles total). Only entry into LO is gated + // on is_new_request - once latched, LO->HI->ACK continues + // every cycle regardless of whether bus_stb ever reads + // low, since the master is expected to hold (addr, we, + // din) steady for the whole multi-cycle transaction. + case (bus_state) + BUS_STATE_IDLE: begin + if (is_new_request) begin + last_bus_addr <= bus_addr; + last_bus_we <= bus_we; + last_bus_valid <= 1'b1; + bus_state <= BUS_STATE_LO; + end + end + BUS_STATE_LO: begin + bus_state <= BUS_STATE_HI; + end + BUS_STATE_HI: begin + if (bus_we) begin + // Physical write already committed this cycle + // via the combinational bank_we/wr_addr/ + // wr_data_lo/wr_data_hi feeding the RAM + // instances above - just handle the pointer + // advance and ack here. + if (bus_addr == REG_STEP_SEG_SPDM && tx_write_ptr < MAX_SEG_IDX_LAST) + tx_write_ptr <= tx_write_ptr + 8'd1; + bus_dout <= 32'h00000000; + bus_ack <= 1'b1; + bus_state <= BUS_STATE_IDLE; + end else begin + bus_state <= BUS_STATE_ACK; + end + end + BUS_STATE_ACK: begin + bus_dout <= shared_rdata; + bus_ack <= 1'b1; + bus_state <= BUS_STATE_IDLE; + if (bus_addr == REG_STEP_SEG_SPDM && tx_read_ptr < MAX_SEG_IDX_LAST) + tx_read_ptr <= tx_read_ptr + 8'd1; + end + default: bus_state <= BUS_STATE_IDLE; + endcase + end else if (is_new_request) begin + last_bus_addr <= bus_addr; + last_bus_we <= bus_we; + last_bus_valid <= 1'b1; + case (bus_addr) + REG_STEP_CTRL: begin + if (bus_we) begin + pending_start <= pending_start | {bus_din[19:16], bus_din[11:8]}; + pending_stop <= pending_stop | {bus_din[23:20], bus_din[15:12]}; + bus_dout <= 32'h00000000; + end else begin + bus_dout <= {24'd0, + mot_moving[7], mot_moving[6], mot_moving[5], mot_moving[4], + mot_moving[3], mot_moving[2], mot_moving[1], mot_moving[0]}; + end + bus_ack <= 1'b1; + end + + REG_STEP_TX_CONFIG: begin + if (bus_we) begin + tx_num_points <= bus_din[7:0]; + tx_motor_instance <= bus_din[10:8]; + tx_write_ptr <= 8'd0; + tx_read_ptr <= 8'd0; + // Only this one motor's context is + // touched - every other motor's live + // state and table are completely + // untouched by this write. See "REG_STEP_ + // TX_CONFIG reconfigures..." in the + // standing design choices above. + mot_totalseg[bus_din[10:8]] <= (bus_din[7:0] > MAX_SEG) ? MAX_SEG : bus_din[7:0]; + mot_segptr[bus_din[10:8]] <= 8'd0; + mot_moving[bus_din[10:8]] <= 1'b0; + mot_remaining[bus_din[10:8]] <= 24'd0; + end else begin + bus_dout <= {21'd0, tx_motor_instance, tx_num_points}; + end + bus_ack <= 1'b1; + end + + REG_STEP_PLS_CONFIG: begin + if (bus_we) begin + pls_preset[0] <= bus_din[3:0]; + pls_preset[1] <= bus_din[7:4]; + pls_preset[2] <= bus_din[11:8]; + pls_preset[3] <= bus_din[15:12]; + pls_preset[4] <= bus_din[19:16]; + pls_preset[5] <= bus_din[23:20]; + pls_preset[6] <= bus_din[27:24]; + pls_preset[7] <= bus_din[31:28]; + end else begin + bus_dout <= {pls_preset[7], pls_preset[6], pls_preset[5], pls_preset[4], + pls_preset[3], pls_preset[2], pls_preset[1], pls_preset[0]}; + end + bus_ack <= 1'b1; + end + + REG_STEP_PLS_PRESCALER: begin + if (bus_we) begin + pls_prescaler[0] <= bus_din[5:0]; + pls_prescaler[1] <= bus_din[21:16]; + end else begin + bus_dout <= {10'd0, pls_prescaler[1], 10'd0, pls_prescaler[0]}; + end + bus_ack <= 1'b1; + end + + REG_STEP_STATUS_0: begin bus_dout <= {31'd0, mot_moving[0]}; bus_ack <= 1'b1; end + REG_STEP_STATUS_1: begin bus_dout <= {31'd0, mot_moving[1]}; bus_ack <= 1'b1; end + REG_STEP_STATUS_2: begin bus_dout <= {31'd0, mot_moving[2]}; bus_ack <= 1'b1; end + REG_STEP_STATUS_3: begin bus_dout <= {31'd0, mot_moving[3]}; bus_ack <= 1'b1; end + REG_STEP_STATUS_4: begin bus_dout <= {31'd0, mot_moving[4]}; bus_ack <= 1'b1; end + REG_STEP_STATUS_5: begin bus_dout <= {31'd0, mot_moving[5]}; bus_ack <= 1'b1; end + REG_STEP_STATUS_6: begin bus_dout <= {31'd0, mot_moving[6]}; bus_ack <= 1'b1; end + REG_STEP_STATUS_7: begin bus_dout <= {31'd0, mot_moving[7]}; bus_ack <= 1'b1; end + + REG_STEP_POS_0: begin bus_dout <= mot_position[0]; bus_ack <= 1'b1; end + REG_STEP_POS_1: begin bus_dout <= mot_position[1]; bus_ack <= 1'b1; end + REG_STEP_POS_2: begin bus_dout <= mot_position[2]; bus_ack <= 1'b1; end + REG_STEP_POS_3: begin bus_dout <= mot_position[3]; bus_ack <= 1'b1; end + REG_STEP_POS_4: begin bus_dout <= mot_position[4]; bus_ack <= 1'b1; end + REG_STEP_POS_5: begin bus_dout <= mot_position[5]; bus_ack <= 1'b1; end + REG_STEP_POS_6: begin bus_dout <= mot_position[6]; bus_ack <= 1'b1; end + REG_STEP_POS_7: begin bus_dout <= mot_position[7]; bus_ack <= 1'b1; end + + default: begin + bus_dout <= 32'hDEADBEEF; + bus_ack <= 1'b1; + end + endcase + end + // else (bus_stb && !is_new_request && not CTST/SPDM): the + // master is still holding a request already fully serviced + // (ack already pulsed on the cycle it was accepted) - nothing + // further to do until (addr,we) changes or stb drops. + + // ============================================================ + // Step pulse width. Runs every cycle for all 8 motors + // independently of the round-robin below - a motor's pulse + // can still be counting down long after scan_idx has moved + // on to service other motors. + // + // If pulse_force_reload[j] is set (from the FIRE branch + // below, on some earlier cycle - see its comment for when), + // this cycle starts the deferred new pulse: reload + // pulse_inner/pulse_outer straight from that motor's config + // (pls_prescaler/pls_preset - stable registers, nothing else + // could have changed them in the one cycle since they were + // last read), set pulse_active, assert step_pulse. This is + // exactly what a normal fire does, just one cycle later than + // the tick that actually requested it. + // + // Otherwise, while pulse_active[j]: first check whether BOTH + // pulse_inner[j] and pulse_outer[j] are already zero - if so, + // this cycle is the pulse's end (pulse_active clears, + // step_pulse is NOT asserted, going low via the default + // clear). Otherwise step_pulse[j] stays high, and + // pulse_inner[j] decrements; when IT reaches zero that's one + // tick complete, so pulse_outer[j] decrements and + // pulse_inner[j] reloads from that motor's bank's prescaler + // for the next tick. Checking for exhaustion before asserting + // (rather than asserting unconditionally and checking after) + // is what keeps the width exact rather than one cycle too + // long. The motion engine's FIRE branch below (which runs + // later in program order) loads pulse_inner/pulse_outer + // fresh and re-asserts step_pulse[scan_idx] on the cycle a + // motor actually fires (or sets pulse_force_reload instead, + // if the previous pulse hadn't finished), taking priority + // over this for that one bit. + // ============================================================ + for (j = 0; j < 8; j = j + 1) begin + if (pulse_force_reload[j]) begin + pulse_force_reload[j] <= 1'b0; + pulse_active[j] <= 1'b1; + step_pulse[j] <= 1'b1; + pulse_inner[j] <= pls_prescaler[j[2]]; + pulse_outer[j] <= pls_preset[j]; + end else if (pulse_active[j]) begin + if (pulse_inner[j] == 6'd0 && pulse_outer[j] == 4'd0) begin + // Both exhausted - this cycle is the pulse's end. + // step_pulse[j] is NOT asserted here, going low + // via the default clear at the top of this + // always block. + pulse_active[j] <= 1'b0; + end else begin + step_pulse[j] <= 1'b1; + if (pulse_inner[j] == 6'd0) begin + pulse_outer[j] <= pulse_outer[j] - 4'd1; + pulse_inner[j] <= pls_prescaler[j[2]]; + end else begin + pulse_inner[j] <= pulse_inner[j] - 6'd1; + end + end + end + end + + // ============================================================ + // Shared motion engine + // ============================================================ + case (state) + + ST_IDLE: begin + if (any_stop) begin + pidx = priority_idx(pending_stop); + mot_moving[pidx] <= 1'b0; + mot_remaining[pidx] <= 24'd0; + mot_segptr[pidx] <= 8'd0; + pending_stop[pidx] <= 1'b0; + if (tick_flag) tick_done[pidx] <= 1'b1; + end + + else if (any_start) begin + pidx = priority_idx(pending_start); + pending_start[pidx] <= 1'b0; + if (!mot_moving[pidx] && (mot_segptr[pidx] < mot_totalseg[pidx])) begin + target <= pidx; + state <= ST_RD0; + end + // else: motor already running, or profile + // exhausted (needs a fresh TX_CONFIG) - strobe is + // a no-op. + end + + else if (tick_flag && (tick_done != 8'hFF)) begin + if (tick_done[scan_idx]) begin + // Shouldn't normally happen (every index only + // gets visited once per tick), but rotate on + // if it does, rather than stalling. + scan_idx <= scan_idx + 3'd1; + end else if (!mot_moving[scan_idx]) begin + tick_done[scan_idx] <= 1'b1; + scan_idx <= scan_idx + 3'd1; + end else if (mot_countdown[scan_idx] > 16'd1) begin + new_countdown = mot_countdown[scan_idx] - 16'd1; + mot_countdown[scan_idx] <= new_countdown; + tick_done[scan_idx] <= 1'b1; + scan_idx <= scan_idx + 3'd1; + end else begin + // FIRE: countdown has reached the end of its + // interval. Position update happens this + // exact cycle regardless; everything else + // (remaining count/period/segment bookkeeping) + // is snapshotted into plain registers and + // finished next cycle in ST_FIRE_CALC. + // + // The step pulse itself: if the previous + // pulse has already finished (the normal, + // well-configured case), start the new one + // immediately, this exact cycle - exact + // timing, same as always. If it's STILL + // active (pulse width configured wider than + // this step interval), force a clean gap + // instead of stepping on it: end the previous + // pulse now (letting it run its full + // interval rather than cutting it short) and + // set pulse_force_reload, which starts the + // new pulse exactly one cycle later - see the + // pulse output block above. Either way, + // pulse_outer/pulse_inner get loaded from + // this motor's config exactly once, either + // here or (one cycle later) there. + if (pulse_active[scan_idx]) begin + step_pulse[scan_idx] <= 1'b0; + pulse_active[scan_idx] <= 1'b0; + pulse_force_reload[scan_idx] <= 1'b1; + end else begin + step_pulse[scan_idx] <= 1'b1; + pulse_active[scan_idx] <= 1'b1; + pulse_force_reload[scan_idx] <= 1'b0; + pulse_inner[scan_idx] <= pls_prescaler[scan_idx[2]]; + pulse_outer[scan_idx] <= pls_preset[scan_idx]; + end + tick_done[scan_idx] <= 1'b1; + + pos_delta = (mot_dir[scan_idx] == DIR_NORMAL) ? 32'd1 : 32'hFFFFFFFF; + new_position = mot_position[scan_idx] + pos_delta; + mot_position[scan_idx] <= new_position; + `DBG_LOG(("[STEPPERS DEBUG] t=%0t FIRE motor=%0d dir=%b old_pos=%0d pos_delta=%0d new_pos=%0d remaining_before=%0d cmd=%b countdown_was=%0d", + $time, scan_idx, mot_dir[scan_idx], mot_position[scan_idx], pos_delta, + new_position, mot_remaining[scan_idx], mot_cmd[scan_idx], mot_countdown[scan_idx])); + + target <= scan_idx; + snap_remaining <= mot_remaining[scan_idx]; + snap_period <= mot_period[scan_idx]; + snap_dm <= mot_dm[scan_idx]; + snap_incr <= mot_incr[scan_idx]; + snap_cmd <= mot_cmd[scan_idx]; + snap_segptr <= mot_segptr[scan_idx]; + snap_totalseg <= mot_totalseg[scan_idx]; + state <= ST_FIRE_CALC; + end + end + + else if (tick_flag && (tick_done == 8'hFF)) begin + tick_flag <= 1'b0; + end + // else: nothing pending, hold. + end + + ST_FIRE_CALC: begin + // All inputs here are plain registers snapshotted + // last cycle - no scan_idx-indexed mux in this + // cycle's combinational path. + new_remaining = snap_remaining - 24'd1; + new_period = calc_next_period(snap_period, snap_dm, snap_incr); + + if (new_remaining == 24'd0) begin + if (snap_cmd == CMD_MOVE && + ((snap_segptr + 8'd1) < snap_totalseg)) begin + // Seamless streaming continuation: fresh + // reload from the next segment's own SP, + // discarding this segment's evolved period. + new_segptr = snap_segptr + 8'd1; + mot_segptr[target] <= new_segptr; + mot_remaining[target] <= 24'd0; + state <= ST_RD0; // target already set + end else begin + // CMD_MOVE_HALT / CMD_MOVE_HALT_WAIT, or a + // CMD_MOVE that ran off the end of the table + // (malformed profile - stop safely). + new_segptr = snap_segptr + 8'd1; + mot_moving[target] <= 1'b0; + mot_segptr[target] <= new_segptr; + mot_remaining[target] <= 24'd0; + mot_period[target] <= new_period; + scan_idx <= target + 3'd1; + state <= ST_IDLE; + end + end else begin + mot_remaining[target] <= new_remaining; + mot_period[target] <= new_period; + mot_countdown[target] <= new_period; + scan_idx <= target + 3'd1; + state <= ST_IDLE; + end + end + + ST_RD0: begin + // Present the CTST address this cycle; the physical + // RAM needs it stable for a full cycle before RDATA + // reflects it (see ST_RD1). + segptr_rd = mot_segptr[target]; + mem_addr <= {target, segptr_rd[SEG_IDX_BITS-1:0], 1'b0}; + state <= ST_RD1; + end + + ST_RD1: begin + // CTST address now stable this whole cycle - the RAM + // is capturing it now; RDATA will be valid next + // cycle. + state <= ST_RD2; + end + + ST_RD2: begin + // shared_rdata now reflects the CTST address set in + // ST_RD0. Capture it, then present the SPDM address. + ctst_word <= shared_rdata; + segptr_rd = mot_segptr[target]; + mem_addr <= {target, segptr_rd[SEG_IDX_BITS-1:0], 1'b1}; + state <= ST_RD3; + end + + ST_RD3: begin + // SPDM address now stable this whole cycle. + state <= ST_RD4; + end + + ST_RD4: begin + // shared_rdata now reflects the SPDM address set in + // ST_RD2. + spdm_word <= shared_rdata; + state <= ST_COMMIT; + end + + ST_COMMIT: begin + mot_period[target] <= spdm_word[31:16]; + mot_dm[target] <= spdm_word[15:0]; + mot_remaining[target] <= ctst_word[23:0]; + mot_dir[target] <= ctst_word[26]; + mot_cmd[target] <= ctst_word[25:24]; + mot_incr[target] <= ctst_word[27]; + mot_countdown[target] <= spdm_word[31:16]; + mot_moving[target] <= 1'b1; + tick_done[target] <= 1'b1; + scan_idx <= target + 3'd1; + state <= ST_IDLE; + `DBG_LOG(("[STEPPERS DEBUG] t=%0t COMMIT motor=%0d ctst=%h spdm=%h dir=%b cmd=%b n_steps=%0d sp=%0d dm=%0d segptr=%0d", + $time, target, ctst_word, spdm_word, ctst_word[26], ctst_word[25:24], + ctst_word[23:0], spdm_word[31:16], spdm_word[15:0], mot_segptr[target])); + end + + default: state <= ST_IDLE; + endcase + end + end + +`ifdef SIM_STEPPER_BUS_DEBUG + // Pure observer - never drives anything, just reports every cycle + // bus_stb is asserted plus the cycle immediately after it drops. + reg dbg_stb_d; + always @(posedge sys_clk) begin + dbg_stb_d <= bus_stb; + if (bus_stb || dbg_stb_d) + $display("[BUS DEBUG] t=%0t stb=%b we=%b addr=%h din=%h ack=%b dout=%h bus_state=%0d", + $time, bus_stb, bus_we, bus_addr, bus_din, bus_ack, bus_dout, bus_state); + end +`endif + +endmodule diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers_regs.svh b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers_regs.svh new file mode 100644 index 0000000..fa05b5b --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers_regs.svh @@ -0,0 +1,27 @@ +localparam REG_STEP_CTRL = 8'h00; +localparam REG_STEP_TX_CONFIG = 8'h04; +localparam REG_STEP_PLS_CONFIG = 8'h08; // per-motor step pulse width preset (tick count - 1), 4 bits each: motor0=bits[3:0], motor1=[7:4], ... motor7=[31:28] +localparam REG_STEP_PLS_PRESCALER = 8'h0c; // per-bank step pulse width prescaler (sys_clk cycles per tick - 1), 6 bits each: bank0=bits[5:0] (low 16-bit half), bank1=bits[21:16] (high 16-bit half) +localparam REG_STEP_SEG_CTST = 8'h10; +localparam REG_STEP_SEG_SPDM = 8'h14; + +localparam REG_STEP_STATUS_0 = 8'h20; +localparam REG_STEP_STATUS_1 = 8'h24; +localparam REG_STEP_STATUS_2 = 8'h28; +localparam REG_STEP_STATUS_3 = 8'h2c; +localparam REG_STEP_STATUS_4 = 8'h30; +localparam REG_STEP_STATUS_5 = 8'h34; +localparam REG_STEP_STATUS_6 = 8'h38; +localparam REG_STEP_STATUS_7 = 8'h3c; + +// Current absolute position (signed, in steps) per motor - a plain 32-bit +// two's-complement value read back in one cycle, same as the status +// registers. +localparam REG_STEP_POS_0 = 8'h40; +localparam REG_STEP_POS_1 = 8'h44; +localparam REG_STEP_POS_2 = 8'h48; +localparam REG_STEP_POS_3 = 8'h4c; +localparam REG_STEP_POS_4 = 8'h50; +localparam REG_STEP_POS_5 = 8'h54; +localparam REG_STEP_POS_6 = 8'h58; +localparam REG_STEP_POS_7 = 8'h5c; \ No newline at end of file diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers_shared.svh b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers_shared.svh new file mode 100644 index 0000000..936579b --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/steppers_shared.svh @@ -0,0 +1,15 @@ +localparam DIR_NORMAL = 1'b0; // forward / increasing position +localparam DIR_REVERSE = 1'b1; // reverse / decreasing position + +localparam RAMP_UP = 1'b0; // period shrinks each step (accelerating) +localparam RAMP_DOWN = 1'b1; // period grows each step (decelerating) + +// 2-bit per-segment command, packed into CTST[25:24] +localparam CMD_RESERVED = 2'b00; +localparam CMD_MOVE = 2'b01; // finish segment, fall through to the + // next queued segment with no pause +localparam CMD_MOVE_HALT = 2'b10; // finish segment, then stop and wait + // for a fresh start strobe (end of profile) +localparam CMD_MOVE_HALT_WAIT = 2'b11; // finish segment, then stop and wait + // for a fresh start strobe (more + // segments still queued) diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/map.svh b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/map.svh index 38f7912..e0f78fb 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/map.svh +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/map.svh @@ -6,4 +6,6 @@ localparam IO_BASE = 16'h0400; localparam WS0_BASE = 16'h0800; localparam WS1_BASE = 16'h0900; localparam ENCODER_BASE = 16'h0c00; +localparam STEPPERS_BASE = 16'h0d00; + localparam SYSTEM1_BASE = 16'hFF00; diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/memory.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/memory.v index c8a5aff..a34308d 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/memory.v +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/memory.v @@ -54,8 +54,15 @@ module memory ( output wire [7:0] ws1_addr, output wire [31:0] ws1_din, input wire [31:0] ws1_dout, - input wire ws1_ack - + input wire ws1_ack, + + // Bus Interface to Steppers 0 + output reg steppers_stb, + output wire steppers_we, + output wire [7:0] steppers_addr, + output wire [31:0] steppers_din, + input wire [31:0] steppers_dout, + input wire steppers_ack ); `include "src/main/registers/map.svh" @@ -69,19 +76,20 @@ module memory ( localparam PERIPHERAL_BITS = 8; localparam ADDRESS_BITS = 8; - localparam [7:0] TARGET_SYSTEM0 = SYSTEM0_BASE >> PERIPHERAL_BITS; - localparam [7:0] TARGET_LED = LED_BASE >> PERIPHERAL_BITS; - localparam [7:0] TARGET_IO = IO_BASE >> PERIPHERAL_BITS; - localparam [7:0] TARGET_BUZZER = BUZZER_BASE >> PERIPHERAL_BITS; - localparam [7:0] TARGET_ENCODER = ENCODER_BASE >> PERIPHERAL_BITS; - localparam [7:0] TARGET_WS0 = WS0_BASE >> PERIPHERAL_BITS; - localparam [7:0] TARGET_WS1 = WS1_BASE >> PERIPHERAL_BITS; - localparam [7:0] TARGET_SYSTEM1 = SYSTEM1_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_SYSTEM0 = SYSTEM0_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_LED = LED_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_IO = IO_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_BUZZER = BUZZER_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_ENCODER = ENCODER_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_WS0 = WS0_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_WS1 = WS1_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_STEPPERS = STEPPERS_BASE >> PERIPHERAL_BITS; + localparam [7:0] TARGET_SYSTEM1 = SYSTEM1_BASE >> PERIPHERAL_BITS; // Static Control Registers - reg io_we_r, led_we_r, buzzer_we_r, encoder_we_r, ws0_we_r, ws1_we_r; - reg [8:0] io_addr_r, led_addr_r, buzzer_addr_r, encoder_addr_r, ws0_addr_r, ws1_addr_r; - reg [31:0] io_din_r, led_din_r, buzzer_din_r, encoder_din_r, ws0_din_r, ws1_din_r; + reg io_we_r, led_we_r, buzzer_we_r, encoder_we_r, ws0_we_r, ws1_we_r, steppers_we_r; + reg [8:0] io_addr_r, led_addr_r, buzzer_addr_r, encoder_addr_r, ws0_addr_r, ws1_addr_r, steppers_addr_r; + reg [31:0] io_din_r, led_din_r, buzzer_din_r, encoder_din_r, ws0_din_r, ws1_din_r, steppers_din_r; // Pipeline tracking elements reg req_stb_r; @@ -103,6 +111,7 @@ module memory ( assign encoder_we = encoder_we_r; assign encoder_addr = encoder_addr_r; assign encoder_din = encoder_din_r; assign ws0_we = ws0_we_r; assign ws0_addr = ws0_addr_r; assign ws0_din = ws0_din_r; assign ws1_we = ws1_we_r; assign ws1_addr = ws1_addr_r; assign ws1_din = ws1_din_r; + assign steppers_we = steppers_we_r; assign steppers_addr = steppers_addr_r; assign steppers_din = steppers_din_r; // Fast, localized combinatorial target decode wire [7:0] target_a = addr_a[15:8]; @@ -114,7 +123,7 @@ module memory ( reg unmapped_stb = 0; reg unmapped_ack = 0; - wire active_ack = led_ack | io_ack | buzzer_ack | encoder_ack | ws0_ack | ws1_ack | system0_ack | system1_ack | unmapped_ack; + wire active_ack = led_ack | io_ack | buzzer_ack | encoder_ack | ws0_ack | ws1_ack | system0_ack | system1_ack | unmapped_ack | steppers_ack; // These evaluate completely independently of bus_busy or ack_a logic loops wire system0_select = (req_target_r == TARGET_SYSTEM0); @@ -125,6 +134,7 @@ module memory ( wire io_select = (req_target_r == TARGET_IO); wire buzzer_select = (req_target_r == TARGET_BUZZER); wire encoder_select = (req_target_r == TARGET_ENCODER); + wire steppers_select = (req_target_r == TARGET_STEPPERS); wire unmapped_select = !( system0_select | @@ -134,6 +144,7 @@ module memory ( io_select | led_select | buzzer_select | + steppers_select | encoder_select ); @@ -159,6 +170,7 @@ module memory ( encoder_stb <= 1'b0; ws0_stb <= 1'b0; ws1_stb <= 1'b0; + steppers_stb <= 1'b0; system0_stb <= 1'b0; system1_stb <= 1'b0; unmapped_stb <= 1'b0; @@ -169,6 +181,7 @@ module memory ( encoder_we_r <= 1'b0; ws0_we_r <= 1'b0; ws1_we_r <= 1'b0; + steppers_we_r <= 1'b0; end else begin ack_a <= 1'b0; @@ -189,6 +202,7 @@ module memory ( TARGET_ENCODER: dout_a <= encoder_dout; TARGET_WS0: dout_a <= ws0_dout; TARGET_WS1: dout_a <= ws1_dout; + TARGET_STEPPERS: dout_a <= steppers_dout; // SYSTEM0/SYSTEM1 or un-mapped default: dout_a <= global_dout_r; endcase @@ -237,6 +251,7 @@ module memory ( encoder_stb <= 1'b0; ws0_stb <= 1'b0; ws1_stb <= 1'b0; + steppers_stb <= 1'b0; system0_stb <= 1'b0; system1_stb <= 1'b0; unmapped_stb <= 1'b0; @@ -280,6 +295,12 @@ module memory ( encoder_we_r <= req_we_r; encoder_stb <= 1'b1; end + if (steppers_select) begin + steppers_addr_r <= req_addr_r; + steppers_din_r <= req_din_r; + steppers_we_r <= req_we_r; + steppers_stb <= 1'b1; + end // FUTURE consider making system0 and system1 real peripherals if (system0_select) begin system0_stb <= 1'b1; diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/bus_io.svh b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/bus_io.svh index f2ec459..cf3ef99 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/bus_io.svh +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/bus_io.svh @@ -31,6 +31,9 @@ endtask task bus_write(input [7:0] w_addr, input [31:0] w_data); begin +`ifdef SIM_BUS_VERBOSE + $display("[BUS_WRITE] address: %04h, value: %08h", w_addr, w_data); +`endif @(posedge TCXO); addr <= w_addr; din <= w_data; @@ -70,6 +73,9 @@ task bus_read(input [7:0] r_addr, output [31:0] r_data); while (ack) begin @(posedge TCXO); end +`ifdef SIM_BUS_VERBOSE + $display("[BUS_READ] address: %04h, value: %08h", r_addr, r_data); +`endif end endtask diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/int_core_top_tb.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/int_core_top_tb.v index eb63c18..dde4f0b 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/int_core_top_tb.v +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/int_core_top_tb.v @@ -9,6 +9,8 @@ module int_core_top_tb; `include "src/main/io/leds_regs.svh" `include "src/main/io/buzzer_regs.svh" `include "src/main/io/encoders_regs.svh" + `include "src/main/io/steppers_regs.svh" + `include "src/main/io/steppers_shared.svh" `include "src/main/registers/system0_regs.svh" `include "src/main/registers/system1_regs.svh" @@ -79,6 +81,16 @@ module int_core_top_tb; reg [2:0] ENCODER_Y = 3'd0; reg [2:0] ENCODER_Z = 3'd0; + // + // Stepper step/dir pins + // + reg [3:0] XYZF_STEP; + reg [3:0] XYZF_DIR; + reg XYZF_EN; + reg [3:0] BCDE_STEP; + reg [3:0] BCDE_DIR; + reg BCDE_EN; + core_top uut ( .TCXO(TCXO), .QUADSPI1_CLK(clk), @@ -101,7 +113,13 @@ module int_core_top_tb; .ENCODER_Y(ENCODER_Y), .ENCODER_Z(ENCODER_Z), .RGB_PORTS(RGB_PORTS), - .RGB_UP_CAM(RGB_UP_CAM) + .RGB_UP_CAM(RGB_UP_CAM), + .XYZF_STEP(XYZF_STEP), + .XYZF_DIR(XYZF_DIR), + .XYZF_EN(XYZF_EN), + .BCDE_STEP(BCDE_STEP), + .BCDE_DIR(BCDE_DIR), + .BCDE_EN(PCBE_EN) ); // Clock generator helper - Starts from 1, pulls low, then drives high @@ -213,6 +231,146 @@ module int_core_top_tb; end endtask + // ---------------------------------------------------------------- + // General-purpose QuadSPI single-register write/read, wrapping the + // command/address/data primitives above the same way bus_io.svh's + // bus_write/bus_read do for the direct-bus unit testbenches - saves + // repeating the cs_n/io_en/send_*/dummy_phase sequence at every call + // site. settle_ticks lets a caller ask for extra margin on + // registers whose bus decode takes more than one sys_clk cycle to + // ack (see qspi_bus_write_settled below) - ordinary single-cycle-ack + // registers are fine with the default. + // ---------------------------------------------------------------- + task qspi_bus_write; + input [15:0] address; + input [31:0] data; + begin + cs_n = 0; + io_en = 1; + send_command_byte(8'h90); + send_address_word(address); + send_long_word(data); + cs_n = 1; + // Allow the sys_clk domain several cycles to flush out the + // strobe - same margin used throughout this file's existing + // tests for ordinary (single-cycle-ack) registers, PLUS the + // same extra #100 gap those tests always place before their + // next cs_n=0 (repeat(5)@(posedge TCXO) alone is what's used + // WITHIN a test before its own #100 - chaining bus writes + // back to back without that second piece, as this task + // originally did, is 100ns short of the total gap any + // proven-working consecutive-transaction pair in this file + // actually uses). + repeat (5) @(posedge TCXO); + #100; + end + endtask + + // For REG_STEP_SEG_CTST/REG_STEP_SEG_SPDM specifically: steppers.v + // documents these two registers as going through a multi-cycle + // IDLE->LO->HI->(ACK on read) handshake, needing a couple of sys_clk + // cycles beyond a single-cycle-ack register's usual settle time. + // Extra margin here rather than on qspi_bus_write's default, since + // every other register in this design acks in one cycle. + task qspi_bus_write_settled; + input [15:0] address; + input [31:0] data; + begin + cs_n = 0; + io_en = 1; + send_command_byte(8'h90); + send_address_word(address); + send_long_word(data); + cs_n = 1; + repeat (10) @(posedge TCXO); + #100; + end + endtask + + task qspi_bus_read; + input [15:0] address; + output [31:0] data; + begin + cs_n = 0; + io_en = 1; + send_command_byte(8'h10); + send_address_word(address); + dummy_phase(); + read_long_word_data_be(data); + cs_n = 1; + // Same total gap as qspi_bus_write above - reads chained + // back to back need it just as much as writes do. + repeat (5) @(posedge TCXO); + #100; + end + endtask + + // ---------------------------------------------------------------- + // Uploads a 3-segment ramp-up/coast/ramp-down profile to one motor: + // TX_CONFIG selects the motor and rewinds the segment write pointer, + // then each segment's CTST is written followed by its SPDM (SPDM is + // what advances the write pointer to the next segment - see + // steppers.v's bus protocol comment). All three segments share the + // same step counts/periods/delta - only dir varies between the + // forward and reverse passes this test makes. + // ---------------------------------------------------------------- + task upload_trapezoidal_profile; + input [2:0] motor; + input dir; + input [23:0] ramp_steps; + input [23:0] coast_steps; + input [15:0] start_period; + input [15:0] coast_period; + input [15:0] delta_mag; + begin + qspi_bus_write(STEPPERS_BASE + REG_STEP_TX_CONFIG, {16'h0000, {5'd0, motor}, 8'd3}); + + // Segment 0: ramp up (accelerating) - period shrinks from + // start_period down to coast_period over ramp_steps steps, + // then falls straight through (CMD_MOVE) into segment 1. + qspi_bus_write_settled(STEPPERS_BASE + REG_STEP_SEG_CTST, + {4'd0, RAMP_UP, dir, CMD_MOVE, ramp_steps}); + qspi_bus_write_settled(STEPPERS_BASE + REG_STEP_SEG_SPDM, + {start_period, delta_mag}); + + // Segment 1: coast at a constant period (delta_mag=0, so + // RAMP_UP vs RAMP_DOWN makes no difference here) - falls + // through into segment 2. + qspi_bus_write_settled(STEPPERS_BASE + REG_STEP_SEG_CTST, + {4'd0, RAMP_UP, dir, CMD_MOVE, coast_steps}); + qspi_bus_write_settled(STEPPERS_BASE + REG_STEP_SEG_SPDM, + {coast_period, 16'd0}); + + // Segment 2: ramp down (decelerating) - period grows back + // from coast_period up to start_period over ramp_steps + // steps, then halts (CMD_MOVE_HALT, end of profile). + qspi_bus_write_settled(STEPPERS_BASE + REG_STEP_SEG_CTST, + {4'd0, RAMP_DOWN, dir, CMD_MOVE_HALT, ramp_steps}); + qspi_bus_write_settled(STEPPERS_BASE + REG_STEP_SEG_SPDM, + {coast_period, delta_mag}); + end + endtask + + // Polls REG_STEP_CTRL (bits[7:0] = per-motor moving status on read) + // every 10us until every motor reports stopped, or timeout_polls + // iterations have passed without that happening. + task poll_steppers_until_stopped; + input integer timeout_polls; + reg [31:0] ctrl_read; + integer tries; + begin + ctrl_read = 32'hFFFF_FFFF; + tries = 0; + while (ctrl_read[7:0] != 8'h00 && tries < timeout_polls) begin + qspi_bus_read(STEPPERS_BASE + REG_STEP_CTRL, ctrl_read); + tries = tries + 1; + if (ctrl_read[7:0] != 8'h00) #10000; + end + `ASSERT_EQ(ctrl_read[7:0], 8'h00, "0b%08b", + "[STEPPER TRAPEZOID] Motors did not all stop within the polling timeout"); + end + endtask + // Testbench execution variables reg [7:0] read_byte; reg [31:0] read_word; @@ -631,6 +789,182 @@ module int_core_top_tb; end end + // ------------------------------------------------------------- + $display("--- Test 12: Stepper 3-Segment Trapezoidal Move, All 8 Motors, Forward then Reverse ---"); + // ------------------------------------------------------------- + begin : STEPPER_TRAPEZOID_TEST + // ---- Reference math: 1us step pulse width ----------------- + // Starts from target_ns = 1_000 (decimal) and derives the + // (prescaler, preset) register pair from it - the same + // calculation an MCU driver will need, given only the sys_clk + // period and the desired pulse width. Pulse width = + // (prescaler+1)*(preset+1) sys_clk cycles (see steppers.v's + // "Step pulse width" section) - prescaler is 6 bits (divide + // 1-64), preset is 4 bits (count 1-16). + localparam integer NS_PER_CYCLE = 20; + localparam integer MAX_COUNT = 16; // preset+1 max (4-bit reg) + localparam integer MAX_DIVIDE = 64; // prescaler+1 max (6-bit reg) + + integer target_ns; + integer target_cycles; + integer min_divide; + integer search_divide, this_count, this_total; + integer best_divide, best_count, best_total; + integer pls_prescaler_reg, pls_preset_reg; + + // ---- Trapezoidal profile parameters (stepper ticks - a + // separate unit/domain from the pulse-width cycles above) ---- + // Kept deliberately small - total simulated ticks (not step + // count) is what drives simulation time, and the effective + // sys_clk rate observed in this simulation makes even a + // modest per-direction tick count take a long time to + // simulate. This is still small enough (28 ticks/direction) + // to comfortably clear the configured 1us pulse width + // regardless of which rate is actually in effect. + localparam integer RAMP_STEPS = 6; + localparam integer COAST_STEPS = 3; + localparam integer DELTA_MAG = 1; + localparam integer COAST_PERIOD = 2; + // Chosen so the ramp-up segment's LAST step (index + // RAMP_STEPS-1, since period_at_step applies delta + // step_index times) lands exactly on COAST_PERIOD - a smooth + // handoff into the coast segment with no period jump, and the + // ramp-down segment mirrors it symmetrically back up. + localparam integer START_PERIOD = COAST_PERIOD + DELTA_MAG * (RAMP_STEPS - 1); + + // Expected total motion duration, computed in closed form + // rather than simulated step by step - displayed as + // reference (and a useful cross-check against how long the + // test actually takes to run), not used to drive the + // polling below directly - see poll_steppers_until_stopped's + // comment for why. Sum of periods across a ramp segment is a + // triangular-number expression: + // sum_{k=0}^{N-1}(start - k*delta) = N*start - delta*N*(N-1)/2 + // (ramp-down mirrors it, period growing instead of + // shrinking). TICKS_TO_SYS_CYCLES=500 matches stepper_clk.v's + // divider (divides sys_clk by 500 to produce one stepper + // tick - the unit start_period/coast_period/delta above are + // all expressed in) - assumes 20ns/cycle (the 50MHz + // synthesis constraint), which this simulation's actual + // effective rate does not seem to match (again, see + // poll_steppers_until_stopped's comment), so treat the + // displayed value as an order-of-magnitude reference rather + // than a precise prediction of this simulation's wall-clock + // or simulated-time behavior. + localparam integer TICKS_TO_SYS_CYCLES = 500; + integer total_ramp_up_ticks, total_coast_ticks, total_ramp_down_ticks, total_ticks; + integer expected_duration_ns; + + integer m; + reg [31:0] readback; + reg [31:0] position_read; + integer expected_position; + + // ---- Compute prescaler/preset for the 1us pulse width ----- + // Ceiling-divides throughout: never produce a pulse shorter + // than requested, and never claim a total narrower than the + // target - matching the driver-safety floor established + // elsewhere in this design. Searches divide upward from the + // smallest value that could possibly reach target_cycles + // within MAX_COUNT, taking the first EXACT factorization + // found (searching from the smallest divide keeps divide - + // and so LC cost on the divide side - minimal); if none + // exists in range, keeps the smallest total that still + // covers the target. + target_ns = 1_000; + target_cycles = (target_ns + NS_PER_CYCLE - 1) / NS_PER_CYCLE; + min_divide = (target_cycles + MAX_COUNT - 1) / MAX_COUNT; + if (min_divide < 1) min_divide = 1; + + best_total = -1; + for (search_divide = min_divide; search_divide <= MAX_DIVIDE; search_divide = search_divide + 1) begin + this_count = (target_cycles + search_divide - 1) / search_divide; + if (this_count <= MAX_COUNT) begin + this_total = search_divide * this_count; + if (best_total == -1 || this_total < best_total) begin + best_total = this_total; + best_divide = search_divide; + best_count = this_count; + end + if (this_total == target_cycles) begin + search_divide = MAX_DIVIDE + 1; // exact match - stop searching + end + end + end + + pls_prescaler_reg = best_divide - 1; + pls_preset_reg = best_count - 1; + + $display("[PLS CALC] target=%0dns -> %0d cycles -> divide=%0d count=%0d (%0d cycles = %0dns) -> prescaler_reg=%0d preset_reg=%0d", + target_ns, target_cycles, best_divide, best_count, best_total, best_total * NS_PER_CYCLE, + pls_prescaler_reg, pls_preset_reg); + + `ASSERT_EQ(best_total, target_cycles, "%0d", + "[STEPPER TRAPEZOID] Expected an exact factorization for 1us at 50MHz"); + + // Same preset on every motor, same prescaler on both banks. + qspi_bus_write(STEPPERS_BASE + REG_STEP_PLS_CONFIG, + {8{pls_preset_reg[3:0]}}); + qspi_bus_write(STEPPERS_BASE + REG_STEP_PLS_PRESCALER, + {10'd0, pls_prescaler_reg[5:0], 10'd0, pls_prescaler_reg[5:0]}); + + qspi_bus_read(STEPPERS_BASE + REG_STEP_PLS_CONFIG, readback); + `ASSERT_EQ(readback, {8{pls_preset_reg[3:0]}}, "0x%08h", + "[STEPPER TRAPEZOID] REG_STEP_PLS_CONFIG readback mismatch"); + qspi_bus_read(STEPPERS_BASE + REG_STEP_PLS_PRESCALER, readback); + `ASSERT_EQ(readback, {10'd0, pls_prescaler_reg[5:0], 10'd0, pls_prescaler_reg[5:0]}, "0x%08h", + "[STEPPER TRAPEZOID] REG_STEP_PLS_PRESCALER readback mismatch"); + + total_ramp_up_ticks = RAMP_STEPS*START_PERIOD - DELTA_MAG*RAMP_STEPS*(RAMP_STEPS-1)/2; + total_coast_ticks = COAST_STEPS*COAST_PERIOD; + total_ramp_down_ticks = RAMP_STEPS*COAST_PERIOD + DELTA_MAG*RAMP_STEPS*(RAMP_STEPS-1)/2; + total_ticks = total_ramp_up_ticks + total_coast_ticks + total_ramp_down_ticks; + expected_duration_ns = total_ticks * TICKS_TO_SYS_CYCLES * NS_PER_CYCLE; + $display("[DURATION CALC] total_ticks=%0d -> expected_duration=%0dns (%0dms)", + total_ticks, expected_duration_ns, expected_duration_ns / 1_000_000); + + // ---- Upload the forward-direction profile to all 8 motors - + for (m = 0; m < 8; m = m + 1) begin + upload_trapezoidal_profile(m[2:0], DIR_NORMAL, RAMP_STEPS, COAST_STEPS, + START_PERIOD, COAST_PERIOD, DELTA_MAG); + end + + // ---- Start all 8 motors simultaneously, then poll -------- + $display("[STEPPER TRAPEZOID] Starting all 8 motors (forward)..."); + qspi_bus_write(STEPPERS_BASE + REG_STEP_CTRL, 32'h000F_0F00); + poll_steppers_until_stopped(30000); + + // ---- Verify final position: 25+50+25 = 100 steps forward - + expected_position = RAMP_STEPS + COAST_STEPS + RAMP_STEPS; + for (m = 0; m < 8; m = m + 1) begin + qspi_bus_read(STEPPERS_BASE + REG_STEP_POS_0 + (m * 4), position_read); + $display("[STEPPER TRAPEZOID] Motor %0d position (forward): %0d", m, $signed(position_read)); + `ASSERT_EQ($signed(position_read), expected_position, "%0d", + $sformatf("[STEPPER TRAPEZOID] Motor %0d position mismatch after forward move", m)); + end + + // ---- Upload the SAME profile again, reversed direction --- + for (m = 0; m < 8; m = m + 1) begin + upload_trapezoidal_profile(m[2:0], DIR_REVERSE, RAMP_STEPS, COAST_STEPS, + START_PERIOD, COAST_PERIOD, DELTA_MAG); + end + + $display("[STEPPER TRAPEZOID] Starting all 8 motors (reverse)..."); + qspi_bus_write(STEPPERS_BASE + REG_STEP_CTRL, 32'h000F_0F00); + poll_steppers_until_stopped(30000); + + // ---- Verify final position: back to 0 (100 forward, then + // 100 back) -------------------------------------------------- + for (m = 0; m < 8; m = m + 1) begin + qspi_bus_read(STEPPERS_BASE + REG_STEP_POS_0 + (m * 4), position_read); + $display("[STEPPER TRAPEZOID] Motor %0d position (reverse): %0d", m, $signed(position_read)); + `ASSERT_EQ($signed(position_read), 0, "%0d", + $sformatf("[STEPPER TRAPEZOID] Motor %0d position mismatch after reverse move", m)); + end + end + + #100; + report(); $finish; end diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/steppers_motion_tb.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/steppers_motion_tb.v new file mode 100644 index 0000000..ddd59bd --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/steppers_motion_tb.v @@ -0,0 +1,1112 @@ +`timescale 1ns/1ps + +`include "src/test/assertions.svh" + +module steppers_motion_tb; + + reg RESET; + reg TCXO = 0; + `include "src/test/bus_io.svh" + `include "src/main/io/steppers_regs.svh" + `include "src/main/io/steppers_shared.svh" + + wire [7:0] step_pins; + wire [7:0] dir_pins; + wire [1:0] bank_enable_pins; + reg global_motor_en; + + reg [31:0] result; + + reg stepper_clk; + + stepper_clk stepper_clk_inst ( + .sys_clk(TCXO), + .reset(RESET), + .stepper_clk(stepper_clk) + ); + + steppers dut ( + .sys_clk(TCXO), + .reset(RESET), + + .stepper_clk(stepper_clk), + + .bus_stb(stb), + .bus_we(we), + .bus_addr(addr), + .bus_din(din), + .bus_dout(dout), + .bus_ack(ack), + + .step_pins(step_pins), + .dir_pins(dir_pins), + .bank_enable_pins(bank_enable_pins), + .global_motor_en(global_motor_en) + ); + + // ============================================================ + // SYSTEM TIMING DEFINITIONS & DERIVED PARAMETERS + // ============================================================ + localparam SYS_CLK_FREQ_HZ = 50_000_000; + localparam BASE_STEP_FREQ_HZ = 100_000; + localparam HALF_SYS_PERIOD_NS = 10; + // 50 MHz clock toggle period + + // Real-time nanoseconds per single master system clock cycle + localparam NS_PER_SYS_CYCLE = HALF_SYS_PERIOD_NS * 2; + + // How many master sys_clk cycles occur per stepper_clk tick - the + // conversion factor between the RTL's period fields (in ticks) and + // capture_duration's units (sys_clk cycles). + localparam TICKS_TO_SYS_CYCLES = SYS_CLK_FREQ_HZ / BASE_STEP_FREQ_HZ; + + // 50 MHz Master Clock Generation + always #HALF_SYS_PERIOD_NS TCXO = ~TCXO; + + // Capture Storage Array Architecture + reg [31:0] capture_duration[2048]; + reg capture_dir[2048]; + integer capture_count = 0; + + // Golden model of stepper_core.v's period recurrence: same starting + // value, same per-step +/- delta_magnitude, same floor-at-1 (period + // register never below 1) and same 16-bit saturate-on-add behavior as + // alu_add_sub/next_period in the RTL. Returns the period (in ticks) + // that produces step `step_index` (0-indexed) within a segment, so + // expected inter-step timing can be asserted exactly rather than with + // a placeholder value. Recomputed from scratch each call rather than + // building an array - segments here are at most ~10 steps, and a + // scalar-only function avoids the array/queue task-port support + // Icarus doesn't have. + function automatic integer period_at_step; + input integer start_period; + input integer delta_magnitude; + input integer ramp; + input integer step_index; + integer i; + integer current; + begin + current = start_period; + for (i = 0; i < step_index; i = i + 1) begin + if (ramp == RAMP_DOWN) begin + if (current + delta_magnitude > 65535) current = 65535; + else current = current + delta_magnitude; + end else begin + if (delta_magnitude >= current) current = 1; + else current = current - delta_magnitude; + end + end + period_at_step = current; + end + endfunction + + // Safety ceiling for wait_for_captures below - not a timing prediction, + // just a bound so a genuine DUT bug (steps never firing) ends the + // simulation instead of hanging forever. The wait itself is event- + // driven and returns as soon as the expected steps are captured, + // regardless of this value. + localparam SAFETY_TIMEOUT_NS = 30_000_000; + + // Blocks until capture_count reaches `target`, checking every 100ns - + // event-driven completion instead of a fixed #(...) delay sized for + // the worst case. Falls through on timeout (rather than hanging) and + // leaves the mismatch for the caller's own capture_count assertion to + // report. + task automatic wait_for_captures(input integer target, input integer timeout_ns); + integer waited; + begin + waited = 0; + while (capture_count < target && waited < timeout_ns) begin + #100; + waited = waited + 100; + end + end + endtask + + // ============================================================ + // TRAPEZOIDAL PROFILE PLANNER (mirrors the Rust MCU-side planner: + // scalar parameters in, RampSegments out - here written into these + // plain fixed-size module-scope arrays instead of a struct/Vec, since + // Icarus doesn't support dynamic arrays/queues as task ports). Plain + // module-scope arrays (not task ports) sidestep that entirely - same + // pattern already used for capture_duration/capture_dir above. + // ============================================================ + localparam MAX_SEGMENTS = 32; + localparam N_SUB = 8; + localparam PERIOD_MAX = 65535.0; + + reg [15:0] seg_sp[0:MAX_SEGMENTS-1]; + reg [15:0] seg_dm[0:MAX_SEGMENTS-1]; + integer seg_n[0:MAX_SEGMENTS-1]; + reg seg_ramp[0:MAX_SEGMENTS-1]; + reg seg_halt[0:MAX_SEGMENTS-1]; + integer num_segments; + integer plan_total_steps; + + // Plans a simple trapezoidal profile (constant acceleration up to + // v_max, or as far as target_steps allows; cruise; constant + // deceleration back to rest) from just (target_steps, v_max, a_max) - + // same three parameters the Rust planner takes, minus jerk (this is + // deliberately the plain trapezoidal case, not jerk-limited). + // + // A linear-per-step period ramp does not give constant acceleration + // (velocity = 1/period is a reciprocal), so each ramp is evaluated at + // N_SUB equally-spaced time boundaries against the true physics + // (constant acceleration a => v(t) = a*t, step n at t_n = sqrt(2n/a)), + // and only the (small) gap within each boundary pair is approximated + // as linear - the same approach as planner.rs's append_quad_phase. + task automatic plan_trapezoid( + input real target_steps, + input real v_max, + input real a_max + ); + real period_min; + real n_ramp_full; + real v_peak; + real n_ramp; + real n_cruise; + real t_ramp; + real t_b[0:N_SUB]; + real bp[0:N_SUB]; + real raw_counts[0:N_SUB-1]; + integer counts[0:N_SUB-1]; + integer i; + integer sum_c; + integer diff; + integer best_idx; + integer sp_i, ep_i; + integer n_i; + begin + num_segments = 0; + plan_total_steps = 0; + + period_min = 1.0 / v_max; + n_ramp_full = (v_max * v_max) / (2.0 * a_max); + + if (2.0 * n_ramp_full <= target_steps) begin + v_peak = v_max; + n_ramp = n_ramp_full; + n_cruise = target_steps - 2.0 * n_ramp; + end else begin + v_peak = $sqrt(a_max * target_steps); + n_ramp = target_steps / 2.0; + n_cruise = 0.0; + end + + t_ramp = v_peak / a_max; + + for (i = 0; i <= N_SUB; i = i + 1) begin + t_b[i] = t_ramp * i / N_SUB; + end + + for (i = 0; i <= N_SUB; i = i + 1) begin + if (t_b[i] <= 0.0) begin + bp[i] = -1.0; // singular (v=0); filled below once counts[0] is known + end else begin + bp[i] = 1.0 / (a_max * t_b[i]); + if (bp[i] > PERIOD_MAX) bp[i] = PERIOD_MAX; + end + end + + for (i = 0; i < N_SUB; i = i + 1) begin + raw_counts[i] = 0.5 * a_max * (t_b[i+1]*t_b[i+1] - t_b[i]*t_b[i]); + counts[i] = $rtoi(raw_counts[i] + 0.5); + end + sum_c = 0; + for (i = 0; i < N_SUB; i = i + 1) sum_c = sum_c + counts[i]; + diff = $rtoi(n_ramp + 0.5) - sum_c; + best_idx = 0; + for (i = 1; i < N_SUB; i = i + 1) begin + if (counts[i] > counts[best_idx]) best_idx = i; + end + counts[best_idx] = counts[best_idx] + diff; + + if (bp[0] < 0.0) begin + if (counts[0] > 0) bp[0] = t_b[1] / $itor(counts[0]); + else bp[0] = PERIOD_MAX; + end + + // accel: rest -> v_peak + for (i = 0; i < N_SUB; i = i + 1) begin + n_i = counts[i]; + if (n_i > 0) begin + sp_i = $rtoi(bp[i] + 0.5); + ep_i = $rtoi(bp[i+1] + 0.5); + if (sp_i < 1) sp_i = 1; + if (ep_i < 1) ep_i = 1; + seg_sp[num_segments] = sp_i[15:0]; + seg_dm[num_segments] = (sp_i - ep_i) / n_i; + seg_n[num_segments] = n_i; + seg_ramp[num_segments] = RAMP_UP; + seg_halt[num_segments] = 1'b0; + num_segments = num_segments + 1; + plan_total_steps = plan_total_steps + n_i; + end + end + + // cruise at v_peak + if (n_cruise > 0.5) begin + sp_i = $rtoi((1.0/v_peak) + 0.5); + if (sp_i < 1) sp_i = 1; + n_i = $rtoi(n_cruise + 0.5); + seg_sp[num_segments] = sp_i[15:0]; + seg_dm[num_segments] = 16'd0; + seg_n[num_segments] = n_i; + seg_ramp[num_segments] = RAMP_UP; + seg_halt[num_segments] = 1'b0; + num_segments = num_segments + 1; + plan_total_steps = plan_total_steps + n_i; + end + + // decel: v_peak -> rest (exact mirror of accel, reversed) + for (i = N_SUB - 1; i >= 0; i = i - 1) begin + n_i = counts[i]; + if (n_i > 0) begin + sp_i = $rtoi(bp[i+1] + 0.5); + ep_i = $rtoi(bp[i] + 0.5); + if (sp_i < 1) sp_i = 1; + if (ep_i < 1) ep_i = 1; + seg_sp[num_segments] = sp_i[15:0]; + seg_dm[num_segments] = (ep_i - sp_i) / n_i; + seg_n[num_segments] = n_i; + seg_ramp[num_segments] = RAMP_DOWN; + seg_halt[num_segments] = 1'b0; + num_segments = num_segments + 1; + plan_total_steps = plan_total_steps + n_i; + end + end + + // last segment brings the profile to a full stop + seg_halt[num_segments - 1] = 1'b1; + end + endtask + + // Writes every planned segment to the bus (TX_CONFIG then each + // CTST/SPDM pair), printing each segment's data to the console first - + // mirrors motion_control.rs logging each RampSegment before executing + // it. + task automatic write_planned_segments; + integer i; + reg [1:0] cmd_i; + begin + $display("[PLAN] %0d segments, %0d steps total:", num_segments, plan_total_steps); + bus_write(REG_STEP_TX_CONFIG, {16'h0000, 8'h00, num_segments[7:0]}); + #200; + + for (i = 0; i < num_segments; i = i + 1) begin + cmd_i = seg_halt[i] ? CMD_MOVE_HALT : CMD_MOVE; + $display(" segment %0d: SP=%0d DM=%0d n=%0d %s %s", + i, seg_sp[i], seg_dm[i], seg_n[i], + seg_ramp[i] ? "DOWN" : "UP", + seg_halt[i] ? "MOVE_HALT" : "MOVE"); + bus_write(REG_STEP_SEG_CTST, {4'd0, seg_ramp[i], DIR_NORMAL, cmd_i, seg_n[i][23:0]}); + bus_write(REG_STEP_SEG_SPDM, {seg_sp[i], seg_dm[i]}); + #200; + end + end + endtask + + time last_step_time = 0; + time current_time = 0; + reg monitoring_active = 0; + // Automated Step Capture Monitor Loop + always @(posedge step_pins[0]) begin + if (monitoring_active && capture_count < 2048) begin + current_time = $time; + if (capture_count == 0) begin + capture_duration[capture_count] = 0; + end else begin + // Express the duration dynamically in master system clock cycles + capture_duration[capture_count] = (current_time - last_step_time) / NS_PER_SYS_CYCLE; + end + capture_dir[capture_count] = dir_pins[0]; + capture_count = capture_count + 1; + last_step_time = current_time; + end + end + + // ============================================================ + // MULTI-MOTOR TEST INFRASTRUCTURE + // ============================================================ + // Per-motor counterpart to capture_duration/capture_dir/capture_count/ + // monitoring_active/last_step_time above - those stay exactly as they + // are (still used by the three existing single-motor tests). This is + // the same shape, just one of everything per motor instead of a + // single motor-0-only instance. + reg [31:0] capture_duration_m [0:7][0:2047]; + reg capture_dir_m [0:7][0:2047]; + integer capture_count_m [0:7]; + time last_step_time_m [0:7]; + reg monitoring_active_m = 0; + + // One step/dir edge monitor per motor, generated rather than + // hand-duplicated 8 times. Otherwise identical to the motor-0 monitor + // above, right down to the blocking assignments and the capture_count + // bounds check. + genvar gen_m; + generate + for (gen_m = 0; gen_m < 8; gen_m = gen_m + 1) begin : STEP_MONITOR_M + always @(posedge step_pins[gen_m]) begin + if (monitoring_active_m && capture_count_m[gen_m] < 2048) begin + if (capture_count_m[gen_m] == 0) begin + capture_duration_m[gen_m][capture_count_m[gen_m]] = 0; + end else begin + capture_duration_m[gen_m][capture_count_m[gen_m]] = + ($time - last_step_time_m[gen_m]) / NS_PER_SYS_CYCLE; + end + capture_dir_m[gen_m][capture_count_m[gen_m]] = dir_pins[gen_m]; + capture_count_m[gen_m] = capture_count_m[gen_m] + 1; + last_step_time_m[gen_m] = $time; + end + end + end + endgenerate + + // Pulse-width monitor - separate from the interval monitor above, + // since this measures how long step_pins[j] stays HIGH (posedge to + // the following negedge) rather than the time between successive + // rising edges. Always active (no enable flag needed - only the + // pulse-width test reads these, and nothing else on step_pins[j] + // changes what gets measured). + reg [63:0] pulse_rise_time [0:7]; + reg [31:0] pulse_width_cycles [0:7]; + reg pulse_width_valid [0:7]; + + genvar gen_pw; + generate + for (gen_pw = 0; gen_pw < 8; gen_pw = gen_pw + 1) begin : PULSE_WIDTH_MONITOR + always @(posedge step_pins[gen_pw]) begin + pulse_rise_time[gen_pw] = $time; + end + always @(negedge step_pins[gen_pw]) begin + pulse_width_cycles[gen_pw] = ($time - pulse_rise_time[gen_pw]) / NS_PER_SYS_CYCLE; + pulse_width_valid[gen_pw] = 1'b1; + end + end + endgenerate + + // Blocks until REG_STEP_CTRL reports all 8 motors stopped + // (bits[7:0]==0), checking every 100ns like wait_for_captures above. + // Cheaper than polling 8 separate STATUS registers - CTRL's read + // already packs every motor's moving bit into one word. + task automatic wait_for_all_motors_stopped(input integer timeout_ns); + integer waited; + reg [31:0] ctrl_read; + begin + waited = 0; + bus_read(REG_STEP_CTRL, ctrl_read); + while (ctrl_read[7:0] != 8'h00 && waited < timeout_ns) begin + #100; + waited = waited + 100; + bus_read(REG_STEP_CTRL, ctrl_read); + end + end + endtask + + // REG_STEP_STATUS_ / REG_STEP_POS_ are separate symbols, not a + // computed offset, so a motor index needs a lookup rather than + // arithmetic on the address. + function [7:0] status_reg_for_motor; + input [2:0] motor; + begin + case (motor) + 3'd0: status_reg_for_motor = REG_STEP_STATUS_0; + 3'd1: status_reg_for_motor = REG_STEP_STATUS_1; + 3'd2: status_reg_for_motor = REG_STEP_STATUS_2; + 3'd3: status_reg_for_motor = REG_STEP_STATUS_3; + 3'd4: status_reg_for_motor = REG_STEP_STATUS_4; + 3'd5: status_reg_for_motor = REG_STEP_STATUS_5; + 3'd6: status_reg_for_motor = REG_STEP_STATUS_6; + default: status_reg_for_motor = REG_STEP_STATUS_7; + endcase + end + endfunction + + function [7:0] pos_reg_for_motor; + input [2:0] motor; + begin + case (motor) + 3'd0: pos_reg_for_motor = REG_STEP_POS_0; + 3'd1: pos_reg_for_motor = REG_STEP_POS_1; + 3'd2: pos_reg_for_motor = REG_STEP_POS_2; + 3'd3: pos_reg_for_motor = REG_STEP_POS_3; + 3'd4: pos_reg_for_motor = REG_STEP_POS_4; + 3'd5: pos_reg_for_motor = REG_STEP_POS_5; + 3'd6: pos_reg_for_motor = REG_STEP_POS_6; + default: pos_reg_for_motor = REG_STEP_POS_7; + endcase + end + endfunction + + // Uploads a single one-segment profile to `motor` (TX_CONFIG with + // num_points=1, then one CTST/SPDM pair) - does not start it. The + // existing write_planned_segments task always targets motor 0 (it's + // driven by the trapezoid planner, which is itself single-motor), so + // this is a separate, motor-parameterized equivalent for a single + // hand-specified segment rather than a planned profile. + task automatic upload_single_segment( + input [2:0] motor, + input [1:0] cmd, + input dir, + input incr, + input [23:0] n_steps, + input [15:0] sp, + input [15:0] dm + ); + begin + bus_write(REG_STEP_TX_CONFIG, {16'h0000, {5'd0, motor}, 8'd1}); + bus_write(REG_STEP_SEG_CTST, {4'd0, incr, dir, cmd, n_steps}); + bus_write(REG_STEP_SEG_SPDM, {sp, dm}); + end + endtask + + // Issues a start strobe for exactly one motor (bits[11:8] for motors + // 0-3, bits[19:16] for motors 4-7 - see REG_STEP_CTRL's layout). + task automatic start_motor(input [2:0] motor); + begin + if (motor < 3'd4) + bus_write(REG_STEP_CTRL, (32'h1 << (8 + motor))); + else + bus_write(REG_STEP_CTRL, (32'h1 << (16 + (motor - 3'd4)))); + end + endtask + + // Issues start strobes for all 8 motors in a single bus transaction. + task automatic start_all_motors; + begin + bus_write(REG_STEP_CTRL, 32'h000F_0F00); + end + endtask + + // sys_reset() (defined in bus_io.svh, shared with steppers_tb.v) + // resets the DUT, including REG_STEP_PLS_CONFIG/PRESCALER back to + // their reset default - now the shortest possible pulse (1 cycle, + // 20ns) - so every test in this file just calls sys_reset() directly + // and inherits that narrow default without needing to configure + // anything itself, matching the "pulse width is negligible" + // assumption the capture_duration-based timing checks were written + // under. Tests that care about a specific, wider pulse width (e.g. + // PULSE_WIDTH_TEST) configure it explicitly regardless. + + reg [7:0] test_index = 0; + initial begin + $dumpfile("steppers_motion_tb.vcd"); + $dumpvars(0, steppers_motion_tb); + // Hardware Initialization + global_motor_en = 1'b1; + sys_reset(); + bus_init(); + + #200; + // ============================================================ + $display("TEST %0d: Single step", test_index); + test_index += 1; + // ============================================================ + begin : SINGLE_POINT_STEP_TEST + reg [31:0] status_read; + reg [31:0] position_read; + integer k; + + // Dynamic Trajectory Parameters + integer target_steps = 1; + integer start_period = 1; + integer delta_magnitude = 1; + capture_count = 0; + monitoring_active = 1; + + // 1) Configure Transmit Framework for Motor 0 (1 Point) + $display("[TX_CONFIG] Configuring Motor 0 for a single target position profile..."); + bus_write(REG_STEP_TX_CONFIG, {16'h0000, 8'h00, 8'h01}); + #200; + + // 2) Send payload + bus_write(REG_STEP_SEG_CTST, {4'd0, 2'b00, CMD_MOVE_HALT, target_steps[23:0]}); + bus_write(REG_STEP_SEG_SPDM, {start_period[15:0], delta_magnitude[15:0]}); + #200; + + // 3) Strobe Start command utilizing REG_STEP_CTRL + $display("[CTRL] Issuing Start Strobe to Core 0..."); + bus_write(REG_STEP_CTRL, 32'h0000_0100); + + // 4) Verify dynamic engine status transitions to MOVING - the + // shared run engine services motors round-robin (up to 10 + // sys_clk cycles, 200ns, before it reaches this one), so this + // isn't instantaneous the way a per-motor engine would be. + #200; + bus_read(REG_STEP_STATUS_0, status_read); + `ASSERT_EQ(status_read[0], 1'b1, "0x%08h", "Motor 0 status expected to register MOVING (1) upon start strobe activation."); + // Wait for the step to actually be captured (event-driven) + wait_for_captures(target_steps, SAFETY_TIMEOUT_NS); + // 5) Verify dynamic engine status transitions to IDLE/STOPPED after time-to-complete + bus_read(REG_STEP_STATUS_0, status_read); + `ASSERT_EQ(status_read[0], 1'b0, "0x%08h", "Motor 0 status expected to register STOPPED (0) after completing steps."); + + monitoring_active = 0; + // 6) Assert total count and timing separation intervals + $display("[VERIFY] Evaluating recorded capture matrices..."); + `ASSERT_EQ(capture_count, target_steps, "%0d", "Total generated output steps mismatch target input profile configuration."); + for (k = 0; k < capture_count; k = k + 1) begin + $display(" -> Step Index %0d: Delta = %0d sys_clk cycles, DIR = %b", k, capture_duration[k], capture_dir[k]); + `ASSERT_EQ(capture_dir[k], 1'b0, "%b", $sformatf("Direction mismatch at step phase array element %0d (Expected Forward)", k)); + if (k > 0) begin + `ASSERT_EQ(capture_duration[k], period_at_step(start_period, delta_magnitude, RAMP_UP, k) * TICKS_TO_SYS_CYCLES, "%0d", $sformatf("Step time interval mismatch at historical sequence point %0d", k)); + end + end + + // 7) Assert the FPGA's own tracked position matches exactly - + // a single forward step from a fresh reset. + bus_read(REG_STEP_POS_0, position_read); + `ASSERT_EQ($signed(position_read), 1, "%0d", "Motor 0 final position mismatch after a single forward step."); + end + + // ============================================================ + $display("TEST %0d: Multi-Point with Intentional Pause", test_index); + test_index += 1; + // ============================================================ + begin : MULTI_POINT_PAUSE_TEST + reg [31:0] status_read; + reg [31:0] position_read; + integer k; + + sys_reset(); + + // 1) Configure Transmit Framework for Motor 0 (3 Points Total) + $display("[TX_CONFIG] Configuring Motor 0 for 3 distinct trajectory profile segments..."); + bus_write(REG_STEP_TX_CONFIG, {16'h0000, 8'h00, 8'h03}); + #200; + + // 2) Load 3 point trajectory segments (CTST steps field is a + // relative step count for that segment, not an absolute + // coordinate) + bus_write(REG_STEP_SEG_CTST, {4'd0, RAMP_UP, DIR_NORMAL, CMD_MOVE_HALT_WAIT, 24'd10}); + bus_write(REG_STEP_SEG_SPDM, {16'd1, 16'd1}); + #200; + + bus_write(REG_STEP_SEG_CTST, {4'd0, RAMP_UP, DIR_REVERSE, CMD_MOVE, 24'd6}); + bus_write(REG_STEP_SEG_SPDM, {16'd2, 16'd2}); + #200; + + bus_write(REG_STEP_SEG_CTST, {4'd0, RAMP_UP, DIR_REVERSE, CMD_MOVE_HALT, 24'd4}); + bus_write(REG_STEP_SEG_SPDM, {16'd3, 16'd3}); + #200; + + // -------------------------------------------------------------------- + // PHASE 1: Capture Point 1 Steps and Verify Segment Stop Boundary + // -------------------------------------------------------------------- + capture_count = 0; + last_step_time = $time; + monitoring_active = 1; + + $display("[PHASE 1] Issuing Start Strobe to Core 0..."); + bus_write(REG_STEP_CTRL, 32'h0000_0100); + + // Wait for point 1's 10 steps to be captured (event-driven) + wait_for_captures(10, SAFETY_TIMEOUT_NS); + + // Verify core engine dropped into PAUSED state via command descriptor boundary + bus_read(REG_STEP_STATUS_0, status_read); + `ASSERT_EQ(status_read[0], 1'b0, "0x%08h", "[PHASE 1] Motor 0 failed to enter PAUSED state after Point 1 execution."); + + monitoring_active = 0; + + // Evaluate Recorded Performance Phase 1 + $display("[PHASE 1 VERIFY] Evaluating recorded capture ..."); + `ASSERT_EQ(capture_count, 10, "%0d", "[PHASE 1] Generated step count error for Point 1."); + for (k = 0; k < capture_count; k = k + 1) begin + $display(" -> Phase 1, Step Index %0d: Delta = %0d sys_clk cycles, DIR = %b", k, capture_duration[k], capture_dir[k]); + `ASSERT_EQ(capture_dir[k], 1'b0, "%b", $sformatf("[PHASE 1] Expected FORWARD direction (0) at index %0d", k)); + if (k > 0) begin + // point 1: SP=1, DM=1 + `ASSERT_EQ(capture_duration[k], period_at_step(1, 1, RAMP_UP, k) * TICKS_TO_SYS_CYCLES, "%0d", $sformatf("[PHASE 1] Step time period drift at step index %0d", k)); + end + end + + // Position after 10 forward (DIR_NORMAL) steps from a fresh reset + bus_read(REG_STEP_POS_0, position_read); + `ASSERT_EQ($signed(position_read), 10, "%0d", "[PHASE 1] Motor 0 position mismatch after 10 forward steps."); + + // -------------------------------------------------------------------- + // PHASE 2: Resume Sequence and Capture Points 2 and 3 Seamlessly + // -------------------------------------------------------------------- + capture_count = 0; + last_step_time = $time; + monitoring_active = 1; + + $display("[PHASE 2] Issuing Resume Strobe via Start Command..."); + bus_write(REG_STEP_CTRL, 32'h0000_0100); + + // Wait for points 2 & 3's streaming pipeline steps to be + // captured (6 + 4 = 10 steps total), event-driven + wait_for_captures(10, SAFETY_TIMEOUT_NS); + + // Verify engine has completely finished profile segments and returned to full IDLE/STOPPED + bus_read(REG_STEP_STATUS_0, status_read); + `ASSERT_EQ(status_read[0], 1'b0, "0x%08h", "[PHASE 2] Motor 0 failed to return to IDLE/STOPPED status after profile tail."); + + monitoring_active = 0; + + // Evaluate Recorded Performance for Phase 2 + $display("[PHASE 2 VERIFY] Evaluating seamless streaming capture ..."); + `ASSERT_EQ(capture_count, 10, "%0d", "[PHASE 2] Combined steps mismatch across streaming segments."); + for (k = 0; k < capture_count; k = k + 1) begin + $display(" -> Phase 2, Step Index %0d: Delta = %0d sys_clk cycles, DIR = %b", k, capture_duration[k], capture_dir[k]); + `ASSERT_EQ(capture_dir[k], 1'b1, "%b", $sformatf("[PHASE 2] Expected REVERSE direction (1) at index %0d", k)); + if (k > 0) begin + if (k < 6) begin + // point 2: SP=2, DM=2 - local step index k + `ASSERT_EQ(capture_duration[k], period_at_step(2, 2, RAMP_UP, k) * TICKS_TO_SYS_CYCLES, "%0d", $sformatf("[PHASE 2] Step period drift at step index %0d", k)); + end else begin + // point 3: SP=3, DM=3 - fresh reload at the segment + // boundary, so its own local step index is k - 6 + `ASSERT_EQ(capture_duration[k], period_at_step(3, 3, RAMP_UP, k - 6) * TICKS_TO_SYS_CYCLES, "%0d", $sformatf("[PHASE 2] Step period drift at step index %0d", k)); + end + end + end + + // Position back at 0: 10 forward (Phase 1) then 10 reverse + // (Phase 2) is a net round trip. + bus_read(REG_STEP_POS_0, position_read); + `ASSERT_EQ($signed(position_read), 0, "%0d", "[PHASE 2] Motor 0 position mismatch after the round trip (expected net 0)."); + end + + // ============================================================ + $display("TEST %0d: Trapezoidal Capture and CSV Export", test_index); + test_index += 1; + // ============================================================ + begin : SCURVE_CAPTURE_TEST + reg [31:0] status_read; + reg [31:0] position_read; + integer k; + integer csv_captured_file; + integer total_steps; + + sys_reset(); + + // 1) & 2) Plan a trapezoidal profile from just three input + // parameters (target_steps, v_max, a_max) - mirroring the + // Rust MCU-side planner - instead of hand-picked segment + // values, then write whatever it generates to the bus + // (TX_CONFIG followed by each segment's CTST/SPDM), with + // every segment printed to the console first. + $display("[TX_CONFIG] Configuring Motor 0 for a planned trapezoidal profile..."); + plan_trapezoid(100.0, 0.2, 0.0005); // target_steps, v_max (steps/tick), a_max (steps/tick^2) + total_steps = plan_total_steps; + write_planned_segments(); + + // 3) Capture, as per TEST 1 + capture_count = 0; + last_step_time = $time; + monitoring_active = 1; + + $display("[CTRL] Issuing Start Strobe to Core 0..."); + bus_write(REG_STEP_CTRL, 32'h0000_0100); + + wait_for_captures(total_steps, SAFETY_TIMEOUT_NS); + + bus_read(REG_STEP_STATUS_0, status_read); + `ASSERT_EQ(status_read[0], 1'b0, "0x%08h", "[TRAPEZOID] Motor 0 failed to return to STOPPED after profile completion."); + + monitoring_active = 0; + + $display("[TRAPEZOID VERIFY] Evaluating captured profile ..."); + `ASSERT_EQ(capture_count, total_steps, "%0d", "[TRAPEZOID] Captured step count mismatch against the planned profile."); + for (k = 0; k < capture_count; k = k + 1) begin + `ASSERT_EQ(capture_dir[k], 1'b0, "%b", $sformatf("[TRAPEZOID] Expected FORWARD direction (0) at index %0d", k)); + end + + // Entirely forward motion, so final position == the planned + // total step count (dynamic, not hardcoded, since the profile + // is itself parameterized). + bus_read(REG_STEP_POS_0, position_read); + `ASSERT_EQ($signed(position_read), total_steps, "%0d", "[TRAPEZOID] Motor 0 final position mismatch against the planned profile."); + + // 4) Export Physical Step Capture Log to CSV. Velocity, + // acceleration and jerk are recovered as successive + // discrete derivatives of the captured inter-step timing: + // velocity = 1 step / dt, acceleration = dv/dt, jerk = + // da/dt - each divided by the same real elapsed time + // (capture_duration converted from sys_clk cycles to + // seconds), not just the raw difference of the previous + // quantity. + csv_captured_file = $fopen("captured_scurve.csv", "w"); + $fwrite(csv_captured_file, "Time,Step,IntervalCycles,Velocity,Accel,Jerk\n"); + begin + real dt_s; + real last_v, cur_v; + real last_a, cur_a; + real cur_j; + real t; + + t = 0.0; + last_v = 0.0; + last_a = 0.0; + + for (k = 0; k < capture_count; k = k + 1) begin + if (capture_duration[k] > 0) begin + dt_s = $itor(capture_duration[k]) / $itor(SYS_CLK_FREQ_HZ); + cur_v = 1.0 / dt_s; + end else begin + dt_s = 0.0; + cur_v = 0.0; + end + + if (dt_s > 0.0) begin + cur_a = (cur_v - last_v) / dt_s; + cur_j = (cur_a - last_a) / dt_s; + end else begin + cur_a = 0.0; + cur_j = 0.0; + end + + t = t + dt_s; + + $fwrite(csv_captured_file, "%f,%0d,%0d,%f,%f,%f\n", + t, k, capture_duration[k], cur_v, cur_a, cur_j); + + last_v = cur_v; + last_a = cur_a; + end + end + $fclose(csv_captured_file); + $display("[CAPTURE] Exported %0d actual steps to captured_scurve.csv", capture_count); + end + + // ============================================================ + $display("TEST %0d: All 8 Motors, Different Sequences, Simultaneous Start", test_index); + test_index += 1; + // ============================================================ + begin : ALL_MOTORS_SIMULTANEOUS_TEST + // One distinct single-segment profile per motor - different + // step count, period, and direction each, so a mistake that + // swapped two motors' data would show up as a position/count + // mismatch rather than accidentally still passing. + reg [23:0] steps_tbl [0:7]; + reg [15:0] sp_tbl [0:7]; + reg dir_tbl [0:7]; + integer expected_position [0:7]; + reg [31:0] position_read; + integer m, k; + integer csv_file; + real t; + + sys_reset(); + + steps_tbl[0] = 24'd10; sp_tbl[0] = 16'd2; dir_tbl[0] = DIR_NORMAL; + steps_tbl[1] = 24'd15; sp_tbl[1] = 16'd3; dir_tbl[1] = DIR_REVERSE; + steps_tbl[2] = 24'd20; sp_tbl[2] = 16'd2; dir_tbl[2] = DIR_NORMAL; + steps_tbl[3] = 24'd25; sp_tbl[3] = 16'd4; dir_tbl[3] = DIR_REVERSE; + steps_tbl[4] = 24'd30; sp_tbl[4] = 16'd3; dir_tbl[4] = DIR_NORMAL; + steps_tbl[5] = 24'd35; sp_tbl[5] = 16'd2; dir_tbl[5] = DIR_REVERSE; + steps_tbl[6] = 24'd40; sp_tbl[6] = 16'd5; dir_tbl[6] = DIR_NORMAL; + steps_tbl[7] = 24'd45; sp_tbl[7] = 16'd3; dir_tbl[7] = DIR_REVERSE; + + for (m = 0; m < 8; m = m + 1) begin + // Widen to 32 bits and $signed() BEFORE negating - steps_tbl + // is unsigned reg[23:0], so "-steps_tbl[m]" on its own would + // compute the negation in unsigned 24-bit arithmetic and + // then zero-extend into expected_position, turning e.g. -45 + // into a large positive number instead of staying -45. + expected_position[m] = (dir_tbl[m] == DIR_NORMAL) + ? $signed({8'd0, steps_tbl[m]}) + : -$signed({8'd0, steps_tbl[m]}); + capture_count_m[m] = 0; + end + monitoring_active_m = 1; + + $display("[TX_CONFIG] Uploading a distinct single-segment profile to each of the 8 motors..."); + for (m = 0; m < 8; m = m + 1) begin + upload_single_segment(m[2:0], CMD_MOVE_HALT, dir_tbl[m], 1'b0, steps_tbl[m], sp_tbl[m], 16'd0); + end + + $display("[CTRL] Starting all 8 motors in a single strobe..."); + start_all_motors(); + + // The shared engine services all 8 pending starts sequentially + // (a handful of cycles each) rather than instantaneously - give + // it room to actually get every motor moving before the first + // "all stopped" check below, or a check landing before any + // motor has transitioned would misread "nothing moving yet" as + // "already finished". + #3000; + + wait_for_all_motors_stopped(SAFETY_TIMEOUT_NS); + monitoring_active_m = 0; + + $display("[VERIFY] Checking each motor's captured step count and final position..."); + for (m = 0; m < 8; m = m + 1) begin + `ASSERT_EQ(capture_count_m[m], steps_tbl[m], "%0d", + $sformatf("[ALL MOTORS] Motor %0d captured step count mismatch", m)); + + bus_read(pos_reg_for_motor(m[2:0]), position_read); + `ASSERT_EQ($signed(position_read), expected_position[m], "%0d", + $sformatf("[ALL MOTORS] Motor %0d final position mismatch", m)); + + for (k = 0; k < capture_count_m[m]; k = k + 1) begin + `ASSERT_EQ(capture_dir_m[m][k], dir_tbl[m], "%b", + $sformatf("[ALL MOTORS] Motor %0d direction mismatch at step %0d", m, k)); + end + end + + $display("[CAPTURE] Writing one CSV per motor..."); + for (m = 0; m < 8; m = m + 1) begin + csv_file = $fopen($sformatf("captured_motor%0d.csv", m), "w"); + $fwrite(csv_file, "Time,Step,IntervalCycles,Dir\n"); + t = 0.0; + for (k = 0; k < capture_count_m[m]; k = k + 1) begin + t = t + ($itor(capture_duration_m[m][k]) / $itor(SYS_CLK_FREQ_HZ)); + $fwrite(csv_file, "%f,%0d,%0d,%b\n", t, k, capture_duration_m[m][k], capture_dir_m[m][k]); + end + $fclose(csv_file); + end + $display("[CAPTURE] Exported captured_motor0.csv .. captured_motor7.csv"); + end + + // ============================================================ + $display("TEST %0d: Staggered Start - Each Motor Started While Previous Still Moving", test_index); + test_index += 1; + // ============================================================ + begin : STAGGERED_START_TEST + // Longer, slower profiles than the simultaneous-start test + // above - deliberately sized so each motor is still many steps + // from done (never mind still on its very first step) by the + // time the next motor's upload+start bus transactions complete + // a few hundred nanoseconds later. + reg [23:0] steps_tbl [0:7]; + reg [15:0] sp_tbl [0:7]; + reg dir_tbl [0:7]; + integer expected_position [0:7]; + reg [31:0] position_read; + reg [31:0] status_read; + integer m; + + sys_reset(); + + for (m = 0; m < 8; m = m + 1) begin + steps_tbl[m] = 20 - m; // 20,19,...,13 - always well > 1 + sp_tbl[m] = 16'd2; + dir_tbl[m] = (m % 2 == 0) ? DIR_NORMAL : DIR_REVERSE; + expected_position[m] = (dir_tbl[m] == DIR_NORMAL) + ? $signed({8'd0, steps_tbl[m]}) + : -$signed({8'd0, steps_tbl[m]}); + end + + for (m = 0; m < 8; m = m + 1) begin + $display("[UPLOAD+START] Motor %0d: %0d steps, dir=%b, SP=%0d", m, steps_tbl[m], dir_tbl[m], sp_tbl[m]); + upload_single_segment(m[2:0], CMD_MOVE_HALT, dir_tbl[m], 1'b0, steps_tbl[m], sp_tbl[m], 16'd0); + + if (m > 0) begin + // The whole point of this test: motor (m-1) must still + // be MOVING right now, before we start motor m. + bus_read(status_reg_for_motor(m[2:0] - 3'd1), status_read); + `ASSERT_EQ(status_read[0], 1'b1, "0x%08h", + $sformatf("[STAGGERED START] Motor %0d should still be moving when motor %0d starts", m - 1, m)); + end + + start_motor(m[2:0]); + end + + wait_for_all_motors_stopped(SAFETY_TIMEOUT_NS); + + $display("[VERIFY] Checking each motor's final position..."); + for (m = 0; m < 8; m = m + 1) begin + bus_read(pos_reg_for_motor(m[2:0]), position_read); + `ASSERT_EQ($signed(position_read), expected_position[m], "%0d", + $sformatf("[STAGGERED START] Motor %0d final position mismatch", m)); + end + end + + // ============================================================ + $display("TEST %0d: Position Persistence Across Two Independent Uploads", test_index); + test_index += 1; + // ============================================================ + begin : POSITION_PERSISTENCE_TEST + reg [31:0] position_read; + + sys_reset(); + + // ---- First profile: 100 steps reverse ---- + capture_count = 0; + monitoring_active = 1; + $display("[TX_CONFIG] Motor 0: 100 steps reverse..."); + upload_single_segment(3'd0, CMD_MOVE_HALT, DIR_REVERSE, 1'b0, 24'd100, 16'd2, 16'd0); + start_motor(3'd0); + wait_for_captures(100, SAFETY_TIMEOUT_NS); + monitoring_active = 0; + + bus_read(REG_STEP_POS_0, position_read); + `ASSERT_EQ($signed(position_read), -100, "%0d", + "[POSITION PERSISTENCE] Position after 100 reverse steps should be -100."); + + // ---- Second profile: 100 steps forward, no reset in between ---- + // The point of this test: position must carry over across an + // entirely new TX_CONFIG/upload/start, not reset when a fresh + // profile is loaded. + capture_count = 0; + monitoring_active = 1; + $display("[TX_CONFIG] Motor 0: 100 steps forward (no reset since the previous move)..."); + upload_single_segment(3'd0, CMD_MOVE_HALT, DIR_NORMAL, 1'b0, 24'd100, 16'd2, 16'd0); + start_motor(3'd0); + wait_for_captures(100, SAFETY_TIMEOUT_NS); + monitoring_active = 0; + + bus_read(REG_STEP_POS_0, position_read); + `ASSERT_EQ($signed(position_read), 0, "%0d", + "[POSITION PERSISTENCE] Position after a net round trip (-100 then +100) should be 0."); + end + + // ============================================================ + $display("TEST %0d: Step Pulse Width Configuration (exhaustive)", test_index); + test_index += 1; + // Exhaustively exercises every one of the 16 presets x 64 + // prescalers = 1024 possible (preset, prescaler) combinations. + // Each of 128 iterations (64 prescaler values x 2 "halves") sets + // both banks to the same prescaler value and assigns motors 0-7 + // one distinct preset each - half=0 covers presets 0-7, half=1 + // covers presets 8-15 - so every combination is exercised + // exactly once across the full sweep. Each iteration steps all + // 8 motors once and checks every motor's measured pulse width + // against its own exact (prescaler+1)*(preset+1) cycle count. + // ============================================================ + begin : PULSE_WIDTH_TEST + integer m; + integer prescaler_val; + integer half; + integer preset_tbl [0:7]; + integer expected_cycles [0:7]; + integer max_expected_cycles; + reg [31:0] pls_config_write, pls_config_read; + reg [31:0] pls_prescaler_write, pls_prescaler_read; + + for (prescaler_val = 0; prescaler_val < 64; prescaler_val = prescaler_val + 1) begin + for (half = 0; half < 2; half = half + 1) begin + sys_reset(); + + for (m = 0; m < 8; m = m + 1) begin + pulse_width_valid[m] = 1'b0; + pulse_width_cycles[m] = 32'd0; + preset_tbl[m] = half * 8 + m; + end + + max_expected_cycles = 0; + for (m = 0; m < 8; m = m + 1) begin + // width = (prescaler+1) * (preset+1) sys_clk + // cycles - a plain product, no saturation needed + // (max is a fixed 64*16=1024, well within range). + expected_cycles[m] = (prescaler_val + 1) * (preset_tbl[m] + 1); + if (expected_cycles[m] > max_expected_cycles) + max_expected_cycles = expected_cycles[m]; + end + + $display("[PLS_CONFIG] prescaler=%0d half=%0d: presets %0d..%0d", + prescaler_val, half, preset_tbl[0], preset_tbl[7]); + pls_config_write = {preset_tbl[7][3:0], preset_tbl[6][3:0], preset_tbl[5][3:0], preset_tbl[4][3:0], + preset_tbl[3][3:0], preset_tbl[2][3:0], preset_tbl[1][3:0], preset_tbl[0][3:0]}; + bus_write(REG_STEP_PLS_CONFIG, pls_config_write); + bus_read(REG_STEP_PLS_CONFIG, pls_config_read); + `ASSERT_EQ(pls_config_read, pls_config_write, "0x%08h", + $sformatf("[PULSE WIDTH] REG_STEP_PLS_CONFIG readback mismatch (prescaler=%0d half=%0d).", prescaler_val, half)); + + pls_prescaler_write = {10'd0, prescaler_val[5:0], 10'd0, prescaler_val[5:0]}; + bus_write(REG_STEP_PLS_PRESCALER, pls_prescaler_write); + bus_read(REG_STEP_PLS_PRESCALER, pls_prescaler_read); + `ASSERT_EQ(pls_prescaler_read, pls_prescaler_write, "0x%08h", + $sformatf("[PULSE WIDTH] REG_STEP_PLS_PRESCALER readback mismatch (prescaler=%0d half=%0d).", prescaler_val, half)); + + for (m = 0; m < 8; m = m + 1) begin + upload_single_segment(m[2:0], CMD_MOVE_HALT, DIR_NORMAL, 1'b0, 24'd1, 16'd2, 16'd0); + end + + start_all_motors(); + #3000; // let the engine finish servicing all 8 pending starts + // before polling - see ALL_MOTORS_SIMULTANEOUS_TEST above + // for why this matters. + wait_for_all_motors_stopped(SAFETY_TIMEOUT_NS); + + // mot_moving clears in ST_FIRE_CALC just 1-2 cycles + // after a motor fires - independent of, and well + // before, the pulse-width mechanism actually + // finishes running its full (prescaler+1)*(preset+1) + // cycles. A generous 2x margin on the widest width in + // THIS iteration (recomputed each time, up to the + // fixed 1024-cycle max) comfortably covers that plus + // round-robin staggering across all 8 motors. + #(max_expected_cycles * 20 * 2); + + for (m = 0; m < 8; m = m + 1) begin + $display("[VERIFY] Motor %0d pulse width measured as: %0dns, prescaler_val: %0d", m, pulse_width_cycles[m] * NS_PER_SYS_CYCLE, prescaler_val); + `ASSERT_EQ(pulse_width_valid[m], 1'b1, "%0d", + $sformatf("[PULSE WIDTH] prescaler=%0d half=%0d: Motor %0d never produced a measurable step pulse", + prescaler_val, half, m)); + `ASSERT_EQ(pulse_width_cycles[m], expected_cycles[m], "%0d", + $sformatf("[PULSE WIDTH] prescaler=%0d half=%0d: Motor %0d width mismatch (preset=%0d)", + prescaler_val, half, m, preset_tbl[m])); + end + end + end + + // Restore the default (preset 0 on every motor, prescaler 0 + // on both banks - the shortest pulse) so nothing after this + // test is affected. + bus_write(REG_STEP_PLS_CONFIG, 32'd0); + bus_write(REG_STEP_PLS_PRESCALER, 32'd0); + end + + // ============================================================ + $display("TEST %0d: Pulse Width Wider Than Step Interval", test_index); + test_index += 1; + // Deliberately configures the widest possible pulse (preset=15, + // prescaler=63 -> 1024 cycles = 20.48us) against a short SP=1 + // step interval (500 cycles = 10us) - a genuine configuration + // error (you cannot physically step faster than your own pulse + // width permits), but one that must not corrupt anything or + // produce an ambiguous, unmeasurable output. Steps exactly + // twice and checks that exactly two rising edges were captured + // (not one merged pulse) and that the final position reflects + // exactly two steps - both should hold given the forced one- + // cycle gap the pulse output logic now guarantees between + // consecutive steps for the same motor, regardless of + // configured width vs. interval. + // ============================================================ + begin : PULSE_WIDER_THAN_INTERVAL_TEST + reg [31:0] position_read; + + sys_reset(); + + $display("[PLS_CONFIG] Setting motor 0 to the widest pulse (1024 cycles = 20.48us)..."); + bus_write(REG_STEP_PLS_CONFIG, {4'd0, 4'd0, 4'd0, 4'd0, 4'd0, 4'd0, 4'd0, 4'd15}); + bus_write(REG_STEP_PLS_PRESCALER, {10'd0, 6'd0, 10'd0, 6'd63}); + + capture_count = 0; + monitoring_active = 1; + + $display("[TX_CONFIG] Motor 0: 2 steps at SP=1 (500-cycle interval, well under the 1024-cycle pulse)..."); + upload_single_segment(3'd0, CMD_MOVE_HALT, DIR_NORMAL, 1'b0, 24'd2, 16'd1, 16'd0); + start_motor(3'd0); + + // wait_for_captures only needs to see the two rising edges, + // not the full tail of the second (uninterrupted) pulse - + // this returns quickly (~10us in), well before + // SAFETY_TIMEOUT_NS. + wait_for_captures(2, SAFETY_TIMEOUT_NS); + + // Give the second, uninterrupted pulse room to run its full + // configured width (1024 cycles = 20.48us) before checking + // anything else - not required for the assertions below + // (motion state and position are already final well before + // this), but keeps this test from overlapping the next one. + #30000; + monitoring_active = 0; + + `ASSERT_EQ(capture_count, 32'd2, "%0d", + "[PULSE WIDER THAN INTERVAL] Expected exactly 2 captured step pulses."); + + bus_read(REG_STEP_POS_0, position_read); + `ASSERT_EQ($signed(position_read), 2, "%0d", + "[PULSE WIDER THAN INTERVAL] Position after 2 steps should be exactly 2."); + end + + report(); + $finish; + end + +endmodule diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/steppers_tb.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/steppers_tb.v new file mode 100644 index 0000000..ffcc7a7 --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/steppers_tb.v @@ -0,0 +1,179 @@ +`timescale 1ns/1ps + +`include "src/test/assertions.svh" + +module steppers_tb; + + reg RESET; + reg TCXO = 0; + + `include "src/test/bus_io.svh" + `include "src/main/io/steppers_regs.svh" + `include "src/main/io/steppers_shared.svh" + + reg [3:0] step_pins; + reg [3:0] dir_pins; + reg global_motor_en; + + wire [15:0] debug; + + reg [31:0] result; + + steppers dut ( + .sys_clk(TCXO), + .reset(RESET), + + .bus_stb(stb), + .bus_we(we), + .bus_addr(addr), + .bus_din(din), + .bus_dout(dout), + .bus_ack(ack), + + .step_pins(step_pins), + .dir_pins(dir_pins), + .global_motor_en(global_motor_en) + ); + + always #10 TCXO = ~TCXO; // 50 MHz + + reg [7:0] test_index = 0; + + initial begin + $dumpfile("steppers_tb.vcd"); + $dumpvars(0, steppers_tb); + + sys_reset(); + bus_init(); + + // ============================================================ + $display("TEST: CTRL enable"); + test_index += 1; + // Expect: output enabled BUT no STEP/DIR signals + // ============================================================ + begin : CONFIGURE_AND_ENABLE + + bus_write(REG_STEP_CTRL, 32'b0000_0000_0000_0000_0000_0000_0000_0001); + + #200; + + // TODO check that step/dirs are all low + end + + // ============================================================ + $display("TEST: SET POINTS (Motor 0)"); + test_index += 1; + // ============================================================ + begin : SET_POINTS + + // motor 0 + // 2 points + bus_write(REG_STEP_TX_CONFIG, {8'h00, 8'h00, 8'h00, 8'h02}); + + #200; + end + + // ============================================================ + $display("TEST: Write and verify streaming Point Data"); + test_index += 1; + // ============================================================ + begin : STREAM_DATA_1 + integer i; + + // embed a 2 bit command into the upper 2 bits of the position + reg [31:0] control_and_steps[6] = '{ + { 8'h00, 24'h111111 }, + { 8'h05, 24'h222222 }, + { 8'h50, 24'h333333 }, + { 8'hA5, 24'h444444 }, + { 8'h5A, 24'h555555 }, + { 8'hFF, 24'h666666 } + }; + + + reg [31:0] start_period_and_delta_magnitude[6] = '{ + {16'ha511, 16'h0110}, + {16'hff22, 16'h0220}, + {16'h5a34, 16'h0330}, + {16'hff44, 16'h0440}, + {16'ha555, 16'h0550}, + {16'hff66, 16'h0660} + }; + + begin : STEAM_DATA_WRITE + $display("TEST: Write streaming Point Data"); + + for (i = 0; i < 6; i = i + 1) begin + bus_write(REG_STEP_SEG_CTST, control_and_steps[i]); + bus_write(REG_STEP_SEG_SPDM, start_period_and_delta_magnitude[i]); + + // XXX + #100; + end + end + + begin : STREAM_DATA_READ + reg [31:0] ctps_verify; + reg [31:0] spdm_verify; + + $display("TEST: Verify streaming Point Data"); + + for (i = 0; i < 6; i = i + 1) begin + bus_read(REG_STEP_SEG_CTST, ctps_verify); + bus_read(REG_STEP_SEG_SPDM, spdm_verify); + + // XXX + #100; + + `ASSERT_EQ(ctps_verify, control_and_steps[i], "0x%08h", $sformatf("control_and_steps mismatch at index %d", i)); + `ASSERT_EQ(spdm_verify, start_period_and_delta_magnitude[i], "0x%08h", $sformatf("start_period_and_delta_magnitude mismatch at index %d", i)); + end + end + end + + // ============================================================ + $display("TEST: Exhaustive memory exercise - all motors, all segments"); + test_index += 1; + // Writes and reads back dut.MAX_SEG segments for every one of the + // 8 motors - every (motor, segment, word) address the segment + // table can express gets exercised at least once. Uses a + // deterministic, recomputable value per (motor, segment) rather + // than a fixed table, so it automatically adapts if MAX_SEG is + // ever changed again. + // ============================================================ + begin : EXHAUSTIVE_MEMORY_TEST + integer m, i; + reg [31:0] ctst_expected, spdm_expected; + reg [31:0] ctst_readback, spdm_readback; + + for (m = 0; m < 8; m = m + 1) begin + $display(" [WRITE] motor %0d: writing all %0d segments...", m, dut.MAX_SEG); + bus_write(REG_STEP_TX_CONFIG, {16'h0000, {5'd0, m[2:0]}, dut.MAX_SEG}); + + for (i = 0; i < dut.MAX_SEG; i = i + 1) begin + ctst_expected = ((m & 8'hF) << 28) | ((i & 8'hFF) << 8) | ((m ^ i) & 8'hFF); + spdm_expected = ((i & 16'hFFFF) << 16) | (((m << 8) | (i ^ 8'hAA)) & 16'hFFFF); + bus_write(REG_STEP_SEG_CTST, ctst_expected); + bus_write(REG_STEP_SEG_SPDM, spdm_expected); + end + + $display(" [READ] motor %0d: reading all %0d segments back...", m, dut.MAX_SEG); + for (i = 0; i < dut.MAX_SEG; i = i + 1) begin + ctst_expected = ((m & 8'hF) << 28) | ((i & 8'hFF) << 8) | ((m ^ i) & 8'hFF); + spdm_expected = ((i & 16'hFFFF) << 16) | (((m << 8) | (i ^ 8'hAA)) & 16'hFFFF); + bus_read(REG_STEP_SEG_CTST, ctst_readback); + bus_read(REG_STEP_SEG_SPDM, spdm_readback); + `ASSERT_EQ(ctst_readback, ctst_expected, "0x%08h", $sformatf("motor %0d segment %0d CTST mismatch", m, i)); + `ASSERT_EQ(spdm_readback, spdm_expected, "0x%08h", $sformatf("motor %0d segment %0d SPDM mismatch", m, i)); + end + end + end + + // ============================================================ + // END + // ============================================================ + report(); + $finish; + end + +endmodule diff --git a/firmware/firmware-makerpnpcontrolcore/stm32h7/.cargo/config.toml b/firmware/firmware-makerpnpcontrolcore/stm32h7/.cargo/config.toml index cfdf485..f24cae1 100644 --- a/firmware/firmware-makerpnpcontrolcore/stm32h7/.cargo/config.toml +++ b/firmware/firmware-makerpnpcontrolcore/stm32h7/.cargo/config.toml @@ -13,7 +13,7 @@ rustflags = [ [env] #DEFMT_LOG = "debug" -DEFMT_LOG = "debug,firmware_makerpnpcontrolcore::fpga=trace" +DEFMT_LOG = "debug,firmware_makerpnpcontrolcore::fpga=trace,firmware_makerpnpcontrolcore::fpga::steppers=debug" [profile.release] # Allows defmt to display log locations even in release diff --git a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/firmware.rs b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/firmware.rs index 6af5179..5967fc8 100644 --- a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/firmware.rs +++ b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/firmware.rs @@ -46,6 +46,7 @@ use firmware_makerpnpcontrolcore::fpga::FpgaCore; use firmware_makerpnpcontrolcore::fpga::ws2812::ColorOrdering; use firmware_makerpnpcontrolcore::rgb::rainbow_wave; use firmware_makerpnpcontrolcore::stepper::bitbash::{GpioBitbashStepper, StepperEnableMode}; +use firmware_makerpnpcontrolcore::stepper::fpgastepper::FpgaStepper; use firmware_makerpnpcontrolcore::stepper::tmc5160::Tmc5160Stepper; #[cfg(feature = "tracepin")] use firmware_makerpnpcontrolcore::trace::TracePinsService; @@ -209,8 +210,8 @@ async fn init_task(lp_spawner: Spawner, hp_spawner: SendSpawner, p: Peripherals) wrap_size: WrapSize::None, // TODO increase this speed as much as possible // clock_prescaler: 5, // 133.33Mhz / (5+1) = 22.22Mhz - clock_prescaler: 13, // 133.33Mhz / (13+1) = 9.5Mhz - // clock_prescaler: 132, // 133.33Mhz / (132+1) = 1.0Mhz + // clock_prescaler: 13, // 133.33Mhz / (13+1) = 9.5Mhz + clock_prescaler: 132, // 133.33Mhz / (132+1) = 1.0Mhz // clock_prescaler: 254, // 133.33Mhz / (254+1) = 0.522Mhz sample_shifting: true, delay_hold_quarter_cycle: false, @@ -379,7 +380,7 @@ async fn init_task(lp_spawner: Spawner, hp_spawner: SendSpawner, p: Peripherals) // Detection circuits // let base_present = fpga.base_present(); - if (!base_present) { + if !base_present { // enable the buzzer output, however, if the base board is not present, the buzzer will not // be heard. if the base board is badly connected the user will hear the buzzer tone and // be able to identify and resolve the issue. @@ -445,6 +446,9 @@ async fn init_task(lp_spawner: Spawner, hp_spawner: SendSpawner, p: Peripherals) let fpga_adc_mux = fpga.adc_mux(); + let fpga_stepper_bank_0 = fpga.stepper_bank(0); + let fpga_stepper_bank_1 = fpga.stepper_bank(1); + lp_spawner.spawn(unwrap!(fpga_task(fpga))); let adc1 = Adc::new(p.ADC1); @@ -523,43 +527,77 @@ async fn init_task(lp_spawner: Spawner, hp_spawner: SendSpawner, p: Peripherals) // Launch network task lp_spawner.spawn(unwrap!(embassy_net_task(runner))); - info!("Initializing Stepper"); - - // Setup spi i/o - let p1_sck = p.PD3; - let p1_mosi = p.PB15; - let p1_miso = p.PB14; - let mut p1_nss_1 = Output::new(p.PB12, Level::High, Speed::Low); - let mut p1_nss_2 = Output::new(p.PG3, Level::High, Speed::Low); - // enable - // Via PA8 to FPGA IOR_140_GBIN3, FPGA needs to route internally to the WAKE_1 output. - // enable is ACTIVE_LOW. - let p1_wake = Output::new(p.PA8, Level::High, Speed::Low); - - let mut spi_config = spi::Config::default(); - spi_config.frequency = mhz(1); - - let spi = spi::Spi::new_blocking(p.SPI2, p1_sck, p1_mosi, p1_miso, spi_config); - - let mut stepper = Tmc5160Stepper::new( - spi, - p1_nss_1, - p1_wake, - Delay, - // step - // TIM1_CH1 = P1_T16_CH1 -> STEP_A_I (isolated) -> P1 MOTOR1 - Output::new(p.PE9, Level::Low, Speed::Low), - // direction - // TIM1_CH2 = P1_T16_CH2 -> DIR_A_I (isolated) -> P1 MOTOR1 - Output::new(p.PE11, Level::Low, Speed::Low), - 1000, - 1000, - ); - stepper.initialize_io().unwrap(); + if false { + info!("Initializing Port Stepper"); + + // Setup spi i/o + let p1_sck = p.PD3; + let p1_mosi = p.PB15; + let p1_miso = p.PB14; + let p1_nss_1 = Output::new(p.PB12, Level::High, Speed::Low); + let _p1_nss_2 = Output::new(p.PG3, Level::High, Speed::Low); + // enable + // Via PA8 to FPGA IOR_140_GBIN3, FPGA needs to route internally to the WAKE_1 output. + // enable is ACTIVE_LOW. + let p1_wake = Output::new(p.PA8, Level::High, Speed::Low); + + let mut spi_config = spi::Config::default(); + spi_config.frequency = mhz(1); + + let spi = spi::Spi::new_blocking(p.SPI2, p1_sck, p1_mosi, p1_miso, spi_config); + + let mut port_stepper = Tmc5160Stepper::new( + spi, + p1_nss_1, + p1_wake, + Delay, + // step + // TIM1_CH1 = P1_T16_CH1 -> STEP_A_I (isolated) -> P1 MOTOR1 + Output::new(p.PE9, Level::Low, Speed::Low), + // direction + // TIM1_CH2 = P1_T16_CH2 -> DIR_A_I (isolated) -> P1 MOTOR1 + Output::new(p.PE11, Level::Low, Speed::Low), + 1000, + 1000, + ); + port_stepper.initialize_io().unwrap(); + hp_spawner.spawn(unwrap!(port_stepper_task(StepperRunner::new(port_stepper)))); + } - info!("Initialisation complete"); + if false { + info!("Initializing Bitbash Stepper"); + let mut bitbash_stepper = GpioBitbashStepper::new( + // enable + // TIM8_ETR + Output::new(p.PG12, Level::High, Speed::Low), + // step + // TIM8_CH1 = P3_T16_CH1 + Output::new(p.PE14, Level::Low, Speed::Low), + // direction + // TIM8_CH2 = P3_T16_CH2 + Output::new(p.PE15, Level::Low, Speed::Low), + StepperEnableMode::ActiveHigh, + 1000, + 1000, + ); + bitbash_stepper.initialize_io().unwrap(); + + hp_spawner.spawn(unwrap!(bitbash_stepper_task(StepperRunner::new(bitbash_stepper)))); + } - hp_spawner.spawn(unwrap!(stepper_task(StepperRunner::new(stepper)))); + if true { + info!("Initializing Fpga Stepper"); + let fpga_stepper = FpgaStepper::new( + fpga_stepper_bank_0, + 0, + 1000, + 1000, + ); + + hp_spawner.spawn(unwrap!(fpga_stepper_task(StepperRunner::new(fpga_stepper)))); + } + + info!("Initialisation complete"); info!("running"); @@ -751,12 +789,25 @@ async fn embassy_net_task(mut runner: embassy_net::Runner<'static, Device>) -> ! runner.run().await } -type StepperInstance = Tmc5160Stepper, Output<'static>, Output<'static>, Delay, Output<'static>, Output<'static>>; +type PortStepperInstance = Tmc5160Stepper, Output<'static>, Output<'static>, Delay, Output<'static>, Output<'static>>; +#[embassy_executor::task] +async fn port_stepper_task(runner: StepperRunner) { + runner.run().await +} + +type BitBashStepperInstance = GpioBitbashStepper, Output<'static>, Output<'static>>; +#[embassy_executor::task] +async fn bitbash_stepper_task(runner: StepperRunner) { + runner.run().await +} + +type FpgaStepperInstance = FpgaStepper; #[embassy_executor::task] -async fn stepper_task(runner: StepperRunner) { +async fn fpga_stepper_task(runner: StepperRunner) { runner.run().await } + struct StepperRunner { stepper: STEPPER, } @@ -886,7 +937,7 @@ mod rcc_setup { pub fn dump_mem_u8_be(mem: &[u8]) { for row in (0..mem.len()).step_by(0x10 * 4) { let mut row_values: [u32; 0x10] = [0x00000000; 0x10]; - for col in (0..0x10) { + for col in 0..0x10 { let offset: usize = row + (col * 4); if offset < mem.len() { row_values[col] = diff --git a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/fpga/mod.rs b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/fpga/mod.rs index 6f0d6aa..e4e8f1c 100644 --- a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/fpga/mod.rs +++ b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/fpga/mod.rs @@ -6,6 +6,7 @@ use embassy_stm32::ospi::{ }; use embassy_stm32::ospi::enums::DummyCycles; use defmt::*; +use fpga_pac::steppers::vals::cmd; mod commands { pub const CMD_READ_U32_BE: u8 = 0x10; @@ -23,7 +24,9 @@ mod registers { } pub use registers::*; use crate::fpga::adc::FpgaAdcMux; +use crate::fpga::steppers::FpgaStepperBank; use crate::fpga::ws2812::Ws2812LedControllerBuilder; +use crate::stepper::fpgastepper::Command; pub struct FpgaCore { ospi: Ospi<'static, I, Blocking>, @@ -71,7 +74,7 @@ impl FpgaCore { let value = fpga_pac::IO.io_in_1().read(); let buttons = (value.user0() as u8) | ((value.user1() as u8) << 1); - defmt::debug!("FPGA value: 0x{:08x}, buttons: 0b{:02b}", value.0, buttons); + defmt::trace!("FPGA value: 0x{:08x}, buttons: 0b{:02b}", value.0, buttons); buttons } @@ -86,7 +89,7 @@ impl FpgaCore { let value = fpga_pac::IO.io_in_1().read(); let iak = (value.iak1() as u8) | ((value.iak2() as u8) << 1); - defmt::debug!("FPGA value: 0x{:08x}, iak: 0b{:02b}", value.0, iak); + defmt::trace!("FPGA value: 0x{:08x}, iak: 0b{:02b}", value.0, iak); iak } @@ -99,7 +102,7 @@ impl FpgaCore { let value = fpga_pac::IO.io_in_2().read(); let din = value.din(); - defmt::debug!("FPGA value: 0x{:08x}, din: 0b{:08b}", value.0, din); + defmt::trace!("FPGA value: 0x{:08x}, din: 0b{:08b}", value.0, din); din } @@ -448,7 +451,7 @@ impl FpgaCore { let value = fpga_pac::IO.io_in_1().read(); let present = value.base_present(); - defmt::debug!("FPGA value: 0x{:08x}, base_present: 0b{:01b}", value.0, present); + defmt::info!("FPGA value: 0x{:08x}, base_present: 0b{:01b}", value.0, present); present } @@ -462,11 +465,14 @@ impl FpgaCore { let present = value.port_present() & 0b1111; - defmt::debug!("FPGA value: 0x{:08x}, port_present: 0b{:04b}", value.0, present); + defmt::info!("FPGA value: 0x{:08x}, port_present: 0b{:04b}", value.0, present); present } + pub fn stepper_bank(&self, bank: u8) -> FpgaStepperBank { + FpgaStepperBank::new(bank) + } } #[derive(defmt::Format)] @@ -499,6 +505,116 @@ impl FpgaVersion { } } +pub mod steppers { + use fpga_pac::steppers::vals::{dir, ramp}; + use ioboard_main::stepper::StepperDirection; + use crate::stepper::fpgastepper::{RampMode, Segment}; + + pub struct FpgaStepperBank { + pub index: u8, + instance: fpga_pac::steppers::steppers, + } + + impl FpgaStepperBank { + pub fn new(index: u8) -> Self { + Self { + index, + instance: fpga_pac::STEPPERS, + } + } + + pub fn enable(&mut self) { + // currently a No-OP + } + + pub fn set_pulse_width(&mut self, motor: u8, pulse_width: u16) { + + // TODO math for calculating preset and prescaler from pulse width + // hard-coded to 1us for now (FPGA sys clock = 50Mhz, stepper clock = 100kHz). + let preset = 9; + let prescaler = 4; + + // TODO select the right motor, hardcoded to motor 0 for now + self.instance.step_pls_config().modify(|w| { + w.set_preset0(preset); + defmt::trace!("PLS_CONFIG: {:08x}", w.0); + }); + + // TODO fix silently overriding the prescaler for other motors on the same bank + // TODO select the right bank, hardcoded to bank 0 for now + self.instance.step_pls_prescaler().modify(|w| { + w.set_prescaler0(prescaler); + defmt::trace!("PLS_PRESCALER: {:08x}", w.0); + }); + } + + pub fn send_sequence(&self, motor: u8, segments: &[Segment; 1]) { + + // TODO fail gracefully if the sequence is too long or empty + assert!(segments.len() <= 127, "Current FPGA implementation only supports up to 127 segments per motor"); + assert!(segments.len() > 0, "Current FPGA implementation requires at least one segment"); + + self.instance.step_tx_config().write(|w| { + w.set_motor_instance(motor); + w.set_num_points(segments.len() as u8); + defmt::trace!("TX_CONFIG: {:08x}", w.0); + }); + + for segment in segments { + + // TODO extract these match blocks into 'into' impls. + + let ramp = match segment.ramp_mode { + RampMode::Up => ramp::UP, + RampMode::Down => ramp::DOWN, + }; + + let direction = match segment.direction { + StepperDirection::Normal => dir::NORMAL, + StepperDirection::Reversed => dir::REVERSE, + }; + + self.instance.step_seg_ctst().write(|w| { + w.set_cmd(segment.command.into()); + w.set_n_steps(segment.steps); + w.set_dir(direction); + w.set_ramp(ramp); + defmt::trace!("CTST: {:08x}", w.0); + }); + self.instance.step_seg_spdm().write(|w|{ + w.set_start_period(segment.start_period); + w.set_delta_magnitude(segment.delta_magnitude); + defmt::trace!("SPDM: {:08x}", w.0); + }); + } + } + + pub fn start_motor(&self, motor: u8) { + if self.index == 0 { + self.instance.step_ctrl().write(|w| { + w.set_start_bank0(1 << motor); + defmt::trace!("CTRL: {:08x}", w.0); + }) + } else { + self.instance.step_ctrl().write(|w| { + w.set_start_bank1(1 << motor); + defmt::trace!("CTRL: {:08x}", w.0); + }) + } + } + } +} + +impl Into for Command { + fn into(self) -> cmd { + match self { + Command::Move => cmd::MOVE, + Command::MoveHalt => cmd::MOVE_HALT, + Command::MoveHaltPause => cmd::MOVE_HALT_WAIT, + } + } +} + pub mod ws2812 { pub struct Ws2812LedController { instance: fpga_pac::ws2812_0::ws2812_0, diff --git a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/stepper/fpgastepper.rs b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/stepper/fpgastepper.rs new file mode 100644 index 0000000..b9a922c --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/stepper/fpgastepper.rs @@ -0,0 +1,114 @@ +use embassy_time::{Duration, Instant, Timer}; +use ioboard_main::stepper::{Stepper, StepperDirection, StepperError}; +use crate::fpga::steppers::FpgaStepperBank; + +/// The FPGA stepper driver hardware implementation is not designed for single stepping, but can be made to... +pub struct FpgaStepper { + stepper_bank: FpgaStepperBank, + motor_index: u8, + pulse_width: u32, + pulse_delay: u32, + next_direction: StepperDirection, +} + +impl FpgaStepper { + pub fn new(stepper_bank: FpgaStepperBank, motor_index: u8, pulse_width: u32, pulse_delay: u32,) -> Self { + Self { + stepper_bank, + motor_index, + pulse_width, + pulse_delay, + next_direction: StepperDirection::Normal, + } + } +} + +impl Stepper for FpgaStepper { + fn set_pulse_width_us(&mut self, pulse_width: u32) { + self.pulse_width = pulse_width; + self.stepper_bank.set_pulse_width(self.motor_index, self.pulse_width as u16); + } + + fn set_pulse_delay_us(&mut self, pulse_delay: u32) { + self.pulse_delay = pulse_delay; + } + + fn enable(&mut self) -> Result<(), StepperError> { + self.stepper_bank.enable(); + Ok(()) + } + + fn disable(&mut self) -> Result<(), StepperError> { + // NoOp + Ok(()) + } + + fn direction(&mut self, direction: StepperDirection) -> Result<(), StepperError> { + self.next_direction = direction; + Ok(()) + } + + async fn step_and_wait(&mut self) -> Result<(), StepperError> { + + let delay = self.step().await?; + let now = Instant::now(); + let deadline = now + Duration::from_micros(delay as u64); + Timer::at(deadline).await; + + // TODO use the API to wait for the motor to stop + + Ok(()) + } + + async fn step(&mut self) -> Result { + let sequence = [ + Segment::new(Command::MoveHalt, 1, 1, 1, self.next_direction, RampMode::Up), + ]; + self.stepper_bank.send_sequence(self.motor_index, &sequence); + defmt::debug!("stepper step. bank: {}, motor: {}, sequence: {:?}", self.stepper_bank.index, self.motor_index, sequence); + self.stepper_bank.start_motor(self.motor_index); + + Ok(self.pulse_width + self.pulse_delay) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(defmt::Format)] +pub enum Command { + Move, + MoveHalt, + MoveHaltPause, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(defmt::Format)] +pub enum RampMode { + Up, + Down, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(defmt::Format)] +pub struct Segment { + pub command: Command, + // 24 bit, unsigned + pub steps: u32, + pub start_period: u16, + pub delta_magnitude: u16, + pub direction: StepperDirection, + // aka 'increasing' + pub ramp_mode: RampMode, +} + +impl Segment { + fn new(command: Command, steps: u32, start_period: u16, delta_magnitude: u16, direction: StepperDirection, ramp_mode: RampMode) -> Self { + Self { + command, + steps, + start_period, + delta_magnitude, + direction, + ramp_mode, + } + } +} \ No newline at end of file diff --git a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/stepper/mod.rs b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/stepper/mod.rs index 2c0e257..8a72205 100644 --- a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/stepper/mod.rs +++ b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/stepper/mod.rs @@ -1,2 +1,4 @@ pub mod bitbash; -pub mod tmc5160; \ No newline at end of file +pub mod tmc5160; + +pub mod fpgastepper; \ No newline at end of file diff --git a/ioboard/ioboard_main/src/lib.rs b/ioboard/ioboard_main/src/lib.rs index 3491f4f..f06db29 100644 --- a/ioboard/ioboard_main/src/lib.rs +++ b/ioboard/ioboard_main/src/lib.rs @@ -62,7 +62,7 @@ pub async fn run(mut stepper: STEPPER) { let steps_per_unit = motor_steps as f64 / 360.0; loop { - if false { + if true { for i in 0..2 { info!("Run simple loop {}", i); stepper.enable().unwrap(); @@ -79,19 +79,21 @@ pub async fn run(mut stepper: STEPPER) { } } - for i in 0..1 { - info!("Run trajectory {}", i); - stepper.enable().unwrap(); - Timer::after(Duration::from_millis(100)).await; - if run_trajectory_loop(&mut stepper, trajectory_units, steps_per_unit) - .await - .is_err() - { - break; + if true { + for i in 0..2 { + info!("Run trajectory {}", i); + stepper.enable().unwrap(); + Timer::after(Duration::from_millis(100)).await; + if run_trajectory_loop(&mut stepper, trajectory_units, steps_per_unit) + .await + .is_err() + { + break; + } + stepper.disable().unwrap(); + info!("Stopped trajectory {}", i); + Timer::after(Duration::from_millis(1000)).await; } - stepper.disable().unwrap(); - info!("Stopped trajectory {}", i); - Timer::after(Duration::from_millis(5000)).await; } } } diff --git a/ioboard/ioboard_main/src/stepper.rs b/ioboard/ioboard_main/src/stepper.rs index a907ffd..bf3162d 100644 --- a/ioboard/ioboard_main/src/stepper.rs +++ b/ioboard/ioboard_main/src/stepper.rs @@ -1,4 +1,5 @@ -#[derive(Debug, Default, PartialEq, Clone)] +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +#[derive(defmt::Format)] pub enum StepperDirection { #[default] Normal,